错误码方案及考虑的问题

一码一条消息

我之前在做中台项目,我们的错误码方案为一码一个消息,具体实现中我们有一个错误消息枚举类,里面包含了我们所有的错误码消息。如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

public enum CodeAndMsg{
    // 会员
    MEMEBER_NOT_FOUND("1000", "会员不存在"),
    MEMBER_STATUS_WRONG("1001", "会员状态错误"),

    // 会员卡
    MEMBER_CARD_NOT_FOUND("2000", "会员卡不存在"),
    MEMBER_CARD_STATUS_WRONG("2001", "会员卡状态错误"),
    MEMEBER_CARD_NO_COIN("2002", "会员积分不足"),
    ;
}

实践的过程中,为了方便管理,我们又将各个模块的错误消息定义在自己的枚举类中,然后在CodeAndMsg中引用这些枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 会员
public enum MemberCodeAndMsg{
    MEMEBER_NOT_FOUND("1000", "会员不存在"),
    MEMBER_STATUS_WRONG("1001", "会员状态错误"),
}

// 会员卡
public enum MemberCardCodeAndMsg{
    MEMBER_CARD_NOT_FOUND("2000", "会员卡不存在"),
    MEMBER_CARD_STATUS_WRONG("2001", "会员卡状态错误"),
    MEMEBER_CARD_NO_COIN("2002", "会员积分不足"),
}

public enum CodeAndMsg{
    // 会员
    MEMEBER_NOT_FOUND(MemberCodeAndMsg.MEMEBER_NOT_FOUND),
    MEMBER_STATUS_WRONG(MemberCodeAndMsg.MEMBER_STATUS_WRONG),

    // 会员卡
    MEMBER_CARD_NOT_FOUND(MemberCardCodeAndMsg.MEMBER_CARD_NOT_FOUND),
    MEMBER_CARD_STATUS_WRONG(MemberCardCodeAndMsg.MEMBER_CARD_STATUS_WRONG),
    MEMEBER_CARD_NO_COIN(MemberCardCodeAndMsg.MEMEBER_CARD_NO_COIN),
    ;
}

CodeAndMsg中有个main方法,调用后会读取各个枚举的信息,然后自动生成CodeAndMsg的代码。我们之所以如此热衷于将所有的错误信息定义在同一个枚举中,是因为我们还有一套用于参数校验的注解,该套注解支持传入一个CodeAndMsg参数,当我们的参数校验失败了,会自动返回这个CodeAndMsg定义的错误码和错误消息(客户要求我们,不同的字段错误需要返回不同的错误)。

需要提到的是,我们之前使用的是单体仓库,错误码被放到了common项目下,这样全局都可以使用到这些错误码。

我个人是比较喜欢这种错误码方案的,原因有下:

  1. 定位问题快速,我们可以通过错误码,快速检索到代码行

  2. 定义错误码时不需要费脑,我们定义错误码时,只需要在之前的码值上进行递增,不需要思考哪个码可以用于我们现在的场景。但是,修改已经定义的码的含义,是不被允许的(除非可以确保这个码值只有自己在使用)。

  3. 非常便利生成文档,我们需要将错误码写到文档中,提交给下游的用户,所有的错误码写在CodeAndMsg中,我们可以很轻松的生成文档。

  4. 和参数校验注解结合,用起来比较舒服,在参数校验的时候,我们不需要写message,只用提供一个CodeAndMsg,这样可以避免让一些硬编码的Message充斥在代码中。

一码多条消息

但是,最近接触到了新的错误码方案,要求我们一码多条消息,也就是说,让我们尽量复用已有的错误码。我本人比排斥这个方案,这意味着我们定义错误码的成本更高了,我们需要去思考,我们当前的场景下应该适用于哪个错误码。当然这其实也意味着足够的便利,因为不在要求我们为每个message定义枚举了。我们可以在任何我们需要的地方,传递一个Code值,加一个Msg值即可。

但是我肯定还是不愿意将Msg值充斥在代码中,所以在实践中我趋于按如下方式进行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

public class CodeConstant {
    public static int DATA_NOT_FOUND = 1000;
    public static int PARAM_WRONG = 1001;
}

public enum Code {
    USER_NOT_EXIST(CodeConstant.DATA_NOT_FOUND, "项目不存在"),
    PROJECT_NOT_EXIST(CodeConstant.DATA_NOT_FOUND, "项目不存在"),

    EMAIL_FORMAT_WRONG(CodeConstant.PARAM_WRONG, "邮箱格式错误"),
    PHONE_FORMAT_WRONG(CodeConstant.PARAM_WRONG, "手机号格式错误"),
    ;
}

两种方案的比较

实际上,到底使用哪种方案,我觉得要考虑如下的问题:

  1. 接口的消费者需不需要根据不同的码值进行逻辑处理。
  2. 接口的消费者是直接将我们接口中的msg暴露给用户,还是说需要拦截下错误码,然后返回统一的错误消息。

第一个问题

