前言
在上一篇文章中,我们阐述了如何优雅地处理异常,从创建、抛出到处理异常的完整链路中提供一种处理方案。在本文中,将尝试以实际开发经验对如何分类异常,构建一个合理的异常体系进行最佳实践。
为什么要自定义异常
在探究异常如何分类时,先思考下为什么要自定义异常类?我认为有以下几点原因
- 原始异常提供的信息载体较少。通常仅有Message、Cause可用,自定义用于扩展信息载体,如增加异常码;
- 原始异常对用户不友好。通过定义一个业务异常类来说明业务异常,并携带用户友好的提示引导用户执行正确的操作;
- 对不同的异常进行针对性的操作。像一些用于提示用户的异常,如密码错误、权限不足等应返回给用户查看,且该类异常并不需要进行额外处理。而对于系统出现的底层异常,如数据库语句执行失败,该类异常需要打印详细的现场日志信息,和对异常携带的信息转换后返回给用户。
系统异常很多开发者没有重视,经常会看到界面错误弹框携带了系统底层信息,如接口参数解析异常
这种情况就会泄露系统底层信息,在信息安全方面,这是一个中危漏洞,归根到底就是没对系统异常进行转换,只是简单地在异常处理器中把原始异常信息返回了。
总得来说,自定义异常对象是为了更好的对异常情况进行针对性的处理,同时提高代码的可读性。
异常如何分类
在系统中,我们通常根据业务场景会细分出认证类、业务类、系统类等异常。但本人认为不管异常怎么细分,都能够归属到两类异常,一是让用户看的,即业务异常;二是让开发人员看到,即系统异常。
业务异常
编写业务代码时,当业务逻辑未按照预定执行时,系统应抛出一个对用户友好的异常给用户,引导用户执行正确的操作,最基础的场景就是登陆时密码错误:
java
if(用户密码不匹配){
throw new BusinessException("密码错误,请重新输入");
}
这类可预期的,用于创建对用户友好提示且能自行处理的异常,我将其称为业务异常。该类异常由统一异常处理器处理后,应将携带的异常信息返回给用户,因为这是业务信息。
系统异常
对于各种用户无法处理的异常,通常是系统错误的运行(BUG),该类异常通常需要开发人员解决。出现该类异常后,应打印详细的异常信息(现场信息),并在必要时进行报警,及时通知开发人员处理,同时携带的异常信息应转换成用户友好的提示,如"系统出错了,请稍后再试",避免向外界透露系统底层信息。
java
try {
//业务代码
}catch (Exception e){
//必要的错误日志
log.error("必要的错误日志", e);
//异常转换
throw new SysException("系统错误");
}
结合TemplateException异常对象
在上文中,我们声明了TemplateException异常对象,这里我们结合该对象进行异常处理。
- 声明业务异常和系统异常
scala
/**
* 该类表示一个业务异常
* 业务异常通常是用户可解决的错误,通过该类携带一个对用户友好的提示
*/
public class BusinessException extends TemplateException{
//define
}
/**
* 该类表示一个系统错误
* 系统错误通常是用户无法解决异常,通过该类携带必要的现场信息
*/
public class SystemException extends TemplateException{
//define
}
- 执行业务,声明两个接口
java
@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", "其他描述信息");
}
}
- 异常统一处理
java
@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 "系统异常";
}
- 效果
shell
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异常对象进行异常处理。系统可以基于这两类对象延伸出细分类,但还是需要根据异常特点进行针对性的处理,即让用户看的,显示异常细节;让开发人员看的,则隐藏异常细节。