Spring Boot 统一异常这样处理和剖析,安否?

摘要:
注意,这是一个运行时异常@Data@AllArgsConstructorpublicfinalclassBusinessExceptionextendsRuntimeException{privateStringerrorCode;publicstatic<commonResult.errorMsg=errorMsg;

话说异常

「欲渡黄河冰塞川,将登太行雪满天」,无论生活还是计算机世界难免发生异常,上一篇文章RESTful API 返回统一JSON数据格式 说明了统一返回的处理,这是请求一切正常的情形;这篇文章将说明如何统一处理异常,以及其背后的实现原理,老套路,先实现,后说明原理,有了上一篇文章的铺底,相信,理解这篇文章就驾轻就熟了

Spring Boot 统一异常这样处理和剖析,安否?第1张

实现

新建业务异常

新建 BusinessException.class 类表示业务异常,注意这是一个 Runtime 异常

@Data
@AllArgsConstructor
public final class BusinessException extends RuntimeException {

	private String errorCode;

	private String errorMsg;
	
}

添加统一异常处理静态方法

在 CommonResult 类中添加静态方法 errorResult 用于接收异常码和异常消息:

public static <T> CommonResult<T> errorResult(String errorCode, String errorMsg){
    CommonResult<T> commonResult = new CommonResult<>();
    commonResult.errorCode = errorCode;
    commonResult.errorMsg = errorMsg;
    commonResult.status = -1;
    return commonResult;
}

配置

同样要用到 @RestControllerAdvice 注解,将统一异常添加到配置中:

@RestControllerAdvice("com.example.unifiedreturn.api")
static class UnifiedExceptionHandler{

    @ExceptionHandler(BusinessException.class)
    public CommonResult<Void> handleBusinessException(BusinessException be){
        return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());
    }
}

三部搞定,到这里无论是 Controller 还是 Service 中,只要抛出 BusinessException, 我们都会返回给前端一个统一数据格式

测试

将 UserController 中的方法进行改造,直接抛出异常:

@GetMapping("/{id}")
public UserVo getUserById(@PathVariable Long id){
    throw new BusinessException("1001", "根据ID查询用户异常");
}

浏览器中输入: http://localhost:8080/users/1

Spring Boot 统一异常这样处理和剖析,安否?第2张

在 Service 中抛出异常:

@Service
public class UserServiceImpl implements UserService {

	/**
	 * 根据用户ID查询用户
	 *
	 * @param id
	 * @return
	 */
	@Override
	public UserVo getUserById(Long id) {
		throw new BusinessException("1001", "根据ID查询用户异常");
	}
}

运行是得到同样的结果,所以我们尽可能的抛出异常吧 (作为一个程序猿这种心理很可拍)

解剖实现过程

解剖这个过程是相当纠结的,为了更好的说(yin)明(wei)问(wo)题(lan),我要说重中之重了,真心希望看该文章的童鞋自己去案发现场发现线索
还是在 WebMvcConfigurationSupport 类中实例化了 HandlerExceptionResolver Bean

@Bean
public HandlerExceptionResolver handlerExceptionResolver() {
    List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
    configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        addDefaultHandlerExceptionResolvers(exceptionResolvers);
    }
    extendHandlerExceptionResolvers(exceptionResolvers);
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    composite.setOrder(0);
    composite.setExceptionResolvers(exceptionResolvers);
    return composite;
}

和上一篇文章一毛一样的套路,ExceptionHandlerExceptionResolver 实现了 InitializingBean 接口,重写了 afterPropertiesSet 方法:

@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBodyAdvice beans
    initExceptionHandlerAdviceCache();
    ...
}

private void initExceptionHandlerAdviceCache() {
    if (getApplicationContext() == null) {
        return;
    }

    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    AnnotationAwareOrderComparator.sort(adviceBeans);

    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);
        }
    }
}

重点看上面我用注释标记的构造方法,代码很好懂,仔细看看吧,其实就是筛选出我们用 @ExceptionHandler 注解标记的方法并放到集合当中,用于后续全局异常捕获的匹配

/**
 * A constructor that finds {@link ExceptionHandler} methods in the given type.
 * @param handlerType the type to introspect
 */
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
        for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
            addExceptionMapping(exceptionType, method);
        }
    }
}


/**
 * Extract exception mappings from the {@code @ExceptionHandler} annotation first,
 * and then as a fallback from the method signature itself.
 */
@SuppressWarnings("unchecked")
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
    List<Class<? extends Throwable>> result = new ArrayList<>();
    detectAnnotationExceptionMappings(method, result);
    if (result.isEmpty()) {
        for (Class<?> paramType : method.getParameterTypes()) {
            if (Throwable.class.isAssignableFrom(paramType)) {
                result.add((Class<? extends Throwable>) paramType);
            }
        }
    }
    if (result.isEmpty()) {
        throw new IllegalStateException("No exception types mapped to " + method);
    }
    return result;
}

private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
    ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
    Assert.state(ann != null, "No ExceptionHandler annotation");
    result.addAll(Arrays.asList(ann.value()));
}

到这里,我们用 @RestControllerAdvice@ExceptionHandler 注解就会被 Spring 扫描到上下文,供我们使用

让我们回到你最熟悉的调用的入口 DispatcherServlet 类的 doDispatch 方法:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            ...
            // 当请求发生异常,该方法会通过 catch 捕获异常
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
        ...
            
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 调用该方法分析捕获的异常
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    ...
}

