统一异常处理解决方案-程序员宅基地

技术标签: spring  spring boot  java  异常处理  

作者:小瓦匠
欢迎关注我的个人公众号:小瓦匠学编程。微信号:xiaowajiangxbc
本中涉及到的所有代码资源,可以在公众号中获取,关注并回复:源码下载
本文涉及的源码资源在 coding-003-exception 项目中


你在目前负责的系统中,系统异常情况是如何处理的?还在使用try catch代码块来处理异常吗?这里介绍三种统一异常处理方案。

在上一篇:Java项目参数校验最佳实践中,用到了统一异常处理,但是当时并没有详细介绍,那么今天就带着大家一起来了解一下统一异常处理的常见解决方案,希望能对大家有所帮助。

丑陋的 try catch 代码块

对于异常处理来说,最简单直接的方式就是使用 try catch 代码块来捕获系统异常。代码如下:

@ApiOperation(value = "try catch 代码块")
@RequestMapping("unified/exception/tryCatch")
public R tryCatch(String bizCode) {
    
    try {
    
        log.info("tryCatch 入参:{}", bizCode);
        // do something
        return R.ok();
    } catch (Exception e) {
    
        log.error("业务执行失败,系统异常:{}", e.getMessage(), e);
        return R.error();
    }
}

但是这种处理方式需要我们编写大量的代码,而且异常信息不易于统一维护,增加了开发工作量,甚至可能还会出现异常没有被捕获的情况。为了能够高效的处理好各种系统异常,我们需要在项目中统一集中处理我们的异常。

统一异常处理

在 Spring 项目中,我们可以通过如下三种常见方案来实现全局统一异常处理。

  1. 基于 SpringBoot 的全局统一异常处理,需要实现 ErrorController 接口。
  2. 基于 Spring AOP 实现全局统一异常处理。
  3. 基于 @ControllerAdvice 注解实现 Controller 层全局统一异常处理。

使用统一异常处理的优点:

  • 标准统一的返回结果,系统交互更加友好
  • 有效防止业务异常没有被捕获的情况
  • 代码更加干净简洁,不需要开发者自己定义维护异常

基于SpringBoot的统一异常处理

通过实现 ErrorController 接口,来实现自定义错误异常返回。支持返回 JSON 字符串、自定义错误页面,可以做到根据不同 status 跳转不同的页面,代码示例:

@Slf4j
@RestController
@EnableConfigurationProperties({
    ServerProperties.class})
public class ExceptionController implements ErrorController {
    
    private static final String ERROR_PATH = "/error";
    private ErrorAttributes errorAttributes;
    @Autowired
    private ServerProperties serverProperties;
    @Override
    public String getErrorPath() {
    
        return ERROR_PATH;
    }
    @Autowired
    public ExceptionController(ErrorAttributes errorAttributes) {
    
        this.errorAttributes = errorAttributes;
    }
    /**
     * web页面错误处理
     */
    @RequestMapping(value = ERROR_PATH, produces = "text/html")
    @ResponseBody
    public ModelAndView errorHtml404(HttpServletRequest request, HttpServletResponse response) {
    
        ModelAndView modelAndView = null;
        ServletWebRequest requestAttributes = new ServletWebRequest(request);
        Map<String, Object> model = getErrorAttributes(requestAttributes, isIncludeStackTrace(request, MediaType.ALL));
        model.put("queryString", request.getQueryString());
        // 根据不同状态码返回不同页面,这里以404/500为例
        HttpStatus status = getStatus(request);
        if (status.value() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
     // 500
            modelAndView = new ModelAndView("500", model);
        } else if (status.value() == HttpStatus.NOT_FOUND.value()) {
     // 404
            modelAndView = new ModelAndView("404", model);
        } else {
    
            modelAndView = new ModelAndView("error", model);
        }
        return modelAndView;
    }
    /**
     * 除web页面外的错误处理,比如json/xml等
     */
    @RequestMapping(value = ERROR_PATH)
    @ResponseBody
    public R errorApiHander(HttpServletRequest request) {
    
        ServletWebRequest requestAttributes = new ServletWebRequest(request);
        Map<String, Object> attr = getErrorAttributes(requestAttributes, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return R.error(status.value(), attr.get("message").toString());
    }
    /**
     * 确定是否应该包含 StackTrace 属性
     */
    protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
    
        ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();
        if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
    
            return true;
        }
        return include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM && getTraceParameter(request);
    }
    /**
     * 获取错误的信息
     */
    private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    
        return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
    }
    /**
     * 是否包含 trace
     */
    private boolean getTraceParameter(HttpServletRequest request) {
    
        String parameter = request.getParameter("trace");
        return parameter != null && !"false".equalsIgnoreCase(parameter);
    }
    /**
     * 获取错误编码
     */
    private HttpStatus getStatus(HttpServletRequest request) {
    
        Integer statusCode = (Integer) request
                .getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
    
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        try {
    
            return HttpStatus.valueOf(statusCode);
        } catch (Exception ex) {
    
            log.error("获取当前 HttpStatus 发生异常", ex);
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }
}

