写点什么

SpringBoot 如何优雅的进行全局异常处理?

作者:这我可不懂
  • 2023-09-07
    福建
  • 本文字数:4872 字

    阅读完需:约 16 分钟

在 SpringBoot 的开发中,为了提高程序运行的鲁棒性,我们经常需要对各种程序异常进行处理,但是如果在每个出异常的地方进行单独处理的话,这会引入大量业务不相关的异常处理代码,增加了程序的耦合,同时未来想改变异常的处理逻辑,也变得比较困难。这篇文章带大家了解一下如何优雅的进行全局异常处理。


为了实现全局拦截,这里使用到了 Spring 中提供的两个注解,@RestControllerAdvice @ExceptionHandler,结合使用可以拦截程序中产生的异常,并且根据不同的异常类型分别处理。下面我会先介绍如何利用这两个注解,优雅的完成全局异常的处理,接着解释这背后的原理。

1.如何实现全局拦截?

1.1 自定义异常处理类

在下面的例子中,我们继承了 ResponseEntityExceptionHandler 并使用 @RestControllerAdvice 注解了这个类,接着结合 @ExceptionHandler 针对不同的异常类型,来定义不同的异常处理方法。这里可以看到我处理的异常是自定义异常,后续我会展开介绍。


ResponseEntityExceptionHandler 中包装了各种 SpringMVC 在处理请求时可能抛出的异常的处理,处理结果都是封装成一个 ResponseEntity 对象。ResponseEntityExceptionHandler 是一个抽象类,通常我们需要定义一个用来处理异常的使用 @RestControllerAdvice 注解标注的异常处理类来继承自 ResponseEntityExceptionHandler。ResponseEntityExceptionHandler 中为每个异常的处理都单独定义了一个方法,如果默认的处理不能满足你的需求,则可以重写对某个异常的处理。


@Log4j2  @RestControllerAdvice  public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {        /**       * 定义要捕获的异常 可以多个 @ExceptionHandler({})     *       * @param request  request       * @param e        exception       * @param response response       * @return 响应结果       */      @ExceptionHandler(AuroraRuntimeException.class)      public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {          AuroraRuntimeException exception = (AuroraRuntimeException) e;           if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {             response.setStatus(HttpStatus.BAD_REQUEST.value());         } else if (exception.getCode() == ResponseCode.FORBIDDEN) {             response.setStatus(HttpStatus.FORBIDDEN.value());         } else {             response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());         }            return new GenericResponse(exception.getCode(), null, exception.getMessage());      }        @ExceptionHandler(NotLoginException.class)      public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {          log.error("token exception", e);          response.setStatus(HttpStatus.FORBIDDEN.value());          return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);      }    }
复制代码

1.2 定义异常码

这里定义了常见的几种异常码,主要用在抛出自定义异常时,对不同的情形进行区分。


@Getter  public enum ResponseCode {        SUCCESS(0, "Success"),        INTERNAL_ERROR(1, "服务器内部错误"),        USER_INPUT_ERROR(2, "用户输入错误"),        AUTHENTICATION_NEEDED(3, "Token过期或无效"),        FORBIDDEN(4, "禁止访问"),        TOO_FREQUENT_VISIT(5, "访问太频繁,请休息一会儿");        private final int code;        private final String message;        private final Response.Status status;        ResponseCode(int code, String message, Response.Status status) {          this.code = code;          this.message = message;          this.status = status;      }        ResponseCode(int code, String message) {          this(code, message, Response.Status.INTERNAL_SERVER_ERROR);      }    }
复制代码

1.3 自定义异常类

这里我定义了一个 AuroraRuntimeException 的异常,就是在上面的异常处理函数中,用到的异常。每个异常实例会有一个对应的异常码,也就是前面刚定义好的。


@Getter  public class AuroraRuntimeException extends RuntimeException {        private final ResponseCode code;        public AuroraRuntimeException() {          super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));          this.code = ResponseCode.INTERNAL_ERROR;      }        public AuroraRuntimeException(Throwable e) {          super(e);          this.code = ResponseCode.INTERNAL_ERROR;      }        public AuroraRuntimeException(String msg) {          this(ResponseCode.INTERNAL_ERROR, msg);      }        public AuroraRuntimeException(ResponseCode code) {          super(String.format("%s", code.getMessage()));          this.code = code;      }        public AuroraRuntimeException(ResponseCode code, String msg) {          super(msg);          this.code = code;      }    }
复制代码

1.4 自定义返回类型

为了保证各个接口的返回统一,这里专门定义了一个返回类型。


