前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >异常设计实践

异常设计实践

原创
作者头像
码代码的陈同学
发布2018-05-21 18:27:09
9310
发布2018-05-21 18:27:09
举报

欢迎访问作者博客原文,阅读体验更佳

前段时间结合SpringCloud网关处理异常写了篇 异常处理实践,侧重于异常的处理。作为强迫症患者,本次撰写下如何进行异常设计并提供具体的代码。

如何设计异常结构?

异常结构取决于其应用场景,与其关联的角色有:用户、运营人员、技术人员.

  • 用户:需对用户操作进行直接反馈,异常消息需要非常友好
  • 运营人员:需立即知晓哪位客户、什么时候、在做什么操作时、因为什么原因、发生了什么问题,再主动处理问题
  • 技术人员:除上述运营人员的数据外,还需知道用户用什么设备、请求参数、响应的数据、异常Stacktrace、日志等基本信息;最好能够用户环境信息,如:token、应用实例、所在主机等

由于大部分数据在处理异常时均可以获取到,因此异常结构可以十分精简,结构如下:

  • 状态码:提供给使用API的开发人员
  • 提示信息:对状态码做出描述
  • 日志:提供给开发人员判断问题,往往带有数据的ID、Number等

需要设计多少种异常?

业务系统和纯技术类 "产品"(泛指技术框架、组件等)在异常设计时最大的区别是:技术类产品的用户都是技术人员

  • 面向技术人员:通过不同的异常和日志,就可以表示不同的异常场景,异常往往 顾名思义,看一眼就能判断大概发生了什么问题
  • 面向用户:必须向用户提供 简明易懂 的描述

因此,业务系统可以设计通用的异常类,例如:BusinessException.

异常设计简单Demo

类图

Code为接口,因Mac StartUML 接口无法展现方法,这里用类代替

classDiagram
classDiagram

类图简述及代码

StatusCode 状态码结构

简单的提示信息可以hardcode, 一般提示信息都存DB然后缓存在Redis,msgData用于存储提示信息中的数据

代码语言:txt
复制
/**
 * 状态码
 */
public class StatusCode {

    // 状态码
    private String code;

    // 提示信息, 反馈给用户
    private String msg;

    // 提示消息相关数据
    private List<String> msgData;

    // 日志, 用于开发人员定位问题
    private String log;
}
Code 状态码行为

所有枚举类状态码实例都需提供获取状态码和提示消息的方法

代码语言:txt
复制
/**
 * 状态码
 * <p>规范异常状态码行为</p>
 */
public interface Code {
    String getCode();
    String getMsg();
}
UserCode 用户模块状态码

提供两个样例状态码,一个hardcode提示信息,一个仅提供状态码

代码语言:txt
复制
/**
 * 用户模块状态码
 */
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类型构造器参数,所有枚举类型的状态码都可以作为参数

代码语言:txt
复制
/**
 * 异常基类
 */
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;
    }
}

单元测试

代码语言:txt
复制
/**
 * 处理异常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.返回状态码和提示信息给前端, 同时异步持久化异常并预警
    }
}

为什么将日志设计在异常中?

一般而言,抛出异常时我们会打印日志,例如:

代码语言:txt
复制
logger.warn("发生了XXX问题,ID:{}", "1001");
throw new XXXException("发生了XXX问题");

在平台拥有良好的日志收集、日志分析工具时(如:采用ELK),可以采用这种方式。在每个Request进来时分配一个requestId贯穿整个调用过程,处理异常时通过当前requestId就可以获取所有信息.

在不具备上述能力时,带着日志一起跟随异常抛出并持久化。在发生问题时,开发人员不用在各个服务器上到处找日志,通过预警邮件就可以获取全部异常信息来定位问题.

源码下载

本文源码见Github: https://github.com/genter/exceptionDemo

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 如何设计异常结构?
  • 需要设计多少种异常?
  • 异常设计简单Demo
    • 类图
      • 类图简述及代码
        • StatusCode 状态码结构
        • Code 状态码行为
        • UserCode 用户模块状态码
        • BaseException 基础异常类
      • 单元测试
      • 为什么将日志设计在异常中?
      • 源码下载
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档