后端开发如何优雅地进行错误码管理
0. 前言
在一个项目乃乃至整个团队在所有项目的开发过程中,统一设计和规范使用错误码,是必须要落实的一个点。
错误码的设计,直接关系到后端接口的条理性和优雅性;错误码的使用,直接关系到开发人员的开发规范和开发效率。
所以,对错误码进行优雅的管理,是非常有必要的,是一个团队的基石之一。
下面,我会从如何设计,管理和多语言错误码的具体实现等三个方面一一阐述。1. 设计
首先,需要对错误码大致分为两类:通用错误码特殊含义错误码
其次,错误码需要包含的要素:错误码中文错误信息英文错误信息1.1 通用错误码
主要包含:成功和失败(未知错误)SUCCESS("10000", "成功", "ok"),UNKNOWN_ERROR("-1", "未知错误", "unknown error")1.2 特殊含义错误码和用户相关的错误,如:
A_PARAM_VALIDATION_ERROR("A0100", "参数校验失败", "parameter validation error")和第三方依赖相关的错误,如:
B_STORAGE_ERROR("B0100", "持久化存储错误", "storage error")和当前业务系统相关的错误,如:
C_RESOURCE_OUT_ERROR("C0100", "系统资源异常", "system resource error")2.管理
由于目前没有人力投入到错误码管理平台的开发,市面上也没有比较合适的开源项目可以使用,因此,暂时使用飞书文档做错误码管理。详情见开放平台错误码管理3.具体实现
代码侧,首先要有一个枚举类,用于存放所有错误码。
错误码枚举类public enum ReturnEnum { /** * 成功 */ SUCCESS("10000", "成功", "ok"), /** * 未知错误 */ UNKNOWN_ERROR("-1", "未知错误", "unknown error"), /** * 一级宏观错误码,和用户相关 */ A_USER_CLIENT_ERROR("A0000", "用户端错误", "user client error"), /** * 二级宏观错误码,用户传参相关错误 */ A_PARAM_VALIDATION_ERROR("A0100", "参数校验失败", "parameter validation error"), A_REQ_PARAM_VALIDATION_ERROR("A0101", "传参校验失败", "request parameter validation error"), A_MAIL_FORMAT_ERROR("A0102", "邮箱格式错误", "mail format error"), A_PHONE_FORMAT_ERROR("A0103", "手机号格式错误", "phone format error"), /** * 二级宏观错误码,用户注册/登录相关错误 */ A_USER_LOGIN_FAIL("A0200", "用户登录失败", "user login failed"), A_USER_ACCESS_DENIED_ERROR("A0201", "权限不足", "access denied"), A_USER_AUTHENTICATE_ERROR("A0202", "用户认证失败", "user authenticate failed"), A_MAIL_EXIST_ERROR("A0203", "邮箱已注册", "mail address already registered"), A_CAPTCHA_VALIDATION_ERROR("A0204", "验证码校验失败", "captcha validation error"), A_MAIL_VALIDATION_ERROR("A0205", "邮箱验证码校验失败", "mail code validation error"), A_USER_NOT_EXIST_ERROR("A0206", "用户不存在", "user not existed"), A_USER_PASSWORD_ERROR("A0207", "用户名或密码错误", "user or password error"), A_USER_CHANGE_CODE_ERROR("A0208", "修改密码凭证错误或者已过期", "change password code error or expired"), A_USER_REGISTER_ERROR("A0209", "用户注册失败", "register error"), A_USER_EXISTED_ERROR("A0210", "用户已存在", "username already existed"), A_USER_TOKEN_EXPIRE_ERROR("A0211", "用户token已过期", "user token expired"), /** * 一级宏观错误码,和第三方服务相关:数据库,缓存,第三方接口等 */ B_GENERAL_3RD_ERROR("B0000", "一般三方依赖错误", "general 3rd error"), /** * 二级宏观错误码,持久化存储错误,主要和 mysql 相关 */ B_STORAGE_ERROR("B0100", "持久化存储错误", "storage error"), B_SQL_INTEGRITY_CONSTRAINT_VIOLATION_ERROR("B0101", "违反数据库索引约束错误", "sql integrity constraint violation"), B_DUPLICATE_KEY_ERROR("B0102", "违反数据库唯一索引约束错误", "duplicated key"), B_QUERY_NO_RESULT("B0103", "没有查询到数据", "no data queried"), B_MINIO_UPLOAD_URL_ERROR("B0104", "获取文件上传链接错误", "get upload file url error"), /** * 二级宏观错误码,缓存存储错误,主要和 redis 相关 */ B_CACHE_ERROR("B0200", "缓存错误", "cache error"), B_CACHE_REDIS_SET_ERROR("B0201", "缓存写入异常", "cache set error"), B_CACHE_REDIS_DEL_ERROR("B0202", "缓存删除异常", "cache del error"), B_CACHE_REDIS_GET_ERROR("B0203", "缓存获取异常", "cache get error"), /** * 二级宏观错误码,和第三方接口有关 */ B_3RD_API_ERROR("B0300", "第三方接口异常", "3rd api error"), B_GITHUB_OAUTH_ERROR("B0301", "github oauth 接口异常", "github oauth api error"), B_FEISHU_TENANT_TOKEN_ERROR("B0302", "获取飞书tenant token异常", "get feishu tenant token error"), B_FEISHU_USER_INFO_RREOR("B0303", "获取飞书用户异常", "get feishu user info error"), B_FEISHU_SEND_MSG_RREOR("B0304", "发送飞书消息异常", "send feishu msg error"), B_EMAIL_SEND_RREOR("B0305", "发送邮件异常", "send email error"), /** * 一级宏观错误码,和当前系统相关:业务逻辑,程序健壮性等 */ C_GENERAL_BUSINESS_ERROR("B0000", "一般业务错误", "general business error"), /** * 二级宏观错误码,系统资源异常 */ C_RESOURCE_OUT_ERROR("C0100", "系统资源异常", "system resource error"), C_MEMORY_OUT_ERROR("C0101", "系统内存异常", "memory out"), /** * 二级宏观错误码,业务逻辑错误 */ C_BUSINESS_LOGIC_ERROR("C0200", "业务逻辑错误", "business logic error"), C_BUSINESS_USER_NOT_FOUND_ERROR("C0201", "未找到用户", "user not found"), C_BUSINESS_CHANGE_PASSWORD_ERROR("C0202", "修改密码出错", "change password error"), /** * 二级宏观错误码,程序鲁棒性异常 */ C_CODE_ROBUST_ERROR("C0200", "程序鲁棒性缺陷", "code robustness weak"), ; String msgCode; String msgCn; String msgEn; }
增加一个拦截器LanguageInterceptorpublic class LanguageInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String acceptLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE); RequestHolderUtil.addLang(acceptLanguage); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { RequestHolderUtil.remove(); } } public class RequestHolderUtil { private static final ThreadLocal requestHolder = new ThreadLocal(); public RequestHolderUtil() { } public static void addLang(String language) { requestHolder.set(language); } public static String getLang() { return (String)requestHolder.get(); } public static void remove() { requestHolder.remove(); } }
注册拦截器@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LanguageInterceptor()).addPathPatterns("/**"); } }
返回包装类@Data @AllArgsConstructor @Slf4j public class ReturnBase implements Serializable { private static final long serialVersionUID = 1L; /** * 日志跟踪标识 */ public static final String TRACE_ID = "TRACE_ID"; private String traceId; private String msgCode; private String msg; private Object data; private Integer total; ... public static ReturnBase error(ReturnEnum returnEnum) { return error(returnEnum, RequestHolderUtil.getLang()); } public static ReturnBase error(ReturnEnum returnEnum, String language) { String msg; if (Objects.isNull(language)) { return new ReturnBase(MDC.get(TRACE_ID), returnEnum.getMsgCode(), returnEnum.getMsgEn(), null, 0); } switch(language){ case CommonConstant.SIMPLE_CHINESE: msg = returnEnum.getMsgCn(); break; default: msg = returnEnum.getMsgEn(); break; } return new ReturnBase(MDC.get(TRACE_ID), returnEnum.getMsgCode(), msg, null, 0); } ... public static String MsgFormat(String code, String msg) { return String.format("%s (error: %s)", msg, code); } }
使用示例
前端发起请求,在header 里携带HttpHeaders. ACCEPT_LANGUAGE=zh-Hans ;
后端LanguageInterceptor 拦截器拦截后,将ACCEPT_LANGUAGE 塞入当前请求的Threadlocal 变量里;
后端返回错误码给前端:ReturnBase.error(ReturnEnum. C_GENERAL_BUSINESS_ERROR );
在ReturnBase.error() 方法中,会根据之前塞入到Threadlocal 里的ACCEPT_LANGUAGE 变量,自动选择返回中文还是英文的错误文案。