我们可以根据不同异常状态码跳转不同页面,比如:400、403、404、500等异常,并且对于非 web 端请求可以返回 JSON 数据。但是它无法获取到异常的具体错误码,同时也无法根据异常类型进行不同的响应。

基于Spring AOP实现统一异常处理

首先使用 @Aspect 来声明一个切面,使用 @Pointcut 来定义切入点位置,然后使用 @Around 环绕通知来处理方法请求,当请求方法抛出异常后,使用 catch 捕获异常并通过 handlerException 方法处理异常信息。

通过上面的操作我们就可以实现异常的统一管理以及通过切面获取接口信息等。

@Slf4j
@Component
@Aspect
public class AspectException {
    
    @Pointcut("execution(* com.xwj.exception.solution_2.demo.controller.*.*(..))") // 切入点
    public void pointCut(){
    }
    @Around("pointCut()")
    public R handleControllerMethod(ProceedingJoinPoint pjp) {
    
        Stopwatch stopwatch = Stopwatch.createStarted();
        R r;
        try {
    
            log.info("执行Controller开始: " + pjp.getSignature() + " 参数:" + Lists.newArrayList(pjp.getArgs()).toString());
            r = (R) pjp.proceed(pjp.getArgs());
            log.info("执行Controller结束: " + pjp.getSignature() + ", 返回值:" + r.toString());
            log.info("耗时:" + stopwatch.stop().elapsed(TimeUnit.MILLISECONDS) + "(毫秒).");
        } catch (Throwable throwable) {
    
            r = handlerException(pjp, throwable);
        }
        return r;
    }
    /**
     * 处理异常信息
     */
    private R handlerException(ProceedingJoinPoint pjp, Throwable e) {
    
        R r = null;
        if (e instanceof BusinessException) {
    
            BusinessException businessException = (BusinessException) e;
            log.error("BusinessException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + businessException.getMessage() + "}", e);
            r = R.error(businessException.getCode(), businessException.getMessage());
        } else if (e instanceof RuntimeException) {
    
            log.error("RuntimeException{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
            r = R.error(400, "未知异常");
        } else {
    
            log.error("异常{方法:" + pjp.getSignature() + ", 参数:" + pjp.getArgs() + ",异常:" + e.getMessage() + "}", e);
            r = R.error(500, "系统异常");
        }
        return r;
    }
}

基于AOP原理实现的异常捕获,可以有效的捕获 controller、service 中出现的异常,而且还可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志等,但是无法处理进入 controller 前出现的异常以及参数校验异常等情况。

@ControllerAdvice 统一异常处理

基于 @ControllerAdvice 注解实现的 Controller 层全局统一异常处理,同时还需要配合 @ExceptionHandler 注解一起使用。

@ControllerAdvice

作用于类上,使用该注解可以实现三个方面的功能:1. 全局异常处理;2. 全局数据绑定;3. 全局数据预处理。在项目中使用这个注解可以帮我们简化很多工作,它是 SpringMVC 提供的功能,并且在 SpringBoot 中也可以直接使用。

在进行全局异常处理时,需要配合 @ExceptionHandler 注解使用。

@RestControllerAdvice

同样也是作用于类上,它是 @ControllerAdvice 和 @ResponesBody 的合体,可以支持返回 JSON 格式的数据。在后面的代码示例中就会使用这个注解。

@ExceptionHandler

作用于方法上,顾明思议,它就是一个异常处理器,作用是统一处理某一类异常,可以很大程度的减少代码重复率和复杂度。该注解的 value 属性可以用于指定具体的拦截异常类型。

如果有多个 @ExceptionHandler 存在,则需要指定不同的 value 类型,由于异常类拥有继承关系,所以 @ExceptionHandler 会首先执行在继承树中靠前的异常类型。基于这个特性,我们可以使用 @ExceptionHandler 来处理程序中各种具体异常了,比如处理:

