多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot_1:业务异常与全局异常处理

SpringBoot_1:业务异常与全局异常处理 目录一、构建Result统一结果封装二、定义业务异常三、定义全局异常处理四、全局异常捕获底层原理五、总结1、请求到达2、异常捕获3、异常处理入口4、责任链模式遍历解析器5、核心解析ExceptionHandlerExceptionResolver6、返回响应在做业务开发时我们希望将预期异常捕获并返回给前端反馈给用户并让用户按照异常信息进行重试。业务异常区分于系统异常能让用户明白到底是自身操作的问题还是系统崩溃了。因此异常处理就显得非常重要。我们先学习如何定义异常并捕获异常然后再学习底层原理。一、构建Result统一结果封装不同的异常信息格式有所不同我们希望能给前端返回统一的格式进行解析。所以需要先在common中定义Result统一结果返回。package com.miao.user.common; import lombok.Data; Data public class ResultT { // 响应码200成功其他代表业务错误 private Integer code; // 提示信息 private String msg; // 返回数据 private T data; // 成功静态方法 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } // 不带数据成功 public static T ResultT success() { return success(null); } // 业务失败 public static T ResultT fail(Integer code,String msg) { ResultT result new Result(); result.setCode(code); result.setMsg(msg); result.setData(null); return result; } }我们这里使用到了Data注解其功能就是不用手写set和get方法使用这个注解需要引入Lombok依赖。dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyoptional设置为true表示不需要将此依赖传递给其他项目。比如当其他项目引入本项目时如果设置成false会强制要求下载这个jar包。Lombok只在编译期工作编译时生成我们需要的方法编译后运行时就直接使用这些方法所以Lombok的职责就结束了。我们尝试编译发现set和get方法以及equals方法hashCodetoString方法都给我们生成好了。定义好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-serviceservice抛出异常异常在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.doResolveExceptionNullable 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判断有异常则进入processHandlerException4、责任链模式遍历解析器processHandlerException遍历handlerExceptionResolvers列表默认3个解析器找到第一个能处理的解析器1.ExceptionHandlerExceptionResolver - 处理ControllerAdvice2.ResultStatusException - 处理ResponseStatus3.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全局异常处理的所有内容如有错误请指出作者会及时勘误。
返回列表