分析微服务模式下的错误码设计

一、设计原则

错误码用于承载后端处理请求过程的结果摘要,前端或者其他下游服务需要根据错误码快速地判断此次请求的结果信息。所以错误码应当具有可阅读性、可比较性快速定位

(一)可阅读性

可阅读性是指调用方可清晰、快速地从返回的错误码中获取需要的信息。错误码作为请求处理结果的摘要,需要明确地包含处理结果来源标识处理结果责任方。

  • 处理结果来源标识:标识本次请求最终结果生成点,根据该值可标识一个请求处理过程中的返回点(每一个返回点都代表着一个具体的原因)。
  • 处理结果责任方:标识引发这个处理结果的责任方,即是在整个处理流程(从请求到响应)中所有参与方谁对最终处理结果负最终责任。通常是调用者、处理者或是第三方服务中的一个。

(二)可比较性

可比较性是指调用方可方便地判断错误码,从而采取不同的处理方式。所以错误码应当设计为由简单内容组成的易表达的信息。在此基础上,设计错误码时也需要注册到一个统一维护的地点,保证错误码的唯一性
除此之外引入子码的定义,即将特定的一组连续错误码分配给一个宏观错误原因,其中的首个错误码作为宏观错误码,当产生无法判断错误原因的异常时使用这个宏观错误码。而宏观错误码是具有层次性的,在每个宏观错误码的子码范围内允许分配下一级的子码段作为次级宏观错误码。宏观错误的层级深度不应该是耗尽整个错误码的,一般将子码段的低3位作为序列码,用于标识详细异常,而宏观错误码所被分配的子码段范围在统一进制体系下具有相同的长度关系(例如十进制下子码段范围是父段长度去除最高位的10的整数倍的长度范围:4位长度的子码段将被分配3位的长度)。
总结:

  • 唯一性
  • 组成内容需要简易
  • 层次性的宏观错误码
  • 序列码

(三)快速定位

快速定位指的是在分析异常时可清晰地从返回的错误码中获得错误原因错误发生地点。这要求错误码为每一个可能的返回点分配一个唯一的、没有歧义的错误码。

二、错误码设计

根据错误码设计原则,我们需要设计一个可标识错误来源、错误责任方的,唯一并且组成信息简易具有连续性的错误码系统。我们采用十进制数字作为错误码的主体,每个十进制数字的错误码配备一个字符串用于描述错误
十进制数字(code)规定为7位长度从高到低1位作为责任方标识,其中0作为保留无责任方标识(此时错误码应该固定为7位0,标识调用成功,无错误),1作为保留标识调用方2作为保留标识第三方服务3作为保留标识业务服务;第25位**作为**宏观错误码标识**,**每2位**作为一个层级,**二级00**保留为**宏观保留**,**一级**标识**处理返回点所在微服务**,**二级**标识**功能模块**;第**67位作为流水标识,保留00作为默认流水标识(当无法确定错误来源时使用),共可标识99种详细错误。其中,宏观错误码标识和流水标识共同组成错误标识
分析微服务模式下的错误码设计_第1张图片

字符串(msg)作为错误描述,需要承担对于数字的在可阅读性上的补充。需要简要阐明错误码产生的原因以及处理指导

{
  "code": "10010100",
  "msg": "用户注册错误,请检查"
}

上述例子的code字段是错误码主体,第1位作为责任人标识使用1保留标识指定为调用方(确定错误责任方);第25位的两层宏观错误码意思是01(标志用户服务下)一级宏观错误码的01(标志注册模块)二级宏观错误码门类下的异常;第67位使用保留的00流水标识指明是两层宏观错误码的默认流水标识。而msg代表错误码描述,提供code在阅读性上的补充。

{
  "code": "1010232",
  "msg": "查询的用户不存在,请检查"
}

上述例子的code字段是错误码主体,第1位作为责任人标识使用1保留标识指定为调用方(确定错误责任方);第25位的两层宏观错误码意思是01(标志用户服务下)一级宏观错误码的02(标志用户查询模块)二级宏观错误码门类下的异常;第67位流水标识32指明是传入的用户信息指向的用户不存在。而msg代表错误码描述,提供code在阅读性上的补充。

三、错误码管理

