经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring Boot » 查看文章
SpringBoot 如何优雅的进行全局异常处理?
来源:cnblogs  作者:码老思  时间:2023/7/3 9:18:51  对本文有异议

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

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

1. 如何实现全局拦截?

1.1 自定义异常处理类

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

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

  1. @Log4j2
  2. @RestControllerAdvice
  3. public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  4. /**
  5. * 定义要捕获的异常 可以多个 @ExceptionHandler({}) *
  6. * @param request request
  7. * @param e exception
  8. * @param response response
  9. * @return 响应结果
  10. */
  11. @ExceptionHandler(AuroraRuntimeException.class)
  12. public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {
  13. AuroraRuntimeException exception = (AuroraRuntimeException) e;
  14. if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {
  15. response.setStatus(HttpStatus.BAD_REQUEST.value());
  16. } else if (exception.getCode() == ResponseCode.FORBIDDEN) {
  17. response.setStatus(HttpStatus.FORBIDDEN.value());
  18. } else {
  19. response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
  20. }
  21. return new GenericResponse(exception.getCode(), null, exception.getMessage());
  22. }
  23. @ExceptionHandler(NotLoginException.class)
  24. public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {
  25. log.error("token exception", e);
  26. response.setStatus(HttpStatus.FORBIDDEN.value());
  27. return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);
  28. }
  29. }

1.2 定义异常码

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

  1. @Getter
  2. public enum ResponseCode {
  3. SUCCESS(0, "Success"),
  4. INTERNAL_ERROR(1, "服务器内部错误"),
  5. USER_INPUT_ERROR(2, "用户输入错误"),
  6. AUTHENTICATION_NEEDED(3, "Token过期或无效"),
  7. FORBIDDEN(4, "禁止访问"),
  8. TOO_FREQUENT_VISIT(5, "访问太频繁,请休息一会儿");
  9. private final int code;
  10. private final String message;
  11. private final Response.Status status;
  12. ResponseCode(int code, String message, Response.Status status) {
  13. this.code = code;
  14. this.message = message;
  15. this.status = status;
  16. }
  17. ResponseCode(int code, String message) {
  18. this(code, message, Response.Status.INTERNAL_SERVER_ERROR);
  19. }
  20. }

1.3 自定义异常类

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

  1. @Getter
  2. public class AuroraRuntimeException extends RuntimeException {
  3. private final ResponseCode code;
  4. public AuroraRuntimeException() {
  5. super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));
  6. this.code = ResponseCode.INTERNAL_ERROR;
  7. }
  8. public AuroraRuntimeException(Throwable e) {
  9. super(e);
  10. this.code = ResponseCode.INTERNAL_ERROR;
  11. }
  12. public AuroraRuntimeException(String msg) {
  13. this(ResponseCode.INTERNAL_ERROR, msg);
  14. }
  15. public AuroraRuntimeException(ResponseCode code) {
  16. super(String.format("%s", code.getMessage()));
  17. this.code = code;
  18. }
  19. public AuroraRuntimeException(ResponseCode code, String msg) {
  20. super(msg);
  21. this.code = code;
  22. }
  23. }

1.4 自定义返回类型

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

  1. @Getter
  2. @Setter
  3. public class GenericResponse<T> {
  4. private int code;
  5. private T data;
  6. private String message;
  7. public GenericResponse() {};
  8. public GenericResponse(int code, T data) {
  9. this.code = code;
  10. this.data = data;
  11. }
  12. public GenericResponse(int code, T data, String message) {
  13. this(code, data);
  14. this.message = message;
  15. }
  16. public GenericResponse(ResponseCode responseCode) {
  17. this.code = responseCode.getCode();
  18. this.data = null;
  19. this.message = responseCode.getMessage();
  20. }
  21. public GenericResponse(ResponseCode responseCode, T data) {
  22. this(responseCode);
  23. this.data = data;
  24. }
  25. public GenericResponse(ResponseCode responseCode, T data, String message) {
  26. this(responseCode, data);
  27. this.message = message;
  28. }
  29. }

实际测试异常

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

  1. public User getUserInfo(Long userId) {
  2. // some logic
  3. User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);
  4. if (user == null) {
  5. throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "用户id不存在");
  6. }
  7. // some logic
  8. ....
  9. }

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

全局拦截的背后原理?

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

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

  1. public class DispatcherServlet extends FrameworkServlet {
  2. // ......
  3. protected void initStrategies(ApplicationContext context) {
  4. initMultipartResolver(context);
  5. initLocaleResolver(context);
  6. initThemeResolver(context);
  7. initHandlerMappings(context);
  8. initHandlerAdapters(context);
  9. // 重点关注
  10. initHandlerExceptionResolvers(context);
  11. initRequestToViewNameTranslator(context);
  12. initViewResolvers(context);
  13. initFlashMapManager(context);
  14. }
  15. // ......
  16. }

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

  1. public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
  2. implements ApplicationContextAware, InitializingBean {
  3. // ......
  4. private void initExceptionHandlerAdviceCache() {
  5. // ......
  6. List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  7. AnnotationAwareOrderComparator.sort(adviceBeans);
  8. for (ControllerAdviceBean adviceBean : adviceBeans) {
  9. ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
  10. if (resolver.hasExceptionMappings()) {
  11. // 找到所有ExceptionHandler标注的方法并保存成一个ExceptionHandlerMethodResolver类型的对象缓存起来
  12. this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
  13. if (logger.isInfoEnabled()) {
  14. logger.info("Detected @ExceptionHandler methods in " + adviceBean);
  15. }
  16. }
  17. // ......
  18. }
  19. }
  20. // ......
  21. }

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

  1. public class ExceptionHandlerMethodResolver {
  2. // ......
  3. private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
  4. List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
  5. // 找到所有适用于Controller抛出异常的处理方法,例如Controller抛出的异常
  6. // 是AuroraRuntimeException(继承自RuntimeException),那么@ExceptionHandler(AuroraRuntimeException.class)和
  7. // @ExceptionHandler(Exception.class)标注的方法都适用此异常
  8. for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
  9. if (mappedException.isAssignableFrom(exceptionType)) {
  10. matches.add(mappedException);
  11. }
  12. }
  13. if (!matches.isEmpty()) {
  14. /* 这里通过排序找到最适用的方法,排序的规则依据抛出异常相对于声明异常的深度,例如
  15. Controller抛出的异常是是AuroraRuntimeException(继承自RuntimeException),那么AuroraRuntimeException
  16. 相对于@ExceptionHandler(AuroraRuntimeException.class)声明的AuroraRuntimeException.class其深度是0,
  17. 相对于@ExceptionHandler(Exception.class)声明的Exception.class其深度是2,所以
  18. @ExceptionHandler(BizException.class)标注的方法会排在前面 */
  19. Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
  20. return this.mappedMethods.get(matches.get(0));
  21. }
  22. else {
  23. return null;
  24. }
  25. }
  26. // ......
  27. }

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


关注公众号【码老思】,第一时间获取最通俗易懂的原创技术干货。

原文链接:https://www.cnblogs.com/way2backend/p/17519487.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号