0

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

 3 months ago
source link: https://segmentfault.com/a/1190000041299750
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is damaged, please click the button below to view the snapshot at that time.

之前我们业务团队在处理全局异常时,在每个业务微服务中都加入了@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,可能会发生的问题。这边有涉及到一些细节点没讲,比如为啥要定义服务前缀+业务模块码+错误码,其实主要还是为了好排查问题。

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

demo链接

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK