SpringBoot全局错误捕捉与通用错误类

定义报错的日志级别枚举

import lombok.Getter;

@Getter
public enum ExceptionLogLevelEnum {
    //抛出错误时打印的错误日志级别
    ERROR(),
    WARN(),
}

项目中定义不同的错误枚举

import lombok.Getter;

/**
 * job=300000-399999
 * 业务异常枚举
 */
@Getter
public enum JobErrorEnum implements ErrorEnum {

    /**
     * 通用异常枚举
     */
    SYSTEM_ERROR(300000,"网络超时,请稍后重试!"),
    PARAM_NULL_ERROR(300001,"参数为空异常!"),
    Enum_NULL_ERROR(300002,"该值不存在!"),
    GENERATE_FAILURE_ERROR(300003,"生成失败!"),
    NOT_LOGIN(300004,"用户未登陆!"),
    UPLOAD_ATTACHMENT(300005,"请先上传附件!"),
    REDIS_NULL_ERROR(300006,"该值在redis中不存在!"),
    STRING_DECRYPT_ERROR(300007, "密文解密失败"),
    FILE_READ_ERROR(300008, "文件读取失败"),
    ;

    /**
     * 错误码
     */
    private Integer code;
    /**
     * 错误描述
     */
    private String msg;

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

    @Override
    public String getCodeStr(){
        return this.code.toString();
    }

}

定义基本错误捕捉类,根据日志级别的不同打印日志

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BaseGlobalExceptionHandler {
    /**
     * 继承BaseException的异常都可以自动按级别记错误日志
     * @param s
     * @param ex
     * @return void
     * @author linton.cao
     * @date 2021/9/4 20:35
     */
    public static void log(String s, BaseException ex){
        switch (ex.getErrorLevel()) {
            case WARN:
                log.warn(s, ex);
                break;
            case ERROR:
            default:
                log.error(s, ex);
        }
    }

    /**
     * 其他类型异常需要指定日志级别
     * @param s
     * @param ex
     * @param level
     * @return void
     * @author linton.cao
     * @date 2021/9/4 20:35
     */
    public static void log(String s, Throwable ex, ExceptionLogLevelEnum level){
        switch (level) {
            case WARN:
                log.warn(s, ex);
                break;
            case ERROR:
            default:
                log.error(s, ex);
        }
    }
}

