SpringBoot优雅地处理全局异常,返回前端

笔者这边提供了两种处理全局异常的方式。这两种方式各有千秋,都很优雅。至于伙伴们想用哪种方式,那就仁者见仁,智者见智了。

0、公共部分

在介绍异常处理方式前,先定义一些公共的类。这些类在两种处理方式中都会用到。

【自定义业务异常】

/**
 * 自定义业务异常
 */
@Data
public class SunException extends RuntimeException {

    private Integer code;
    private String msg;

    public SunException(SystemEnum systemEnum) {
        this.code = systemEnum.getCode();
        this.msg = systemEnum.getDesc();
    }
    public SunException(BusinessEnum businessEnum) {
        this.code = businessEnum.getCode();
        this.msg = businessEnum.getDesc();
    }

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

【自定义系统枚举】

/**
 * 系统枚举
 */
public enum SystemEnum {
    SUCCESS(0, "success"),
    FAIL(-1, "fail"),
    PARAM_ILLEGAL(100, "参数非法!"),
    SERVICE_TIME_OUT(200, "服务间调用超时"),
    UNEXPECTED_EXCEPTION(500, "系统内部错误,请联系管理员!"),
    OTHER(9999, "Unknown Exception.");

    private Integer code;
    private String desc;

    SystemEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

【自定义业务枚举】

/**
 * 业务类枚举
 *
 * @author: dong
 * @date: 2023/2/12 21:50
 * @since: 1.0
 */
public enum BusinessEnum {
    /**--------用户相关----------**/
    USER_ID_NOT_EXIST(1000, "userId not exist."),

    /**--------任务相关----------**/
    TASK_ID_NOT_EXIST(2000, "taskId not exist."),
    TASK_NAME_NOT_EXIST(2001, "taskName not exist."),
    TASK_TYPE_NOT_EXIST(2002, "taskType not exist."),
    DEADLINE_NOT_EXIST(2003, "deadline not exist."),
    CONTENT_NOT_EXIST(2004, "content not exist.")
    ;

    private Integer code;
    private String desc;

    BusinessEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

【统一封装响应体】

/**
 * 统一封装响应体
 * @param <T>
 */
public class BaseResult<T> {
    private Integer code;
    private String msg;
    private T data;

    public BaseResult() {
    }

    public BaseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public BaseResult(T data) {
        this.code = SystemEnum.SUCCESS.getCode();
        this.msg = SystemEnum.SUCCESS.getDesc();
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

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

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

【响应结果工具类】

/**
 * 响应结果工具类
 */
public class ResultUtil {

    public static <R> BaseResult<R> outSuccess() {
        return new BaseResult<>(SystemEnum.SUCCESS.getCode(), SystemEnum.SUCCESS.getDesc(), null);
    }
    public static <R> BaseResult<R> outSuccess(R data) {
        return new BaseResult<>(data);
    }

    public static <R> BaseResult<R> outFail(String errorMsg) {
        return new BaseResult<>(SystemEnum.FAIL.getCode(), errorMsg, null);
    }
    public static <R> BaseResult<R> outFail(Integer errorCode, String errorMsg) {
        return new BaseResult<>(errorCode, errorMsg, null);
    }
}

方式一、@RestControllerAdvice + @ExceptionHandler

如下所示,

a. 新建一个全局异常处理类,并在类名前加上@RestControllerAdvice注解,该注解可以拦截项目中抛出的异常;

b. 同时在新建一个处理异常的方法,并在方法上加上@ExceptionHandler注解,并在该注解的属性中指定具体的异常。如下代码中指定的具体异常即是 SunException(也就是上一小节中笔者自定义的业务异常)。

c. 在处理异常的方法中,通过ResultUtil.outFail() 方法统一封装返回给前端的响应体。

/**
 * 全局异常处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(SunException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public BaseResult handlerBusinessException(SunException sunException) {
        LOGGER.error("exception happened at {}", sunException.getMsg());
        return ResultUtil.outFail(sunException.getCode(), sunException.getMsg());
    }
}

那么在具体的业务接口中如何抛出异常能被GlobalExceptionHandler所捕获呢?

其实很简单,只要用 throw new SunException(...); 就可以了。注意:这里只能抛出SunException,不能抛出RuntimeException或者任何其他异常。因为@ExceptionHandler已经指定了具体的异常类型。

    @GetMapping("/hello")
    public String sayHello(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new SunException(SystemEnum.PARAM_ILLEGAL);
        }
        return "The sun is rising.";
    }

Swagger测试效果:

方式二、AOP实现

很明显,aop天生就是干这个的料。aop在业务解耦方面简直如鱼得水,像统一打印日志,统一捕获异常等等。talk is cheap,show me the code.

如下代码所示:

a. 新建这个aop监控类,新增切点,切所有模块的Controller层的所有方法;

b. 实现一个环绕通知接口,打印方法入参以及请求结果;

c. 注意代码40行到45行,就是捕获项目中所有的SunException,并封装成统一的响应体返回前端。

/**
 * aop监控类
 **/
@Slf4j
@Aspect
@Component
public class AspectMonitor {

    /**
     * 日志切点,切所有模块controller层中的所有方法
     */
    @Pointcut("execution(* com.bxbro.*..controller..*.*(..))")
    public void logPointCut() {
        // do nothing.
    }


    /**
     * 对接口做统一的日志及异常处理
     * @param pjp
     * @return
     */
    @Around("logPointCut()")
    public Object apiMonitor(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        Object[] arguments = new Object[args.length];
        for (int i=0;i<args.length;i++) {
            // ServletRequest不能序列化,从入参里排除,
            // 否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
            if(args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) {
                continue;
            }
            arguments[i] = args[i];
        }
        log.info("请求入参:{},方法名:{}.{}", JSON.toJSONString(arguments),pjp.getSignature().getDeclaringTypeName(),pjp.getSignature().getName());
        Object result;
        try {
            result = pjp.proceed();
        } catch (Throwable throwable) {
            if (throwable instanceof SunException) {
                SunException sunException = (SunException) throwable;
                return ResultUtil.outFail(sunException.getCode(), sunException.getMsg());
            }
            log.error("接口请求异常:{}", throwable);
            return ResultUtil.outFail("系统内部错误" + throwable.getMessage());
        }
        log.info("请求结果:{}", JSON.toJSONString(result));
        return result;
    }
}

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邓不利东

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值