优雅的SpringBoot项目异常处理方案?

前言

在SpringBoot项目中,我们知道使用统一异常处理器(@RestControllerAdvice、@ExceptionHandler)处理异常是一种非常优雅的方式,统一对异常进行解析、打印异常日志和包装响应消息。但我认为异常处理还应包括异常的创建和抛出过程,如何优雅的创建一个异常,并携带必要的现场信息?这是本文想解决的问题。在这里是本人的一些思考,提供一种优雅的异常处理方案,同时阅读本文需要具备统一异常处理器知识。

如何处理异常?

先回顾一下在SpringBoot项目中对异常进行统一处理的方式。我们都希望在统一异常处理器中进行异常处理,在这里根据异常打印日志并生成响应结果,在业务代码有异常的时候只需抛出异常。这样我们就不必在异常产生处进行日志打印、返回错误信息了。下面是简单的异常处理例子,定义了一个自定义的业务异常(BusinessException):

  1. 抛出异常
java 复制代码
if(有异常,如参数错误){
    //创建异常
    throw new BusinessException("xx参数错误");
}
  1. 处理异常
java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public String handleBusinessException(BusinessException e) {
        //打印异常日志
        log.error("异常:{}", exception.getMessage(), e);
        //返回响应结果
        return e.getMessage();
    }

}

在普通的异常处理中,我认为存在一些问题:

  1. 抛出异常携带现场信息困难

Exception中通常只有message属性提供异常信息描述,如果我们需要携带更多的信息,就要通过繁琐的方式构建一个错误信息,如:

java 复制代码
if(有异常,如参数错误){
    //创建异常
    String errorMessage = "flag参数错误,当前参数为:" + flag;
    throw new BusinessException(errorMessage);
}

如果需要携带的现场信息太多,可能直接在当前进行必要的日志打印更加方便了,但这又会造成日志代码冗余,与建议在统一异常处理器中进行错误信息打印不符。

  1. 异常携带的错误信息没有层次

如1所示,错误信息为一体无法分离。我们更希望有一个简明的异常描述和相应的异常详细信息,让我们能够有选择的处理信息。像简明的异常描述,如果是用户可处理的我们就直接响应给用户,异常详细信息描述异常现场让开发者能快速知道错误细节。

优雅地创建异常对象

在前面阐述了普通的异常处理中存在的一些问题,针对这些问题,我将一个异常对象分为三个部分来提供解决上述问题的方案,分别为:

  1. 简要的异常主题(subject):提供简单明了的异常描述
  2. 详细的异常描述(descriptions):提供详细的异常现场信息
  3. 原始的错误对象(cause):提供异常定位

通过这三部分构建一个异常对象,能够有效的携带错误信息,我将其声明为TemplateException异常对象:

java 复制代码
/**
 * 作为异常类的通用模板
 * 该异常携带一个主题(subject)、一组错误信息描述对象(descriptions)和一个原始错误对象共同描述错误的详细信息
 * 1.当表示一个系统错误时,通过主题(subject)、描述对象(descriptions)和原始错误对象共同描述错误的详细信息
 * 2.当是业务异常时,主要使用主题(subject),主题是面向用户的,应提供用户友好提示信息,
 *   如果需要描述更详细的信息,通过描述对象(descriptions)和原始错误对象携带
 */
@Slf4j
public class TemplateException extends RuntimeException{

    /**
     * 错误信息描述对象
     * 存放异常现场的描述信息
     */
    private final List<String> descriptions = new ArrayList<>();

    /**
     * 构建一个仅声明主题的异常对象
     * @param subject
     */
    public TemplateException(String subject) {
        super(subject);
    }

    /**
     * 构建一个携带主题和原始异常对象的系统异常
     * @param subject 错误主题
     * @param cause 原始异常对象
     */
    public TemplateException(String subject, Throwable cause) {
        super(subject, cause);
    }

    public TemplateException() {
        super();
    }

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

    protected TemplateException(String subject, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(subject, cause, enableSuppression, writableStackTrace);
    }

    /**
     * 链式创建一条描述信息
     * @param format 信息主体
     * @param arguments 占位符实际参数 0..n
     * @return 当前异常对象
     */
    public TemplateException describe(String format, Object... arguments){
        String description = String.format(format, arguments);
        this.descriptions.add(description);
        return this;
    }

    /**
     * 获取异常主题
     * @return 异常主题
     */
    public String getSubject(){
        return super.getMessage();
    }

    /**
     * 获取错误信息描述对象
     * @return 错误信息描述对象
     */
    public List<String> getDescriptions() {
        return descriptions;
    }

}

该异常对象作为其他自定义异常对象的父类

java 复制代码
/**
 * 该类表示一个业务异常
 * 业务异常通常是用户可解决的错误,通过该类携带一个对用户友好的提示。
 */
public class BusinessException extends TemplateException{

    public BusinessException(String subject) {
        super(subject);
    }

    public BusinessException(String subject, Throwable cause) {
        super(subject, cause);
    }

    public BusinessException() {
        super();
    }

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

    protected BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

下面是使用示例

  1. 创建异常
java 复制代码
if(有异常,如参数错误){
    //引起错误的原始异常
    NullPointerException nullPointerException = new NullPointerException();
    throw new BusinessException("flag参数错误", nullPointerException)
            .describe("当前flag为: %s",flag)
            .describe("其他描述信息:%s", "更多的现场信息");
}
  1. 统一异常处理
java 复制代码
@ExceptionHandler(BusinessException.class)
public String handleBusinessException(BusinessException exception) {
    List<String> descriptions = exception.getDescriptions();
    Throwable cause = exception.getCause();
    cause = Objects.isNull(cause) ? exception : cause;
    log.error("业务异常:{}", exception.getSubject());
    for (String description : descriptions) {
        log.error("{}", description);
    }
    log.error("触发异常对象", cause);
    //响应结果
    return exception.getSubject();
}
  1. 效果
yaml 复制代码
2023-12-02 15:33:53.225 ERROR 12520 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 业务异常:flag参数错误
2023-12-02 15:33:53.226 ERROR 12520 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 当前flag为: 2
2023-12-02 15:33:53.226 ERROR 12520 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 其他描述信息:更多的现场信息
2023-12-02 15:33:53.228 ERROR 12520 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 触发异常对象

java.lang.NullPointerException: null
	at com.xiaoyan.monadicapptemplate.controller.CommonController.businessException(CommonController.java:33) ~[classes/:na]

这种方式创建的异常对象相对于普通异常对象能够更容易的创建一个存放现场信息的异常对象。

最后

说明一下,TemplateException更多是一种如何优雅地创建异常的方案,皆在于构建一个高效的异常描述对象。这些只是本人的思考,方案并没有落地,只是在开发项目时纠结于如何有效地进行错误日志打印的一些思考,当然也是希望能够通过实际项目的验证。各位有什么更好的想法欢迎一起交流,可能会觉得一个自定义异常对象为什么需要这么大费周章,但我想一个高效的标准能够减少很多不必要的工作,提高代码的质量。

相关推荐
捂月25 分钟前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
Estar.Lee1 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪1 小时前
Django:从入门到精通
后端·python·django
一个小坑货1 小时前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet271 小时前
【Rust练习】22.HashMap
开发语言·后端·rust
uhakadotcom2 小时前
如何实现一个基于CLI终端的AI 聊天机器人?
后端
Iced_Sheep2 小时前
干掉 if else 之策略模式
后端·设计模式
XINGTECODE3 小时前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶3 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露