乐趣区

关于springboot:聊聊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,可能会产生的问题。这边有波及到一些细节点没讲,比方为啥要定义服务前缀 + 业务模块码 + 错误码,其实次要还是为了好排查问题。

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

demo 链接

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

退出移动版