【SpringMVC】深入解析 SpringMVC:@ControllerAdvice(含底层源码分析) 、@ExceptionHandler、@ResponseBody 配合实现统一异常处理功能
@ControllerAdvice、@ResponseBody、@ExceptionHandler、e.getMessage()



统一异常处理
情景引入
我们先来看下面这两个接口:

有的接口中手动处理了异常,有的接口则没有处理异常,那在一些特殊情况下,没有处理异常的接口数据访问错误,也是可以向前端返回不合理的结果的:

我们把表名改错后,重新运行程序,调用没有处理异常的 getListByPage() 接口:

明明后端逻辑错误(状态码 500),封装的结果中 code、errMessage 的值对不上的;

因此,我们引入统一异常处理,对这些接口都进行异常捕获;
统一异常处理
先创建一个类:

编写代码
统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的;
@ControllerAdvice表示控制器通知类;@ExceptionHandler是异常处理器;- 两个结合表示
当出现异常的时候执行某个通知,也就是执行某个方法事件
具体代码如下:

- 类名,方法名和返回值可以自定义,重要的是注解
接口返回为数据时,需要加 @ResponseBody 注解,不加这个注解,返回的结果是页面;
以上代码表示:
- 如果代码出现
Exception异常(包括Exception的子类),就返回一个Result的对象; Result对象的设置参考Result.fail(e.getMessage())

模拟制造异常:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public Integer t1(){
int a = 10/0; // 除数为 0 异常 ArithmeticException
return 1;
}
@RequestMapping("/t2")
public boolean t2(){
int[] arr = {1, 2, 3};
System.out.println(arr[4]); // 数组越界异常
return true;
}
@RequestMapping("/t3")
public String t3(){
String a = null;
System.out.println(a.length()); // 空指针异常 NullPointerException
return "t3";
}
}
测试接口:

统一错误响应与日志记录
在实际开发中,后端出现的异常信息是不会返回给前端的,而是在后端打印错误日志,并且统一向前端返回一致的错误消息:

接口测试:

异常类型差异化响应
我们可以针对不同的异常,返回不同的结果
@ControllerAdvice
@ResponseBody
@Slf4j
public class ExceptionAdvice {
@ExceptionHandler
public Result handler(Exception e){
log.error(e.getMessage());
return Result.fail("内部错误");
}
@ExceptionHandler
public Result handler(ArithmeticException e){
log.error(e.getMessage());
return Result.fail("算术异常");
}
@ExceptionHandler
public Result handler(IndexOutOfBoundsException e){
log.error(e.getMessage());
return Result.fail("数组越界异常");
}
@ExceptionHandler
public Result handler(NullPointerException e){
log.error(e.getMessage());
return Result.fail("空指针错误");
}
}
测试接口:

后端打印日志:

Spring 异常顺序匹配机制
当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配 :

指定 ExceptionHandler 捕获的异常类型
在 ExceptionHandler 指定捕获的异常类型,就不需要在方法参数中声明异常类型:

@ControllerAdvice 源码分析
对于@ControllerAdvice注解,我们重点关注 initHandlerAdapters(context) 和initHandlerExceptionResolvers(context) 这两个方法。
我们先进入入口类:org.springframework.web.servlet.DispatcherServlet

下列是对 initHandlerAdapters(context) 和initHandlerExceptionResolvers(context) 这两个方法的底层分析代码图,接下来的文章是这两个方法的关键部分讲解;
图书管理系统/@ControllerAdvice/@ControllerAdvice 底层源码分析 · a2fb429 · XiaoLei/思维导图 - Gitee.com

initHandlerAdapters(context)
initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的bean并保存起来;
其中有一个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是 @RequestMapping 注解能起作用的关键;
这个bean在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象,并做进一步处理,关键代码如下:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
//...
/**
* 添加ControllerAdvice bean的处理
*/
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
//获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
if (logger.isDebugEnabled()) {
int modelSize = this.modelAttributeAdviceCache.size();
int binderSize = this.initBinderAdviceCache.size();
int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +
" @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
}
}
}
//...
}
这个方法在执行时会查找使用所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发生某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装
至于DispatcherServlet和RequestMappingHandlerAdapter是如何交互的这就是另一个复杂的话题 了,此处不赘述,源码部分难度比较高,且枯燥,大家以了解为主。
initHandlerExceptionResolvers(context)
接下来看看 DispatcherServlet 的 initHandlerExceptionResolvers(context) 方法,
这个方法会取得所有实现了 HandlerExceptionResolver 接口的bean并保存起来;
其中就有一个类型为 ExceptionHandlerExceptionResolver 的bean;
这个bean在应用启动过程中会获取所有被@ControllerAdvice 注解标注的bean对象做进一步处理,代码如下:
public class ExceptionHandlerExceptionResolver extends
AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
//...
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for
ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new
ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
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");
}
}
}
//...
}
当Controller抛出异常时, DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常;
而 ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常;
ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法是这里:
public class ExceptionHandlerMethodResolver {
//...
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList();
//根据异常类型,查找匹配的异常处理方法
//比如NullPointerException会匹配两个异常处理方法:
//handler(Exception e) 和 handler(NullPointerException e)
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
//如果查找到多个匹配,就进行排序,找到最使用的方法。排序的规则依据抛出异常相对于声明异常的深度
//比如抛出的是NullPointerException(继承于RuntimeException,RuntimeException又继承于Exception)
//相对于handler(NullPointerException e) 声明的NullPointerException深度为0,
//相对于handler(Exception e) 声明的Exception 深度为2
//所以 handler(NullPointerException e)标注的方法会排在前面
if (!matches.isEmpty()) {
if (matches.size() > 1) {
matches.sort(new ExceptionDepthComparator(exceptionType));
}
return this.mappedMethods.get(matches.get(0));
}
else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
}
}
//...
}


更多推荐



所有评论(0)