  1. ServletException,即进入 Controller 前的异常,如:
    NoHandlerFoundException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException等
  2. 基于特定业务的自定义业务异常,如:BusinessException、BaseException
  3. 参数校验异常,如:BindException、
    MethodArgumentNotValidException、ConstraintViolationException
  4. 未知异常,当上面的异常处理无法捕获某个异常时,统一使用 Throwable 来捕获,并响应为未知异常

统一异常处理类

@Slf4j
@RestControllerAdvice
public class UnifiedExceptionHandler {
    
    /**
     * 未知异常
     */
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable t) {
    
        log.error("未知异常,{},异常类型:{}", t.getMessage(), t.getClass());
        return R.error();
    }
    /**
     * 业务异常
     */
    @ExceptionHandler(value = BusinessException.class)
    public R handleBusinessException(BusinessException e) {
    
        log.error("业务处理异常,{}", e.getMessage(), e);
        return R.error(e.getCode(), e.getMessage());
    }
    /**
     * 进入 Controller 前的相关异常
     */
    @ExceptionHandler({
    
            NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            HttpMediaTypeNotAcceptableException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    @ResponseBody
    public R handleServletException(Exception e) {
    
        log.error("网络请求异常,{}", e.getMessage(), e);
        return R.error(CommonResponseEnum.SERVLET_ERROR);
    }
    /**
     * 参数校验(Valid)异常
     */
    @ExceptionHandler(value = {
    MethodArgumentNotValidException.class})
    public R handleValidException(MethodArgumentNotValidException e) {
    
        log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        Map<String, String> errorMap = getErrorMap(bindingResult);
        return R.error(CommonResponseEnum.PARAM_ERROR).put("data", errorMap);
    }
    /**
     * 参数绑定异常
     */
    @ExceptionHandler(value = {
    BindException.class})
    public R handleValidException(BindException e) {
    
        log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass());
        BindingResult bindingResult = e.getBindingResult();
        Map<String, String> errorMap = getErrorMap(bindingResult);
        return R.error(CommonResponseEnum.PARAM_ERROR).put("data", errorMap);
    }
    /**
     * 约束校验异常
     */
    @ExceptionHandler(value = {
    ConstraintViolationException.class})
    public R handleValidException(ConstraintViolationException e) {
    
        log.error("数据校验异常,{},异常类型:{}", e.getMessage(), e.getClass());
        List<String> violations = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage).collect(Collectors.toList());
        String error = violations.get(0);
        return R.error(CommonResponseEnum.PARAM_ERROR).put("data", error);
    }
    /**
     * 获取校验失败的结果
     */
    private Map<String, String> getErrorMap(BindingResult result) {
    
        return result.getFieldErrors().stream().collect(
                Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (k1, k2) -> k1)
        );
    }
    /**
     * DataBinder 数据绑定访问器,集合参数校验时需要这个数据绑定
     */
    @InitBinder
    private void activateDirectFieldAccess(DataBinder dataBinder) {
    
        dataBinder.initDirectFieldAccess();
    }
}

从上面的代码中可以看出,在不同异常情况下会响应不同的异常错误码,根据异常错误码我们可以快速定位系统问题。当然也可以根据异常类型响应HTTP状态码,推荐阅读:错误码应该如何设计?

无法捕获404异常
完成上面的工作后,在默认配置情况下,我发现404异常 (NoHandlerFoundException ),不会被统一异常处理器处理,经过翻阅相关资料发现,需要在项目配置文件中增加如下配置:

