• JAVA轻量级错误码设计最佳实践


    设计目标

    先谈公司现状,我们目前就是通过抛出Java异常,将异常信息Message返回给其他服务或者前端,这样有什么问题?

    1. 不好交流沟通,本来只要通过一个明确的错误码就能沟通清楚,用异常信息会带来模糊性。
    2. 只有异常消息,很难定位具体是哪个服务的原因,具体原因是什么,因为异常消息比较灵活,可能是不同服务、同服务不同模块都有相似的。

    因此,我们需要设计一套错误码机制,那一个优秀的错误码设计应该达到什么目标或者有什么原则呢?

    我们可以参考下阿里巴巴《Java 开发手册》- 异常日志-错误码的内容。

    错误码的制定原则:快速溯源、简单易记、沟通标准化。

    说明:错误码想得过于完美和复杂,就像康熙字典中的生僻字一样,用词似乎精准,但是字典不容易随身携带并且简单易懂。

    正例:错误码回答的问题是谁的错?错在哪?

    1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。

    2)错误码易于记忆和比对(代码中容易 equals)。

    3)错误码能够脱离文档和系统平台达到线下轻量化地自由沟通的目的。

    错误码设计

    错误码设计,主要考虑下面几个点:

    1. 错误码的格式该是什么样?
    2. 错误码在java中该如何表示,枚举,常量?
    3. 错误码怎么保证唯一性,开发可能不知道已经被占用了,该如何避免?
    4. 错误码该如何返回?

    错误码格式

    错误码格式一方面要求精简,同时也要体现出它的服务组件和模块信息。

    错误码的数据类型可以是字符串,比如"SDM_USER_001", 也可以用数值表示10203等,他们可以利弊,用字符串可能比较直观,用数字比较精简,但是使用纯数字来进行错误码编排不利于感性记忆和分类。

    我们本例采用了6位数字的方式演示,前面两位是项目编码,中间两位是模块编码,最后3位是错编码:

    错误码在项目中表示

    错误码在项目中该如何呈现呢?

    1. 错误码统一通过枚举类表现,可以轻松的表示枚举的代码和对应的含义,易于维护。
    2. 按照项目+模块粒度定义成多个错误码枚举类,也就是一个模块一个错误码的枚举类。
    3. 定义出公共的错误码的接口,让各个服务组件、模块实现,统一错误码的参数。
    4. 分离出项目模块编码,增加模块编码枚举的复用性。

    模块接口代码如下:

    1. public interface ProjectModule {
    2. /**
    3. * 项目编码
    4. */
    5. int getProjectCode();
    6. /**
    7. * 模块编码
    8. */
    9. int getModuleCode();
    10. /**
    11. * 项目名称
    12. */
    13. String getProjectName();
    14. /**
    15. * 模块名称
    16. */
    17. String getModuleName();
    18. }
    19. 复制代码

    具体的模块:

    1. @Getter
    2. @AllArgsConstructor
    3. public enum UserProjectCodes implements ProjectModule {
    4. /**
    5. * 登录模块
    6. */
    7. LOGIN(1, 1, "用户中心", "登录模块"),
    8. /**
    9. * 用户管理模块
    10. */
    11. USER(1, 2, "用户中心", "用户模块");
    12. private int projectCode;
    13. private int moduleCode;
    14. private String projectName;
    15. private String moduleName;
    16. }
    17. 复制代码

    错误码接口代码如下:

    1. public interface ErrorCode {
    2. /**
    3. * 最细粒度code,不包含project、module信息
    4. */
    5. int getNodeNum();
    6. /**
    7. * 异常信息 英文
    8. */
    9. String getMsg();
    10. /**
    11. * 拼接project、module、node后的完整的错误码
    12. */
    13. default int getCode() {
    14. return ErrorManager.genCode(this);
    15. }
    16. default ProjectModule projectModule(){
    17. return ErrorManager.projectModule(this);
    18. }
    19. }
    20. 复制代码

    具体错误码枚举:

    1. @Getter
    2. public enum UserErrorCodes implements ErrorCode {
    3. /**
    4. * 用户不存在
    5. */
    6. USER_NOT_EXIST(0, "用户名不存在"),
    7. /**
    8. * 密码错误
    9. */
    10. PASSWORD_ERROR(1, "密码错误");
    11. private final int nodeNum;
    12. private final String msg;
    13. UserErrorCodes(int nodeNum, String msg) {
    14. this.nodeNum = nodeNum;
    15. this.msg = msg;
    16. // 注册错误码,也就是绑定这个错误码属于哪个模块的
    17. ErrorManager.register(UserProjectCodes.USER, this);
    18. }
    19. }
    20. 复制代码

    错误码防重校验

    错误码要求全局唯一,不能存在重复冲突,那么怎么通过程序来做一个校验。

    我们在上面定义错误码枚举的构造函数中,有了一个注册错误码的操作,我们其实可以在这里做重复校验的逻辑。

    1. ErrorManager.register(UserProjectCodes.USER, this);
    2. 复制代码
    1. public static void register(ProjectModule projectModule, ErrorCode errorCode) {
    2. Preconditions.checkNotNull(projectModule);
    3. Preconditions.checkArgument(projectModule.getProjectCode() >= 0);
    4. Preconditions.checkArgument(projectModule.getModuleCode() >= 0);
    5. Preconditions.checkArgument(errorCode.getNodeNum() >= 0);
    6. int code = genCode(projectModule, errorCode);
    7. // 如果存在重复,抛出异常
    8. Preconditions.checkArgument(!GLOBAL_ERROR_CODE_MAP.containsKey(code), "错误码重复:" + code);
    9. GLOBAL_ERROR_CODE_MAP.put(code, errorCode);
    10. ERROR_PROJECT_MODULE_MAP.put(errorCode, projectModule);
    11. }
    12. 复制代码

    这样就会抛出异常,提前知道存在重复的错误码。

    返回错误码信息

    接口调用后,该如何返回错误码信息呢? 一般我们的项目中都会定义统一的返回对象,该对象一般会有code, msg信息,如下:

    1. public class Result extends ErrorInfo {
    2. private int code;
    3. private String msg;
    4. private T data;
    5. .....
    6. }
    7. 复制代码
    1. 可以直接通过调用Result方法返回

    1. 通过统一异常处理返回,下面详细讲解

    错误码和异常

    错误码和异常往往是分不开的,可以通过设计良好的异常体系,将错误码优雅的返回给调用方:

    1. 自定义异常, 自定义异常中包含了错误信息的属性字段
    2. 如果程序逻辑中出错,可以设置对应的错误码信息,抛出自定义异常
    3. 统一拦截自定义异常,后去其中的异常信息,返回给请求方

    异常体系:

    抽象出异常的统一接口:

    1. public interface IErrorCodeException {
    2. /**
    3. * 错误信息,获取异常的错误信息
    4. */
    5. ErrorInfo getErrorInfo();
    6. /**
    7. * 模块信息,获取异常属于哪个模块的
    8. */
    9. ProjectModule projectModule();
    10. }
    11. 复制代码
    1. public class ErrorInfo {
    2. static final Map<Integer, ErrorInfo> NO_PARAM_CODES_MAP = new ConcurrentHashMap<>();
    3. static final Map<String, ErrorInfo> ERROR_MSG_CODES_MAP = new ConcurrentHashMap<>();
    4. /**
    5. * 错误码
    6. */
    7. @Getter
    8. private final int code;
    9. /**
    10. * 返回错误信息 英文
    11. */
    12. @Getter
    13. private final String msg;
    14. .......
    15. }
    16. 复制代码

    自定义运行时异常抽象基类:

    1. @Getter
    2. public abstract class BaseRuntimeException extends RuntimeException implements IErrorCodeException{
    3. final ErrorInfo errorInfo;
    4. protected BaseRuntimeException(String message) {
    5. super(message);
    6. this.errorInfo = ErrorInfo.parse(message);
    7. }
    8. protected BaseRuntimeException(String message, Throwable cause) {
    9. super(message, cause);
    10. this.errorInfo = ErrorInfo.parse(message);
    11. }
    12. protected BaseRuntimeException(Throwable cause) {
    13. super(cause);
    14. this.errorInfo = ErrorInfo.parse(cause.getMessage());
    15. }
    16. protected BaseRuntimeException(ErrorInfo errorInfo) {
    17. super(errorInfo.toString());
    18. this.errorInfo = errorInfo;
    19. }
    20. protected BaseRuntimeException(ErrorCode errorCode) {
    21. this(ErrorInfo.parse(errorCode));
    22. ProjectModule.check(projectModule(), errorCode.projectModule());
    23. }
    24. protected BaseRuntimeException(ErrorCode errorCode, Object... args) {
    25. this(ErrorInfo.parse(errorCode, args));
    26. ProjectModule.check(projectModule(), errorCode.projectModule());
    27. }
    28. @Override
    29. public ErrorInfo getErrorInfo() {
    30. return errorInfo;
    31. }
    32. }
    33. 复制代码

    具体的模块异常:

    1. public class UserException extends BaseRuntimeException {
    2. protected UserException(String message) {
    3. super(message);
    4. }
    5. protected UserException(String message, Throwable cause) {
    6. super(message, cause);
    7. }
    8. protected UserException(Throwable cause) {
    9. super(cause);
    10. }
    11. protected UserException(ErrorInfo errorInfo) {
    12. super(errorInfo);
    13. }
    14. protected UserException(ErrorCode errorCode) {
    15. super(errorCode);
    16. }
    17. protected UserException(ErrorCode errorCode, Object... args) {
    18. super(errorCode, args);
    19. }
    20. @Override
    21. public ProjectModule projectModule() {
    22. return UserProjectCodes.USER;
    23. }
    24. }
    25. 复制代码

    这里的异常体系中绑定了属于哪个模块,主要是从严谨性的角度出发,因为错误码本身是绑定了模块的,这时候再将错误码设置到异常中,可以做一个校验,是否是属于同一个模块。

    统一拦截异常返回

    通过spring提供的异常拦截注解@ControllerAdvice,实现对异常的统一处理。

    1. @ResponseBody
    2. @ExceptionHandler(value = Throwable.class)
    3. public ResponseEntity<Result<?>> processException(HttpServletRequest request, Exception e) {
    4. Pair<Throwable, String> pair = getExceptionMessage(e);
    5. // 如果是自定义异常
    6. if (e instanceof IErrorCodeException) {
    7. if (e.getCause() != null) {
    8. log.error("error, request: {}", parseParam(request), e.getCause());
    9. } else {
    10. log.error("error: {}, request: {}", pair.getRight(), parseParam(request));
    11. }
    12. ErrorInfo errorInfo = ((IErrorCodeException) e).getErrorInfo();
    13. Result<?> apiResult;
    14. if (errorInfo == null) {
    15. apiResult = Result.error(SystemErrorCodes.SYSTEM_ERROR.getCode(), pair.getRight());
    16. } else {
    17. apiResult = Result.error(errorInfo.getCode(), errorInfo.getMsg());
    18. }
    19. return new ResponseEntity<>(apiResult, HttpStatus.OK);
    20. }
    21. log.error("error, request: {}", parseParam(request), e);
    22. // 返回系统异常
    23. Result<String> errorResult = Result.error(SystemErrorCodes.SYSTEM_ERROR.getCode(), pair.getLeft().getClass().getSimpleName() + ": " + pair.getRight());
    24. return new ResponseEntity<>(errorResult, HttpStatus.OK);
    25. }
    26. 复制代码

    系统错误码处理

    因为不同模块会存在一些公共的系统异常,针对这种,我们需要内置定义一个系统的错误码:

    1. public enum SystemErrorCodes implements ErrorCode {
    2. SUCCESS(0, "ok"),
    3. SYSTEM_ERROR(1, "system error");
    4. private final int nodeNum;
    5. private final String msg;
    6. SystemErrorCodes(int nodeNum, String msg) {
    7. this.nodeNum = nodeNum;
    8. this.msg = msg;
    9. ErrorManager.register(SystemProjectModule.INSTANCE, this);
    10. }
    11. }
    12. 复制代码

    方便查看错误码表

    最后这个也很关键,往往我们需要看下系统中全量的错误码,这时候我们可以开放一个接口获取系统中全量错误码,或者将他们展示到前端页面中。

    1. public static List<TreeNode> getAllErrorCodes() {
    2. return ERROR_PROJECT_MODULE_MAP.entrySet().stream()
    3. .sorted((it1, it2) -> ERROR_CODE_COMPARATOR.compare(it1.getKey(), it2.getKey()))
    4. .collect(Collectors.groupingBy(Map.Entry::getValue,
    5. Collectors.mapping(Map.Entry::getKey, Collectors.toList())))
    6. .entrySet()
    7. .stream()
    8. .sorted((it1, it2) -> PROJECT_MODULE_COMPARATOR.compare(it1.getKey(), it2.getKey()))
    9. .collect(Collectors.groupingBy(
    10. e -> new TreeNode(e.getKey().getProjectCode(), e.getKey().getProjectName()),
    11. Collectors.groupingBy(
    12. it -> new TreeNode(it.getKey().getModuleCode(), it.getKey().getModuleName()),
    13. Collectors.mapping(Map.Entry::getValue, Collectors.toList())
    14. )
    15. )
    16. )
    17. .entrySet()
    18. .stream()
    19. .map(e -> {
    20. TreeNode top = e.getKey();
    21. List<TreeNode> middleNode = e.getValue()
    22. .entrySet()
    23. .stream()
    24. .map(e1 -> {
    25. TreeNode key = e1.getKey();
    26. List<TreeNode> leftNode = e1.getValue().stream()
    27. .flatMap(Collection::stream)
    28. .map(errorCode -> new TreeNode(errorCode.getCode(), errorCode.getMsg()))
    29. .collect(Collectors.toList());
    30. key.setNodes(leftNode);
    31. return key;
    32. })
    33. .collect(Collectors.toList());
    34. top.setNodes(middleNode);
    35. return top;
    36. })
    37. .collect(Collectors.toList());
    38. }
    39. 复制代码

    \

    更多的想法和注意事项

    在参考了阿里巴巴关于错误码和异常的规范后,梳理出了有效的几条,得出了更多的想法,大家可以根据自己的实际项目去做抉择:

    1. 错误码分类的另外一种格式规范, 也很合理,这样调用方基本知道是自己的问题还是谁的问题了。

    2. 很多大型公司,会有统一开发平台,上面可以维护错误码,这样所以服务组件可以快捷查询、新建错误码,也不会重现重复的情况,当然,这一般都是中大型公司会有。

    1. 其实如果单纯是服务内部的前后端交互,可以直接通过异常抛出,更加简单。

    总结

    本文主要讲解了错误码设计的方案,希望对大家有帮助,如果有更好的方案设计,也可以留言,一起成长提高。


     

  • 相关阅读:
    python 基于django协同过滤的旅游推荐系统
    猫声音嘶哑的常见原因
    Navicat 16 下载、安装、重装
    【打卡】【Linux 设备管理机制】21天学习挑战赛—RK3399平台开发入门到精通-Day17
    FILE类与IO流
    shell脚本常用语句记录--持续更新
    vue中$nextTick的使用
    零基础转行网络安全,难度大吗?
    E签宝面试题
    CSS 常见属性设置
  • 原文地址:https://blog.csdn.net/Trouvailless/article/details/126663476