项目沉淀之返回对象、错误码、异常切面等总结

655 阅读8分钟

写了这么多年代码,经历了太多从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。公司内都基本上通用。

下面给一个示例:

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种基本够用),当然可以扩展。

方式一、游标

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机制来实现高效的分页,这两种机制都基于游标的概念

方式二、翻页

public class PageData<T> {

    private T data;

    private Long currentPage;

    private Long totalCount;

   .......
}

注:可以在此基础上增加一些分页判断的逻辑。

和返回 Result 常常一起使用的就是错误码了,接下来谈一谈错误码的设计。


二、 错误码

错误理论,每个系统是不同的。只有少数一些错误可以通用。但是大多数是与业务相关的错误码。

设计错误码,通常会有 code 和 msg,至于表达形式,则不固定。可以是枚举类,也可以通过 Properties(方便国际化)

字段描述
errorCode1. 数字情况:需要与 http status 区分开,http 常常为3位数,而我们的业务错误较多。常常会设置 4 位或者 5 位。
2. 在设置错误的时候,一般会采用一定规则。比如:10开头为系统;20开头为中间件;30开头为系统
3. 禁止将 errorCode 设置成枚举类型。
errorMsg如果有国际化需求,一般 errorMsg 单独配置,通过 errorCode 再获取错误消息。当然只针对对客户场景。 内部的 RPC 错误码则要考虑此这个字段。- errorMsg 如果是程序级别的错误提示,可以适当做一些简化处理,别把整个栈帧都抛出去

下面是采用枚举方式,定义了一个错误码枚举,仅做参考。

public enum ExceptionCodeEnum {
   ...... 

  /**
   * 错误码
   */
  private String errorCode;
  
  /**
   * 错误消息
   */
  private String errorMsg;
  
  .....

}

错误码组成规则。一部分是分类信息或者来源,一部分是具体细节场景

image.png 这里的错误码,更多的是业务意义上的错误码,和 http status的错误码要区别开

注:设计错误码也是非常考究,大多数情况,参考上面逻辑设计就基本够用,如果场景特殊,则可以因地制宜。

  • 不推荐将错误码设计过于复杂,比如把严重级别、发生位置、外部内部等多维度设计到错误码中,因为随着人员变动、时间推移,会变得不可维护。凡事有两面性,例如在大型系统中如果维护一份优秀的错误码,会非常重要。在除非严格遵守,否则通过在分类部分区分,完全能够 cover 得住场景。
  • 设计多少位,几部分,可以根据自己的系统而定。

errorMsg 消息: 针对 errorMsg 消息,会根据群体不一样有不一样的策略

例如面向的群体是用户:尽量是友好的文字。尽量不要给一些程序化的错误消息。

  1. 程序级别的异常消息:尽量不要把所有的原始错误栈消息都返回。
  2. errorMsg 保证双方边界的清晰。不要将一些内部的过多堆栈信息给到外部,从高内聚低耦合的职责划分来讲,这是不合理的。 如果调用链服务之间很多,也是一种灾难 。 而且堆栈信息向外抛出也存在一定的安全风险


三、 错误码的坏现象

业务系统复杂起来后,会不断增加错误码,很多同学会觉得很麻烦,常常用一些非常宽泛的错误和提示,比如“系统异常,请联系管理员”,“系统异常,请稍微再试”,“网络异常,请稍微再试”等等。

方便了自己,但是却把麻烦留给了别人!粗暴的解决常常会让调用方一头雾水!

建议按照标准进行合理设计。运行有兜底的错误码,但不应该图省事。

对于系统错误码,比如 systemError、dbError,使用方是不能被处理的。这一部分的错误码更多的是给自己系统使用的,对于完善的监控机制是非常有必要的。


随着业务增长,错误码必然会越来越多。保持干净整洁变成一件困难的事情,可以增加多个异常类和错误码code类。(可以成对使用)


建议提供具体的异常类设计示例,展示如何根据不同异常类型进行区分和处理。

  • BaasExcpetion
  • SystemException
  • BizException
  • .......

通过规范化的处理是可以解决单个类中类型不断膨胀的问题。


四、 Excpetion、Result 的使用

对于 Exceptino、Result、也十分关键。

情况一、对于所有分支都处理成 Result

很高很高

弊端:这种情况,虽然可以处理好每一个细分分支,代码严谨,但是过多的分支结构不利于单元测试,即使是简单的逻辑也会变得复杂。比较不推荐。

在try .... catch 中处理大量业务逻辑,非常不优雅。 处处 try...catch 可读性变得较差。

另外:service -> service -> service 的情况,每个service 都通过 try...catch 包装 serviceResult 简直是灾难,会让系统变得很糟糕,可读性变差,当然也是对程序员的一种折磨。

情况二、异常抛出 + 统一切面捕获处理

更加推荐第二种

基本原则

  1. 底层使用 Exception; 或者内部尽量减少 Result,返回参数多一层嵌套,增加使用成本。因此尽量保持简单。
  2. 最上层使用 Result, 切面层将 Exception 转换成 Result 的错误码和错误消息
  3. 对外使用 Result。例如:不同系统之间,可以使用不同的语言,在序列化和反序列化都不会有影响。边界和职责更清晰。

  • 内部系统与外部系统,尽量通过 result ,保持统一简单;
  • 调用方/使用方,可以拿着错误码去找上游对原因,沟通成本更低。

五、 警惕情况

  1. 不合理的错误类型转换,导致原始错误丢失。比如是业务异常的,结果转换成系统异常。特别是兜底处理的时候,要千万注意!
try {
    ......
} catch(BizException ex) {
    ......
    throw new SysException("10000","系统异常")
} 
  1. 吞掉异常,或者转换错误类型是,相关信息丢失
try {
    ......
} catch(Exception ex) {
    // 不做任何处理
} 
.....

六、 ControllerExceptionAdvice

对于异常的抛出,常常需要有一个统一的切面来处理,系统也常常会有一个advice。其主要作用

  • 保持统一的返回体。 将exception 转换成 Result
  • 打印错误日志,包括错误码、错误消息、错误参数等等
  • 严重级别日志,预警/告警 处理等

通用的模版代码


@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;
    }
}

七、总结

image.png

场景是相似的,解决方案是可以被复制的。

本文到此结束。