SpringBoot实现统一异常处理

目录

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。本项目为前后端分离开发,后端基于Java21SpringBoot3开发,后端使用Spring SecurityJWTSpring Data JPA等技术栈,前端提供了vueangularreactuniapp微信小程序等多种脚手架工程。

项目地址:https://gitee.com/breezefaith/fast-alden

在前后端分离的项目开发过程中,我们通常会对数据返回格式进行统一的处理,这样可以方便前端人员取数据,后端发生异常时同样会使用此格式将异常信息返回给前端。本文将介绍在SpringBoot项目中如何实现统一异常处理。

实现步骤

定义统一响应对象类

java 复制代码
/**
 * 响应结果类
 *
 * @param <T> 任意类型
 */
@Data
public class ResponseResult<T> {
    /**
     * 响应状态码,200是正常,非200表示异常
     */
    private int status;
    /**
     * 异常编号
     */
    private String errorCode;
    /**
     * 异常信息
     */
    private String message;
    /**
     * 响应数据
     */
    private T data;

    public static <T> ResponseResult<T> success() {
        return success(HttpServletResponse.SC_OK, null, null);
    }

    public static <T> ResponseResult<T> success(T data) {
        return success(HttpServletResponse.SC_OK, null, data);
    }

    public static <T> ResponseResult<T> fail(String message) {
        return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, null);
    }

    public static <T> ResponseResult<T> fail(String errorCode, String message) {
        return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorCode, message, null);
    }

    public static <T> ResponseResult<T> success(int status, String message, T data) {
        ResponseResult<T> r = new ResponseResult<>();
        r.setStatus(status);
        r.setMessage(message);
        r.setData(data);

        return r;
    }

    public static <T> ResponseResult<T> fail(int status, String errorCode, String message) {
        return fail(status, errorCode, message, null);
    }

    public static <T> ResponseResult<T> fail(int status, String errorCode, String message, T data) {
        ResponseResult<T> r = new ResponseResult<>();
        r.setStatus(status);
        r.setErrorCode(errorCode);
        r.setMessage(message);
        r.setData(data);
        return r;
    }

}

定义业务异常枚举接口和实现

通常一个系统中的自定义业务异常是可穷举的,可以考虑通过定义枚举的方式来列举所有的业务异常。

首先我们来定义一个异常信息枚举的基类接口。

java 复制代码
public interface IBizExceptionEnum {
    String getCode();

    String getMessage();
}

再给出一个常用的异常信息的枚举类,如果有其他业务模块的异常信息,同样可以通过实现IBizExceptionEnum接口来进行定义。

java 复制代码
@Getter
public enum BizExceptionEnum implements IBizExceptionEnum {
    ENTITY_IS_NULL("Base_Entity_Exception_0001", "实体为空"),
    ENTITY_ID_IS_NULL("Base_Entity_Exception_0002", "实体id字段为空"),
    ENTITY_ID_IS_DUPLCATED("Base_Entity_Exception_0003", "实体id字段%s重复");

    private final String code;
    private final String message;

    BizExceptionEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }
}

定义业务异常基类

业务异常基类BizException继承自RuntimeException,代码中主动抛出的异常建议都包装为该类的实例。

java 复制代码
/**
 * 业务异常基类,支持参数化的异常信息
 */
@Getter
@Setter
public class BizException extends RuntimeException {
    private String code;
    private Object[] args;

    public BizException() {
        super();
    }

    public BizException(String message) {
        super(message);
    }

    public BizException(Throwable cause) {
        super(cause);
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
    }

    public BizException(Throwable cause, String code, String message, Object... args) {
        super(message, cause);
        this.code = code;
        this.args = args;
    }

    public BizException(String code, String message, Object... args) {
        super(message);
        this.code = code;
        this.args = args;
    }

    public BizException(IBizExceptionEnum exceptionEnum, Object... args) {
        this(exceptionEnum.getCode(), exceptionEnum.getMessage(), args);
    }

