先谈公司现状,我们目前就是通过抛出Java异常,将异常信息Message返回给其他服务或者前端,这样有什么问题?
因此,我们需要设计一套错误码机制,那一个优秀的错误码设计应该达到什么目标或者有什么原则呢?
我们可以参考下阿里巴巴《Java 开发手册》- 异常日志-错误码的内容。
错误码的制定原则:快速溯源、简单易记、沟通标准化。
说明:错误码想得过于完美和复杂,就像康熙字典中的生僻字一样,用词似乎精准,但是字典不容易随身携带并且简单易懂。
正例:错误码回答的问题是谁的错?错在哪?
1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
2)错误码易于记忆和比对(代码中容易 equals)。
3)错误码能够脱离文档和系统平台达到线下轻量化地自由沟通的目的。
错误码设计,主要考虑下面几个点:
错误码格式一方面要求精简,同时也要体现出它的服务组件和模块信息。
错误码的数据类型可以是字符串,比如"SDM_USER_001", 也可以用数值表示10203等,他们可以利弊,用字符串可能比较直观,用数字比较精简,但是使用纯数字来进行错误码编排不利于感性记忆和分类。
我们本例采用了6位数字的方式演示,前面两位是项目编码,中间两位是模块编码,最后3位是错编码:

错误码在项目中该如何呈现呢?
模块接口代码如下:
- 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
extends ErrorInfo { -
- private int code;
-
- private String msg;
-
- private T data;
-
- .....
-
- }
- 复制代码

错误码和异常往往是分不开的,可以通过设计良好的异常体系,将错误码优雅的返回给调用方:
异常体系:
抽象出异常的统一接口:
- 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());
- }
- 复制代码

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

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


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