spring:
  mvc:
    throw-exception-if-no-handler-found: true # 表示当没有对应处理器时,允许抛出异常
  resources:
    add-mappings: false # 表示是否为静态资源添加对应的处理器

添加上述配置后就可以让404异常在统一异常处理器中生效了,详细的原因分析,在这里就不进行描述了,感兴趣的小伙伴找度娘。另外需要注意的是,如果你的项目中使用到了静态资源,那么请忽略上面的配置,否则会导致无法处理静态资源。

总结

上面介绍了目前主流的三种全局统一异常处理方案,每种处理方案都有各自的优缺点和适合场景,最后总结:

  1. ErrorController 接口实现类 虽然可以对全局错误进行处理,但是它无法获取到异常的具体错误码,同时也无法根据异常类型进行不同的响应。

  2. 使用 AOP 方案不仅可以做全局统一异常处理,还可以统一打印接口请求入参和返回结果日志,打印接口访问性能日志等。但是无法处理进入 controller 前出现的异常以及参数校验异常。

  3. @ControllerAdvice 配合 @ExceptionHandler 一起使用可以捕获全部异常情况,包括ServletException、参数校验异常、自定义业务异常、其他未知异常等,但是在默认情况下无法捕获 404 异常,需要在项目配置中进行额外处理。

到此我们也仅仅是介绍了项目中统一捕获异常信息的内容,关于如何优雅的判定并抛出异常,我会在下一篇中详细介绍。点击“关注”不迷路。


在这里插入图片描述
学习 积累 沉淀 分享
欢迎关注我的个人公众号:小瓦匠学编程! 微信号:xiaowajiangxbc
扫描二维码或微信搜索 “小瓦匠学编程” 即可关注。

(本文完)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013635487/article/details/124660977

智能推荐

关于Self-Attention(自注意力机制)以及 Multi-head Attention(多头注意力机制)_multi-head attention与self attention-程序员宅基地

文章浏览阅读786次。本文深入一下Self-Attention(自注意力机制)以及 Multi-head Attention(多头注意力机制)的原理以及计算过程,主要的参考资料是台大李宏毅教授的授课内容,同时增加了一些从其他文章那里参考的细节,以及一些些个人的理解和心得。_multi-head attention与self attention

go语言数据类型之字符串string-程序员宅基地

文章浏览阅读654次。初识一个字符串是一个不可改变的字节序列。文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。内置的len函数len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值s := "hello, world"fmt.Println(len(s)) // "12"fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')ss := "你好,世界"fmt.Println(len(ss

poj3635 Full Tank?(dp+bfs/dij)-程序员宅基地

文章浏览阅读352次。http://poj.org/problem?id=3635给一张图,n个点,m条无向边,每个点都有自己的油价,每条路都需要消耗相应体积的油。q次询问,问一个小车,给定起点终点以及油箱容量,问到达终点的最小花费。懵了!看了下题解,是dp加bfs或者是dijdp[i][j]是指到达i点剩j体积的油量的最小花费。因为油最多只有100,所以可以这么做。对于每一个状态,我们可以有2个选择,1 在这个点加1...

java session过期页面跳转,session失效页面跳转-程序员宅基地

