领域驱动下(DDD)错误码处理

错误处理的方式

错误的使用场景

  1. 通过错误快速定位问题,看见错误就知道错误原因
  2. 后端的服务通过错误码进行错误的传递。
  3. 前端根据错误码进行友好提示
  4. 根据错误码配置服务监控

错误的作用

在服务内部

  • 快速定位问题

在服务外部

  • 传递服务内错误让服务外部系统通过错误码进行错误码进行后续的动作或是中断操作或是记录日志继续执行。
  • 前端通过错误码给出用户准确的错误提示或者忽略错误进行重试。

导致异常原因梳理

异常原因
系统异常_S
业务异常_B
第三方异常_T
数据库连接超时
配置找不到
非法参数
业务条件不满足
第三方接口调用
第三方SDK调用

错误码设计

错误码流派

流派一 HTTP状态码

Http提供了状态码,比如2开头,3开头,4开头,5开头各自都有一部分用来标识Http的请求状态,而且浏览器也会根据这些状态做一些判断和优化操作,因此有一部分人比较喜欢使用Http内部的状态码,而非自定义

流派二 body里面塞状态

另一部分人认为Http虽然提供了状态码,但是状态码是太少的,根本不能完全覆盖自己的业务场景,因此需要在返回结果里面格式化为 code, data, message 三个字段,然后在code里面放入业务的状态码,这样就可以自行拓展,

错误码设计

我们是坚定不移的流派二

为什么我们是流派二,因为确实Http的状态码覆盖场景太少了,不够用呀,在本人的理解里面Http请求所有都是返回200,这个仅仅代表请求的动作是否成功,body里面的状态码代表请求代表的业务操作是否成功。

错误的设计

错误码由枚举类有用枚举类的属性有两个

  1. 错误码
  2. 错误描述

错误码设计规则

  1. Context编码
  2. 异常原因
  3. 异常功能
  4. 具体异常

XX-XX-XX-XXXX

编码规则(以人为本规则)
  1. 编码采用分隔符 - 因为人眼是不能自动切分字符串的
  2. 编码采用适当英文字母 因为字母人好记

Context编码

 DDD 编程时一个Context也就相当于一个微服务

异常原因

暂时就这些,新场景就新添加

系统异常 S
 数据库异常 S1
 配置异常 S2

业务异常 B
 参数校验异常 B1
 业务条件不满足异常 B2
 
第三方异常 T
 第三方调用不通T1
 第三方调用通了结果不符合预期T2

异常功能

哪个功能模块的错误异常

错误原因

具体导致错误的原因
eg: 参数过长、参数不能为空


预留编码

比如参数错误,按照上述设计, `Context--->异常原因---->异常功能----> 具体异常`  那么每一个context异常功能下都会有参数错误,
就会造成出现编码对应不同的Context的参数错误。
因此用到了保留编码0

它不区分Context,也不区分异常功能,仅仅区分异常原因和具体异常
比如
00-S1-00-0001 
00-B1-00-0001 
00-T1-00-0001 

错误描述

错误描述要指出具体错误信息也就是说错误描述是要包含业务的
具体做法
错误描述支持占位符

参数{0}长度不能大于{1}

{0} 代表第一个占位符
{1} 代表第二个占位符

这些占位符在后续统一都会被MesssageFormat给格式化成为具体的值
格式化后
参数name长度不能大于100

错误码定义规范

错误码不能随意定义,每次遇到一个新的错误就按照 Context--->异常原因---->异常功能----> 具体异常 里面查找有没有定义过符合你的预期的异常,大致相似即可。如果可以找到就一定不能去定义新的异常

编码统一编写,编写之后永久不改

抛错规范

错误的抛出统一使用RuntimeException的继承类,类里面包含错误枚举和具体参数

错误定义

//伪代码

@Data
public static enum ErrorMessage{
	//共有异常的枚举name只写原因
	PARAM_NOT_NULL("00-B1-00-0001", "Param {0} Not Null")
	//非共有异常的枚举name 为功能+原因
	AP_NOT_ALLOW_ACTION("10-B2-01-0001","AP {0} Not Allow Action {1}")

	private String code;
	private String message;
}

@Data
public static class MyException extends RuntimeException{
	private ErrorMessage errorMessage;
	private List<String> params;
	
	public MyException(ErrorMessage emsg, String... params){
		super(CollectionUtils.isEmpty(Arrays.asList(params))?emsg.getMessage(): MessageFormat.format(emsg.getMessage(), params))
	}

