异常设计实践

文章来源:陈同学 | 异常设计实践

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

如何设计异常结构?

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

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

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

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

需要设计多少种异常?

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

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

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

异常设计简单Demo

类图

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

classDiagram

类图简述及代码

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 = "[email protected]";
        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, "邮箱[[email protected]]或手机号[1310000]格式不正确");

        // 2.获取日志信息
        String log = code.getLog();
        assertEquals(log, "User credential validate failed, email:[email protected], mobile:1310000");

        // 3.获取当前用户环境的更多信息

        // 4.返回状态码和提示信息给前端, 同时异步持久化异常并预警
    }
}

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

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

logger.warn("发生了XXX问题,ID:{}", "1001");
throw new XXXException("发生了XXX问题");

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

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

源码下载

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

猜你喜欢

转载自blog.csdn.net/myle69/article/details/80207939