API接口错误码设计最佳实践

简介

  • 服务器接口设计中最重要的环节之一便是接口错误码的定义了,通常情况下服务端会定义一些列错误码用以指示接口调用者或者用户进行正确的操作。例如接口参数确实、参数非法、无权限访问、用户身份认证信息过期等等类似反馈。

** 本文将以错误码的类型、易用性、易读性、简洁性等方面进行讲解,内容有**

  • 接口错误码是用来干嘛的?
  • 接口错误码有哪些类型?
  • 接口错误码的定义方式有哪些?
  • 设计的接口错误码需要具有哪些能力?
  • 设计接口错误码的最佳实践

原文:地址

1. 接口错误码是用来干嘛的?

接口错误码,顾名思义,肯定是调用接口失败后反馈给客户端的信息,一般我们调用第三方的开放接口会看到一些非常规范的错误码定义,如下:

  • 系统错误示例:
{
"code": 0x02001023,
"msg": "应用秘钥错误"
}
{
"code": 0x02001111,
"msg": "系统未知错误,请稍后重试!"
}
  • 复杂错误类型示例:
{
"code": 0x02001024,  
"msg": "请求参数非法",   
"data": {  
    "subErrors": [  
        {    
            "error": "NotEmpty",   
            "msg": "请求参数name为必填项"   
        },    
        {    
            "error": "NotNull",    
            "msg": "请求参数age为必填项"    
        }    
    ]    
  }
}
  • 其他错误响应示例
{
"code": 0x0210ffc1,    
"subCode": "LoginAccountNotFound",
"msg": "未找到该账户信息,请核实后再登录!"
}

通过上面示例的错误码得知,错误码的作用有

  1. 诱导接口调用者使用正确的调用方式
  2. 指示调用方依据不同的错误码做逻辑控制处理
  3. 指示用户,引导用户进行正确的操作
  4. 明确指示服务器接口处理异常信息,便于开发人员及时发现与排查

所以设计一个好用又方便的错误码体系很重要!

2. 接口错误码有哪些类型?

主要分为两大类,系统错误码和业务错误码

  • 系统错误码: 一些通用错误信息的定义,一般用于指示开发者正确的进行接口调用和告知调用者接口服务的状态信息。
  • 业务错误码:根据具体业务流程提示或诱导用户进行正确的操作,如用户登录时,账号密码输入错误,接口错误码和提示信息会引导用户重新检查账号密码的正确性并进行重试。

3. 接口错误码的定义方式有哪些?

我们做开发时经常会使用到别人提供的接口,比如百度开放平台、支付宝、微信公众号提供的开放API等等。他们都会提供一些系统错误码说明,常见的形式有:

  1. 一个数字型的code标识

    • 长度可控,且字段值比较短,可节省传输带宽
    • 可读性不高,看到错误码后需要参考文档才能知其含义
    • 一般都会通过数字的范围对code进行分段,用于标识不同子服务、业务等等
  2. 一个字符串code标识

    • 长度会比数值型code长
  • 可读性高,见错误码便知其意
  • 通过业务领域对应的名字来进行描述,例如: 服务名+操作类型+失败原因

4. 我设计的接口错误码需要具有哪些能力?

本文所设计的错误码需要兼顾以下几个特点

  • 可读性一定要高
  • 兼容性要好,要能支持常见的两种错误码类型,字符串和数值型都需要提供
  • 易于维护,错误码要便于维护
  • 易用性,错误码的定义要对开发人员友好,最好开发人员不需要关心错误码的值
  • 灵活可控,错误码可以手动指定,也可以自动进行维护,而且支持错误码自动分段和手动分段

5. 接口错误码的最佳实践

