HTTP状态码和我们平时的错误码不同,状态码太少了,无法满足我们业务中的需求。
然而,所有请求都返回200,然后数据体里包含错误码的方式,又抛弃了HTTP状态码,抛弃了普遍共识。
本来应该遵照HTTP协议的约定,根据HTTP状态码设计返回体。然而,很多电信营业商会拦截非200响应。
采用 {error} or {result} 的形式:
程序遇到错误时,前端返回错误码和错误信息。正常时,直接返回期望的结果。
这种方法是作死,增加了复杂性!

所以推荐的做法是采用{error,result} 的形式:
正常响应和错误都放在body体里

一、返回格式

正常访问服务器时,返回200的HTTP状态码,然后内部描述正常或错误信息

{
    "errcode":"A2001",
    "errmsg":"access_token expired",
    "data":{
       "userid": "zhangsan",
       "name": "张三",
       "department": [1, 2],
       "open_userid": "xxxxxx"
	}
}

二、错误码的设计

好的错误码解决的基本问题是“谁的错?”。

更高的要求是

(1)易于记忆和比对(Equals)。

(2)脱离文档和系统平台达到线下轻量化地自由沟通的目的。

spring boot 返回html spring boot 返回错误码_错误码

(一)设计原则

根据《阿里巴巴开发手册》建议,错误码的制定需

  • 快速溯源,沟通标准化
  • 不体现版本号和错误等级
  • 全部正常时,返回00000
  • 为字符串类型,共5位(产生来源,四位数字编号)
  • 不得与公司业务和组织架构挂钩,应该自增数字
  • 因为国际化i18n,不要用枚举定义错误码
  • 不能直接输出给用户作为提示信息使用,不要替代错误信息(errorMessage)
  • 错误码可分为一二三级宏观错误,跟HTTP状态码没有关系

1.为什么不能用纯数字错误码?

因为数字错误码,不便于感性记忆和分类,不容易记住。

在日志中无法确定某串数字是否是错误码,还是某个数值。

2.为什么不能用纯字母错误码?

纯字母错误码不便于排序,用英语不能准确描述错误。

3.错误码的输出路径有哪些?

分为两类:

3.1面向日志输出
  • 服务或域内传递,最终是输出到日志。
3.2面向外部传递(常用)
  • 域内向域外
  • 服务端传递到前端
  • OpenAPI 错误码
  • 内部不同域之间

4.输出到日志和输出到外部的错误码有什么区别?

按照《手册》的建议设计出的面向日志的错误码定义共十三位(十位有意义,三位连接符),并且应该具有如下分类:

  • 应用标识,表示错误属于哪个应用,三位数字。
  • 功能域标识,表示错误属于应用中的哪个功能模块,三位数字。
  • 错误类型,表示错误属于那种类型,一位字母。
  • 错误编码,错误类型下的具体错误,三位数字。

《手册》还有一条是规定错误码应该如何定义:

错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。

五位错误码的好处是易记,但是对于面向日志的错误码场景利用错误码制作需要分类的业务监控大盘将变得比较困难,比如统计应用 A 的功能 B 的错误出现次数。

同样在系统间传递这个类型的错误码非常有可能发生错误码冲突。

当然对于分为四段的错误码同样尤其不好的一面,应用标识和功能域标识需要有专人去管理或者开发一个错误码管理工具,否则时间一长很容易产生定义的混乱形成破窗。

因此,五位错误码适合传递到外部

5.面向外部传递的错误码如何分工协作?

域外系统通过错误码进行后续的动作,或是中断操作,或是记录日志继续执行。

前端通过错误码,给出用户准确的错误提示或者忽略错误进行重试。

(二)实现方式

错误码可以和国际化结合起来实现

spring boot 返回html spring boot 返回错误码_spring boot 返回html_02

在springboot项目的resources目录中创建i18n文件夹,在文件夹下创建messages.properties等三个国际化配置文件,IDEA会自动创建Resource Bundle并绑定这三个国际化配置文件。