接下来,我们来看 processDispatchResult 方法,这里只要展示调用栈你就会眼前一亮了,又是为了返回统一格式数据:

Spring Boot 统一异常这样处理和剖析,安否?第3张

总结

上一篇文章的返回统一数据格式是基础,当异常情况发生时,只不过需要将异常信息提取出来。本文主要为了说明问题,剖析原理,好多地方设计方式是不可取,比如我们最好将异常封装在一个 Enum 类,通过 enum 对象抛出异常等,如果你用到这些,去完善你的设计方案吧

回复 「demo」,打开链接,查看文件夹 「unifiedreturn」下内容,获取完整代码,更好阅读体验:
https://fraseryu.github.io/2019/08/09/ru-he-tong-yi-chu-li-yi-chang-bing-fan-hui-tong-yi-ge-shi/

附加说明

之前看到的一本书对异常的分类让我印象深刻,在此摘录一小段分享给大家:
Spring Boot 统一异常这样处理和剖析,安否?第4张

结合出国旅行的例子说明异常分类:

  • 机场地震,属于不可抗力,对应异常分类中的 Error,在制订出行计划时,根本不需要把这个部分的异常考虑进去
  • 堵车属于 checked 异常,应对这种异常,我们可以提前出发,或者改签机票。而飞机延误异常,虽然也需要 check,但我们无能为力,只能持续关注航班动态
  • 没有带护照,明显属于可提前预测的异常,只要出发前检查即可避免;去机场路上车子抛锚,这个异常是突发的,虽然难以预料,但是必须处理,属于需要捕捉的异常,可以通过更换交通工具;应对检票机器故障属于 可透出异常,交由航空公司处理,我们无须关心

灵魂追问

  1. 这两篇文章,你学到了哪些设计模式?
  2. 你能熟练的使用反射吗?当看源码是会看到很多反射的应用
  3. 你了解 Spring CGLIB 吗?它的工作原理是什么?

提高效率工具

Spring Boot 统一异常这样处理和剖析,安否?第5张

JSON-Viewer

JSON-Viewer 是 Chrome 浏览器的插件,用于快速解析及格式化 json 内容,在 Chrome omnibox(多功能输入框)输入json-viewer + TAB ,将 json 内容拷贝进去,然后输入回车键,将看到结构清晰的 json 数据,同时可以自定义主题

Spring Boot 统一异常这样处理和剖析,安否?第6张

另外,前端人员打开开发者工具,双击请求链接,会自动将 response 中的 json 数据解析出来,非常方便

推荐阅读


欢迎持续关注公众号:「日拱一兵」

  • 前沿 Java 技术干货分享
  • 高效工具汇总 | 回复「工具」
  • 面试问题分析与解答
  • 技术资料领取 | 回复「资料」

以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......

Spring Boot 统一异常这样处理和剖析,安否?第7张

免责声明:文章转载自《Spring Boot 统一异常这样处理和剖析,安否?》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Echarts图表在VUE项目中使用动态数据源Excel 工具集&amp;lt;Excel 2010 插件&amp;gt;&amp;lt;VSTO 插件&amp;gt;下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Linux 后台运行python .sh等程序,以及查看和关闭后台运行程序操作

1、运行.sh文件 直接用./sh 文件就可以运行,但是如果想后台运行,即使关闭当前的终端也可以运行的话,需要nohup命令和&命令。 (1)&命令 功能:加在一个命令的最后,可以把这个命令放在后台执行   (2)nohup命令 功能:不挂断的运行命令   2、查看当前后台运行的命令 有两个命令可以用,jobs和p...

offcputime 火焰图

基于bcc提供的命令工具 dnf iinstall -y bcc-tools 参考命令 测试一个pg 的off cpu 火焰图 运行命令 注意修改pid   /usr/share/bcc/tools/offcputime -df -p 50476 --state=2 60 > pg.stacks clone 火焰图生成工具 git clone...

MongoDB副本集提高读写速率

一、提高副本集写的速率 1、通过设置Write Concern for Replica Sets¶ cfg = rs.conf()##cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 } cfg.settings.getLastErrorDefaults ={w:1}r...

Linux中的输入输出重定向

  有三个最重要的输入输出流:标准输入(stdin),标准输出(stdout),标准错误(stderr)。它们对于控制台(“控制台”指的是键盘用于输入,屏幕用于输出)来说是缺省的,但是它们可以被重定向。   重定向标准输出:可以使用“>”符号, 举例:   dir my_dir > filelisting.txt                ...

从零开始编写自己的C#框架(12)——T4模板在逻辑层中的应用(一)(附源码)

对于T4模板很多朋友都不太熟悉,它在项目开发中,会帮我们减轻很大的工作量,提升我们的开发效率,减少出错概率。所以学好T4模板的应用,对于开发人员来说是非常重要的。   园子里对于T4模板的介绍与资料已经太多了,所以在这里我就不再详细讲述基础知识了,只是说说T4模板在本框架中的具体应用与实践。   一、创建逻辑层项目         二、添加引用      ...

做个开源博客学习Vite2 + Vue3 (四)实现博客功能

我们再来看一下管理类的设计。 Composition API,就是组合API的意思,那么是不是应该把js代码分离出来,做成独立的管理类的形式呢? 这样代码可以更整洁一些,主要是setup里面的代码就不会乱掉了。 管理类 import webSQLHelp from '../store/websql-help' import { blog, blogForm...