在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就完成了对不同异常的灵活处理。


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

SpringBoot 如何优雅的进行全局异常处理?的更多相关文章

  1. SpringBoot:如何优雅地处理全局异常?

    之前用springboot的时候,只知道捕获异常使用try{}catch,一个接口一个try{}catch,这也是大多数开发人员异常处理的常用方式,虽然屡试不爽,但会造成一个问题,就是一个Contro ...

  2. springboot中 简单的SpringMvc全局异常处理

    1.全局异常处理类:GlobalExceptionHandler.java package com.lf.exception; import java.util.HashMap; import jav ...

  3. springboot学习(五) 全局异常处理

    创建全局异常处理 /** * 全局异常配置管理 */ @ControllerAdvice public class GlobalExceptionConfig extends ResponseEnti ...

  4. SpringBoot优雅的全局异常处理

    前言 本篇文章主要介绍的是SpringBoot项目进行全局异常的处理. SpringBoot全局异常准备 说明:如果想直接获取工程那么可以直接跳到底部,通过链接下载工程代码. 开发准备 环境要求 JD ...

  5. SpringBoot系列教程web篇之全局异常处理

    当我们的后端应用出现异常时,通常会将异常状况包装之后再返回给调用方或者前端,在实际的项目中,不可能对每一个地方都做好异常处理,再优雅的代码也可能抛出异常,那么在 Spring 项目中,可以怎样优雅的处 ...

  6. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  7. 全局异常处理及参数校验-SpringBoot 2.7 实战基础 (建议收藏)

    优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验 前后端分离开发非常普遍,后端处理业务,为前端提供接口.服务中总会出现很多运行时异常和业务异常,本文主要讲解在 Sp ...

  8. SpringBoot整合全局异常处理&SpringBoot整合定时任务Task&SpringBoot整合异步任务

    ============整合全局异常=========== 1.整合web访问的全局异常 如果不做全局异常处理直接访问如果报错,页面会报错500错误,对于界面的显示非常不友好,因此需要做处理. 全局异 ...

  9. 【第二十三章】 springboot + 全局异常处理

    一.单个controller范围的异常处理 package com.xxx.secondboot.web; import org.springframework.web.bind.annotation ...

  10. 第二十三章 springboot + 全局异常处理

    一.单个controller范围的异常处理 package com.xxx.secondboot.web; import org.springframework.web.bind.annotation ...

随机推荐

  1. pyhon之编译成exe

    1安装pyinstaller pip install pyinstaller 2 编译 pyinstaller -F -w game.py  (-F表示打包单个文件,-w是为了打开exe时候不弹出黑框 ...

  2. [Linux]Linux大文件已删除,但df查看已使用的空间并未减少解决【待续】

    1 问题描述 X 参考文献 Linux大文件已删除,但df查看已使用的空间并未减少解决 - ChinaUnix linux磁盘空间未及时释放 - 博客园 linux磁盘目录占用空间分析工具之ncdu ...

  3. Java设计模式 —— 享元模式

    14 享元模式 14.1 享元模式概述 Flyweight Pattern: 运用共享技术有效地支持大量细粒度对象的复用. 当系统中存在大量相同或相似的对象时,它通过共享技术实现相同或相似的细粒度对象 ...

  4. stm32报错

    1. declaration may not appear after executable statement in block 关于编译错误的小伙伴:error: #268: declaratio ...

  5. C语言中,取反运算符~a=-(a+1)的原因

    1.因为计算机直接拿读取到的数据去运算付出的代价是最小的,所以计算机存储的数据的形式应该满足读取后不必经过任何加工就能直接用来运算由于原码不经加工无法实现(+a)+(-a)=0,所以不满足该要求,为了 ...

  6. TiDB在X86和ARM混合平台下的离线部署和升级

    [是否原创]是 [首发渠道]TiDB 社区 背景 在之前我们团队发布了TiDB基于X86和ARM混合部署架构的文章:TiDB 5.0 异步事务特性体验--基于X86和ARM混合部署架构,最近有朋友问到 ...

  7. C# 禁用窗口激活

    如果界面点击时,不想让窗口激活,可以按如下操作: 1 public MainWindow() 2 { 3 InitializeComponent(); 4 SourceInitialized += O ...

  8. 【Azure Developer】Azure AD 注册应用的 OAuth 2.0 v2 终结点获取的 Token 解析出来依旧为v1.0, 这是什么情况!

    问题描述 使用 Azure AD 注册应用 Oauth2 v2.0的终结点(OAuth 2.0 token endpoint (v2):https://login.partner.microsofto ...

  9. 2022-11-30:小红拿到了一个仅由r、e、d组成的字符串 她定义一个字符e为“好e“ : 当且仅当这个e字符和r、d相邻 例如“reeder“只有一个“好e“,前两个e都不是“好e“,只有第三个

    2022-11-30:小红拿到了一个仅由r.e.d组成的字符串 她定义一个字符e为"好e" : 当且仅当这个e字符和r.d相邻 例如"reeder"只有一个&q ...

  10. Jenkins - 更改插件镜像源地址

    Jenkins - 更改插件镜像源地址 进入 系统管理 > 插件管理 > 高级 在升级站点输入框,输入清华的Jenkins镜像源地址:点击提交保存即可: jenkins插件清华大学镜像地址 ...