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 / 链路埋点」感兴趣,欢迎看看这个还在打磨的小项目:

🔗 GitHub :github.com/iweidujiang...

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

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

相关推荐
Wx-bishekaifayuan13 小时前
springboot社区扶贫救助管理系统13300-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
鸿观工坊14 小时前
第5篇 Nginx 监控:流量、连接与性能指标
监控
DolphinDB14 小时前
数据库自带 Agent:10 分钟搭起 DolphinX-Web 数据入库与分析框架
后端·架构
弈栈录14 小时前
基于 Spring Boot 构建生产级 AI 应用平台
后端·面试·架构
小小张说故事14 小时前
LightGBM 入门指南:更快的梯度提升树,Python 实战
后端·python·机器学习
站大爷IP14 小时前
Python的默认参数把我坑惨了,原来写[]和写None的区别这么大
后端
Ticnix14 小时前
RAG 检索不准,九成的锅不在向量——不同文件,就该有不同的入库方案
后端·python·agent
福兮说14 小时前
Go 优雅退出:Shutdown 之后,后台 goroutine 还在跑(四个实测的坑)
后端·go
用户2986985301414 小时前
Python 文档格式转换:Word 转 EPUB 实践
后端·python·api
德先生14 小时前
1.1-main.cpp入口解剖
后端