继承基本错误处理类,实现对于各种错误处理的操作,Result类为统一的返回类

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.io.UnsupportedEncodingException;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends BaseGlobalExceptionHandler {
    @ExceptionHandler(value = {BaseException.class})
    @ResponseBody
    public Result handler(BaseException exception){
        // 根据BaseException默认的日志级别记ERROR日志
        log("Job Service Error BaseException:", exception);

        return Result.fail(exception.getErrorEnum());
    }

    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result handler(Exception exception){
        // 可以指定这次的日志级别
        log("Job Service Error Exception:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }

    @ExceptionHandler(value = {JobException.class})
    @ResponseBody
    public Result handler(JobException exception){
        // JobException有覆写了父类的errorLevel,该日志级别为JobException中的errorLevel
        log("Job Service Error JobException:", exception);

        return Result.fail(exception.getErrorEnum());
    }

    @ExceptionHandler(value = {IllegalAccessException.class})
    @ResponseBody
    public Result handler(IllegalAccessException exception){
        // 选你喜欢的,log.error或者BaseGlobalExceptionHandler.log,后者可以省引入@Slf4j
//        log.error("Job Service Error IllegalAccessException:", exception);
        log("Job Service Error IllegalAccessException:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }

    @ExceptionHandler(value = {UnsupportedEncodingException.class})
    @ResponseBody
    public Result handler(UnsupportedEncodingException exception){
        // 选你喜欢的,log.error或者BaseGlobalExceptionHandler.log,后者可以省引入@Slf4j
        log("Job Service Error UnsupportedEncodingException:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }
}

基本错误类

import lombok.Getter;

@Getter
public class BaseException extends RuntimeException{
    private final ErrorEnum errorEnum;

    // 默认错误日志级别为ERROR
    protected ExceptionLogLevelEnum errorLevel = ExceptionLogLevelEnum.ERROR;

    private Object resultBody = null;

    /**
     * 异常时需要返回的resultBody
     * @param result
     * @return void
     * @author linton.cao
     * @date 2021/9/8 16:29
     */
    public void setResultBody(Object result){
        this.resultBody = result;
    }

    /**
     * 默认错误
     * @param
     * @return
     * @author linton.cao
     * @date 2021/9/2 13:33
     */
    public BaseException() {
        super(CommonErrorEnum.SYSTEM_ERROR.getCodeStr());
        this.errorEnum = CommonErrorEnum.SYSTEM_ERROR;
    }

    public BaseException (ErrorEnum errorEnum) {
        super(errorEnum.getCodeStr());
        this.errorEnum = errorEnum;
    }

    /**
     * @param errorEnum
     * @param logMessage 日志记录内容
     * @return
     * @author linton.cao
     * @date 2021/9/2 13:56
     */
    public BaseException (ErrorEnum errorEnum, String logMessage) {
        super(errorEnum.getCodeStr() + " - " + logMessage);
        this.errorEnum = errorEnum;
    }

    public BaseException(ErrorEnum errorEnum, String logMessage, Throwable cause) {
        super(errorEnum.getCodeStr() + " - " + logMessage, cause);
        this.errorEnum = errorEnum;
    }
}

继承基本错误类,调用父类方法实现错误处理

import com.job51.dev.job.enums.JobErrorEnum;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Getter
@Slf4j
public class JobException extends BaseException {
    //异常日志级别
    private final ExceptionLogLevelEnum errorLevel = ExceptionLogLevelEnum.ERROR;

    public JobException() {
        super();
    }

    public JobException(JobErrorEnum jobErrorEnum){
        super(jobErrorEnum);
    }

    public JobException(JobErrorEnum jobErrorEnum, String logMessage){
        super(jobErrorEnum, logMessage);
    }

    public JobException(JobErrorEnum jobErrorEnum,String logMessage, Throwable cause){
        super(jobErrorEnum,logMessage,cause);
    }
}

业务中实际使用

        try {
            delete = stringRedisTemplate.delete(keyName);
        }catch (Exception e){
            throw new JobException(JobErrorEnum.SYSTEM_ERROR , MessageFormat.format("删除key发生错误,key:{0},e:{1}",keyName,e));
        }

2022.8.19更新

报错有许多的场景应用,为了应对这些复杂的场景,需要新的处理错误方式
基本的错误处理类,用于在出错时增加日志

import java.text.MessageFormat;

@Getter
public class BaseException extends RuntimeException{
    private static final long serialVersionUID = 1L;

    private final ErrorEnum errorEnum;

    /**
     * 默认错误日志级别为ERROR
     */
    protected ExceptionLogLevelEnum errorLevel = ExceptionLogLevelEnum.ERROR;

    private Object resultBody = null;

    /**
     * 异常时需要返回的resultBody
     * @param result 返回的body
     */
    public void setResultBody(Object result){
        this.resultBody = result;
    }

    /**
     * 默认错误
     */
    public BaseException() {
        super(CommonErrorEnum.SYSTEM_ERROR.getCodeStr());
        this.errorEnum = CommonErrorEnum.SYSTEM_ERROR;
    }

    public BaseException (ErrorEnum errorEnum) {
        super(errorEnum.getCodeStr());
        this.errorEnum = errorEnum;
    }

    public BaseException (ErrorEnum errorEnum, String logMessage) {
        super(errorEnum.getCodeStr() + " - " + logMessage);
        this.errorEnum = errorEnum;
    }

    public BaseException(ErrorEnum errorEnum, String logMessage, Throwable cause) {
        super(errorEnum.getCodeStr() + " - " + logMessage, cause);
        this.errorEnum = errorEnum;
    }

    public BaseException(ErrorEnum errorEnum, String logMessage, Object... messageParams) {
        super(errorEnum.getCodeStr() + " - " + formatLogMessage(logMessage, messageParams));
        this.errorEnum = errorEnum;
    }

    private static String formatLogMessage(String logMessage, Object... messageParams){
        String logStr = logMessage;
        if (messageParams.length>0) {
            logStr = MessageFormat.format(logMessage, messageParams);
        }
        return logStr;
    }
}

有些报错,是实际业务中会出现的报错,不需要记录error日志,则使用业务错误类抛出

package com.job51.dev.common.exception;

import com.job51.dev.common.enums.ErrorEnum;
import com.job51.dev.common.enums.ExceptionLogLevelEnum;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

/**
 * 
 */
@Getter
@Slf4j
public class BaseBusinessException extends BaseException {
    private static final long serialVersionUID = 1L;

    private final ExceptionLogLevelEnum errorLevel = ExceptionLogLevelEnum.INFO;

    public BaseBusinessException() {
        super();
    }

    public BaseBusinessException(ErrorEnum errorEnum){
        super(errorEnum);
    }

    public BaseBusinessException(ErrorEnum errorEnum, String logMessage){
        super(errorEnum, logMessage);
    }

    public BaseBusinessException(ErrorEnum errorEnum, String logMessage, Throwable cause){
        super(errorEnum,logMessage,cause);
    }

    public BaseBusinessException(ErrorEnum errorEnum, String logMessage, Object... messageParams) {
        super(errorEnum, logMessage, messageParams);
    }
}

捕捉不同的失败实例,比如FeignException,特殊场景下,失败了,也要返回实体

package com.job51.dev.common.exception;

import com.job51.dev.common.entity.dto.Result;
import com.job51.dev.common.enums.CommonErrorEnum;
import com.job51.dev.common.enums.ExceptionLogLevelEnum;
import com.job51.dev.common.utils.JacksonUtils;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.validation.ConstraintViolationException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author linton.cao
 */
@ControllerAdvice
@Slf4j
public class BaseGlobalExceptionHandler {
    /**
     * 继承BaseException的异常都可以自动按级别记错误日志
     * @param s 日志内容
     * @param ex 异常堆栈
     * @author linton.cao
     * @date 2021/9/4 20:35
     */
    public static void log(String s, BaseException ex){
        switch (ex.getErrorLevel()) {
            case INFO:
                log.info(s, ex);
                break;
            case WARN:
                log.warn(s, ex);
                break;
            case ERROR:
            default:
                log.error(s, ex);
        }
    }

    /**
     * 其他类型异常需要指定日志级别
     * @param s 日志内容
     * @param ex 异常堆栈
     * @param level 日志级别
     * @author linton.cao
     * @date 2021/9/4 20:35
     */
    public static void log(String s, Throwable ex, ExceptionLogLevelEnum level){
        switch (level) {
            case INFO:
                log.info(s, ex);
                break;
            case WARN:
                log.warn(s, ex);
                break;
            case ERROR:
            default:
                log.error(s, ex);
        }
    }

    @ExceptionHandler(value = {BaseException.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handler(BaseException exception){
        // 根据BaseException默认的日志级别记ERROR日志
        log("Service Exception:", exception);
        return Result.fail(exception.getErrorEnum());
    }

    @ExceptionHandler(value = {BaseBusinessException.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> handler(BaseBusinessException exception){
        // 业务上的错误抓取,INFO级别
        log("Service INFO BusinessException:", exception);
        return Result.fail(exception.getErrorEnum());
    }

    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handler(Exception exception){
        // 可以指定这次的日志级别
        log("Service Exception:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }

    /**
     * 微服务响应失败处理
     * @param exception FeignException
     * @return 响应
     */
    @ExceptionHandler(value = {FeignException.class})
    @ResponseBody
    public Result<?> handler(FeignException exception){
        // feign调用失败
        log("Service Feign Exception:", exception, ExceptionLogLevelEnum.ERROR);

        int state = exception.status();
        ServletRequestAttributes res = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (res != null && res.getResponse() != null){
            res.getResponse().setStatus(state);
        }

        Result<?> preResult = JacksonUtils.jsonDecode(exception.contentUTF8(), Result.class);
        if (preResult == null){
            return Result.fail();
        } else {
            return Result.fail(preResult.getStatus(), preResult.getMessage(), preResult.getResultbody());
        }
    }

    @ExceptionHandler(value = {IllegalAccessException.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handler(IllegalAccessException exception){
        log("Service IllegalAccessException:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }

    @ExceptionHandler(value = {UnsupportedEncodingException.class})
    @ResponseBody
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handler(UnsupportedEncodingException exception){
        log("Service UnsupportedEncodingException:", exception, ExceptionLogLevelEnum.ERROR);

        return Result.fail();
    }

    @ResponseBody
    @ExceptionHandler(BindException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> handler(BindException exception) {
        BindingResult result = exception.getBindingResult();
        log("Service Valid Exception:", exception, ExceptionLogLevelEnum.INFO);
        if (result.hasErrors()) {
            Map<String,String> map = new HashMap<>(16);
            result.getFieldErrors().forEach((item)->{
                String message = item.getDefaultMessage();
                // 获取错误的属性字段名
                String field = item.getField();
                map.put(field,message);
            });
            return Result.fail(CommonErrorEnum.PARAM_VALIDATE_ERROR, map);
        }
        return Result.fail(CommonErrorEnum.PARAM_VALIDATE_ERROR);
    }

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> handler(MethodArgumentNotValidException exception) {
        BindingResult result = exception.getBindingResult();
        log("Service Valid Exception:", exception, ExceptionLogLevelEnum.INFO);
        if (result.hasErrors()) {
            Map<String,String> map = new HashMap<>(16);
            result.getFieldErrors().forEach((item)->{
                String message = item.getDefaultMessage();
                // 获取错误的属性字段名
                String field = item.getField();
                map.put(field,message);
            });
            return Result.fail(CommonErrorEnum.PARAM_VALIDATE_ERROR, map);
        }
        return Result.fail(CommonErrorEnum.PARAM_VALIDATE_ERROR);
    }


    @ResponseBody
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result<?> handler(ConstraintViolationException exception) {
        log("Service Valid Exception:", exception, ExceptionLogLevelEnum.INFO);
        return Result.fail(CommonErrorEnum.PARAM_VALIDATE_ERROR,exception.getMessage());
    }
}

抛出错误时,需要返回对象,则定义一个特殊的Exception

public class CupidLoginDivulgeException extends BaseBusinessException {
    private static final long serialVersionUID = 1L;

    private final ExceptionLogLevelEnum errorLevel = ExceptionLogLevelEnum.INFO;

    private final LoginInfoVO userInfo;

    public CupidLoginDivulgeException(LoginInfoVO userInfo, ErrorEnum errorEnum, String logMessage, Object... messageParams) {
        super(errorEnum, logMessage, messageParams);
        this.userInfo = userInfo;
    }
}

实际使用场景:

            Result<?> preResult = JacksonUtils.jsonDecode(exception.contentUTF8(), Result.class);

            //passport密码为泄露库密码,不可登录
            if (preResult.getStatus().equals(PassportErrorEnum.LOGIN_DIVULGE_PASSWORD.getCodeStr())){
                String logMessage = MessageFormat.format("调用passport账号密码登录,密码为泄露库密码,不可登录 请求参数:{0};passport返回:{1}", passwordLoginQuery, preResult);
                LoginInfoVO loginInfoVO = new LoginInfoVO();
                //需要返回app跳转链接
                loginInfoVO.setUsername(getDivulgePasswordUrl((LoginUserDTO)preResult.getResultbody()));
                throw new CupidLoginDivulgeException(loginInfoVO, CupidErrorEnum.LOGIN_FAILURE_COLLISION_LIBRARY, logMessage);
            }

对应的handler中也需要捕捉这个错误

    @ResponseBody
    @ExceptionHandler(CupidLoginDivulgeException.class)
    @ResponseStatus(code = HttpStatus.OK)
    public Result<?> handler(CupidLoginDivulgeException exception) {
        log("Service INFO PasswordLoginDivulgeException: ", exception);
        return Result.fail(exception.getErrorEnum(), exception.getUserInfo());
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容