一次动态接口替换经历

接口改造

背景

现有旧订单接口 orderDetail,该接口会返回全量节点,部分节点不会使用因此造成了冗余,给数据库造成了较大压力,因此改造新接口 basicOrderDetail(xxx) 支持传入需要赋值的节点,方便赋值。

有如下改造方案:

  • 现有多个其他部分接口调用当前 orderDetail 接口获取订单详情,并且再调用订单详情的 getter() 方法,统计所有调用订单接口的方法,以及这些方法内部调用订单详情 getter() 方法;
  • 将旧接口替换为新接口,并传入需要赋值的节点。

上游服务对该接口的 qps 访问量约为:

难点

难点在于调用 getter() 方法的地方太多,无法手动一个个统计,工作量巨大。

解决方案

新接口如何设计?

// 调用旧接口伪代码
Response resp = orderDetail(request.orderId(), "");
// Response 中返回了全量节点值


// 新接口设计
// 1.各服务自定义封装 Request,并设置需要赋值的节点信息
class NewRequest {
    int orderId;
    List<ItemFlag> flags;  // ItemFlag 为枚举类,对应 Response 中各字段信息
}
// 2.服务调用时构造 Request,然后发起请求,这样依赖就同意了调用格式,各服务以统一形式调用新接口,同时可以设置需要赋值的节点
NewResponse resp = basicOrderDetail(new NewRequest(1, Arrays.asList(ItemFlag.xxx, ItemFlag.xxx)));

其他服务中获取字段信息 getter() 方法如何识别替换?
这是一类对方法增强的功能,所以很自然联想到动态代理,SpringBoot 中可以通过 注解+AOP切面实现。

方案一:注解+AOP

  • 在不修改原代码逻辑的基础上,新增自定义注解 @ItemRecord;
  • 将注解标注在获取 OrderDetail 结果的方法上;
  • 定义对应切面,以 @Pointcut("@annotation(xxx.xxx.xxx.ItemRecord)") 为切点,并定义切点的 AfterReturning 通知,这样在运行时就能获取到方法返回的结果中哪些字段非空,但是无法确定各接口实际需要的节点值。

方案二:编译时注解+Processor

  • 定义两个编译时注解,一个用于获取 OrderDetail 类型变量,并统计获取代码中 getter() 方法;
  • 再定义一个编译时注解,用于替换 OrderDetail 的获取方法,替换为 BasicOrderDetail 获取方法。

但是这种方式解析语法树过于麻烦,实行起来复杂。

方案三:Cglib 动态代理

初步思路:

  • 对原始接口返回的对象 OrderDetail 创建动态代理对象,每当调用该对象的 getter() 方法时,触发增强后的 getter() 方法,记录下对应的执行线路、当前方法调用所在的外层方法名、该服务方法需要 getter() 的字段信息,记录上述信息存储到内存 HashMap 中;
  • 在第一步操作记录完毕后,针对调用老接口 orderDetail 的服务对象创建动态代理对象,之后再调用 orderDetail 老接口时,方法内部会代替调用增强后的代理对象执行增强后的方法,方法内只针对部分目标属性赋值,避免了全量赋值。

复现 Demo:

https://gitee.com/loserii/proxy_demo

重点步骤:

  • 创建 App 对象的动态代理类,收集 getter() 方法所需的属性,并存储 Map。

img

  • 创建非全量新接口,修改老接口逻辑,老接口执行时判断代理类是否为空,不为空调用代理方法,否则调用旧方法。新接口构造代理方法所需参数,调用代理方法。

  • 代理方法会根据请求参数中枚举的信息,针对指定字段赋值。

img

img

结果

改造后接口如何验证数据正确率?

  • 可以先对接口 Mock 编写简单的测试用例确保接口逻辑正确,对于一些边界情况也可以模拟到;
  • 将下游服务打包为镜像发布到测试环境,找测试构造产品id 对接口调用,在日志平台抓包查看接口返回的结构体是否正确;
  • 将服务发布到生产环境的镜像集群上,该集群不提供生产服务,而对接生产流量,将生产流量数据同时运行在镜像集群、生产集群,然后在 diff 平台对比响应报文结果。可以一次拉取1000个报文流量,重复多次保证数据正确性;
  • 将服务发布到生产环境,通过灰度切流的方式逐步替换旧服务。

考虑到代码可读性、后续对代码的可维护性,最终没有采取动态代理方案,而是对旧接口的调用进行日志埋点,收集方法调用栈信息,确定当前接口具体使用了哪些字段。然后逐个对接口调用进行改造,确保后续代码可维护。

posted @ 2024-08-01 15:20  Stitches  阅读(65)  评论(0)    收藏  举报