概述
目前公司团队项目中返回异常都没有错误码,如果遇到错误提示只会返回一个错误信息。随着微服务组件越来越多,这样给不同团队之间沟通交流、排查错误带来了极大的困难。于是在网上搜索了一番以后,决心要重新设计下错误码机制,此次设计很多是参考的lesofn.com/archives/er…, 感谢作者。
设计目标
先谈公司现状,我们目前就是通过抛出Java异常,将异常信息Message返回给其他服务或者前端,这样有什么问题?
- 不好交流沟通,本来只要通过一个明确的错误码就能沟通清楚,用异常信息会带来模糊性。
- 只有异常消息,很难定位具体是哪个服务的原因,具体原因是什么,因为异常消息比较灵活,可能是不同服务、同服务不同模块都有相似的。
因此,我们需要设计一套错误码机制,那一个优秀的错误码设计应该达到什么目标或者有什么原则呢?
我们可以参考下阿里巴巴《Java 开发手册》- 异常日志-错误码的内容。
错误码的制定原则:快速溯源、简单易记、沟通标准化。
说明:错误码想得过于完美和复杂,就像康熙字典中的生僻字一样,用词似乎精准,但是字典不容易随身携带并且简单易懂。
正例:错误码回答的问题是谁的错?错在哪?
1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
2)错误码易于记忆和比对(代码中容易 equals)。
3)错误码能够脱离文档和系统平台达到线下轻量化地自由沟通的目的。
错误码设计
错误码设计,主要考虑下面几个点:
- 错误码的格式该是什么样?
- 错误码在java中该如何表示,枚举,常量?
- 错误码怎么保证唯一性,开发可能不知道已经被占用了,该如何避免?
- 错误码该如何返回?
错误码格式
错误码格式一方面要求精简,同时也要体现出它的服务组件和模块信息。
错误码的数据类型可以是字符串,比如"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<T> extends ErrorInfo { private int code; private String msg; private T data; ..... }
- 可以直接通过调用Result方法返回
- 通过统一异常处理返回,下面详细讲解
错误码和异常
错误码和异常往往是分不开的,可以通过设计良好的异常体系,将错误码优雅的返回给调用方:
- 自定义异常, 自定义异常中包含了错误信息的属性字段
- 如果程序逻辑中出错,可以设置对应的错误码信息,抛出自定义异常
- 统一拦截自定义异常,后去其中的异常信息,返回给请求方
异常体系:
抽象出异常的统一接口:
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. 很多大型公司,会有统一开发平台,上面可以维护错误码,这样所以服务组件可以快捷查询、新建错误码,也不会重现重复的情况,当然,这一般都是中大型公司会有。
- 其实如果单纯是服务内部的前后端交互,可以直接通过异常抛出,更加简单。
总结
本文主要讲解了错误码设计的方案,希望对大家有帮助,如果有更好的方案设计,也可以留言,一起成长提高。