如何理解第一个问题?假如我们有一个接口需要先判断用户是否存在,再判断项目是否存在,然后再进行逻辑处理,如果用户不存在,需要进行A操作,如果项目不存在,需要进行B操作(A操作和B操作不仅仅是将错误消息提示给用户),所以这个时候针对用户不存在和项目不存在我们就需要返回不同的错误码。其实两种错误码方案都可以处理这个问题,在一码一条消息中,我们只需要返回不同的码即可;在一码多条消息中,我们需要拆分接口,需要提供判断用户存在的接口、判断项目存在的接口、真正处理业务逻辑的接口,但是即使是这样,考虑并发问题,我们在真正处理业务逻辑的接口中还是需要判断用户信息是否存在、项目信息是否存在,所以说这个接口在并发的场景下还是有可能返回代表多个含义错误码。

再来思考下,我们的项目中可不可能存在这种需求?我觉得可能性非常大,我们的web端和客户端本质上都是编辑器,都是极度重视用户体验的,我们有很大的可能性针对一个接口的不同错误码进行不同的交互逻辑,从而提高我们产品的体验。

第二个问题

这个问题同样两个方案都能处理,针对一码一条消息,接口的消费者通过前缀进行拦截;针对一码多条消息,接口的消费者直接针对不同的码值进行处理。我们需要考虑如下两个问题:

  1. 我们的消费者究竟在怎样的使用我们的错误码及错误消息
  2. 究竟如何使用我们的错误码和错误消息才是合理的

我们项目中目前是这样在处理的:针对一些特定的错误码,前端会写function进行处理(比如未登录:用户被挤下去了,需要重新跳转到登录页面),而绝大多数业务上的错误码,前端不会进行任何处理,直接将msg告知给用户。

前端该不该拦截错误码,返回统一的消息

先讨论下,前端该不该拦截我们的错误码,然后返回统一的msg:

服务端给出的msg其实是最能体现当前业务的问题出现在哪的,将这些错误消息直接提示给用户并非不可以(仅针对业务上的错误码),而且我们在使用互联网产品的时候,我们发现就大多数的情况下,web应用也的确是给你更描述问题存在的错误消息,而非给你一个笼统的错误消息,甚至有些应用会直接告诉你错误的码值。

如果我们拦截某一类的错误码,然后统一返回错误消息,我觉得这样很让人迷惑,举一个表单填写的案例,假如我们拦截参数校验的错误,然后提示用户表单数据有误,而不告知用户究竟是哪个字段有错误,我觉得用户应该会非常的迷惑吧。如果用户将这个问题反馈给我们,我们也很难定位这个问题,我们需要查日志,爬下用户填写的表单,然后再本地复现这个问题。为了更好的定位问题,我们还需要知道用户到底在表单里填写了什么东西,才导致前端竟然没有校验出错误。再举一个数据不存在的案例,如果说前端不能将服务端的返回消息直接返回给用户,我们拦截下了这个消息,提示用户数据不存在,那用户如何知道究竟是什么数据不存在呢?

我们直接将后端返回的错误消息完整的告知给用户,或许不能够帮助到用户定位问题,但是用户将问题反馈给我们的时候,我们可以更快的定位问题。如果提示错误的消息中包含错误码信息,我们甚至可以快速定位到代码行(两种错误码方案都可以,但是一码一消息更快)。

错误消息的国际化

从上面的讨论,我们可以看出我们的的确确是有需求的将服务端的错误消息直接提示给用户,而不需要进行任何包装。在这种需求场景下,我们还需要对错误消息进行国际化,我们需要的是一个错误码可以获取多个语言不同的msg,在这种场景下我们还是得使用一码一消息,如果使用一码多条消息,我们根本没有办法找到这个错误在不同语言中的表述。

一码一消息的实践

综上所述,我认为一码一消息更有研究价值,且code的类型应该为String(更灵活,更容易进行处理)。但是考虑到我们目前的项目已经使用了int,所以各个消费者为了处理方便,可以在消费前将int类型的code转换成String,如果是全新类型的项目的话,我还是比较建议适用String类型的,字符串在扩展性、处理灵活性方面是碾压int类型的。

我们之前的项目使用的是单体仓库,而我们现在的项目中各个服务会独立的成为一个服务,这带来了一些小小的问题,这在使用的时候会带来一些问题,比如我们没有办法再通过修改CodeAndMsg的源码,来增加不同的枚举类型了,从而导致我们没有办法开发一套结合我们错误码系统的注解。

我有一个大胆的想法,我想将错误码单独成一个项目,采用Git子模块的方案集成到项目中,公司的所有项目都需要依赖这个错误码项目,从而实现整个公司的错误码统一管理的。这个错误码项目中可以进行自动构建,并生成Java语言、Go语言、JavaScript语言所需要的源码文件。在开发上,我希望这个这个项目能给我们单体项目的开发感觉,也就是我们可以迅速的定义并使用这个错误码,而不需要等待。这个方案会引入更高级的知识,我觉得接受成本有点高,但是这个方案肯定是非常方便的。

(我打算放一放,等我git技术提升了在研究这些东西)