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就是看中了它的自动配置功能,能省略很多配置代码,简化开发:

	<dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
    </dependency>

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

二.

  1. 新增异常枚举类和自定义异常类,继承RuntimeException。
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;
    }
}
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());
        });
    }
}

  1. 新增处理异常Handler 类
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;
        }
    }
}
  1. ExceptionUtil.getFullStackTrace(e)方法是为了将异常信息存到日志中,以方便后期排查,这里贴一下具体代码,两种方式参数分别是Exception和Throwable:
 /**
     * 异常堆栈信息保存到日志中
     * @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();
        }
    }
  1. ExceptionUtil.ResExHandle(e)方法是封装异常信息返前端的,代码如下:
/**
     * 封装异常类
     * @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的方式来处理全局异常还是比较简单的。

PS: 更多技术干货,欢迎大家来我的个人博客 yak33的技术人生

你可能感兴趣的:(SpringBoot优雅的全局异常处理(一)——非web项目)