5. 1 错误码的格式和模式的选择
  • 错误码格式采用如下方式
{
"code": 0x0210ffc1,    
"strCode": "LoginAccountNotFound",
"msg": "未找到该账户信息,请核实后再登录!",
"data":{}
}
  • code: Long型错误码
  • strCode: 字符串类型错误码
  • msg: 描述信息
  • data: 正常响应内容,json对象,由具体业务接口进行灵活指定
  • 正常响应结构:
{
"code": 0,    
"data":{}
}
  • 错误响应结构(兼容模式):
{
"code": 0x0210ffc1,    
"strCode": "LoginAccountNotFound",
"msg": "未找到该账户信息,请核实后再登录!"
}
  • 错误响应结构(数值型模式):
{
"code": 0x0210ffc1,    
"msg": "未找到该账户信息,请核实后再登录!"
}
  • 错误响应结构(字符串型模式):
{
"code": "LoginAccountNotFound",    
"msg": "未找到该账户信息,请核实后再登录!"
}
5. 2 错误码的定义
  • 系统错误码的定义
 @Getter
 @AllArgsConstructor
 public enum ErrorCodes {
 	SUCCESS(0,"success"),//成功
 	SYSTEM_PARAM_LOST(0x06001000,"接口参数缺失"),
     //... 其他错误码的定义
     ;    
     private Long code;
 	private String msg;
     
 }    
  • 业务错误码的定义
 @Getter
 @AllArgsConstructor
 public enum BussinessError {	
 		
 	TEST_EXAMPLE_OLD_ERROR(0x06600001, "test.exampleOldError", "完整错误码示例")//手动指定错误码	
 	,LOGIN_ACCOUNT_NOT_EXISTS("login.accountNotExists", "用户登录账号【%s】不存在")//不指定数值型错误码,将会自动生成
      //... 其他错误码的定义
     ;
 	private Integer code;
 	private String strCode;
 	private String msg;
     private BussinessError(int code,String strCode){
         this.code=code;
 		this.strCode=strCode;		
 	}
 	
 	private BussinessError(String strCode, String msg){
 		this.strCode=strCode;
 		this.msg=msg;
 	}
 	
 	private BussinessError(String strCode){
 		this.strCode=strCode;
 	}
     /**
        * 获取参数化的msg值
     */
     public String getMsgParams(Object ...params){
 		if(this.msg!=null){
 			return String.format(this.msg, params);
 		}
 		return this.msg;
 	}
     
 }
5. 3 业务错误码的自动生成
  • 通过错误码枚举成员字段名的字符串生成唯一标识数值,能有效保证字符串错误码不重复
/**  
    * Title HashUtil.java  
    * Description  
    * @author danyuan
    * @date Nov 8, 2019
    * @version 1.0.0
    * site: www.danyuanblog.com
*/ 
package com.danyuanblog.common.util;

public class HashUtil {
	
	public static int ELFhash(String str)//思想就是一直杂糅,使字符之间互相影响
	{
	    int h = 0x0, g;
	    for(int i = 0 ; i < str.length() ; i++)
	    {
	        h = (h<<4) + str.charAt(i); //h左移4位,当前字符占8位,加到h中进行杂糅
	        if((g = h & 0xf0000000) != 0) //取h最左四位的值,若均为0,则括号中执行与否没区别,故不执行
	        {
	            h ^= g>>24; //用h的最左四位的值对h的右起5~8进行杂糅
	            h &= ~g;//清空h的最左四位
	        }
	    }
	    return h; //因为每次都清空了最左四位,最后结果最多也就是28位二进制整数,不会超int
	}
	
	public static int limitELFHash(String str , int min, int max)
	{
	    int k = ELFhash(str);
	    k = Math.abs(k - min);
	    int result = k % (max - min);	
	    result += min;	    
	    return result;
	}
}
  • 指定范围的错误码生成
/**  
    * Title ErrorCodeUtil.java  
    * Description  错误码工具包
    * @author danyuan
    * @date Nov 8, 2019
    * @version 1.0.0
    * site: www.danyuanblog.com
*/ 
package com.danyuanblog.common.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import lombok.extern.slf4j.Slf4j;

import com.danyuanblog.base.exception.ErrorCodes;
import com.danyuanblog.common.consts.BussinessErrors;
import com.danyuanblog.common.exception.ErrorCodeDto;

@Slf4j
public final class ErrorCodeUtil {
	private final static HashMap<String, Integer> codeMap = new HashMap<>();
  //定义错误码范围为: 0x02100000 ~ 0x02ffffff
	private final static Integer MIN_VALUE = 0x02100000;
	private final static Integer MAX_VALUE = 0x02ffffff;
	static {
		//生成错误码映射关系
		init();
	}
	
	/**
    	 * 初始化错误码映射关系
    	 * 
    	 * @author danyuan
	 */
	public static void init(){
		codeMap.clear();
		Integer code = 0;
		for(BussinessErrors error :BussinessErrors.values()){
			if(error.getOldCode() == null || (error.getOldCode() == 0)){
				code = HashUtil.limitELFHash(error.name(), ErrorCodeUtil.MIN_VALUE, ErrorCodeUtil.MAX_VALUE);
			}else{//兼容老版错误码
				code = error.getOldCode();
			}			
			if(codeMap.containsValue(code)){
				//说明产生了冲突,打印冲突信息
				log.error("业务错误码HASH值产生了冲突,请更新错误码名字再启动应用,错误码冲突如下:");
				log.error("存在值: {} = {}", error.name(), code);
				System.exit(-1);//直接退出应用
			} else {
				codeMap.put(error.name(), code);
				log.info("业务错误码:{} = {}", error.name(), "0x0" + Integer.toHexString(code).toUpperCase());
			}			
		}
	}
	