	public static class AssetUtils{
		public void AssetTrue(Boolean condition, ErrorMessage emsg, String ... param){
			if(!condition){
				throw new MyException(emsg, param)
			}
		}
	}

}

在APP层和Domain层随时可以抛出MyException

错误抛出

//伪代码
//APP层或者Domain层业务逻辑任意位置
//比如参数paramA 的校验
//业务开始校验
AssetUtils.AssetTrue(paramA, ErrorMessage.PARAM_NOT_NULL, "paramA")
//其他校验

异常拦截与返回 非业务相关

作用: 将异常抛出给前端方便联调测试

//伪代码

@Slf4j
@RestController
@ControllerAdvice
@Order(-1)
public class MyExceptionHandler {
    @ExceptionHandler({MyException.class})
    public ResultDTO<String> handleLogicException(HttpServletRequest request, MyException e) {
        ResultDTO result = new ResultDTO();
        result.message(e.getMessage());
        result.setCode(e.getErrorMessage().getCode());
        log.error("MyException exception: {}", e.getMessage());
        return result;
    }
}

异常拦截与返回 业务相关

这时候如果有需求要前端页面显示错误原因或者返回编码则可以在facade层统一处理就需要在facade层再次定一个优先级更高的错误处理类
每一个facade层都会有自己对应的错误的业务处理逻辑和Domain与App的错误处理互不干扰, 如果没有业务处理就直接使用Doamin和APP层的ErrorMessage

@Data
public enum BusinessErrorMessage{

	PARAM_NOT_NULL(ErrorMessage.PARAM_NOT_NULL, "A01","参数不能为空")
	
	private ErrorMessage emsg;
	private String code;
	private String businessMessage;
	
	//根据ErrorMessage  找到对应的BusinessErrorMessage
	public static BusinessErrorMessage findByErrorMessage(ErrorMessage emsg){
		///省略伪代码
		return BusinessErrorMessage;
	}
}


@Slf4j
@RestController
@ControllerAdvice
@Order(-2)
public class MyExceptionHandler {
    @ExceptionHandler({MyException.class})
    public ResultDTO<String> handleLogicException(HttpServletRequest request, MyException e) {
        ResultDTO result = new ResultDTO();
		BusinessErrorMessage bemsg; BusinessErrorMessage.findByErrorMessage(e.getErrorMessage());
        result.message(bemsg.getMessage());
        result.setCode(bemsg.getErrorMessage().getCode());
        log.error("MyException exception: {}", e.getMessage());
        return result;
    }
}

主要参考资料:

阿里巴巴开发手册-黄山版 (改名为:Java开发手册)

二、异常日志
(一) 错误码

1.【强制】错误码的制定原则:快速溯源、沟通标准化。
说明:错误码想得过于完美和复杂,就像康熙字典的生僻字一样,用词似乎精准,但是字典不容易随身携带且简单易懂。
正例:错误码回答的问题是谁的错?错在哪?
1)错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
2)错误码必须能够进行清晰地比对(代码中容易 equals)。
3)错误码有利于团队快速对错误原因达到一致认知。

2.【强制】错误码不体现版本号和错误等级信息。
说明:错误码以不断追加的方式进行兼容。错误等级由日志和错误码本身的释义来决定。

3.【强制】全部正常,但不得不填充错误码时返回五个零:00000。

4.【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
说明:错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;
B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN
服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100,参考文末附表 3。

5.【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生
效,编号即被永久固定。

6.【强制】错误码使用者避免随意定义新的错误码。
说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。

7.【强制】错误码不能直接输出给用户作为提示信息使用。
说明:堆栈(stack_trace)、错误信息(error_message) 、错误码(error_code)、提示信息(user_tip)是一个有效关
联并互相转义的和谐整体,但是请勿互相越俎代庖。

8.【推荐】错误码之外的业务信息由 error_message 来承载,而不是让错误码本身涵盖过多具体业务属性。

9.【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由 C 转为 B,并且在错误信息上带上原
有的第三方错误码。

10.【参考】错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。

说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、B0001(系
统执行出错)、C0001(调用第三方服务出错)。
正例:调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。

11.【参考】错误码的后三位编号与 HTTP 状态码没有任何关系。

12.【参考】错误码有利于不同文化背景的开发者进行交流与代码协作。
说明:英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发者互相协作。

13.【参考】错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。
说明:数字是一个整体,每位数字的地位和含义是相同的。
反例:一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地拆开并分辨每
位数字的不同含义。

网络文章

知乎文章- 错误码如何设计才合理?(点击进入
知乎文章- 状态码和错误码的一种最佳实践(点击进入

开源项目

暂不列举

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天心有情

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值