写了这么多年代码,经历了太多从0到1的应用技术项目,这种重复性的行为,不禁也让我感叹,工作到底是正弦函数,往复循环;还是一次性函数,日日精进......
大多数项目应用,技术架构都是相似的。有必要做一个提炼总结。这也是我的初衷
本文主要谈论在应用开发中,经常会涉及的:返回 Result 、错误码、controllerExceptionAdvice。
大多数场景都是模板化的,可复制的。
一、 返回对象Result
针对返回体,我们需要有统一的 Result(或者 Response,本文统称 Result)统一的好处很多:
- 前后端都能做切面处理
- 规范专业,不混乱
- 当所有人都达成共识后,一切都变得简单
- ......
在过往的经历中,会因为场景不同,会有不同的 Result,虽然场景和名称不同,但是职责基本一致。
Result类型 | 使用场景 | 群体 |
---|---|---|
HttpResult/Response | 前后端之间 | 用户 |
RpcResult/Response | 内部应用之间 | 开发 |
OpenAIResult/Response | 三方开放之间 | 开发 |
xxxResult/Response | .... | ... |
注意:Result 名称不同是为了在语义上有区分; 其字段基本一致,职责也基本相同。如果粗糙一些,甚至可以用一个 Result 应对上面多种场景
Result 字段说明
字段 | 描述 |
---|---|
success | 操作是否成功,true表示成功,false表示失败 |
errorCode | 错误码,用于标识具体错误类型,可以是数字,也可以是英文 |
errorMsg | 错误消息 |
result | 泛型结果。 |
关于 success 字段的值要特殊说明。 请求成功即为 true 还是说只要符合预期为 true。 语义是不一样的。除此以外:建议明确区分success
字段的true
值与Service层API返回的布尔值,以避免混淆
一般情况,会将统一返回体打成公共 Jar。公司内都基本上通用。
下面给一个示例:
java
public class ServiceResult<T> implements Serializable {
/**
* 操作是否成功
*/
private Boolean success ;
/**
* 错误码
*/
private String errorCode ;
/**
* 错误信息
*/
private String errorMsg;
/**
* 错误信息
*/
private T result;
.......
}
一般会在ServiceResult添加几个静态方法,用于快速生成成功和失败的 Result。
常见的基本就这几个字段。 除此之外:针对 Result 的可能会有一些其他字段。
字段 | 描述 |
---|---|
requestId | 请求requestId,方便问题追踪。但是 Http 会放在返回体,RPC 会放在应用上下文。 |
arguments[] | 请求参数,一般是rpc,链路透传。但是一般不用。 |
可能会有其他特殊的字段。不再赘述。
针对分页返回对象。 可以有以下两种格式(2种基本够用),当然可以扩展。
方式一、游标
Java
public class Page<T> {
/**
* 当前分段
*/
private List<T> dataList;
/**
* 后面是否还有数据
*/
private Boolean hasMore;
/**
* 下次拉取数据的游标
*/
private Long nextCursor;
}
功能描述:请求参数:Long cursor, Integer size。 入参首次 cursor 为 0, 下一次的 cursor 为上一次的 Page#nextCursor 值。
例如:Elasticsearch支持使用滚动(scroll)API和Search After机制来实现高效的分页,这两种机制都基于游标的概念
方式二、翻页
java
public class PageData<T> {
private T data;
private Long currentPage;
private Long totalCount;
.......
}
注:可以在此基础上增加一些分页判断的逻辑。
和返回 Result 常常一起使用的就是错误码了,接下来谈一谈错误码的设计。
二、 错误码
错误理论,每个系统是不同的。只有少数一些错误可以通用。但是大多数是与业务相关的错误码。
设计错误码,通常会有 code 和 msg,至于表达形式,则不固定。可以是枚举类,也可以通过 Properties(方便国际化)
字段 | 描述 |
---|---|
errorCode | 1. 数字情况:需要与 http status 区分开,http 常常为3位数 ,而我们的业务错误较多。常常会设置 4 位或者 5 位。 2. 在设置错误的时候,一般会采用一定规则。比如:10开头为系统;20开头为中间件;30开头为系统 3. 禁止将 errorCode 设置成枚举类型。 |
errorMsg | 如果有国际化需求,一般 errorMsg 单独配置,通过 errorCode 再获取错误消息。当然只针对对客户场景。 内部的 RPC 错误码则要考虑此这个字段。- errorMsg 如果是程序级别的错误提示,可以适当做一些简化处理,别把整个栈帧都抛出去 |
下面是采用枚举方式,定义了一个错误码枚举,仅做参考。
java
public enum ExceptionCodeEnum {
......
/**
* 错误码
*/
private String errorCode;
/**
* 错误消息
*/
private String errorMsg;
.....
}
错误码组成规则。一部分是分类信息或者来源,一部分是具体细节场景。
这里的错误码,更多的是业务意义上的错误码,和 http status的错误码要区别开
注:设计错误码也是非常考究,大多数情况,参考上面逻辑设计就基本够用,如果场景特殊,则可以因地制宜。
- 不推荐将错误码设计过于复杂,比如把严重级别、发生位置、外部内部等多维度设计到错误码中,因为随着人员变动、时间推移,会变得不可维护。凡事有两面性,例如在大型系统中如果维护一份优秀的错误码,会非常重要。在除非严格遵守,否则通过在分类部分区分,完全能够 cover 得住场景。
- 设计多少位,几部分,可以根据自己的系统而定。
errorMsg 消息: 针对 errorMsg 消息,会根据群体不一样有不一样的策略
例如面向的群体是用户:尽量是友好的文字。尽量不要给一些程序化的错误消息。
- 程序级别的异常消息:尽量不要把所有的原始错误栈消息都返回。
- errorMsg 保证双方边界的清晰。不要将一些内部的过多堆栈信息给到外部,从高内聚低耦合的职责划分来讲,这是不合理的。 如果调用链服务之间很多,也是一种灾难 。 而且堆栈信息向外抛出也存在一定的安全风险
三、 错误码的坏现象
业务系统复杂起来后,会不断增加错误码,很多同学会觉得很麻烦,常常用一些非常宽泛的错误和提示,比如"系统异常,请联系管理员","系统异常,请稍微再试","网络异常,请稍微再试"等等。
方便了自己,但是却把麻烦留给了别人!粗暴的解决常常会让调用方一头雾水!
建议按照标准进行合理设计。运行有兜底的错误码,但不应该图省事。
对于系统错误码,比如 systemError、dbError,使用方是不能被处理的。这一部分的错误码更多的是给自己系统使用的,对于完善的监控机制是非常有必要的。
随着业务增长,错误码必然会越来越多。保持干净整洁变成一件困难的事情,可以增加多个异常类和错误码code类。(可以成对使用)
建议提供具体的异常类设计示例,展示如何根据不同异常类型进行区分和处理。
- BaasExcpetion
- SystemException
- BizException
- .......
通过规范化的处理是可以解决单个类中类型不断膨胀的问题。
四、 Excpetion、Result 的使用
对于 Exceptino、Result、也十分关键。
情况一、对于所有分支都处理成 Result
很高很高
弊端:这种情况,虽然可以处理好每一个细分分支,代码严谨,但是过多的分支结构不利于单元测试,即使是简单的逻辑也会变得复杂。比较不推荐。
在try .... catch 中处理大量业务逻辑,非常不优雅。 处处 try...catch 可读性变得较差。
另外:service -> service -> service 的情况,每个service 都通过 try...catch 包装 serviceResult 简直是灾难,会让系统变得很糟糕,可读性变差,当然也是对程序员的一种折磨。
情况二、异常抛出 + 统一切面捕获处理
更加推荐第二种
基本原则:
- 底层使用 Exception; 或者内部尽量减少 Result,返回参数多一层嵌套,增加使用成本。因此尽量保持简单。
- 最上层使用 Result, 切面层将 Exception 转换成 Result 的错误码和错误消息
- 对外使用 Result。例如:不同系统之间,可以使用不同的语言,在序列化和反序列化都不会有影响。边界和职责更清晰。
- 内部系统与外部系统,尽量通过 result ,保持统一简单;
- 调用方/使用方,可以拿着错误码去找上游对原因,沟通成本更低。
五、 警惕情况
- 不合理的错误类型转换,导致原始错误丢失。比如是业务异常的,结果转换成系统异常。特别是兜底处理的时候,要千万注意!
Java
try {
......
} catch(BizException ex) {
......
throw new SysException("10000","系统异常")
}
- 吞掉异常,或者转换错误类型是,相关信息丢失
Java
try {
......
} catch(Exception ex) {
// 不做任何处理
}
.....
六、 ControllerExceptionAdvice
对于异常的抛出,常常需要有一个统一的切面来处理,系统也常常会有一个advice。其主要作用
- 保持统一的返回体。 将exception 转换成 Result
- 打印错误日志,包括错误码、错误消息、错误参数等等
- 严重级别日志,预警/告警 处理等
通用的模版代码
Java
@Slf4j
@ControllerAdvice
public class ControllerExceptionAdvice {
ServiceResult result = ......
.......
@ResponseBody
@ExceptionHandler(Exception.class)
public ResponseDTO<String> handException(Exception e, final HttpServletRequest request) {
.....
if (e instanceof AAAException) {
.......
AAAException ex = (AAAException) e;
result.setSuccess(false);
result.setErrorCode(ex.getErrorCode());
result.setErrorMsg(......);
return result;
}else if (e instanceof BBBException) {
BBBException ex = (BBBException) e;
result.setSuccess(false);
result.setErrorCode(ex.getErrorCode());
result.setErrorMsg(......);
return result;
}else if(e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException argumentNotValidException = (MethodArgumentNotValidException) e;
......
return result;
}
......
return result;
}
}
七、总结
以下是对文章内容的简要总结:
-
返回对象Result:
- 统一的返回对象可以提高前后端处理的一致性和规范性。
- Result对象通常包含:success状态、errorCode、errorMsg和result数据。
- 可以根据不同场景(如Http、Rpc等)定制不同的Result对象,但字段和职责基本一致。
-
错误码设计:
- 错误码应区分业务错误和系统错误,设计应简洁,避免过于复杂。
- 错误码通常包括errorCode和errorMsg,可以采用枚举类或Properties文件进行管理。
- 错误码设计应考虑国际化和程序级别的错误消息处理。
-
异常处理:
- 推荐使用异常抛出和统一切面捕获处理的方式,以保持代码的可读性和维护性。
- 避免在每个Service中过度包装Result对象,这会增加复杂性和降低性能。
-
ControllerExceptionAdvice:
- 用于统一处理异常,将异常转换成Result对象,保持返回体的统一性。
- 可以记录错误日志,进行错误监控和预警。
-
警惕情况:
- 避免不合理的错误类型转换,防止原始错误信息丢失。
- 不要吞掉异常,确保错误信息能够被正确记录和处理。
场景是相似的,解决方案是可以被复制的。
本文到此结束,感谢阅读。