文章浏览阅读576次。简单实现,如果session失效就返回到登录页面。使用Filter,同时过滤对静态页面和controller的访问,并且ajax请求也能跳转。1. web.xml配置loginfiltercom.lty.ebus.custom.filters.CheckLoginFilterrootPath/login.jsploginfilter/webviews/*loginfilter/webapp/*2...._session過期配置頁面

如何生成验证码-程序员宅基地

文章浏览阅读290次。1.随机生成验证码 rand()--返回0到getrandmax()之间的伪随机整数;&lt;?phpheader('content-type:text/html;charset=utf-8');$str = "abcdefghijklmnopqrstuvwxyz0123456789";echo getrandmax(); echo '&lt;br&gt;';ec...

centos6.10如何安装图形界面-程序员宅基地

文章浏览阅读3.5k次。一、登录系统默认是命令行的文本界面,输入账号密码登录,如下面所示:二、yum安装desktop登录完成以后,使用yum命令就可以直接安装图形化桌面,安装命令是:yum groupinstall “Desktop”yum命令会自动分析需要的依赖报,然后下载所有安装包,如下面图中所示:三、修改为图形界面配置包的数量是223,耐心等待安装完毕,安装完成后,执行下面的命令:sed -...

随便推点

Docker exec启动脚本文件中有nohup导致操作无效_docker nohup-程序员宅基地

文章浏览阅读4.2k次,点赞3次,收藏2次。在写docker exec的时候遇到了个大坑进入docker操作的命令可以随便执行,但是在docker exec里写就没有任何反应我的问题出在自己的脚本文件问题一:自己写的脚本开头#!/bin/sh改为#!/bin/bash问题二:nohup出了问题,nohup默认输出为nohup.out,但是使用docker exec 在外面就不自己生成,必须自己指定一个nohup Jiaoben >my.out 2>&1 &问题三:执行多个前后依赖的脚本命令虽_docker nohup

Linux_搭建Samba服务_进行文件的传输(简单版)_linux在机器b制作samba服务,从机器a访问,并且传递一个文件过去-程序员宅基地

文章浏览阅读4.2k次。本文讲解如何 快速搭建Samba 服务, 利用搭建好的Samba 服务进行文件的传输。主要进行简单的讲解,如果有基础的同学可以忽略。第一步: 利用yum安装下samba命令 yum install samba所有的提示 都选 y 即可, 也可以加上 -y 参数第二步: 查看Samba 是否已经加入到自启动服务中使用命令/sbin/chkconfi_linux在机器b制作samba服务,从机器a访问,并且传递一个文件过去

大电容滤低频,小电容滤高频?——滤波电容的选择 ._lc滤波器电容选大选小会怎么样-程序员宅基地

文章浏览阅读1.3k次。一直有个疑惑:电容感抗是1/jwC,大电容C大,高频时 w也大,阻抗应该很小,不是更适合滤除高频信号?然而事实却是:大电容滤除低频信号。 今天找到解答如下: 般的10PF左右的电容用来滤除高频的干扰信号,0.1UF左右的用来滤除低频的纹波干扰,还可以起到稳压的作用滤波电容具体选择什么容值要取决于你PCB上主要的工作频率和可能对系统造成影响的谐波频率,可以查一_lc滤波器电容选大选小会怎么样

python题目n 个人围成一圈,顺序排号。从第一个人开始报数(从 1 到 3 报数),凡报到 3 的人退出圈子,问最后留下的是原来第几号的那位。_python有n个人围成一圈 顺序排号-程序员宅基地

文章浏览阅读1.8k次,点赞2次,收藏9次。# n 个人围成一圈,顺序排号。从第一个人开始报数(从 1 到 3 报数),凡报到 3 的人退出圈子,问最后留下的是原来第几号的那位。n = int(input("please set the number of players:"))game = []for i in range(n): game.append(i + 1)sign = 0order = 0out_players = 0while out_players < n - 1: if game[order] _python有n个人围成一圈 顺序排号

【PTA】【数据结构与算法】概论_给定n×n的二维数组a,则在不改变数组的前提下,查找最大元素的时间复杂度是:-程序员宅基地

文章浏览阅读1.4w次,点赞18次,收藏137次。这里是从PTA平台整理的【概论】题目集_给定n×n的二维数组a,则在不改变数组的前提下,查找最大元素的时间复杂度是:

Apache 配置与应用(Apache 连接保持、访问控制,Apache日志分割,AWStats日志分析,构建虚拟Web主机)-程序员宅基地

文章浏览阅读137次。文章目录一、Apache虚拟主机一、构建虚拟web主机(二)、httpd支持的虚拟主机类型(三)、基于域名访问虚拟主机步骤1、为虚拟主机提供域名解析2、为虚拟主机准备网页文档3、添加虚拟主机配置4、设置访问控制5、加载独立的配置文件6、在客户机中访问虚拟web主机一、Apache虚拟主机一、构建虚拟web主机虚拟web主机指的是在同一台服务器中运行多个web站点,其中每一个站点实际上并不独立占用整个服务器,因此被称为"虚拟"web主机。通过虚拟web主机服务可以充分利用服务器的硬件资源,从而大大降低