文章来源:陈同学 | 异常设计实践
前段时间结合SpringCloud网关处理异常写了篇 异常处理实践,侧重于异常的处理。作为强迫症患者,本次撰写下如何进行异常设计并提供具体的代码。
如何设计异常结构?
异常结构取决于其应用场景,与其关联的角色有:用户、运营人员、技术人员.
- 用户:需对用户操作进行直接反馈,异常消息需要非常友好
- 运营人员:需立即知晓哪位客户、什么时候、在做什么操作时、因为什么原因、发生了什么问题,再主动处理问题
- 技术人员:除上述运营人员的数据外,还需知道用户用什么设备、请求参数、响应的数据、异常Stacktrace、日志等基本信息;最好能够用户环境信息,如:token、应用实例、所在主机等
由于大部分数据在处理异常时均可以获取到,因此异常结构可以十分精简,结构如下:
- 状态码:提供给使用API的开发人员
- 提示信息:对状态码做出描述
- 日志:提供给开发人员判断问题,往往带有数据的ID、Number等
需要设计多少种异常?
业务系统和纯技术类 “产品”(泛指技术框架、组件等)在异常设计时最大的区别是:技术类产品的用户都是技术人员。
- 面向技术人员:通过不同的异常和日志,就可以表示不同的异常场景,异常往往 顾名思义,看一眼就能判断大概发生了什么问题
- 面向用户:必须向用户提供 简明易懂 的描述
因此,业务系统可以设计通用的异常类,例如:BusinessException.
异常设计简单Demo
类图
Code为接口,因Mac StartUML 接口无法展现方法,这里用类代替
类图简述及代码
StatusCode 状态码结构
简单的提示信息可以hardcode, 一般提示信息都存DB然后缓存在Redis,msgData用于存储提示信息中的数据
/**
* 状态码
*/
public class StatusCode {
// 状态码
private String code;
// 提示信息, 反馈给用户
private String msg;
// 提示消息相关数据
private List<String> msgData;
// 日志, 用于开发人员定位问题
private String log;
}
Code 状态码行为
所有枚举类状态码实例都需提供获取状态码和提示消息的方法
/**
* 状态码
* <p>规范异常状态码行为</p>
*/
public interface Code {
String getCode();
String getMsg();
}
UserCode 用户模块状态码
提供两个样例状态码,一个hardcode提示信息,一个仅提供状态码
/**
* 用户模块状态码
*/
public enum UserCode implements Code {
USER_NAME_ERROR("1001", "用户名 %s 不存在"),
EMAIL_OR_MOBILE_FORMAT_ERROR("1002") // 手机号[%s]或邮箱[%s]格式错误
;
private String code;
private String msg;
UserCode(String code) {
this.code = code;
}
UserCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return this.code;
}
public String getMsg() {
return this.msg;
}
}
BaseException 基础异常类
所有业务系统的基类,继承自RuntimeException.
StatusCode
作为异常的私有属性,提供了设置日志、提示信息的方法.
BaseException
接收Code
类型构造器参数,所有枚举类型的状态码都可以作为参数
/**
* 异常基类
*/
public class BaseException extends RuntimeException {
private final StatusCode statusCode;
public BaseException(Code code) {
this(code, null);
}
public BaseException(Code code, Throwable throwable) {
super(code.toString(), throwable);
this.statusCode = new StatusCode();
statusCode.setCode(code.getCode());
statusCode.setMsg(code.getMsg());
}
protected StatusCode getStatusCode() {
return this.statusCode;
}
/**
* 添加日志
*
* @param log 日志信息
* @return
*/
protected BaseException log(String log) {
this.statusCode.setLog(log);
return this;
}
/**
* 设置提示消息, 替换枚举类中占位符
*
* @param args 参数
* @return
*/
protected BaseException msg(Object... args) {
this.statusCode.setMsg(format(this.statusCode.getMsg(), args));
return this;
}
/**
* 设置异常消息所需要的数据
*
* @param msgData 参数
* @return
*/
protected BaseException msgData(List<String> msgData) {
this.statusCode.setMsgData(msgData);
return this;
}
/**
* 设置异常消息所需要的数据
*
* @param args 参数
* @return
*/
protected BaseException msgData(String... args) {
msgData(Arrays.asList(args));
return this;
}
/**
* 格式化模板消息
*
* @param messageTemplate 消息模板
* @param args 参数
* @return
*/
public final static String format(String messageTemplate, Object... args) {
if (args.length > 0) {
try {
messageTemplate = String.format(messageTemplate, args);
} catch (Exception e) {
e.printStackTrace();
}
}
return messageTemplate;
}
}
单元测试
/**
* 处理异常Demo
*/
@Test
public void handleExceptionDemo() {
// 提示消息cache, 数据可以来源于各种缓存中间件, 此处仅做演示
Map<String, String> messageCache = new HashMap<String, String>();
messageCache.put("1002", "邮箱[%s]或手机号[%s]格式不正确");
try {
String email = "cyj@aaa.com.com";
String mobile = "1310000";
throw new BaseException(UserCode.EMAIL_OR_MOBILE_FORMAT_ERROR)
.msgData(email, mobile)
.log("User credential validate failed, email:" + email + ", mobile:" + mobile);
} catch (BaseException e) {
System.out.println(e.getStatusCode());
// 假设此处为异常处理逻辑
StatusCode code = e.getStatusCode();
// 1.获取提示信息
String tips = BaseException.format(messageCache.get(code.getCode()), code.getMsgData().toArray());
assertEquals(tips, "邮箱[cyj@aaa.com.com]或手机号[1310000]格式不正确");
// 2.获取日志信息
String log = code.getLog();
assertEquals(log, "User credential validate failed, email:cyj@aaa.com.com, mobile:1310000");
// 3.获取当前用户环境的更多信息
// 4.返回状态码和提示信息给前端, 同时异步持久化异常并预警
}
}
为什么将日志设计在异常中?
一般而言,抛出异常时我们会打印日志,例如:
logger.warn("发生了XXX问题,ID:{}", "1001");
throw new XXXException("发生了XXX问题");
在平台拥有良好的日志收集、日志分析工具时(如:采用ELK),可以采用这种方式。在每个Request进来时分配一个requestId贯穿整个调用过程,处理异常时通过当前requestId就可以获取所有信息.
在不具备上述能力时,带着日志一起跟随异常抛出并持久化。在发生问题时,开发人员不用在各个服务器上到处找日志,通过预警邮件就可以获取全部异常信息来定位问题.
源码下载
本文源码见Github: https://github.com/genter/exceptionDemo