	public static Integer getCode(BussinessErrors error){
		return codeMap.get(error.name());
	}
	
	/**
    	 * 获取业务错误码集合
    	 * @return
    	 * @author danyuan
	 */
	public static List<ErrorCodeDto> getBussinessCodes(){
		List<ErrorCodeDto> list = new ArrayList<>();
		ErrorCodeDto dto = null;
		for(String key : codeMap.keySet()){
			BussinessErrors error = BussinessErrors.valueOf(key);
			dto = new ErrorCodeDto();
			dto.setError(error.getCode())
				.setMsg(error.getMsg())
				.setCode(codeMap.get(key))
				.setHexCode("0x0" + Integer.toHexString(codeMap.get(key)).toUpperCase());
			
			list.add(dto);
		}
		return list;
	}
	
	/**
    	 * 获取系统错误码集合
    	 * @return
    	 * @author danyuan
	 */
	public static List<ErrorCodeDto> getSystemCodes(){
		List<ErrorCodeDto> list = new ArrayList<>();
		ErrorCodeDto dto = null;
		for(ErrorCodes error : ErrorCodes.values()){			
			dto = new ErrorCodeDto();
			dto.setError(error.getMsgCode())
				.setCode(error.getCode())
				.setHexCode("0x0" + Integer.toHexString(error.getCode()).toUpperCase());
			
			list.add(dto);
		}
		return list;
	}
}

  • 错误码查询接口
/**  
* Title ErrorCodeController.java  
* Description  
* @author danyuan
* @date Nov 19, 2018
* @version 1.0.0
* site: www.danyuanblog.com
*/ 
package com.danyuanblog.api;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.danyuanblog.api.impl.response.ErrorCodeListResponse;
import com.danyuanblog.base.exception.BusinessException;
import com.danyuanblog.base.exception.ErrorCodes;
import com.danyuanblog.common.exception.ErrorCodeDto;
import com.danyuanblog.common.util.ErrorCodeUtil;
import com.danyuanblog.web.api.DictManager;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Api(tags={"错误码服务模块"},value="相关接口")
public class ErrorCodeController   {
	
	@Autowired
	private DictManager dictManager;
	
	@GetMapping(value="errorCodeList",
			name="查询错误码列表")
	@ApiOperation(value="查询错误码列表", notes="查询错误码列表")
	public ErrorCodeListResponse errorCodeList(
			) throws BusinessException{
		List<ErrorCodeDto> systemCodes = ErrorCodeUtil.getSystemCodes();
		for(ErrorCodeDto error : systemCodes){
			error.setMsg(dictManager.get(error.getError(),"zh_CN"));			
		}
		return new ErrorCodeListResponse().setBussinessErrors(ErrorCodeUtil.getBussinessCodes())
			.setSystemErrors(systemCodes);
	}

}
@Data
@Accessors(chain = true)
@ApiModel(value="ErrorCodeDto",description="业务错误码参数")
public class ErrorCodeDto implements Serializable{

	/** 
	 *serialVersionUID
	 */
	private static final long serialVersionUID = 1L;
	@ApiModelProperty(
			value="业务错误码字典值",
			example="user.accountNotFound"
			)
	private String error;
	@ApiModelProperty(
			value="系统错误码整型值对应的十六进制值",
			example="0x06002007"
			)
	private String hexCode;
	@ApiModelProperty(
			value="系统错误码整型值对应的十进制值",
			example="100671495"
			)
	private Integer code;
	
	@ApiModelProperty(
			value="错误描述信息",
			example="该用户账号未找到!"
			)
	private String msg;
}
@Data
@Accessors(chain = true)
@ApiModel(value="ErrorCodeListResponse",description="获取系统错误码信息的返回参数")
public class ErrorCodeListResponse implements Serializable {

	/** 
	 *serialVersionUID
	 */
	private static final long serialVersionUID = 1L;
	
	@ApiModelProperty(
			value="业务错误列表"
			)
	private List<ErrorCodeDto> bussinessErrors;
	@ApiModelProperty(
			value="系统错误列表"
			)
	private List<ErrorCodeDto> systemErrors;
}

通过这个接口便可以获取整个服务器提供的所有错误码及其含义说明

你可能感兴趣的:(业务模型设计)