深入聊一聊springboot项目全局异常处理那些事儿

 更新时间:2022年01月21日 09:07:18   作者:linyb极客之路  
最近在做项目时需要对异常进行全局统一处理,所以下面这篇文章主要给大家介绍了关于springboot项目全局异常处理那些事儿,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

前言

之前我们业务团队在处理全局异常时,在每个业务微服务中都加入了@RestControllerAdvice+@ExceptionHandler来进行全局异常捕获。某次领导在走查代码的时候,就提出了一个问题,为什么要每个微服务项目都要自己在写一套全局异常代码,为什么不把全局异常块抽成一个公共的jar,然后每个微服务以jar的形式引入。后面业务团队就根据领导的要求,把全局异常块单独抽离出来封装成jar。今天聊的话题就是关于把全局异常抽离出来,发生的一些问题

问题一:全局异常抽离出来后,业务错误码如何定义?

之前团队的业务错误码定义是:业务服务前缀 + 业务模块 + 错误码,如果是识别不了的异常,则使用业务前缀 + 固定模块码 + 固定错误码。

之前的全局异常伪代码如下

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {

   
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = "U";
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "服务端异常";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, errorCode);
    }
    }
现在全局异常抽离出来后,那个业务服务前缀如何识别?之前未抽离时,业务服务前缀各个业务服务直接写死在代码里。

当时我们临时的解决方案是通过spring.application.name来解决。因为全局异常代码块抽离出来后,最终还是要被服务引入的。因此获取业务服务前缀的伪代码可以通过如下获取

public enum  ServicePrefixEnum {

    USER_SERVICE("U","用户中心");

    private final String servicePrefix;

    private final String serviceDesc;

    ServicePrefixEnum(String servicePrefix,String serviceDesc) {
        this.servicePrefix = servicePrefix;
        this.serviceDesc = serviceDesc;
    }

    public String getServicePrefix() {
        return servicePrefix;
    }

    public String getServiceDesc() {
        return serviceDesc;
    }
}
public String getServicePrefix(@Value("${spring.application.name}") String serviceName){
      return ServicePrefixEnum.valueOf(serviceName).getServicePrefix();
    }

但这种方案其实是存在弊端

弊端一: 通过枚举硬编码,预设了目前了微服务名称,一旦项目改变了微服务名,就找不到服务前缀了。

弊端二: 如果新上线了业务服务模块,这个枚举类还得改动

后面我们在全局异常jar中增加了自定义业务码的配置,业务人员仅需在springboot配置文件配置,形如下

lybgeek:
  bizcode:
    prefix: U

此时全局异常改造示例形如下

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {
    
    
    @Autowired
    private ServiceCodeProperties serviceCodeProperties;

   
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = serviceCodeProperties.getPrifix();
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "服务端异常";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, errorCode);
    }
}

问题二:全局异常因引入了和业务相同的依赖jar,但jar存在版本差异

如果全局异常直接如下写,是不存在问题。示例如下

@RestControllerAdvice
@Slf4j
public class GlobalExceptionBaseHandler {


    @Autowired
    private ServiceCodeProperties serviceCodeProperties;

    
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public AjaxResult handleException(Exception e) {
        String servicePrifix = serviceCodeProperties.getPrifix();
        String moudleCode = "001";
        String code = "0001";
        String errorCode = servicePrifix + moudleCode + code;
        String msg = e.getMessage();
        if(StringUtils.isEmpty(msg)){
            msg = "服务端异常";
        }
        log.error(msg, e);
        return AjaxResult.error(msg, HttpStatus.INTERNAL_SERVER_ERROR.value());
    }


    @ExceptionHandler(BizException.class)
    public AjaxResult handleException(BizException e)
    {
        return AjaxResult.error(e.getMessage(), e.getErrorCode());
    }

}

即全局异常直接分为业务异常和Execption这两种,这样划分的弊端在于没办法细分异常,而且也使项目组定义的模块码和业务码没法细分。因此我们也列出常用可以预知的系统异常,示例如下

