后端-如何优雅的进行错误码管理

5,978 阅读4分钟

趁热记录下,给未来的自己

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" ) ,
    /**
     * 一级宏观错误码,和第三方服务相关:数据库,缓存,第三方接口等
     */
     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" ) ,



    /**
     * 一级宏观错误码,和当前系统相关:业务逻辑,程序健壮性等
     */
     C_GENERAL_BUSINESS_ERROR( "B0000", "一般业务错误", "general business error" ) ,
        /**
        ;

    String msgCode;
    String msgCn;
    String msgEn;

 }

增加一个拦截器LanguageInterceptor

public 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<String> 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变量,自动选择返回中文还是英文的错误文案。