    public BizException(Throwable cause, IBizExceptionEnum exceptionEnum, Object... args) {
        this(cause, exceptionEnum.getCode(), exceptionEnum.getMessage(), args);
    }

    @Override
    public String getMessage() {
        if (code != null) {
            if (args != null && args.length > 0) {
                return String.format(super.getMessage(), args);
            }
        }
        return super.getMessage();
    }
}

定义全局异常处理切面

本步骤需要使用@RestControllerAdvice注解,它是一个组合注解,由@ControllerAdvice@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler@InitBinder@ModelAttribute方法,适用于所有使用@RequestMapping方法。

还要用到@ExceptionHandler注解,可以认为它是一个异常拦截器,它采用"就近原则",存在多个满足条件的异常处理器时会选择最接近的一个来使用。它本质上就是使用Spring AOP定义的一个切面,在系统抛出异常后执行。

具体实现代码如下:

java 复制代码
/**
 * 全局异常处理切面
 */
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandlerAdvice.class);

    @ExceptionHandler({BizException.class})
    public ResponseResult<Object> handleBizException(BizException e, HttpServletRequest request, HttpServletResponse response) {
        log.error(e.getCode() + ": " + e.getMessage(), e);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        return ResponseResult.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler({RuntimeException.class, Exception.class})
    public ResponseResult<Object> handleRuntimeException(Exception e, HttpServletRequest request, HttpServletResponse response) {
        log.error(e.getMessage(), e);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        return ResponseResult.fail(e.getMessage());
    }
}

上述代码会对系统中抛出的BizExceptionRuntimeExceptionException对象进行处理,把异常包装为ResponseResult对象后将异常编号和异常信息返回给前端。

测试和验证

下面我们就可以定义一个Controller类来进行简单的测试。

java 复制代码
@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/method1")
    public ResponseResult<Integer> method1() {
        throw new BizException(BizExceptionEnum.ENTITY_IS_NULL);
    }

    @GetMapping("/method2")
    public void method2() {
        throw new BizException(BizExceptionEnum.ENTITY_ID_IS_NULL);
    }

    @GetMapping(value = "/method3")
    public String method3() {
        throw new BizException(BizExceptionEnum.ENTITY_ID_IS_DUPLCATED, "1");
    }

    @GetMapping(value = "/method4")
    public String method4() {
        // 抛出ArithmeticException异常
        return String.valueOf(1 / 0);
    }
}

总结

本文介绍了如何在SpringBoot项目中实现统一异常处理,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

相关推荐
敲代码的嘎仔5 小时前
用 Redis 合并写 + DelayQueue 延迟检测,把视频播放进度写库频率降到 1/60
java·开发语言·数据库·redis·缓存·音视频·高并发
youyou-06065 小时前
AUTOSAR‑COM 常见问题 QA (1)
java·linux·服务器·单片机·汽车
行百里er5 小时前
Spring Insight 里如何对 Span 进行清洗
spring boot·spring cloud·监控
谢亮_vipxieliang5 小时前
ValidX时间段验证详解:ISO 8601标准与简化格式
java·spring boot·后端·spring·spring cloud·hibernate
Lsetea5 小时前
Java 请求 HTTPS 报 PKIX path building failed:从证书链到 truststore 的完整排查
java·https·ssl证书·keytool·truststore
不一样的少年_6 小时前
WebP 压缩到底在干嘛?小白也能看懂的原理拆解
前端·后端·图片资源
不一样的少年_6 小时前
PNG/JPG 如何变成 WebP?真相不是改后缀!
前端·后端·图片资源
蓝桉柒76 小时前
java判断语句
java·开发语言
Zane19946 小时前
String::compareTo凭什么能当参数传?方法引用的四种形式讲透
java·后端
JavaGuide6 小时前
阿里 Qoder 又开源了一个专门给 Claude Code、Codex 做“体检”的项目
前端·后端