错误码设计中需要遵从唯一性,即整个错误码系统中每个错误码都是唯一的,标识一个无歧义的错误(返回点)。所以建立规范的错误码管理规范就很有必要,错误码管理需要遵循以下几点。

  1. 宏观错误码分配机制
    1. 由微服务主导开发人员申请一个属于该微服务的一级宏观错误码,下属的二级宏观错误码归由主导开发者自行分配上报管理系统
    2. 功能模块开发者向微服务主导开发者申请功能模块对应的二级宏观错误码
    3. 宏观错误码一经批准不可更改
  2. 错误码注册机制
    1. 功能模块开发者提供已确定流水标识、字符串描述责任方标识,再经由主导开发者上报管理系统,与一二级宏观错误码组成错误码
    2. 错误码一经批准不可更改(描述更改除外)
  3. 未经批准的错误码不可使用

三、错误码后端实现

错误码为微服务架构设计,后端需要有全局的错误码表、模块的错误码对应的异常体系以及微服务调用链中错误码传递功能。

(一)错误码表

错误码表由错误码管理者发布维护,是所有已注册的错误码的集合。此表只开放使用,只能统一向管理者提交申请,审核通过后颁布更新下一版本。错误码表收录的错误码需要记录错误码7位值和字符串的错误码描述,存放于二方包内的enum枚举类中。

/**
 * 错误码
 *
 * @author Dis
 * @version V1.0
 */
