likes
comments
collection
share

异常分类的最佳实践?构建合理的异常体系

作者站长头像
站长
· 阅读数 5

前言

在上一篇文章中,我们阐述了如何优雅地处理异常,从创建、抛出到处理异常的完整链路中提供一种处理方案。在本文中,将尝试以实际开发经验对如何分类异常,构建一个合理的异常体系进行最佳实践。

为什么要自定义异常

在探究异常如何分类时,先思考下为什么要自定义异常类?我认为有以下几点原因

  1. 原始异常提供的信息载体较少。通常仅有Message、Cause可用,自定义用于扩展信息载体,如增加异常码;
  2. 原始异常对用户不友好。通过定义一个业务异常类来说明业务异常,并携带用户友好的提示引导用户执行正确的操作;
  3. 对不同的异常进行针对性的操作。像一些用于提示用户的异常,如密码错误、权限不足等应返回给用户查看,且该类异常并不需要进行额外处理。而对于系统出现的底层异常,如数据库语句执行失败,该类异常需要打印详细的现场日志信息,和对异常携带的信息转换后返回给用户。

系统异常很多开发者没有重视,经常会看到界面错误弹框携带了系统底层信息,如接口参数解析异常

异常分类的最佳实践?构建合理的异常体系

这种情况就会泄露系统底层信息,在信息安全方面,这是一个中危漏洞,归根到底就是没对系统异常进行转换,只是简单地在异常处理器中把原始异常信息返回了。

总得来说,自定义异常对象是为了更好的对异常情况进行针对性的处理,同时提高代码的可读性。

异常如何分类

在系统中,我们通常根据业务场景会细分出认证类、业务类、系统类等异常。但本人认为不管异常怎么细分,都能够归属到两类异常,一是让用户看的,即业务异常;二是让开发人员看到,即系统异常。

业务异常

编写业务代码时,当业务逻辑未按照预定执行时,系统应抛出一个对用户友好的异常给用户,引导用户执行正确的操作,最基础的场景就是登陆时密码错误:

if(用户密码不匹配){
    throw new BusinessException("密码错误,请重新输入");
}

这类可预期的,用于创建对用户友好提示且能自行处理的异常,我将其称为业务异常。该类异常由统一异常处理器处理后,应将携带的异常信息返回给用户,因为这是业务信息。

系统异常

对于各种用户无法处理的异常,通常是系统错误的运行(BUG),该类异常通常需要开发人员解决。出现该类异常后,应打印详细的异常信息(现场信息),并在必要时进行报警,及时通知开发人员处理,同时携带的异常信息应转换成用户友好的提示,如“系统出错了,请稍后再试”,避免向外界透露系统底层信息。

try {
    //业务代码
}catch (Exception e){
    //必要的错误日志
    log.error("必要的错误日志", e);
    //异常转换
    throw new SysException("系统错误");
}

结合TemplateException异常对象

在上文中,我们声明了TemplateException异常对象,这里我们结合该对象进行异常处理。

  1. 声明业务异常和系统异常 异常分类的最佳实践?构建合理的异常体系
/**
 * 该类表示一个业务异常
 * 业务异常通常是用户可解决的错误,通过该类携带一个对用户友好的提示
 */
public class BusinessException extends TemplateException{
    //define
}

/**
 * 该类表示一个系统错误
 * 系统错误通常是用户无法解决异常,通过该类携带必要的现场信息
 */
public class SystemException extends TemplateException{
    //define
}
  1. 执行业务,声明两个接口
@RequestMapping("/businessException/{flag}")
public String businessException(@PathVariable String flag) {
    //测试业务异常
    String errorMessage = "flag输入错误,当前参数为:" + flag;
    throw new BusinessException(errorMessage);
}

@RequestMapping("/systemException/{flag}")
public String systemException(@PathVariable String flag) {
    //测试系统异常
    try {
        //创建异常
        throw new NullPointerException();
    } catch (Exception e) {
        //携带详细的现场信息
        throw new SystemException("出现无法解决的异常", e)
                .describe("flag: %s", flag)
                .describe("其他描述信息:%s", "其他描述信息");
    }
}
  1. 异常统一处理
@ExceptionHandler(BusinessException.class)
public String handleBusinessException(BusinessException exception) {
    String exceptionSubject = exception.getSubject();
    log.warn("业务异常:{}", exceptionSubject);
    //响应结果,直接返回业务异常携带的主题
    return exceptionSubject;
}

@ExceptionHandler(SystemException.class)
public String handleSystemException(SystemException exception) {
    List<String> descriptions = exception.getDescriptions();
    Throwable cause = exception.getCause();
    cause = Objects.isNull(cause) ? exception : cause;
    String exceptionSubject = exception.getSubject();
    log.error("系统异常:{}", exceptionSubject);
    for (String description : descriptions) {
        log.error("{}", description);
    }
    log.error("触发异常对象", cause);
    //响应结果,对异常携带的主题进行转换
    return "系统异常";
}
  1. 效果
2024-01-24 22:35:49.390  WARN 19260 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 业务异常:flag输入错误,当前参数为:1
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 系统异常:出现无法解决的异常
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : flag: 2
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 其他描述信息:其他描述信息
2024-01-24 22:35:57.731 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 触发异常对象

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

总结

本文中,根据异常情况的特点,将异常归纳为业务异常和系统异常,并结合TemplateException异常对象进行异常处理。系统可以基于这两类对象延伸出细分类,但还是需要根据异常特点进行针对性的处理,即让用户看的,显示异常细节;让开发人员看的,则隐藏异常细节。