陈同学
微服务
Accelerator
About
# 异常设计实践 前段时间结合SpringCloud网关处理异常写了篇 [异常处理实践](https://chenyongjun.vip/articles/19),侧重于异常的处理。作为强迫症患者,本次撰写下如何进行异常设计并提供具体的代码。 ## 如何设计异常结构? 异常结构取决于其应用场景,与其关联的角色有:**用户、运营人员、技术人员**. * **用户**:需对用户操作进行直接反馈,异常消息需要非常友好 * **运营人员**:需立即知晓哪位客户、什么时候、在做什么操作时、因为什么原因、发生了什么问题,再主动处理问题 * **技术人员**:除上述运营人员的数据外,还需知道用户用什么设备、请求参数、响应的数据、异常Stacktrace、日志等基本信息;最好能够用户环境信息,如:token、应用实例、所在主机等 由于大部分数据在处理异常时均可以获取到,因此异常结构可以十分精简,结构如下: * 状态码:提供给使用API的开发人员 * 提示信息:对状态码做出描述 * 日志:提供给开发人员判断问题,往往带有数据的ID、Number等 ## 需要设计多少种异常? 业务系统和纯技术类 "产品"(泛指技术框架、组件等)在异常设计时最大的区别是:**技术类产品的用户都是技术人员**。 * **面向技术人员**:通过不同的异常和日志,就可以表示不同的异常场景,异常往往 **顾名思义**,看一眼就能判断大概发生了什么问题 * **面向用户**:必须向用户提供 **简明易懂** 的描述 因此,业务系统可以设计通用的异常类,例如:BusinessException. ## 异常设计简单Demo ### 类图 > Code为接口,因Mac StartUML 接口无法展现方法,这里用类代替  ### 类图简述及代码 #### StatusCode 状态码结构 简单的提示信息可以hardcode, 一般提示信息都存DB然后缓存在Redis,msgData用于存储提示信息中的数据 ```java /** * 状态码 */ public class StatusCode { // 状态码 private String code; // 提示信息, 反馈给用户 private String msg; // 提示消息相关数据 private List<String> msgData; // 日志, 用于开发人员定位问题 private String log; } ``` #### Code 状态码行为 所有枚举类状态码实例都需提供获取状态码和提示消息的方法 ```java /** * 状态码 * <p>规范异常状态码行为</p> */ public interface Code { String getCode(); String getMsg(); } ``` #### UserCode 用户模块状态码 提供两个样例状态码,一个hardcode提示信息,一个仅提供状态码 ```java /** * 用户模块状态码 */ 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`类型构造器参数,所有枚举类型的状态码都可以作为参数 ```java /** * 异常基类 */ 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()); } public StatusCode getStatusCode() { return this.statusCode; } /** * 添加日志 * * @param log 日志信息 * @return */ public BaseException log(String log) { this.statusCode.setLog(log); return this; } /** * 设置提示消息, 替换枚举类中占位符 * * @param args 参数 * @return */ public BaseException msg(Object... args) { this.statusCode.setMsg(format(this.statusCode.getMsg(), args)); return this; } /** * 设置异常消息所需要的数据 * * @param msgData 参数 * @return */ public BaseException msgData(List<String> msgData) { this.statusCode.setMsgData(msgData); return this; } /** * 设置异常消息所需要的数据 * * @param args 参数 * @return */ public 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; } } ``` ### 单元测试 ```java /** * 处理异常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.返回状态码和提示信息给前端, 同时异步持久化异常并预警 } } ``` ## 为什么将日志设计在异常中? 一般而言,抛出异常时我们会打印日志,例如: ```java logger.warn("发生了XXX问题,ID:{}", "1001"); throw new XXXException("发生了XXX问题"); ``` 在平台拥有良好的日志收集、日志分析工具时(如:采用ELK),可以采用这种方式。在每个Request进来时分配一个requestId贯穿整个调用过程,处理异常时通过当前requestId就可以获取所有信息. 在不具备上述能力时,带着日志一起跟随异常抛出并持久化。在发生问题时,开发人员不用在各个服务器上到处找日志,通过预警邮件就可以获取全部异常信息来定位问题. ## 源码下载 本文源码见Github: [https://github.com/genter/exceptionDemo](https://github.com/genter/exceptionDemo)
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
异常设计实践
文章链接:
https://chenyongjun.vip/articles/24
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力