异常处理逻辑上的分类:
对于我们并不期望会发生的事,我们可以使用异常捕捉;
对于我们觉得可能会发生的事,使用返回码。
springboot异常处理
自定义错误页面
- SpringBoot 默认的处理异常的机制:SpringBoot 默认的已经提供了一套处理异常的机制。一旦程序中出现了异常 SpringBoot 会向/error 的 url 发送请求。
- 在 springBoot 中提供了一个叫 BasicExceptionController 来处理/error 请求,然后跳转到默认显示异常的页面来展示异常信息。
- 我们也可以重写、error方法,进行特定的返回处理。eg:/error/500,/error/404;
- 场景:不是前后端分离,返回页面;
自己在controller层对指定异常进行try/catch捕获
```java
@RequestMapping(“doCompute/{n1}/{n2}”)@ResponseBody public String doCompute(@PathVariable Integer n1, @PathVariable Integer n2){ try{ Integer result=n1/n2; return "Result is "+result; }catch(ArithmeticException e){ return "exception is "+e.getMessage(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
一个Controller类中通常会有多个方法,这样多个方法中都写try语句进行异常处理会带来大量重复代码的编写,不易维护。
使用场景:特定的返回结果;
3. #### 注解处理异常
1. Controller内部定义异常处理方法
```java
public String doHandleArithmeticException(ArithmeticException e){
e.printStackTrace();
return "计算过程中出现了异常,异常信息为"+e.getMessage();
}@ExceptionHandler注解描述的方法为异常处理方法(注解中的异常类型为可处理的异常类型),假如Controller类中的逻辑方法中出现异常后没有处理异常,则会查找Controller类中有没有定义异常处理方法,假如定义了,且可以处理抛出的异常类型,则由异常处理方法处理异常。
@ControllerAdvice+@ExceptionHandler 全局处理异常
- ```java
@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {@ExceptionHandler(ArithmeticException.class) public String doHandleArithmeticException(ArithmeticException e){ e.printStackTrace(); return "计算过程中出现了异常,异常信息为"+e.getMessage(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2. @RestControllerAdvice 注解描述的类为全局异常处理类,当控制层方法中的异常没有自己捕获,也没有定义其内部的异常处理方法,底层默认会查找全局异常处理类,调用对应的异常处理方法进行异常处理。
4. #### 自定义的异常类以及枚举类(优化)
上述的示例中,我们对捕获的异常进行简单的二次处理,返回异常的信息,虽然这种能够让我们知道异常的原因,但是在很多的情况下来说,可能还是不够人性化,不符合我们的要求。
1. 定义一个错误码ErrorCode interface
```java
public interface BaseErrorInfoInterface {
/** 错误码*/
int getCode();
/** 错误描述*/
String getMessage();
}
- ```java
实现一个错误码枚举类并实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public enum CommonEnum implements BaseErrorInfoInterface{
BODY_NOT_MATCH(400,"请求的数据格式不符!"),
SIGNATURE_NOT_MATCH(401,"请求错误!"),
NOT_FOUND(404, "未找到该资源!"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误!"),
SERVER_BUSY(503,"服务器正忙,请稍后再试!");
/**错误码*/
private final int code;
/**
* 错误消息
*/
private final String message;
CommonEnum(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 错误码
*/
public int getCode() {
return code;
}
/**
* 错误描述
*/
public String getMessage() {
return message;
}
}然后我们在来自定义一个异常类,用于处理我们发生的业务异常和内部异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72public class BizException extends RuntimeException{
/**
* 错误码
*/
protected int errorCode;
/**
* 错误信息
*/
protected String errorMsg;
//默认无参构造方法
public BizException() {
super();
}
public BizException(BaseErrorInfoInterface errorInfoInterface){
super(errorInfoInterface.getMessage());
this.errorCode = errorInfoInterface.getCode();
this.errorMsg = errorInfoInterface.getMessage();
}
public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
super(errorInfoInterface.getMessage(), cause);
this.errorCode = errorInfoInterface.getCode();
this.errorMsg = errorInfoInterface.getMessage();
}
public BizException(String errorMsg){
super(errorMsg);
this.errorMsg = errorMsg;
}
public BizException(int errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BizException(int errorCode, String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public int getErrorCode() {
return errorCode;
}
public void setErrorCode(int errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
//todo 对性能的影响
public Throwable fillInStackTrace() {
return this;
}
public String toString() {
return "BizException{" +
"errorCode=" + errorCode +
", errorMsg='" + errorMsg + '\'' +
'}';
}
}自定义全局捕获处理异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ExceptionAdvice {
public String doHandleArithmeticException(ArithmeticException e){
//遍历堆栈信息并储存
for (StackTraceElement element:e.getStackTrace()){
log.error(element.toString());
}
return "计算过程中出现了异常,异常信息为"+e.getMessage();
}
/**
* 处理自定义的业务异常
* @param req
* @param e
* @return
*/
public ResultBody bizExceptionHandler(HttpServletRequest req, BizException e){
log.error("发生业务异常!原因是:{}",e.getErrorMsg());
return ResultBody.error(e.getErrorCode(),e.getErrorMsg());
}
/**
* 处理空指针的异常
* @param req
* @param e
* @return
*/
public ResultBody exceptionHandler(HttpServletRequest req, NullPointerException e){
log.error("发生空指针异常!原因是:",e);
return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
}
//处理其他异常
public void handleException(Exception e, HttpServletRequest httpRequest, HttpServletResponse response) throws IOException {
log.error("服务器发生异常",e.getMessage());
//遍历堆栈信息并储存
for (StackTraceElement element:e.getStackTrace()){
log.error(element.toString());
}
String requestHeader = httpRequest.getHeader("x-requested-with");
if (requestHeader.equals("XMLHttpRequest")){
response.setContentType("application/plain;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtils.getJSONString(1,"服务器异常"));
}else {
response.sendRedirect(httpRequest.getContextPath() + "/error");
}
}
}业务异常输出示例:
1
22022-09-22 10:29:24,032 ERROR [http-nio-8081-exec-4] t.t.c.c.a.ExceptionAdvice [ExceptionAdvice.java:46] 发生业务异常!原因是:用户姓名不能为空!
2022-09-22 10:29:24,036 WARN [http-nio-8081-exec-4] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver [AbstractHandlerExceptionResolver.java:208] Resolved [BizException{errorCode=-1, errorMsg='用户姓名不能为空!'}]
实际开发中遇到的问题和最佳实践
第一,注意捕获和处理异常的最佳实践。首先,不应该用 AOP 对所有方法进行统一异常处理,异常要么不捕获不处理,要么根据不同的业务逻辑、不同的异常类型进行精细化、针对性处理;其次,处理异常应该杜绝生吞,并确保异常栈信息得到保留;最后,如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息。
第二,务必小心 finally 代码块中资源回收逻辑,确保 finally 代码块不出现异常,内部把异常处理完毕,避免 finally 中的异常覆盖 try 中的异常;或者考虑使用 addSuppressed 方法把 finally 中的异常附加到 try 中的异常上,确保主异常信息不丢失。此外,使用实现了 AutoCloseable 接口的资源,务必使用 try-with-resources 模式来使用资源,确保资源可以正确释放,也同时确保异常可以正确处理。
第三,虽然在统一的地方定义收口所有的业务异常是一个不错的实践,但务必确保异常是每次 new 出来的,而不能使用一个预先定义的 static 字段存放异常,否则可能会引起栈信息的错乱。
第四,确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。
要注意的问题
- 在哪一层进行抛出?应该在哪里抛,抛什么异常。
A:比如 我们需要知道 参数为空报什么异常,对其进行捕获统一处理;
2、所有异常被吃掉,带来的严重的后果;