BeanPostProcessor:包装 Feign Client

BeanPostProcessor:包装 Feign Client

作者:苏渡苇

项目地址github.com/iweidujiang...(感谢 Star !)

本文要干什么

上一篇把 进站 HTTP 拦住了:请求打进 MVC 或 Gateway,开一个 SERVER Span。

而我们的项目还需要画出拓扑图,它要的不是「谁被访问了」,而是 谁调用了谁

微服务之间最常见的出站方式是 OpenFeign:

text 复制代码
浏览器 → Gateway → order  --Feign-->  user
                         --Feign-->  product

进站拦截器看不到这次 Feign 调用。你也不会指望每个同事在 Feign 接口上自己埋点。

所以这篇的知识点是 BeanPostProcessor :容器把别人的 Bean 初始化完之后,再包一层我们自己的实现。包上去的那层叫装饰器 ,Insight 里就是 TracingFeignClient

源码在此:io.github.iweidujiang.springinsight.feign.TracingFeignClient

所以本文的目的就是讲明白:

不改业务代码,把 Feign 的真实 Client 套一层,出站时记下 CLIENT Span,并填上 remoteService,拓扑边才有东西可画。

一、为啥要动 Feign Client

Feign 对业务是接口:

java 复制代码
@FeignClient("sca-user")
public interface UserFeignClient {
    @GetMapping("/user/{id}")
    User get(@PathVariable Long id);
}

真正发 HTTP 的是 feign.Client#execute。Spring Cloud 里常见的是带负载均衡的那只(类名里有 LoadBalancer)。

监测要插的位置就是这里:调用发出去之前开 Span,回来(或抛错)之后关 Span。这叫 装饰器 ------接口不变,里面先干活再 delegate.execute(...)

马甲从哪来?不能让用户 new TracingFeignClient,得在 Spring 把原始 Client 造好之后,悄悄换掉,这就是 BeanPostProcessor

二、BeanPostProcessor:Bean 出厂后再包一层

BeanPostProcessor 可以拦住容器里每一个 Bean 的创建。Insight 只用了初始化之后那一刀:

java 复制代码
@Bean
public static BeanPostProcessor insightFeignTracingBeanPostProcessor(...) {
    return new BeanPostProcessor() {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) {
            if (!(bean instanceof Client) || bean instanceof TracingFeignClient) {
                return bean;
            }
            // 只包装 LoadBalancer,避免把 Default 也包了导致打两遍
            if (!bean.getClass().getSimpleName().contains("LoadBalancer")) {
                return bean;
            }
            return new TracingFeignClient((Client) bean, ...);
        }
    };
}

本段代码:

1. 不是 Feign 的 Client,原样返回。

别的 Bean 从这里过,动都不动。

2. 已经是 TracingFeignClient,不再包。

防止后置处理器跑两次,套成洋葱。

3. 只认类名里带 LoadBalancer 的。

Spring Cloud 里往往是「负载均衡 Client 里面再握着一个 Default Client」。两个都是 Client,两个都包,一次 Feign 调用会记两条 CLIENT Span。只包外层那只,Default 继续当内部运输队。

4. 方法写成 static @Bean

让这个后置处理器尽早注册。普通 @Bean 有时会晚半拍,想包的 Client 已经创建完了,就套不上。

5. 依赖用 ObjectProvider 注入。

后置处理器自己创建得很早,那时 InsightProperties、上报监听器未必就绪。先拿个「以后再取」的盒子,真正 execute 的时候再 getIfAvailable()。启动顺序打架,这是土办法。

之前的文章 加个依赖就生效?一行搞定 Spring Boot Starter 自动装配 我们提过:这段配置单独挂在 .imports 第二行,还加了 @ConditionalOnClass(Client.class)。项目里没有 Feign,这段 BPP 根本不会注册,零打扰。

三、马甲里面干什么

TracingFeignClient 自己也实现 feign.Client,核心就一个 execute

java 复制代码
@Override
public Response execute(Request request, Options options) throws IOException {
    if (!追踪开着) {
        return delegate.execute(request, options);
    }
    Optional<TraceSpan> parent = TraceContext.currentSpan();
    if (parent.isEmpty()) {
        return delegate.execute(request, options);  // 没进站请求,不瞎记
    }

    TraceSpan clientSpan = new TraceSpan(parent.getTraceId(), parent.getSpanId());
    clientSpan.setSpanKind("CLIENT");
    clientSpan.setComponent("OpenFeign");
    clientSpan.setRemoteService(resolveRemoteService(url));  // 拓扑用这个

    try {
        Response response = delegate.execute(request, options);
        clientSpan.finish();
        listener.reportSpan(clientSpan);
        return response;
    } catch (IOException e) {
        clientSpan.finish("IO_ERROR", e.getMessage());
        listener.reportSpan(clientSpan);
        throw e;
    }
}