@Getter
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ErrorCode {
	// ------------------------ Registry Of Error Codes ------------------------
	/**
	 * 保留的成功错误码(未发生错误,请求处理成功)
	 * 
    *
  • 责任方:系统保留成功
  • *
  • 一级宏观错误码:\
  • *
  • 二级宏观错误码:\
  • *
  • 流水码:\
  • *
*/
SUCCESS(Integer.parseInt("0000000", 10), "成功"); ; /** * 保存了错误码值和错误码定义的映射关系,方便反序列化 */ private static final Map<Integer, ErrorCode> ERROR_CODE_MAP; static { // 初始化映射内容 ErrorCode[] values = values(); ERROR_CODE_MAP = new HashMap<>(values.length); for (ErrorCode code : values) { ERROR_CODE_MAP.putIfAbsent(code.getCode(), code); } } /** * NPE安全的通过错误码值获取定义 * @param code 错误码值 * @throws NullPointerException 不存在错误码值(未注册) * @return 定义 */ @NonNull public static ErrorCode byCode(Integer code) throws NullPointerException { ErrorCode errorCode = ERROR_CODE_MAP.get(code); Assert.notNull(errorCode, () -> new NullPointerException(code + "未注册")); return errorCode; } /** * 十进制数字(code)规定为7位长度。从高到低第1位作为责任方标识,其中1作为保留标识调用方,2作为保留标识第三方服务,3作为保留标识业务服务, * 可标识98个微服务;第2~5位作为宏观错误码标识,每2位作为一个层级,一级标识处理返回点所在微服务,二级标识功能模块;第6~7位作为流水标识, * 保留00作为默认流水标识(当无法确定错误来源时使用),共可标识99种详细错误。其中,宏观错误码标识和流水标识共同组成错误标识。 */ private final Integer code; /** * 字符串(msg)作为错误描述,需要承担对于数字的在可阅读性上的补充。需要简要阐明错误码产生的原因以及处理指导。 */ private final String msg; /** * 聚合位错误码 * * @param errorFrom 错误来源,一位十进制,0作为保留无责任方标识(此时错误码应该固定为7位0,标识调用成功,无错误), 1作为保留标识调用方,2作为保留标识第三方服务,3作为保留标识业务服务 * @param modelCode 功能模块标识,提供一二级宏观错误码 * @param serialCode 错误流水码二位十进制 * @param msg 错误描述 */ ErrorCode(int errorFrom, ModelCode modelCode, int serialCode, String msg) { this.code = (errorFrom % 10) * 1000000 + modelCode.getCompressCode() * 100 + (serialCode % 100); this.msg = msg; } }

其中ModelCode聚合了一二级宏观错误码,是一个枚举类,记录所有的注册的一级宏观错误码和其下的二级宏观错误码及描述名称。

(二)异常体系

异常体系将错误码适配到java的异常中。建立基础异常,接收一个错误码定义(ErrorCode)和一个详细的错误描述信息。

(三)错误码传递

在微服务架构下,处理一个请求将会通过一条由多个微服务组成的调用链。这条调用链中出现错误并且抛出异常后我们期望将这个无法处理的异常原路退回直到响应到最初调用者,并且在此过程中允许拦截上游服务返回的错误码进行处。这个前提下,我们需要设计错误码的拦截并写入响应、微服务间调用结果错误码解析并抛出异常两个内容。配合OpenFeign,使得如同在调用本地方法一般使用其他微服务,并且提供优秀的错误处理功能。
拦截是指将本服务抛出的无法处理的异常映射为错误码表中可找到的定义,并且格式化为统一格式的消息再写入响应体中。一般将功能聚合在统一异常响应中:

/**
 * 统一响应处理器
 *
 * @author Dis
 * @version V1.0
 */
@AllArgsConstructor
@Slf4j
public class UnifiedResponseHandler {
	private static final String APPLICATION_JSON = "application/json";
	private final ObjectMapper objectMapper;
	private final Collection<ExceptionMapper> exceptionMappers;
	private final Collection<ExceptionConverter> exceptionConverters;

	/**
	 * 包装为统一响应
	 *
	 * @param errorCode 错误码
	 * @param data 数据
	 * @param   数据类型
	 *
	 * @return 统一响应实体
	 */
	public <T> UnifiedResponse<T> wrap(ErrorCode errorCode, T data) {
		return UnifiedResponse.of(errorCode, data);
	}

	/**
	 * 包装为统一响应,carrier为空使用defaultCode作为默认状态
	 *
	 * @param carrier     状态携带器
	 * @param data        数据
	 * @param defaultCode 缺省状态
	 * @param          数据类型
	 *
	 * @return 统一响应实体
	 */
	public <T> UnifiedResponse<T> wrap(ErrorCarrier carrier, T data, ErrorCode defaultCode) {
		if (carrier == null) {
			return wrap(defaultCode, data);
		}
		return wrap(carrier.value(), data);
	}

	/**
	 * 处理异常成统一响应实体
	 *
	 * @param e 错误对象
	 *
	 * @return 统一响应实体
	 */
	public UnifiedResponse<String> handleServerException(Exception e) {
		log.error("【统一异常响应】处理了一个异常:", e);

		ErrorCode errorCode = null;
		String message = null;

		Class<? extends Exception> toTest = e.getClass();
		for (ExceptionConverter converter : exceptionConverters) {
			if (converter.isSupported(toTest)) {
				// 转换
				Exception newExp = converter.convert(e);
				if (newExp != null) {
					log.debug("转换异常:{}为{}", e, newExp.toString());
					e = newExp;
					break;
				}
			}
		}

		toTest = e.getClass();
		for (ExceptionMapper mapper : exceptionMappers) {
			if (mapper.isSupported(toTest)) {
				// 提取
				errorCode = mapper.extractErrorCode(e);
				message = mapper.extractMessage(e);
				log.debug("根据类型选择了错误码:{},信息:{}", errorCode, message);
				break;
			}
		}

		if (message == null) {
			message = e.getMessage();
		}
		if (errorCode == null) {
			errorCode = ErrorCode.SYSTEM_UNKNOWN;
		}

		return wrap(e.getClass()
						.getAnnotation(ErrorCarrier.class),
				message, errorCode);
	}

	/**
	 * 设置响应
	 *
	 * @param exception 错误
	 * @param response  响应
	 */
	@SneakyThrows
	public void setExceptionResponse(Exception exception, HttpServletResponse response) {
		UnifiedResponse<?> unifiedResponse = handleServerException(exception);
		response.setCharacterEncoding(StandardCharsets.UTF_8.name());
		response.setContentType(APPLICATION_JSON);
		response.getWriter()
				.write(objectMapper.writeValueAsString(unifiedResponse));
	}

	/**
	 * 设置响应,主要为了适配Lambda
	 *
	 * @param exception 错误
	 * @param request   请求
	 * @param response  响应
	 */
	public void setExceptionResponse(HttpServletRequest request, HttpServletResponse response, Exception exception) {
		setExceptionResponse(exception, response);
	}

	/**
	 * 设置响应,将会把body包装为统一响应,默认state code为SUCCESS
	 *
	 * @param body     data
	 * @param response 响应体
	 *
	 * @throws IOException 写入请求失败
	 */
	public void setResponse(Object body, HttpServletResponse response) throws IOException {
		UnifiedResponse<?> unifiedResponse;
		if (body instanceof UnifiedResponse<?> ur) {
			unifiedResponse = ur;
		} else {
			unifiedResponse = UnifiedResponse.success(body);
		}
		response.setCharacterEncoding(StandardCharsets.UTF_8.name());
		response.setContentType(APPLICATION_JSON);
		response.getWriter()
				.write(objectMapper.writeValueAsString(unifiedResponse));
	}

	/**
	 * 将统一响应编码为json字符串
	 *
	 * @param response 统一响应
	 *
	 * @return JSON
	 */
	@SneakyThrows
	public String toString(UnifiedResponse<?> response) {
		return objectMapper.writeValueAsString(response);
	}
}

解析是指将上游服务的请求处理结果解析出来,如果存在处理异常则将其根据code找到对应的错误码封装为异常抛出,一般在OpenFeign的Decoder中:

/**
 * 统一响应解码器
 *
 * @author Dis
 * @version V1.0
 */
@AllArgsConstructor
@Slf4j
public final class UnifiedResponseDecoder implements Decoder {
	private final ObjectMapper mapper;

	@Override
	public Object decode(final Response response, Type type) throws IOException, FeignException {
		String body = IOUtils.toString(response.body()
				.asInputStream());
		log.debug("解码:{}({})", body, type);
		if (response.body() == null) {
			log.warn("解码失败: 响应为空");
			return null;
		}

		JavaType javaType = mapper.getTypeFactory()
				.constructType(type);
		UnifiedResponse<?> unifiedResponse = null;
		Object result = null;
		try {
			// 尝试读取为统一响应
			unifiedResponse = mapper.readValue(body, UnifiedResponse.class);
			if (unifiedResponse.getData() != null) {
				// 将未转换的data进行转换
				// 因为没有指定UnifiedResponse的泛型
				unifiedResponse.setData(mapper.convertValue(unifiedResponse.getData(), javaType));
			}
		} catch (Exception e) {
			log.warn("尝试解析统一响应失败:{}", e.getMessage());
		}

		if (unifiedResponse != null && unifiedResponse.getCode() != null) {
			log.debug("解码为统一响应:{}", unifiedResponse);

			// 判断code
			checkStateCode(unifiedResponse);

			if (javaType.isTypeOrSubTypeOf(UnifiedResponse.class)) {
				// 需求值为统一响应
				result = unifiedResponse;
			} else if (javaType.isTypeOrSubTypeOf(unifiedResponse.getData()
					.getClass())) {
				// 需求值为统一响应包装的数据
				result = unifiedResponse.getData();
			}
		}

		if (result == null) {
			// 读取为忽略统一响应的包装
			log.debug("无匹配解析,自动使用传入类型进行解析");
			result = mapper.readValue(body, javaType);
		}

		return result;
	}

	/**
	 * 响应是否为成功响应,不是则尝试还原异常并且抛出
	 *
	 * @param unifiedResponse 统一响应
	 */
	private void checkStateCode(UnifiedResponse<?> unifiedResponse) {
		if (!Objects.equals(unifiedResponse.getCode(), ErrorCode.SUCCESS.getCode())) {
			String msg = "";
			if (unifiedResponse.getData() != null) {
				// 取出错误内容
				msg = unifiedResponse.getData()
						.toString();
			}
			FamilyException exp = new FamilyException(
					msg,
					ErrorCode.byCode(unifiedResponse.getCode(), ErrorCode.SYSTEM_UNKNOWN)
			);
			log.debug("统一响应为不成功,自动解析为异常并且抛出[{}]", exp.toString());
			// 还原异常
			throw exp;
		}
	}
}

调用处如果需要对错误调用做出处理则应该catch DecodeException这个异常,取出cause转化为BaseException再取出错误码进行判断(封装到工具类中)。在处理调用链错误依次抛出时,需要在处理统一响应时添加一个converter,将DecodeException取出里面的cause。

/**
 * Feign的异常转换类
 *
 * @author Dis
 * @version V1.0
 */
public class FeignExceptionConverter extends AbstractFunctionExceptionConverter {
	public FeignExceptionConverter() {
		super(e -> (Exception) e.getCause());
	}

	/**
	 * 测试是否支持这个异常
	 *
	 * @param exceptionClass 异常的类
	 *
	 * @return 是否支持
	 */
	@Override
	public boolean isSupported(Class<? extends Exception> exceptionClass) {
		return DecodeException.class.isAssignableFrom(exceptionClass);
	}
}

四、结语

本篇微服务架构下的错误码设计参考了阿里巴巴的Java代码规范,结合自己负责前后端开发的微服务架构应用反复思考后得出,由于是在校学生开发经验不足,如有不足之处敬请指出。

你可能感兴趣的:(Web,微服务,java,springcloud,后端,spring,boot)