SpringBoot 如何优雅的进行全局异常处理?
在SpringBoot的开发中,为了提高程序运行的鲁棒性,我们经常需要对各种程序异常进行处理,但是如果在每个出异常的地方进行单独处理的话,这会引入大量业务不相关的异常处理代码,增加了程序的耦合,同时未来想改变异常的处理逻辑,也变得比较困难。这篇文章带大家了解一下如何优雅的进行全局异常处理。
为了实现全局拦截,这里使用到了Spring中提供的两个注解,@RestControllerAdvice
和@ExceptionHandler
,结合使用可以拦截程序中产生的异常,并且根据不同的异常类型分别处理。下面我会先介绍如何利用这两个注解,优雅的完成全局异常的处理,接着解释这背后的原理。
在下面的例子中,我们继承了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 { 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 AbstractHandlerMethodExceptionResolverimplements ApplicationContextAware, InitializingBean { // ......private void initExceptionHandlerAdviceCache() {// ......List 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> matches = new ArrayList>();// 找到所有适用于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 如何优雅的进行全局异常处理?
>在SpringBoot的开发中,为了提高程序运行的鲁棒性,我们经常需要对各 -
潘功胜任中国人民银行党委书记
大皖新闻讯据中国人民银行消息,2023年7月1日下午,中国人民银行召开领 -
济宁市国动办到兴东社区开展 “庆七一...
大众网记者胡兆杰济宁报道6月29日,济宁市国动办党组书记、常务副主任 -
环球视点!哪吒汽车:6月份哪吒汽车全系...
哪吒汽车:6月份哪吒汽车全系交付12132台,其中海外1201台,哪吒GT交付 -
流氓罪为什么废除_全球热点
为何废除“流氓罪”?“流氓罪”是什么?“流氓罪”,全称“寻衅滋... -
当前热议!送给兄弟的生日礼物,好兄弟过...
一、好兄弟过生日要送什么礼物?送礼物哥们有个建议啊!一定要送与众不 -
全球热门:要五角大楼道歉?翻译中美新...
出品|外宣微记欢迎转发朋友圈,转载全文请申请授权刚刚,朋友发来一张 -
环球热讯:逆水寒手游战力评分组成 绝技...
本文给大家带来逆水寒手游战力评分攻略,本攻略内容来自公众号:逆水寒 -
2023年7月1日上海市乙二醇丁醚价格最新...
中国报告大厅2023年7月1日上海市乙二醇丁醚价格最新走势监测显示:武汉 -
福建莆田东埔派出所深入校园开展禁毒宣...
近日,福建省莆田市公安局东埔派出所到辖区的塔林小学开展禁毒教育主题 -
瓦格纳事件后,中情局局长向俄方表“清白”
据美国《华尔街日报》网站6月30日报道,在瓦格纳事件发生后,美国中央 -
宜阳汾北之战(关于宜阳汾北之战介绍)-...
大家好,小万来为大家解答以上的问题。宜阳汾北之战,关于宜阳汾北之战 -
红楼梦里的四大家族人物关系 红楼梦里...
hello大家好,我是城乡经济网小晟来为大家解答以上问题,红楼梦里的四 -
氮氧自由基调控聚合
1、在自由基聚合体系中,引入稳定的氮氧自由基(如2,2。2、6,6-四甲基 -
当前时讯:校掌团_关于校掌团概略
1、校掌团(Master-T)是校掌传媒2012年推出符合年轻人潮流与便捷并存 -
苏州大学电子邮箱(苏州大学邮箱)
州大学电子邮箱,苏州大学邮箱这个问题很多朋友还不知道,来为大家解答 -
决心书部队训练_决心书
1、老妈说过一句话,她告诉我的“越努力,越幸运”,这句话深刻的印... -
全球关注:重庆万州发生山体滑坡致6人遇难
记者7月1日从重庆市万州区分水镇政府获悉,6月30日晚,分水镇发生山体 -
注重商业秘密保护 海沧区成立联合会-天...
■海沧区商业秘密保护创新联合会成立。厦门网讯(文 厦门晚报记者高金 -
插座电气符号及图形表示_插座电气符号-...
1、在电路图上用R表示。2、根据国际电工委员会(IEC)以及国际标准化组织