SpringBoot优雅的全局异常处理(一)非web项目

前言

本篇文章主要介绍的是SpringBoot非web项目进行全局异常的处理。
SpringBoot版本:2.1.9.RELEASE
Mybatis Plus版本:3.3.0

上个项目使用的是SpringBoot+Mybatis Plus+zbus,项目架构是:zbus分为客户端和服务端,两者通过RPC进行调用。

主要工作:通过Spring AOP处理、捕获异常,并将异常信息记录到日志中。

一. 先看我的pom文件,之所以将Spring换为SpringBoot就是看中了它的自动配置功能,能省略很多配置代码,简化开发:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

注意:此处没有引入web模块!

二.

  1. 新增异常枚举类和自定义异常类,继承RuntimeException。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    package com.junya.util.exception;

    /**
    * 自定义全局异常类
    *
    * @author ZhangChao
    * @date 2019/10/18 13:23
    * @since 1.0.0
    */
    public class GlobalException extends RuntimeException {
    private static final long serialVersionUID = 6958499252468627021L;

    /**
    * 错误码
    */
    private String code;

    public GlobalException(String code, String msg) {
    super(msg);
    this.code = code;
    }

    public GlobalException(GlobalErrorCodeEnum errorCode) {
    super(errorCode.getMsg());
    this.code = errorCode.getCode();
    }

    public GlobalException(GlobalErrorCodeEnum errorCode, String msg){
    super(errorCode.getMsg()+msg);
    this.code = errorCode.getCode();
    }

    public GlobalException(String code, String msg, Throwable throwable){
    super(msg,throwable);
    this.code = code;
    }
    public GlobalException(GlobalErrorCodeEnum errorCode, Throwable throwable){
    super(errorCode.getMsg(),throwable);
    this.code = errorCode.getCode();
    }

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package com.yorma.enterprise.common.exception;

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;

    /**
    * 自定义全局异常枚举
    *
    * @author ZhangChao
    * @date 2019/10/18 13:15
    * @since 1.0.0
    */
    public enum GlobalErrorCode {

    /** 未知异常 */
    UNKNOWN_EXCEPTION("CSEP001","未知异常,请联系管理员"),
    /** 系统错误 */
    SYSTEM_ERROR("CSEP002","系统错误"),
    /** 类转换异常 */
    CLASS_CAST_EXCEPTION("CSEP003","类型强制转换异常"),
    /** 算术条件异常 */
    ARITHMETIC_EXCEPTION("CSEP004","算术条件异常"),
    /** 空指针异常 */
    NULL_POINTER_EXCEPTION("CSEP005","空指针异常"),
    /** 字符串转换为数字异常 */
    NUMBER_FORMAT_EXCEPTION("CSEP006","字符串转换为数字异常"),
    /** 数组下标越界异常 */
    ARRAY_INDEX_OUT_OF_BOUNDS_EXCEPTION("CSEP007","数组下标越界异常"),
    /** 方法未找到异常 */
    NO_SUCH_METHOD_EXCEPTION("CSEP008","方法未找到异常"),
    /** 未找到类定义错误 */
    NO_CLASS_DEF_FOUND_ERROR("CSEP009","未找到类定义错误"),
    /** 未找到类定义错误 */
    CLASS_NOT_FOUND_EXCEPTION("CSEP010","找不到类异常"),
    /** 索引越界异常 */
    INDEX_OUT_OF_BOUNDS_EXCEPTION("CSEP011","索引越界异常")
    ;

    private String code;
    private String msg;

    GlobalErrorCode(String code, String msg) {
    this.code = code;
    this.msg = msg;
    }

    public String getCode() {
    return code;
    }

    public void setCode(String code) {
    this.code = code;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }

    public static void main(String[] args) {
    String[] arr = {"IndexOutOfBoundsException"};
    List<String> resultList = new ArrayList<>(arr.length);
    Collections.addAll(resultList,arr);
    resultList.forEach(item->{
    System.out.println(item.toUpperCase());
    });
    }
    }
  2. 新增处理异常Handler 类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package com.yorma.enterprise.zbus.provider.config;

    import com.yorma.enterprise.common.exception.ExceptionUtil;
    import com.yorma.enterprise.common.result.ResponseMessage;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;

    /**
    * 全局异常拦截处理
    *
    * --注意:不加order注解会导致事务不回滚!!
    *
    * @author ZhangChao
    * @date 2019/9/12 15:30
    * @since 1.0.0
    */
    @Component
    @Aspect
    @Order(1)
    public class GlobalExceptionHandler {

    private static Logger LOGGER;

    /**
    * 切点,找到那些方面要切
    */
    // #排除掉basic包# @Pointcut("execution(public * com.yorma.enterprise.service.impl..*.*(..)) && !execution(public * com.yorma.enterprise.service.impl.basic.*.*(..))")
    @Pointcut("execution(public * com.yorma.enterprise.service.impl..*.*(..)) && !execution(public * com.yorma.enterprise.service.impl.business.*.*(..))\"")
    public void exception(){}

    /**
    * 环切,执行方法前后都切
    * @param joinPoint
    * @return
    */
    @Around("exception()")
    public ResponseMessage ExceptionHandler(ProceedingJoinPoint joinPoint){
    ResponseMessage result = null;
    long startTime = System.currentTimeMillis();
    //获取目标Class
    Class targetClass = joinPoint.getTarget().getClass();
    //目标Class的Logger
    LOGGER = LoggerFactory.getLogger(targetClass);
    //获取目标类名称
    String clazzName = joinPoint.getTarget().getClass().getName();
    //获取目标类方法名称
    String methodName = joinPoint.getSignature().getName();
    try {
    LOGGER.info( "{}: {}: {}: start...", clazzName, methodName, joinPoint.getArgs());
    // 调用目标方法
    result= (ResponseMessage) joinPoint.proceed();
    long endTime = System.currentTimeMillis() - startTime;
    LOGGER.info( "{}: {}: end... cost time: {} ms", clazzName, methodName, endTime);
    return result;
    }catch (Throwable e){
    e.printStackTrace();
    //异常堆栈信息输出到logger日志中
    LOGGER.info(clazzName+"."+methodName+"方法执行出现异常! => "+ e.toString());
    ExceptionUtil.getFullStackTrace(e);
    //使事务生效
    // throw new RuntimeException(clazzName+"."+methodName+"方法执行出现异常! => "+ e.toString());
    result = ExceptionUtil.ResExHandle(e);
    return result;
    }
    }
    }
  3. ExceptionUtil.getFullStackTrace(e)方法是为了将异常信息存到日志中,以方便后期排查,这里贴一下具体代码,两种方式参数分别是Exception和Throwable:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
    * 异常堆栈信息保存到日志中
    * @param ex
    */
    public static void getFullStackTrace(Exception ex) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PrintStream pout = new PrintStream(out);
    ex.printStackTrace(pout);
    String ret = new String(out.toByteArray());
    pout.close();
    try {
    out.close();
    } catch (Exception e) {
    }
    ex.printStackTrace();
    logger.error(ret);
    }

    /**
    * 参数是Throwable
    * @param e
    * @return
    */
    public static void getFullStackTrace(Throwable e){
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw, true);
    try {
    e.printStackTrace(pw);
    pw.flush();
    sw.flush();
    logger.error(sw.toString());
    } finally {
    pw.close();
    }
    }
  4. ExceptionUtil.ResExHandle(e)方法是封装异常信息返前端的,代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /**
    * 封装异常类
    * @param e
    * @return
    */
    public static ResponseMessage ResExHandle(Throwable e){
    ResponseMessage responseMessage = new ResponseMessage();
    responseMessage.setSuccess(false);
    String eStr = e.toString();
    for (GlobalErrorCode global : GlobalErrorCode.values()){
    if (eStr.toUpperCase().contains(global.toString().replaceAll("_",""))){
    responseMessage.setStatus(global.getCode());
    responseMessage.setMsg(global.getMsg());
    break;
    }
    }
    //如果是以前封装的那个YmException异常
    if (eStr.contains("YmException")){
    for (StatusCodeEnum statusCodeEnum : StatusCodeEnum.values()){
    if (eStr.contains(statusCodeEnum.getMsg())){
    if (eStr.contains("Data too long for column")){
    String field = eStr.replaceAll("[\\s\\S]+Data too long for column '(.+)' at[\\s\\S]+","$1");
    responseMessage.setStatus(statusCodeEnum.getCode());
    responseMessage.setMsg(statusCodeEnum.getMsg()+",字段长度超过限制:"+field);
    break;
    }
    if (eStr.contains("Unknown column")){
    String field = eStr.replaceAll("[\\s\\S]+Unknown column '(.+)' in[\\s\\S]+","$1");
    responseMessage.setStatus(statusCodeEnum.getCode());
    responseMessage.setMsg(statusCodeEnum.getMsg()+",未知的字段:"+field);
    break;
    }
    if (eStr.contains("doesn't have a default value")){
    String field = eStr.replaceAll("[\\s\\S]+Field '(.+)' doesn't have a default value[\\s\\S]+","$1");
    responseMessage.setStatus(statusCodeEnum.getCode());
    responseMessage.setMsg(statusCodeEnum.getMsg()+",此字段必须有值:"+field);
    break;
    }
    responseMessage.setStatus(statusCodeEnum.getCode());
    responseMessage.setMsg(statusCodeEnum.getMsg());
    break;
    }
    }
    }
    if ("".equals(responseMessage.getStatus()) || "".equals(responseMessage.getMsg())){
    responseMessage.setStatus(GlobalErrorCode.UNKNOWN_EXCEPTION.getCode());
    responseMessage.setMsg(GlobalErrorCode.UNKNOWN_EXCEPTION.getMsg());
    }
    return responseMessage;
    }
    封装了几种常见的异常信息,如:空指针、数据库字段不存在等。

总结

用aop的方式来处理全局异常还是比较简单的。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020 yak33
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信