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

简介: JAVA轻量级错误码设计最佳实践

概述


目前公司团队项目中返回异常都没有错误码,如果遇到错误提示只会返回一个错误信息。随着微服务组件越来越多,这样给不同团队之间沟通交流、排查错误带来了极大的困难。于是在网上搜索了一番以后,决心要重新设计下错误码机制,此次设计很多是参考的lesofn.com/archives/er…, 感谢作者。


设计目标


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

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

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

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

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

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

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

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

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

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


错误码设计


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

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


错误码格式


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

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

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

1671186479455.jpg


错误码在项目中表示


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

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

模块接口代码如下:

public interface ProjectModule {
    /**
     * 项目编码
     */
    int getProjectCode();
    /**
     * 模块编码
     */
    int getModuleCode();
    /**
     * 项目名称
     */
    String getProjectName();
    /**
     * 模块名称
     */
    String getModuleName();
}

具体的模块:

@Getter
@AllArgsConstructor
public enum UserProjectCodes implements ProjectModule {
    /**
     * 登录模块
     */
    LOGIN(1, 1, "用户中心", "登录模块"),
    /**
     * 用户管理模块
     */
    USER(1, 2, "用户中心", "用户模块");
    private int projectCode;
    private int moduleCode;
    private String projectName;
    private String moduleName;
}

错误码接口代码如下:

public interface ErrorCode {
    /**
     * 最细粒度code,不包含project、module信息
     */
    int getNodeNum();
    /**
     * 异常信息 英文
     */
    String getMsg();
    /**
     * 拼接project、module、node后的完整的错误码
     */
    default int getCode() {
        return ErrorManager.genCode(this);
    }
    default ProjectModule projectModule(){
        return ErrorManager.projectModule(this);
    }
}

具体错误码枚举:

@Getter
public enum UserErrorCodes implements ErrorCode {
    /**
     * 用户不存在
     */
    USER_NOT_EXIST(0, "用户名不存在"),
    /**
     * 密码错误
     */
    PASSWORD_ERROR(1, "密码错误");
    private final int nodeNum;
    private final String msg;
    UserErrorCodes(int nodeNum, String msg) {
        this.nodeNum = nodeNum;
        this.msg = msg;
        // 注册错误码,也就是绑定这个错误码属于哪个模块的
        ErrorManager.register(UserProjectCodes.USER, this);
    }
}


错误码防重校验


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

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

ErrorManager.register(UserProjectCodes.USER, this);
public static void register(ProjectModule projectModule, ErrorCode errorCode) {
        Preconditions.checkNotNull(projectModule);
        Preconditions.checkArgument(projectModule.getProjectCode() >= 0);
        Preconditions.checkArgument(projectModule.getModuleCode() >= 0);
        Preconditions.checkArgument(errorCode.getNodeNum() >= 0);
        int code = genCode(projectModule, errorCode);
        // 如果存在重复,抛出异常
        Preconditions.checkArgument(!GLOBAL_ERROR_CODE_MAP.containsKey(code), "错误码重复:" + code);
        GLOBAL_ERROR_CODE_MAP.put(code, errorCode);
        ERROR_PROJECT_MODULE_MAP.put(errorCode, projectModule);
    }

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


返回错误码信息


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

public class Result<T> extends ErrorInfo {
  private int code;
    private String msg;
    private T data;
    .....
}
  1. 可以直接通过调用Result方法返回

1671186536244.jpg

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


错误码和异常


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

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

异常体系:

抽象出异常的统一接口:

public interface IErrorCodeException {
    /**
     * 错误信息,获取异常的错误信息
     */
    ErrorInfo getErrorInfo();
    /**
     * 模块信息,获取异常属于哪个模块的
     */
    ProjectModule projectModule();
}
public class ErrorInfo {
    static final Map<Integer, ErrorInfo> NO_PARAM_CODES_MAP = new ConcurrentHashMap<>();
    static final Map<String, ErrorInfo> ERROR_MSG_CODES_MAP = new ConcurrentHashMap<>();
    /**
     * 错误码
     */
    @Getter
    private final int code;
    /**
     * 返回错误信息 英文
     */
    @Getter
    private final String msg;
    .......
}

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

@Getter
public abstract class BaseRuntimeException extends RuntimeException implements IErrorCodeException{
    final ErrorInfo errorInfo;
    protected BaseRuntimeException(String message) {
        super(message);
        this.errorInfo = ErrorInfo.parse(message);
    }
    protected BaseRuntimeException(String message, Throwable cause) {
        super(message, cause);
        this.errorInfo = ErrorInfo.parse(message);
    }
    protected BaseRuntimeException(Throwable cause) {
        super(cause);
        this.errorInfo = ErrorInfo.parse(cause.getMessage());
    }
    protected BaseRuntimeException(ErrorInfo errorInfo) {
        super(errorInfo.toString());
        this.errorInfo = errorInfo;
    }
    protected BaseRuntimeException(ErrorCode errorCode) {
        this(ErrorInfo.parse(errorCode));
        ProjectModule.check(projectModule(), errorCode.projectModule());
    }
    protected BaseRuntimeException(ErrorCode errorCode, Object... args) {
        this(ErrorInfo.parse(errorCode, args));
        ProjectModule.check(projectModule(), errorCode.projectModule());
    }
    @Override
    public ErrorInfo getErrorInfo() {
        return errorInfo;
    }
}

具体的模块异常:

public class UserException extends BaseRuntimeException {
    protected UserException(String message) {
        super(message);
    }
    protected UserException(String message, Throwable cause) {
        super(message, cause);
    }
    protected UserException(Throwable cause) {
        super(cause);
    }
    protected UserException(ErrorInfo errorInfo) {
        super(errorInfo);
    }
    protected UserException(ErrorCode errorCode) {
        super(errorCode);
    }
    protected UserException(ErrorCode errorCode, Object... args) {
        super(errorCode, args);
    }
    @Override
    public ProjectModule projectModule() {
        return UserProjectCodes.USER;
    }
}

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

统一拦截异常返回

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

@ResponseBody
    @ExceptionHandler(value = Throwable.class)
    public ResponseEntity<Result<?>> processException(HttpServletRequest request, Exception e) {
        Pair<Throwable, String> pair = getExceptionMessage(e);
        // 如果是自定义异常
        if (e instanceof IErrorCodeException) {
            if (e.getCause() != null) {
                log.error("error, request: {}", parseParam(request), e.getCause());
            } else {
                log.error("error: {}, request: {}", pair.getRight(), parseParam(request));
            }
            ErrorInfo errorInfo = ((IErrorCodeException) e).getErrorInfo();
            Result<?> apiResult;
            if (errorInfo == null) {
                apiResult = Result.error(SystemErrorCodes.SYSTEM_ERROR.getCode(), pair.getRight());
            } else {
                apiResult = Result.error(errorInfo.getCode(), errorInfo.getMsg());
            }
            return new ResponseEntity<>(apiResult, HttpStatus.OK);
        }
        log.error("error, request: {}", parseParam(request), e);
        // 返回系统异常
        Result<String> errorResult = Result.error(SystemErrorCodes.SYSTEM_ERROR.getCode(), pair.getLeft().getClass().getSimpleName() + ": " + pair.getRight());
        return new ResponseEntity<>(errorResult, HttpStatus.OK);
    }


系统错误码处理


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

public enum SystemErrorCodes implements ErrorCode {
    SUCCESS(0, "ok"),
    SYSTEM_ERROR(1, "system error");
    private final int nodeNum;
    private final String msg;
    SystemErrorCodes(int nodeNum, String msg) {
        this.nodeNum = nodeNum;
        this.msg = msg;
        ErrorManager.register(SystemProjectModule.INSTANCE, this);
    }
}


方便查看错误码表


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

public static List<TreeNode> getAllErrorCodes() {
        return ERROR_PROJECT_MODULE_MAP.entrySet().stream()
                .sorted((it1, it2) -> ERROR_CODE_COMPARATOR.compare(it1.getKey(), it2.getKey()))
                .collect(Collectors.groupingBy(Map.Entry::getValue,
                        Collectors.mapping(Map.Entry::getKey, Collectors.toList())))
                .entrySet()
                .stream()
                .sorted((it1, it2) -> PROJECT_MODULE_COMPARATOR.compare(it1.getKey(), it2.getKey()))
                .collect(Collectors.groupingBy(
                                e -> new TreeNode(e.getKey().getProjectCode(), e.getKey().getProjectName()),
                                Collectors.groupingBy(
                                        it -> new TreeNode(it.getKey().getModuleCode(), it.getKey().getModuleName()),
                                        Collectors.mapping(Map.Entry::getValue, Collectors.toList())
                                )
                        )
                )
                .entrySet()
                .stream()
                .map(e -> {
                    TreeNode top = e.getKey();
                    List<TreeNode> middleNode = e.getValue()
                            .entrySet()
                            .stream()
                            .map(e1 -> {
                                TreeNode key = e1.getKey();
                                List<TreeNode> leftNode = e1.getValue().stream()
                                        .flatMap(Collection::stream)
                                        .map(errorCode -> new TreeNode(errorCode.getCode(), errorCode.getMsg()))
                                        .collect(Collectors.toList());
                                key.setNodes(leftNode);
                                return key;
                            })
                            .collect(Collectors.toList());
                    top.setNodes(middleNode);
                    return top;
                })
                .collect(Collectors.toList());
    }

1671186578987.jpg


更多的想法和注意事项


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

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

1671186595207.jpg

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

1671186603086.jpg

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

1671186610714.jpg


总结


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

目录
相关文章
|
25天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
45 5
|
3月前
|
监控 Java 测试技术
Java并发编程最佳实践:设计高性能的多线程系统
Java并发编程最佳实践:设计高性能的多线程系统
65 1
|
3月前
|
Rust Java 文件存储
Java系统中的错误码设计问题之通过properties文件管理Error Code如何解决
Java系统中的错误码设计问题之通过properties文件管理Error Code如何解决
53 1
|
3月前
|
安全 搜索推荐 Java
Java系统中的错误码设计问题之确保内外有别并避免暴露敏感数据如何解决
Java系统中的错误码设计问题之确保内外有别并避免暴露敏感数据如何解决
35 0
|
3月前
|
IDE Java 开发工具
Java系统中的错误码设计问题之为Java项目中的错误消息提供国际化支持如何解决
Java系统中的错误码设计问题之为Java项目中的错误消息提供国际化支持如何解决
51 0
|
14天前
|
JSON 自然语言处理 Java
这款轻量级 Java 表达式引擎,真不错!
AviatorScript 是一个高性能、轻量级的脚本语言,基于 JVM(包括 Android 平台)。它支持数字、字符串、正则表达式、布尔值等基本类型,以及所有 Java 运算符。主要特性包括函数式编程、大整数和高精度运算、完整的脚本语法、丰富的内置函数和自定义函数支持。适用于规则判断、公式计算、动态脚本控制等场景。
|
13天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
16天前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
14天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
41 1
|
17天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
35 2
下一篇
无影云桌面