@Getter  @Setter  public class GenericResponse<T> {        private int code;        private T data;        private String message;        public GenericResponse() {};        public GenericResponse(int code, T data) {          this.code = code;          this.data = data;      }        public GenericResponse(int code, T data, String message) {          this(code, data);          this.message = message;      }        public GenericResponse(ResponseCode responseCode) {          this.code = responseCode.getCode();          this.data = null;          this.message = responseCode.getMessage();      }        public GenericResponse(ResponseCode responseCode, T data) {          this(responseCode);          this.data = data;      }        public GenericResponse(ResponseCode responseCode, T data, String message) {          this(responseCode, data);          this.message = message;      }  }
复制代码

实际测试异常


下面的例子中,我们想获取到用户的信息,如果用户的信息不存在,可以直接抛出一个异常,这个异常会被我们上面定义的全局异常处理方法所捕获,然后根据不同的异常编码,完成不同的处理和返回。


public User getUserInfo(Long userId) {  	// some logic	    User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);      if (user == null) {          throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户id不存在");      }          // some logic	....}
复制代码


以上就完成了整个全局异常的处理过程,接下来重点说说为什么 @RestControllerAdvice @ExceptionHandler 结合使用可以拦截程序中产生的异常?

全局拦截的背后原理?


下面会提到 @ControllerAdvice 注解,简单地说,@RestControllerAdvice 与 @ControllerAdvice 的区别就和 @RestController 与 @Controller 的区别类似,@RestControllerAdvice 注解包含了 @ControllerAdvice 注解和 @ResponseBody 注解。


接下来我们深入 Spring 源码,看看是怎么实现的,首先 DispatcherServlet 对象在创建时会初始化一系列的对象,这里重点关注函数 initHandlerExceptionResolvers(context);


public class DispatcherServlet extends FrameworkServlet {    // ......	protected void initStrategies(ApplicationContext context) {		initMultipartResolver(context);		initLocaleResolver(context);		initThemeResolver(context);		initHandlerMappings(context);		initHandlerAdapters(context);
// 重点关注 initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } // ......}
复制代码


在 initHandlerExceptionResolvers(context)方法中,会取得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的 bean,这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步处理,关键代码在这里:


public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver		implements ApplicationContextAware, InitializingBean {    // ......	private void initExceptionHandlerAdviceCache() {		// ......		List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());		AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) { ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); if (resolver.hasExceptionMappings()) { // 找到所有ExceptionHandler标注的方法并保存成一个ExceptionHandlerMethodResolver类型的对象缓存起来 this.exceptionHandlerAdviceCache.put(adviceBean, resolver); if (logger.isInfoEnabled()) { logger.info("Detected @ExceptionHandler methods in " + adviceBean); } } // ...... } } // ......}
复制代码


当 Controller 抛出异常时,DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常,而 ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的 @ExceptionHandler 标注的方法是这里:


public class ExceptionHandlerMethodResolver {	// ......	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {		List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();		// 找到所有适用于Controller抛出异常的处理方法,例如Controller抛出的异常		// 是AuroraRuntimeException(继承自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和		// @ExceptionHandler(Exception.class)标注的方法都适用此异常		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {			if (mappedException.isAssignableFrom(exceptionType)) {				matches.add(mappedException);			}		}		if (!matches.isEmpty()) {		/* 这里通过排序找到最适用的方法,排序的规则依据抛出异常相对于声明异常的深度,例如	Controller抛出的异常是是AuroraRuntimeException(继承自RuntimeException),那么AuroraRuntimeException	相对于@ExceptionHandler(AuroraRuntimeException.class)声明的AuroraRuntimeException.class其深度是0,	相对于@ExceptionHandler(Exception.class)声明的Exception.class其深度是2,所以	@ExceptionHandler(BizException.class)标注的方法会排在前面 */			Collections.sort(matches, new ExceptionDepthComparator(exceptionType));			return this.mappedMethods.get(matches.get(0));		}		else {			return null;		}	}    // ......}
复制代码


整个 @RestControllerAdvice 处理的流程就是这样,结合 @ExceptionHandler 就完成了对不同异常的灵活处理。

发布于: 刚刚阅读数: 3
用户头像

低代码技术追随者,为全民开发而努力 2023-02-15 加入

大家好,我是老王,专注于分享低代码图文知识,感兴趣的伙伴就请关注我吧!

评论

发布
暂无评论
SpringBoot 如何优雅的进行全局异常处理?_springboot_这我可不懂_InfoQ写作社区