messages.properties和messages_zh_CN.properties文件配置参考如下:

00000=请求成功
A0001=用户端出错
A0002=参数 {0} 缺失
A0003=参数 {0} 长度不合法
A0004=参数 {0} 格式不正确
A0005=参数 {0} 类型不合法
A0006=参数 {0} 状态已过期
A0007=参数 {0} 非法
A0008= {0} 不存在
A0009= {0} 已存在
A0010= {0} 重复
A0011= {0} 不允许被删除
A0101=用户权限不足
A0102=用户操作频繁
B0001=当前系统出错
B0101=系统正忙,请稍后再试
C0001=第三方服务出错

messages_en_US.properties文件配置参考如下:

00000 = request successful
A0001 = client error
A0101 = missing parameter {0}
A0201 = illegal type of parameter {0}
A0301 = parameter {0} exceeds limit
A0401 = invalid parameter {0}
A0501 = {0} has expired
A0601 = result {0} does not exist
A0701= result {0} already exists
A0801 = delete {0} forbidden
A1001 = insufficient user rights
B0001 = current system error
B0101 = the system is busy, please try again later
C0001 = third party service error

其中,A代表用户错误,B代表当前系统错误,C代表依赖的第三方服务错误,用0001表示第一类宏观错误。

错误码可以自己设计,不断自增,先到先得,以100为步长作为大类分类。

一般默认中文,然后设计好后,直接拿去翻译,再粘贴回来。

为了通用匹配错误,我用了{0}来匹配。如果想设置个性化信息,可以接着往后配置A0102…等等提示信息。

spring boot 返回html spring boot 返回错误码_HTTP_03

(三)错误码举例

1.参数缺失

setEmail(null);

若传进的email为空,则判为参数缺失。
2.参数长度不合法

setEmail("99999999999999999999999999999@qq.com");

若传进的email的字符串长度超出或少于预设值,则判为参数长度不合法。
3.参数格式不正确

setEmail("123456");

若传进的email的格式不符合“xxx@xxx.xx”的格式,则判为格式不正确。
4.参数类型不合法

setEmail("123456@error.com");

若传进的email的后缀不是服务端允许的“@qq.com、@163.com”等格式,则判为参数类型不合法。

以上为从输入就能做出的验证,接下来是必须查数据源或经计算才能做出的验证

5.参数状态已过期

checkToken("daf2$11.sdf34asasf4ewrjjmp94xg");

若输入的token值的有效期与当前时间比较已超过,则判为参数状态已过期。

6.参数非法

checkToken("daf2$11.sdf34asasf4ewrjjmp94xg");

若输入的token值解密失败,则判为参数非法。
7.数据不存在

getUser("123");
updateUser("123");
deleteUser("123")

若查询的userid在数据库中查不到,则判为数据不存在。

8.数据已存在

addUser("123");

若添加的用户userid在数据库中已存在,为保证唯一性,则判为数据已存在。
9.数据重复

setEmail("123456@qq.com");
setEmail("123456@qq.com");

若传进的email为无变化,则判为参数重复输入。

错误码,也不用太多(几百上千种),要不然自己眼花缭乱,陷入麻烦里了。最好做到,不查文档,就了解是什么错误了。

三、工具类

需要自行将错误码进行封装,默认是中文

/**
 * 错误码相关方法
 */
public class ErrorCodeUtils {
    /**
     * 获得错误码对应的提示信息
     * @param errcode 错误码
     * @param params 匹配参数
     * @return 返回错误提示信息
     */
    public static String getMessage(String errcode, String ...params){
        ResourceBundle bundle = ResourceBundle.getBundle("i18n/messages", Locale.getDefault());
        String msg = bundle.getString(errcode);
        MessageFormat messageFormat = new MessageFormat(msg);
        return messageFormat.format(params);
    }
}

然后调用编写的方法

@Test
public void testI18n() throws IOException {
	System.out.println(ErrorCodeUtils.getMessage("A0101","ID"));
}
//输出:参数ID缺少