目录
一、构建Result统一结果封装
二、定义业务异常
三、定义全局异常处理
四、全局异常捕获底层原理
五、总结
1、请求到达
2、异常捕获
3、异常处理入口
4、责任链模式遍历解析器
5、核心解析:ExceptionHandlerExceptionResolver
6、返回响应
在做业务开发时,我们希望将预期异常捕获并返回给前端,反馈给用户并让用户按照异常信息进行重试。业务异常区分于系统异常,能让用户明白到底是自身操作的问题,还是系统崩溃了。
因此,异常处理就显得非常重要。我们先学习如何定义异常并捕获异常,然后再学习底层原理。
一、构建Result统一结果封装
不同的异常信息格式有所不同,我们希望能给前端返回统一的格式进行解析。
所以需要先在common中定义Result统一结果返回。
package com.miao.user.common; import lombok.Data; @Data public class Result<T> { // 响应码:200成功,其他代表业务错误 private Integer code; // 提示信息 private String msg; // 返回数据 private T data; // 成功静态方法 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } // 不带数据成功 public static <T> Result<T> success() { return success(null); } // 业务失败 public static <T> Result<T> fail(Integer code,String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); result.setData(null); return result; } }我们这里使用到了@Data注解,其功能就是不用手写set和get方法,使用这个注解需要引入Lombok依赖。
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>optional设置为true表示,不需要将此依赖传递给其他项目。比如当其他项目引入本项目时,如果设置成false会强制要求下载这个jar包。Lombok只在编译期工作,编译时生成我们需要的方法,编译后运行时就直接使用这些方法所以Lombok的职责就结束了。
我们尝试编译发现:
set和get方法,以及equals方法,hashCode,toString方法都给我们生成好了。
定义好Result后,就开始定义我们的业务异常。
二、定义业务异常
自定义业务异常拥有两个属性,一个是错误码code,另一个是错误信息。我们在抛出异常的同时,需要将错误码和错误信息同时交给Resul进行处理。
随着业务的扩展,方便开发者快速定位错误,我们考虑将错误码,错误信息升级成一个枚举类。每一个错误码都对应着一段错误信息,涵盖了我们业务的方方面面。同时我们还兼容原来的方式,如果枚举类里的错误信息过于模糊,可以手动指定错误信息的内容。
因此枚举类可以构建如下:
package com.miao.user.common; import lombok.Getter; @Getter public enum ErrorCodeEnum { //业务异常 //用户异常 USER_NOT_EXIST(1001,"用户不存在"), USER_PASSWORD_ERROR(1002,"用户名密码错误"); // 错误码 private final Integer code; // 错误信息 private final String msg; ErrorCodeEnum(Integer code, String msg) { this.code = code; this.msg = msg; } }我们使用了@Getter注解,这也是Lombok依赖提供的一个注解,帮我们只生成get方法,而无需生成set方法。因为两个属性code和msg都是fianl类型,而且都已提前定义好,无需设置。
接着我们定义业务异常,在exception中定义BuisnessException。
package com.miao.user.exception; import com.miao.user.common.ErrorCodeEnum; public class BusinessException extends RuntimeException { private final Integer code; //方式1:只传错误信息,错误码默认为500客户端报错 public BusinessException(String message) { super(message); this.code = 500; } //方式2:使用错误枚举 public BusinessException(ErrorCodeEnum errorCodeEnum) { super(errorCodeEnum.getMsg()); this.code = errorCodeEnum.getCode(); } //方式3:借助错误枚举的错误码,晚上错误信息 public BusinessException(ErrorCodeEnum errorCodeEnum, String message) { super(message); this.code = errorCodeEnum.getCode(); } public Integer getCode() { return code; } }我们现在可以试试能否手动抛出我们想要的异常,并打印异常信息。
忽略掉我最上方的MockBean,只看test,可以看到我们的业务异常被成功捕获了。
那么我们想能否将其结合起来,封装到Result中呢 。
我们首先明确,Result封装是在Controller层做的事情,那么也就意味着想在这层抛出错误,底层就不能捕获,必须在controller层捕获才行。所以我们可以这样模拟。
三、定义全局异常处理
其实我们以及清楚了一件事情,Result统一结果封装是在controller层返回的,包括成功和失败的情况。但是我们只会写返回成功的情况,那失败的情况怎么反应呢?如果要写失败的情况的话,那每个controller方法都得try-catch,很麻烦,我们想到只要异常留在controlelr层进行处理,然后有个自动捕获的异常的工具就好了。
我们先思考一个问题,为什么说要让异常在controller层被捕获,service层,其他地方抛出的异常为什么不去处理。
举个例子,比如service层抛出异常,这个异常会抛向哪里?会沿着调用栈往上抛。
在MVC模型中,controller->service->mapper,异常可以发生在任何一处,假如mapper中数据写入发生异常,那么会创建一个异常对象,沿着mapper->service->controller抛出。所以我们不能在service层捕获异常,要让异常能抛到controller层,然后在controller层再抛出并被全局异常捕获。
那么我们就需要配置全局异常处理了。
在exception包下创建GlobalExceptionHandler处理类
package com.miao.user.exception; import com.miao.user.common.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @Slf4j public class GlobExceptionHandler { // 优先捕获自定义异常 @ExceptionHandler(BusinessException.class) public Result<?> handleBusinessException(BusinessException e) { log.error("【系统业务异常】 ",e); return Result.fail(e.getCode(),e.getMessage()); } // 其次捕获其他运行异常 @ExceptionHandler(RuntimeException.class) public Result<?> handleRuntimeException(RuntimeException e) { log.error("【系统运行时异常】 ",e); return Result.fail(500,e.getMessage()); } // Exception兜底 @ExceptionHandler(Exception.class) public Result<?> handleException(Exception e) { log.error("【系统兜底异常】 ",e); return Result.fail(500,e.getMessage()); } }注意我们的书写顺序和执行顺序无关,spring会先去捕获范围更小的异常:业务异常。匹配异常类型是否为BusinessExcepion,如果不是再去匹配其他异常类型。
其次我们需要log.eror时选择打印堆栈的方法,不然看不到堆栈信息,找不到错误源头。
这里贴一张Java异常分类图
值得注意的是,我么用了@Slf4j和@ControllerAdvice这两个注解,前者来自于Lombok后者来自于spring-boot-starter-web这些依赖我们一开始已经引入了,所以无需再引入。
稍后我们去学习全局异常捕获的机制,现在测试一下效果。
考虑这样一个场景:controller->service,service抛出异常,异常在controller处捕获。
预期会分别抛出业务异常、运行时异常,IO异常。
我们编写controller
我们希望,从controller抛出的异常都被全局异常捕获。
测试一下。
这样错误信息,和堆栈都能看到了,用户也能拿到必要的错误信息。
四、全局异常捕获底层原理
我们现在肯定的一件事是:service层及其其他层的异常都统一向上抛了,最后从controller层抛出并被全局异常GlobalException捕获。而且我们知道,@RestControllerAdvice注解的作用肯定是给每个controller接口等价“包裹上了try-catch”。那么我们想具体深入了解,@RestControllerAdvice到底是怎么做到呢?我们并没有去手动给每一个controller方法都添加try-catch却能实现这样的效果。
从@RestControllerAdvice进去后,我们发现这个注解是一个复合注解,它由@ControllerAdvice + @ResponseBody组成,后者我们熟悉,这在controller往前端返回时告诉前端浏览器返回的是数据而非视图。那么@ControllerAdvice呢?
先看DispatcherServlet.doDispatch()方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... try { try { ModelAndView mv = null; Exception dispatchException = null; try { ...// 调用controller mv = ha.handle(processedRequest, response,mappedHandler.getHandler()); } catch (Exception ex) { dispatchException = ex; //将异常缓存起来 } catch (Throwable err) { ... } //处理异常 this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { //处理异常时又出现异常,触发完成回调 this.triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + err, err)); } } finally { ... } }DispatcherServlet.processDispatchResult()方法
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { // 特殊异常:已经定义好了ModelAndView直接用 if (exception instanceof ModelAndViewDefiningException) { ... mv = mavDefiningException.getModelAndView(); } else { // 普通异常调用异常处理器 Object handler = mappedHandler != null ? mappedHandler.getHandler() : null; mv = this.processHandlerException(request, response, handler, exception); errorView = mv != null; } } ... }DispatcherServlet.processHandlerException()
@Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { // 找到能处理的解析器 for(HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { ... } else { // 没找到继续抛 throw ex; } }resolveException()是父类实现,具体是:AbstractHandlerExceptionResolver.resolveException()
@Nullable public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { if (!this.shouldApplyTo(request, handler)) { return null; } else { this.prepareResponse(ex, response); //子类重写 ModelAndView result = this.doResolveException(request, response, handler, ex); if (result != null) { ... } return result; } }AbstractHandlerMethodExceptionResolver.doResolveException()
@Nullable protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { HandlerMethod var10000; if (handler instanceof HandlerMethod hm) { // 将handerl 转成 HandlerMethod var10000 = hm; } else { var10000 = null; } HandlerMethod handlerMethod = var10000; // 调用核心方法 return this.doResolveHandlerMethodException(request, response, handlerMethod, ex); }看其子类实现ExceptionHandlerExceptionResolver.doResolveHandlerMethodException()
@Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 从缓存中找到@ExcetpionHandler方法 ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { // 找不到就交给下一个解析器 return null; } else { ... try { ... // 通过反射执行异常处理方法 Object[] arguments = new Object[]{exception}; exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { ... return null; } if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ... // 创建ModelAndView ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); ... //返回ModelAndView return mav; } } }把结果返回给前端,整个流程不涉及AOP,不涉及代理,就是靠的是反射机制!!
五、总结
现在我们梳理一下全局异常处理的流程:
1、请求到达
HTTP 请求首先被 Servlet 容器(如 Tomcat)接收,然后交给DispatcherServlet。DispatcherServlet 的核心方法是doDispatch,负责将请求分发给对应的 Controller。
2、异常捕获
在doDispatch中,Controller的执行被包裹在try-catch中:
try { mv = ha.handle(...); // 调用 Controller } catch (Exception ex) { dispatchException = ex; // 异常暂存 }3、异常处理入口
doDisaptch捕获异常后,调用processDispatchResult,判断有异常则进入processHandlerException
4、责任链模式遍历解析器
processHandlerException遍历handlerExceptionResolvers列表(默认3个解析器),找到第一个能处理的解析器:
1.ExceptionHandlerExceptionResolver -> 处理@ControllerAdvice
2.ResultStatusException -> 处理@ResponseStatus
3.DefaultHandlerExceptionResolver -> 处理SpringMVC内置异常
5、核心解析:ExceptionHandlerExceptionResolver
当遍历到 ExceptionHandlerExceptionResolver时,调用其doResolveHandlerMethodException方法:
第一步:查找 @ExceptionHandler 方法
Spring 从两个缓存中查找:
第一级:
exceptionHandlerCache→ 当前 Controller 自己的@ExceptionHandler第二级:
exceptionHandlerAdviceCache→ 全局@ControllerAdvice中的@ExceptionHandler
查找过程就是从Map<异常类型, Method>中根据异常类型获取Method对象。
第二步:反射调用
找到Method后,通过ServletInvocableHandlerMethod.invokeAndHandle执行:
method.invoke(bean, args);6、返回响应
@ExceptionHandler方法执行完成后:返回@ResponseBody或者视图名。
ModelAndView沿着调用链向上返回,最终通过HttpServletResponse返回给客户端。
以上就是Spring全局异常处理的所有内容,如有错误,请指出,作者会及时勘误。