SpringBoot中的全局异常处理
- 在项⽬开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、
不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很⾼,此外,开发⼯作量也会加⼤⽽且不好统
⼀,这也增加了代码的维护成本。 - 针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单⼀,也实现了异常信息的
统⼀处理和维护。同时,我们也不希望直接把异常抛给⽤户,应该对异常进⾏处理,对错误信息进⾏封装,然后返回⼀个友好的信息给⽤
户。
定义返回的统一Json返回结构
- 前端或者其他服务请求本服务的接⼝时,该接⼝需要返回对应的 json 数据,⼀般该服务只需要返回请求着需要的参数即可,但是在实际项
⽬中,我们需要封装更多的信息,⽐如状态码 code、相关信息 msg 等等,这⼀⽅⾯是在项⽬中可以有个统⼀的返回结构,整个项⽬组都
适⽤,另⼀⽅⾯是⽅便结合全局异常处理信息,因为异常处理信息中⼀般我们需要把状态码和异常内容反馈给调⽤⽅。
@Setter
@Getter
public class JsonResult<T> {
private T data;
private String code;
private String message;
public JsonResult() {
this.code = StatusCode.SUCCESS.getCode();
this.message = "操作成功!";
}
public JsonResult(String code, String message) {
this.code = code;
this.message = message;
}
public JsonResult(T data){
this.data = data;
this.code = StatusCode.SUCCESS.getCode();
this.message = "操作成功!";
}
public JsonResult(T data, String code, String message){
this.data = data;
this.code = code;
this.message = message;
}
}
处理异常
- 创建对应的全局异常处理类,然后加上@ControllerAdvice注解就可以拦截项目中抛出的异常
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
}
- 点开 @ControllerAdvice 注解可以看到,@ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该
类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是⽤来拦截哪个包中的异常信息,⼀般我们不指定
这个属性,我们拦截项⽬⼯程中的所有异常。@ResponseBody 注解是为了异常处理完之后给调⽤⽅输出⼀个 json 格式的封装数据。 - 在⽅法上通过 @ExceptionHandler 注解来指定具体的异常,然后在⽅法中处理该异常信
息,最后将结果通过统⼀的 json 结构体返回给调⽤者。 - 例子1:处理参数缺失异常
- 在前后端分离的架构中,前端请求后台的接⼝都是通过 rest 风格来调⽤,有时候,⽐如 POST 请求 需要携带⼀些参数,但是往往有时候
参数会漏掉。另外,在微服务架构中,涉及到多个微服务之间的接⼝调⽤时,也可能出现这种情况,此时我们需要定义⼀个处理参数缺失异
常的⽅法,来给前端或者调⽤⽅提⽰⼀个友好信息。 - 参数缺失的时候,会抛出 HttpMessageNotReadableException,我们可以拦截该异常,做⼀个友好处理。
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public JsonResult handleHttpMessageNotReadableException(
MissingServletRequestParameterException ex) {
logger.error("缺少请求参数,{}", ex.getMessage());
return new JsonResult("400", "缺少必要的请求参数");
}
}
- 例子2:处理空指针异常
- 在微服务中,经常会调⽤其他服务获取数据,这个数据主要是 json 格式的,但是在解析 json 的过程
中,可能会有空出现,所以我们在获取某个 jsonObject 时,再通过该 jsonObject 去获取相关信息时,应该要先做⾮空判断。
还有⼀个很常见的地⽅就是从数据库中查询的数据,不管是查询⼀条记录封装在某个对象中,还是查询多条记录封装在⼀个 List 中,我们
接下来都要去处理数据,那么就有可能出现空指针异常,因为谁也不能保证从数据库中查出来的东西就⼀定不为空,所以在使⽤数据时⼀定
要先做⾮空判断。
@ExceptionHandler(NullPointerException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleTypeMismatchException(NullPointerException ex) {
logger.error("空指针异常,{}", ex.getMessage());
return new JsonResult("500", "空指针异常了");
}
// test2
@RequestMapping("/test2")
public JsonResult test2(){
String a = null;
System.out.println(a.toString());
return new JsonResult();
}
- 综合处理:异常很多,⽐如还有 RuntimeException,数据库还有⼀些查询或者操作异常等等。由于 Exception 异常是⽗类,所有异常都会
继承该异常,所以我们可以直接拦截 Exception 异常
/**
* 系统异常 预期以外异常
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleUnexpectedServer(Exception ex) {
logger.error("系统异常:", ex);
return new JsonResult("500", "系统发⽣异常,请联系管理员");
}
- 但是项⽬中,我们⼀般都会⽐较详细的去拦截⼀些常见异常,拦截 Exception 虽然可以⼀劳永逸,但是不利于我们去排查或者定位问题。
实际项⽬中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下⾯,如果都没有找到,最后再拦截⼀下 Exception 异常,保
证输出信息友好。 - 异常拦截,如果特性类型的异常没有被拦截则会被Exception拦截,如果有声明特性异常的拦截,则先走特定的异常拦截。
拦截自定义异常
- 在实际项⽬中,除了拦截⼀些系统异常外,在某些业务上,我们需要⾃定义⼀些业务异常,⽐如在微服务中,服务之间的相互调⽤很平凡,
很常见。要处理⼀个服务的调⽤时,那么可能会调⽤失败或者调⽤超时等等,此时我们需要⾃定义⼀个异常,当调⽤失败时抛出该异常,给
GlobalExceptionHandler 去捕获。 - 定义异常信息
public enum BusinessMsgEnum {
PARMETER_EXCEPTION("102", "参数异常!"),
SERVICE_TIME_OUT("103", "服务调⽤超时!"),
PARMETER_BIG_EXCEPTION("102", "输⼊的图⽚数量不能超过50张!"),
UNEXPECTED_EXCEPTION("500", "系统发⽣异常,请联系管理员!");
private String code;
private String msg;
private BusinessMsgEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
}
public class BusinessErrorException extends RuntimeException {
private static final long serialVersionUID = -7480022450501760611L;
private String code;
private String message;
public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
this.code = businessMsgEnum.getCode();
this.message = businessMsgEnum.getMsg();
}
}
- 在构造⽅法中,传⼊我们上⾯⾃定义的异常枚举类,所以在项⽬中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很⽅
便,做到统⼀维护,然后再拦截该异常时获取即可。 - 在之前定义的异常处理器中拦截自定义的异常并处理
@ExceptionHandler(BusinessErrorException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public JsonResult handleBusinessError(BusinessErrorException ex) {
String code = ex.getCode();
String message = ex.getMessage();
return new JsonResult(code, message);
}
- 在业务代码中,使用try/catch捕获异常并处理
@GetMapping("/business")
public JsonResult testException() {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new BusinessErrorException(BusinessMsgEnum.UNEXPECTED_EXCEPTION);
}
return new JsonResult();
}
- 结果:如果业务代码中出现了异常,则会被try/catch捕获到,并抛出自定义异常,抛出后会被自定义的异常处理器拦截并根据异常枚举信息进行处理。