/**
     *参数验证失败
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult handleException(ConstraintViolationException e)
    {
        log.error("参数验证失败", e);
        return AjaxResult.error("参数验证失败", HttpStatus.BAD_REQUEST.value());
    }

   /**
     * 数据库异常
     * @param e
     * @return
     */
    @ExceptionHandler({SQLException.class, MybatisPlusException.class,
            MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
            BadSqlGrammarException.class
    })
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult dbException(Exception e) {
        String msg = ExceptionUtil.getExceptionMessage(e);
        log.error(msg, e);
        return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value());
    }

    /**
     * 数据库中已存在该记录
     * @param e
     * @return
     */
    @ExceptionHandler(DuplicateKeyException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public AjaxResult handleException(DuplicateKeyException e)
    {
        log.error("数据库中已存在该记录", e);
        return AjaxResult.error("数据库中已存在该记录", HttpStatus.CONFLICT.value());
    }

不过这样导致了一个问题,就是全局异常和业务方使用相同的依赖jar,但存在版本差异时,可能就会存在依赖冲突,导致业务项目启动报错。因此解决方案就是在pom文件加入optional标签。示例如下

<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>

这标签的意思这jar坐标是可选的,因此如果项目中已经有引入该jar的坐标,就直接用该jar的坐标

问题三:引入maven optional标签后,因业务没引入全局异常需要的jar,导致项目启动报错

这个问题的产生:举个示例,我们的业务微服务项目有聚合层,某些聚合层是不需要依赖存储介质,比如mysql。因此这些聚合层项目pom就不会引入类似mybatis相关的依赖。但我们的全局异常又需要类似mybatis相关的依赖,这样导致如果要引用全局异常模块,有得额外加入业务方不需要的jar。

因此springboot的条件注解就派上用场了,利用@ConditionalOnClass注解。示例如下

@RestControllerAdvice
@Slf4j
@ConditionalOnClass({SQLException.class, MybatisPlusException.class,
        MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
        BadSqlGrammarException.class, DuplicateKeyException.class})
public class GlobalExceptionDbHandler {

    /**
     * 数据库异常
     * @param e
     * @return
     */
    @ExceptionHandler({SQLException.class, MybatisPlusException.class,
            MyBatisSystemException.class, org.apache.ibatis.exceptions.PersistenceException.class,
            BadSqlGrammarException.class
    })
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public AjaxResult dbException(Exception e) {
        String msg = ExceptionUtil.getExceptionMessage(e);
        log.error(msg, e);
        return AjaxResult.error(msg,HttpStatus.BAD_REQUEST.value());
    }

    /**
     * 数据库中已存在该记录
     * @param e
     * @return
     */
    @ExceptionHandler(DuplicateKeyException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public AjaxResult handleException(DuplicateKeyException e)
    {
        log.error("数据库中已存在该记录", e);
        return AjaxResult.error("数据库中已存在该记录", HttpStatus.CONFLICT.value());
    }
}

@ConditionalOnClass这个注解的作用就是如果classpath存在指定的类,则该注解上的类会生效。

同时这边有个细节点,就是全局异常可能就得细分,即把原来的大一统的全局异常,按业务场景分开,比如存储介质相关的存储异常,web相关异常

总结

本文主要讲当将全局异常抽离成jar,可能会发生的问题。这边有涉及到一些细节点没讲,比如为啥要定义服务前缀+业务模块码+错误码,其实主要还是为了好排查问题。

也许有朋友会问,你们都搞了微服务,难道不上分布式链路追踪?根据分布式链路追踪可以很方便定位到整个链路了。但真的开发微服务的时候,如果公司原来就就没运维平台,有时候为了成本考量,测试、开发环境都不会上的分布式链路追踪的,甚至线上项目初期也不会上分布式链路追踪。因此定义好相关的业务码就变得格外重要

到此这篇关于springboot项目全局异常处理那些事儿的文章就介绍到这了,更多相关springboot项目全局异常处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-exception

相关文章

  • SpringBoot @SpringBootTest加速单元测试的小诀窍

    SpringBoot @SpringBootTest加速单元测试的小诀窍

    这篇文章主要介绍了SpringBoot @SpringBootTest加速单元测试的小诀窍,具有很好的参考价值,对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • java中带参数的try(){}语法含义详解

    java中带参数的try(){}语法含义详解

    这篇文章主要介绍了java中带参数的try(){}语法含义详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • springboot支持https请求的实现

    springboot支持https请求的实现

    本文主要介绍了springboot支持https请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • Springmvc nginx实现动静分离过程详解

    Springmvc nginx实现动静分离过程详解

    这篇文章主要介绍了Springmvc nginx实现动静分离过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • SpringBoot从配置文件中获取属性的四种方法总结

    SpringBoot从配置文件中获取属性的四种方法总结

    这篇文章主要介绍了SpringBoot从配置文件中获取属性的四种方法总结,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • spring boot启动时加载外部配置文件的方法

    spring boot启动时加载外部配置文件的方法

    这篇文章主要给大家介绍了关于spring boot启动时加载外部配置文件的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-02-02
  • 关于IDEA配置文件字符集的问题

    关于IDEA配置文件字符集的问题

    这篇文章主要介绍了关于IDEA配置文件字符集的问题,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • spring注解之@Valid和@Validated的区分总结

    spring注解之@Valid和@Validated的区分总结

    @Validated和@Valid在基本验证功能上没有太多区别,但在分组、注解地方、嵌套验证等功能上有所不同,下面这篇文章主要给大家介绍了关于spring注解之@Valid和@Validated区分的相关资料,需要的朋友可以参考下
    2022-03-03
  • Java基于注解实现的锁实例解析

    Java基于注解实现的锁实例解析

    这篇文章主要介绍了Java基于注解实现的锁实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 深入解析StringBuffer和StringBuilder的区别

    深入解析StringBuffer和StringBuilder的区别

    以下是对java中StringBuffer与StringBuilder的区别进行了详细的分析介绍,需要的朋友可以参考下
    2013-07-07

最新评论