在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


统一异常处理


情景引入


我们先来看下面这两个接口:

在这里插入图片描述


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

image-20250418202154230


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

image-20250418202324759


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

image-20250418202647247

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


统一异常处理


先创建一个类:

image-20250418204004767


编写代码


统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的;

  • @ControllerAdvice 表示控制器通知类
  • @ExceptionHandler异常处理器
  • 两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件

具体代码如下:

image-20250418203821179

  • 类名,方法名和返回值可以自定义,重要的是注解
  • 接口返回为数据时,需要加 @ResponseBody 注解,不加这个注解,返回的结果是页面;

以上代码表示:

  1. 如果代码出现Exception异常(包括Exception的子类),就返回一个Result的对象;
  2. Result 对象的设置参考 Result.fail(e.getMessage())

image-20250418205701689


模拟制造异常:


@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";
    }
}

测试接口:

image-20250418210348502


统一错误响应与日志记录


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

image-20250418210720189


接口测试:

image-20250418211211614


异常类型差异化响应


我们可以针对不同的异常,返回不同的结果

@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("空指针错误");
    }
}

测试接口:

image-20250418212352443


后端打印日志:

image-20250418212608383


Spring 异常顺序匹配机制


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

image-20250418220651264


指定 ExceptionHandler 捕获的异常类型


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

image-20250418221046992


@ControllerAdvice 源码分析


对于@ControllerAdvice注解,我们重点关注 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个方法。

我们先进入入口类:org.springframework.web.servlet.DispatcherServlet

image-20250420101953722


下列是对 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个方法的底层分析代码图,接下来的文章是这两个方法的关键部分讲解;

图书管理系统/@ControllerAdvice/@ControllerAdvice 底层源码分析 · a2fb429 · XiaoLei/思维导图 - Gitee.com

在这里插入图片描述


initHandlerAdapters(context)


initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的bean并保存起来;

其中有一个类型为 RequestMappingHandlerAdapterbean,这个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 方法,比如返回数据前调用统一数据封装

至于DispatcherServletRequestMappingHandlerAdapter是如何交互的这就是另一个复杂的话题 了,此处不赘述,源码部分难度比较高,且枯燥,大家以了解为主。


initHandlerExceptionResolvers(context)


接下来看看 DispatcherServletinitHandlerExceptionResolvers(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;
        }
    }
    //...
}

在这里插入图片描述

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