springboot优雅实现异常处理
前言
在平时的 API 开发过程中,总会遇到一些错误异常没有捕捉到的情况。那有的小伙伴可能会想,这还不简单么,我在 API 最外层加一个 try...catch
不就完事了。
哈哈哈,没错。这种方法简单粗暴。指北君曾经也是这么干的,但是你转过来想一想,你会在每一个 API 入口,都去做 try...catch
吗?这样不是代码非常丑陋的。小伙伴开始思考,突然灵光一现,说我们实现一个 AOP 来做这事不就完了。没错,使用 AOP 来实现是最佳的选择。
现在就给大家来介绍介绍 Spring Boot
怎么通过注解来实现全局异常处理的。
主角 @ControllerAdvice
和 @ExceptionHandler
我们先来介绍一下今天的主角,分别是 @ControllerAdvice
和 @ExceptionHandler
。
@ControllerAdvice
相当于controller
的切面,主要用于@ExceptionHandler
,@InitBinder
和@ModelAttribute
,使注解标注的方法对每一个controller
都起作用。默认对所有controller
都起作用,当然也可以通过@ControllerAdvice
注解中的一些属性选定符合条件的controller
。
源码如下:
package org.springframework.web.bind.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
@ExceptionHandler
用于异常处理的注解,可以通过value
指定处理哪种类型的异常还可以与@ResponseStatus
搭配使用,处理特定的http
错误。标记的方法入参与返回值都有很大的灵活性,具体可以看注释也可以在后边的深度探究。
源码如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
案例分析
今天我们就通过几种案例的方式,来给大家分析分析,怎么通过全局异常处理的方式玩转 Spring Boot 的全局异常处理。
案例一
一般的异常处理,所有的API都需要有相同的异常结构。
exception1
在这种情况下,实现是非常简单的,我们只需要创建 GeneralExceptionHandler
类,用 @ControllerAdvice
注解来注解它,并创建所需的 @ExceptionHandler
,它将处理所有由应用程序抛出的异常,如果它能找到匹配的 @ExceptionHandler
,它将相应地进行转换。
@ControllerAdvice
public class GeneralExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.text(ex.getMessage())
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
案例二
我们有一个API,它需要有一个或多个异常以其他格式处理,与其他应用程序的 API 不同。
exception2
我们可以采取两种方式来实现这种情况。我们可以在 OtherController
内部添加 @ExceptionHandler
来处理 OtherException
,或者为 OtherController
创建新的@ControllerAdvice
,以备我们也想在其他 API 中处理 OtherException
。
在 OtherController
中添加 @ExceptionHandler
来处理 OtherException
的代码示例。
@RestController
@RequestMapping("/other")
public class OtherController {
@ExceptionHandler(OtherException.class)
protected ResponseEntity<Error> handleException(OtherException ex) {
MyOtherError myOtherError = MyOtherError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myOtherError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
只针对 OtherController
控制器的 @ControllerAdvice
的代码示例
@ControllerAdvice(assignableTypes = OtherController.class)
public class OtherExceptionHandler {
@ExceptionHandler(OtherException.class)
protected ResponseEntity<Error> handleException(OtherException ex) {
MyOtherError myOtherError = MyOtherError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myOtherError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
案例三
与案例二类似,我们有一个 API 需要以不同于应用程序中其他 API 的方式对异常进行格式化,但这次所有的异常都需要进行不同的转换。
exception3
为了实现这个案例,我们将不得不使用两个 @ControllerAdvice
,并加上 @Order
注解的注意事项。因为现在我们需要告诉 Spring
,在处理同一个异常时,哪个 @ControllerAdvice
的优先级更高。如果我们没有指定 @Order
,在启动时,其中一个处理程序将自动注册为更高的顺序,我们的异常处理将变得不可预测。例如,我最近看到一个案例,如果你使用 mvn springboot:run
任务启动一个应用程序,OtherExceptionHandler
是主要的,但是当以jar形式启动时,GeneralExceptionHandler
是主要的。
@ControllerAdvice
public class GeneralExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.text(ex.getMessage())
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
@ControllerAdvice(assignableTypes = OtherController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class OtherExceptionHandler {
@ExceptionHandler(Exception.class)
protected ResponseEntity<Error> handleException(Exception ex) {
MyError myError = MyError.builder()
.message(ex.getMessage())
.origin("Other API")
.code(ex.getErrorCode()).build();
return new ResponseEntity(myError,
HttpStatus.valueOf(ex.getErrorCode()));
}
}
@ExceptionHandler 运行原理分析
ExceptionHandler的初始化
上面讲了通过xml配置的方式或者@ExceptionHandler
注解的方式可以得到ExceptionHandler。是如何做到的呢?
Spring容器初始化阶段,在初始化ExceptionHandlerExceptionResolver的时候,会执行afterPropertiesSet()方法,这是Bean生命周期中的一步。
进一步的初始化Bean。在ExceptionHandlerExceptionResolver的afterPropertiesSet()方法中,会调用initExceptionHandlerAdviceCache()
代码如下:
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 扫描 @ControllerAdvice 注解的Bean,并进行排序
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 遍历 ControllerAdviceBean 数组
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 扫描该 ControllerAdviceBean 对应的类型
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
// 有 @ExceptionHandler 注解,则添加到 exceptionHandlerAdviceCache 中
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
// 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
首先找到加了注解@ControllerAdvice的Bean
遍历找到的所有的Bean,根据Bean类型构建ExceptionHandlerMethodResolver
对象
ExceptionHandlerMethodResolver(beanType)
方法如下:
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
// 遍历 @ExceptionHandler 注解的方法
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
// 遍历处理的异常集合
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
// 添加到 mappedMethods 中
addExceptionMapping(exceptionType, method);
}
}
}
逻辑很简单,就是遍历这个类中的所有的方法,找到加了注解@ExceptionHandler的方法。然后遍历这个方法中的异常类型的映射。也就是该方法可以处理的异常类型。将它添加到mappedMethods当中。key是异常类型,value是对应的异常处理的方法。
这样一个类就对应这一个ExceptionHandlerMethodResolver对象,保存在exceptionHandlerAdviceCache缓存当中。就可以做到不仅一个Controller可以使用了。单例池中结果如下:
之后在DispatcherServlet
初始化的时候,会调用initHandlerExceptionResolvers()
,该方法从spring容器中找HandlerExceptionResolver
类型的Bean,添加到成员变量handlerExceptionResolvers
当中。并排序。
对应xml配置的就更为简单了。
容器初始化的时候,扫描xml配置文件,解析Bean标签,构建SimpleMappingExceptionResolver对象。填充xml中配置的属性。就完成了SimpleMappingExceptionResolver的初始化。
ExceptionHandler的触发时机
在doDispatch
中有一个局部变量Exception dispatchException = null
,用于存储catch到的异常。并在调用processDispatchResult的时候,会将这个局部变量传入。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) {//异常视图处理
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);//执行异常处理
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);//解析视图 分发结果
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);//触发拦截器完成处理
}
}
调用processHandlerException来调用异常处理器处理异常,代码如下:
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 判断是否可以应用
if (shouldApplyTo(request, handler)) {
// 阻止缓存
prepareResponse(ex, response);
// 执行解析异常,返回 ModelAndView 对象 由子类实现
ModelAndView result = doResolveException(request, response, handler, ex);
// 如果 ModelAndView 对象非空,则进行返回
if (result != null) {
// Print debug message when warn logger is not enabled.
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
// 打印异常日志
logException(ex, request);
}
// 返回 ModelAndView 对象
return result;
}
else {
return null;
}
}
真正的异常处理是调用doResolveException,由子类实现,根据不同类型的HandlerExceptionResolver执行不同的逻辑。
根据上面异常处理的时机,可以得出的结论是异常处理拦截的是视图解析之前的逻辑。也就是从getHandler开始到执行了拦截器后置处理的地方。
总结
经过上述的几个案例,指北君觉得大家应该已经能够轻松应对 Spring Boot 中大部分的全局异常处理的情况。
细心的同学也许会觉得为什么不使用 @RestControllerAdvice
呢?如果是用的 @RestControllerAdvice
注解,它会将数据自动转换成JSON格式,不再需要 ResponseEntity
的处理来。这种与 Controller
和 RestController
类似,本质是一样的,所以我们在使用全局异常处理之后可以进行灵活的选择处理。
参考文档
web九大组件之---HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】
ExceptionHandlerExceptionResolver类源码解析
SpringBoot源码解析-ExceptionHandler处理异常的原理
springboot优雅实现异常处理的更多相关文章
- springboot优雅的异常处理
springboot全局异常处理 @ControllerAdvice 尽管springboot会对一些异常进行处理,不过对于开发者来说,这还不太便于维护,因此我们需要自己来对异常进行统一的捕获与处理. ...
- SpringBoot优雅的全局异常处理
前言 本篇文章主要介绍的是SpringBoot项目进行全局异常的处理. SpringBoot全局异常准备 说明:如果想直接获取工程那么可以直接跳到底部,通过链接下载工程代码. 开发准备 环境要求 JD ...
- springboot 常用的异常处理方式
springboot常用的异常处理推荐: 一.创建一个异常控制器,并实现ErrorController接口: package com.example.demo.controller; import o ...
- Springboot项目统一异常处理
Springboot项目统一异常处理 一.接口返回值封装 1. 定义Result对象,作为通用返回结果封装 2. 定义CodeMsg对象,作为通用状态码和消息封装 二.定义全局异常类 三.定义异常处理 ...
- SpringBoot实战 之 异常处理篇
在互联网时代,我们所开发的应用大多是直面用户的,程序中的任何一点小疏忽都可能导致用户的流失,而程序出现异常往往又是不可避免的,那该如何减少程序异常对用户体验的影响呢?其实方法很简单,对异常进行捕获,然 ...
- SpringBoot中对于异常处理的提供的五种处理方式
1.自定义错误页面 SpringBoot 默认的处理异常机制:SpringBoot默认的已经提供了一套处理异常的机制.一旦程序中出现了异常,SpringBoot会向/error的url发送请求.在Sp ...
- SpringBoot整合全局异常处理&SpringBoot整合定时任务Task&SpringBoot整合异步任务
============整合全局异常=========== 1.整合web访问的全局异常 如果不做全局异常处理直接访问如果报错,页面会报错500错误,对于界面的显示非常不友好,因此需要做处理. 全局异 ...
- Springboot 优雅停止服务的几种方法
在使用Springboot的时候,都要涉及到服务的停止和启动,当我们停止服务的时候,很多时候大家都是kill -9 直接把程序进程杀掉,这样程序不会执行优雅的关闭.而且一些没有执行完的程序就会直接退出 ...
- SpringBoot优雅地配置日志
本文主要给大家介绍SpringBoot中如何通过sl4j日志组件优雅地记录日志.其实,我们入门 JAVA 的第一行代码就是一行日志,那你现在还在使用System.out.println("H ...
随机推荐
- virtualenv笔记
virtualenv venv --python=python2.7 mkvirtualenv venv --python=python linux中使用virtualenvwrapper 安装 pi ...
- (一)初识MySQL
JavaEE:企业级Java开发 Web 前端(页面:展示,数据) 后台(连接点,连接数据库JDBC,链接前端(控制,控制视图跳转和给前端传递数据)) 数据库(存数据,Txt,Excel,word) ...
- C# 两个具有相同属性的类赋值
最近有遇到两个类之间的赋值问题,两个类的属性几乎都一样的,所以写了个通过反射获取属性的然后赋值的方法,把一个类的属性的值赋值给另一个类. 框架是.net 4.5 public static D Map ...
- Java核心技术--Java程序设计
Java术语 术语名 缩写 解释 Java Development Kit(Java开发工具包) JDK 编写Java程序的程序员使用的软件 Java Runtime Environment(Java ...
- 大爽Python入门教程 3-1 布尔值: True, False
大爽Python入门公开课教案 点击查看教程总目录 1 布尔值介绍 从判断说起 回顾第一章介绍的简单的判断 >>> x = 10 >>> if x > 5: ...
- [loj3146]路灯
显然,能从$l$到$r$当且仅当$[l,r)$中的灯全部都亮,以下不妨令询问的$r$全部减1 当修改节点$x$时,找到包含$x$的极大的灯(除$x$以外)全部都亮的区间$[l,r]$,即令$l_{0} ...
- java 后台通过IO流把文件传到前端并下载
我的业务需求是两个不同的web程序放在不同的服务器上,web程序A要访问到web程序B上传上来的文件,所以用到了这一个IO读取文件的接口 JAVA代码(排版有点问题 已经尽力补救了(:3_ヽ)_) ...
- npm 配置 registry 以及使用 nrm
由于众所周知的原因,我们的内网链接互联网时非常不稳定,速度慢而且经常下载失败.为了提高下载安装 npm 包的体验,很多人都会把 npm 的 registry 配置成国内镜像,我们一般用的比较多的就是淘 ...
- java番外茶余饭后闲聊
java番外茶余饭后闲聊 **本人博客网站 **IT小神 www.itxiaoshen.com 今天聊点题外话没事时可以作为平时沟通交流的谈资,接下来一起简单了解下个人知晓对Java界开发产生深远影响 ...
- Java开发最实用最好用的11个技术网站
作为一个Java开发者,学习最新技术和关注行业内容是你不断提升自我的有效手段.因此,我会特别关注一些质量高口碑好的Java技术网站,在这里分享给大家. 1.Stackoverflow Stackove ...