和进站拦截器是同一件事,换了个方向:

进站 Interceptor 出站 TracingFeignClient
拦的位置 请求进来 Feign 发出去
Span 类型 SERVER CLIENT
父 Span 自己当根(或以后做头传播) TraceContext 取当前栈顶
失败怎么记 HTTP 状态码 / 异常 响应码 / IOException

有两处故意「不记」:

  • 追踪开关关了 → 原样转发,零开销
  • TraceContext 里没有父 Span → 也原样转发

第二种很常见:定时任务、启动时预热、有人在非 Web 线程里调了 Feign。没有进站请求,就不要凭空造一条 CLIENT 边,拓扑会被这些野点弄脏。

这也解释了上一篇文章 HandlerInterceptor 和 WebFilter:两套 HTTP 请求拦截 为啥把 SERVER Span 放进 ThreadLocal:同一条 Tomcat 线程里,Controller 接着调 Feign,栈顶正好是刚才那个入口 Span,父子就能串上。

四、remoteService:拓扑边不是前端画的

CLIENT Span 上最要紧的字段不是花活,是这一句:

java 复制代码
clientSpan.setRemoteService(resolveRemoteService(url));

当前实现很土:从 Feign 的 URL 里抠 host。Nacos + LoadBalancer 常见形态是:

text 复制代码
http://sca-user/user/1
         ↑
      这个 host 往往就是服务名

于是 remoteService = "sca-user"

Server 聚合拓扑时,不看调用树,只扫 Span:

text 复制代码
谁调用谁 = (span.serviceName) → (span.remoteService)

serviceName 是调用方(order),remoteService 是被调方(user)。有这两头,前端才能画箭头、数次数。

没有 Feign 这层马甲,进站 SERVER Span 上通常没有 remoteService,拓扑就是一堆孤点。

这里的设计其实有局限:

  • host 不一定永远是服务名,直连 IP 时图上会出现 IP 节点
  • 跨进程还没有传同一个 traceId(上一篇说过),拓扑是「调用关系」,不是一条 Zipkin 大树
  • 只包了 Servlet 栈 + LoadBalancer Feign;纯 WebClient、RestTemplate 出站现在没套

五、自己做类似能力时可以记住的

  1. 先找真正发请求的那一层接口 (Feign 就是 Client#execute),用装饰器包它,不要去改每个 @FeignClient
  2. BeanPostProcessor 换 Bean,业务无感。 记得防重复包装,也要搞清楚包外层还是内层。
  3. 出站 Span 尽量挂在已有的进站上下文上。 没有父 Span 就别硬记,脏数据比漏记更烦。
  4. 拓扑要的是「从哪到哪」,记得填一个稳定的对端名字(服务名优于 IP)。

六、小结

你以为的 实际是
拦了进站 HTTP,拓扑自然有边 边来自出站 Span 的 remoteService
要改 Feign 接口才能埋点 包装 feign.Client 即可
所有 Client Bean 都该包 只包 LoadBalancer 外层,避免打两遍
装饰器要业务自己 new BeanPostProcessor 在出厂后替换

进站拦截器解决「有请求进来」,后置处理器解决「它又调了别人」。两头都有 Span,控制台才像一张网,而不是几个点。

下一篇我们看看数据怎么离开业务进程:请求线程里别打 HTTP,队列里攒一攒再上报。

🌟 最后:欢迎围观 Spring Insight

如果你对「轻量监测 / Spring Boot Starter / 链路埋点」感兴趣,欢迎看看这个还在打磨的小项目:

🔗 GitHubgithub.com/iweidujiang...

当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。

你的 Star 是对我最大的鼓励。

相关推荐
花生了什么事o1 小时前
自定义Spring Boot Starter全流程
java·spring boot·后端
云上小朱2 小时前
开发测试环境-Kubernetes离线部署指南
后端
遨翔在知识的海洋里2 小时前
nestjs(1)-模块的相互调用
后端
Profile排查笔记2 小时前
指纹浏览器推荐:用一套验收清单筛选 Profile、代理与自动化能力
前端·人工智能·后端·自动化
jsl_jsl_jsl2 小时前
JUC速记
后端
摇滚侠2 小时前
《SpringBoot 3:入门与应用实战》第 12 章 JDBC 与事务 使用 JdbcTemplate 阅读笔记 31
android·spring boot·笔记
站大爷IP2 小时前
Python的pip依赖把我折腾惨了,原来requirements.txt和poetry能打出火星撞地球
后端
torpidcat2 小时前
ruoyi-vue-pro 若依芋道 java springboot +mybatis 生日查询相关
java·vue.js·spring boot
狗哥哥2 小时前
用“十步学习法”带你学会事件驱动架构
后端