项目地址 :github.com/iweidujiang...(感谢 Star !)
本文要干什么
Spring Insight 要监测的是一整条微服务调用链 。链路上不只有 order、user 这种普通 Web 服务,门口通常还有一个 Spring Cloud Gateway 做统一入口------请求先到网关,再转发到后面的服务。
业务方希望的是:每个进程都加同一个 spring-insight-agent-starter,HTTP 入口都能被埋点。麻烦在于,这些进程的 Web 技术栈并不一样:

- MVC 服务:用
HandlerInterceptor拦请求,这是 Spring MVC 的活 - Gateway:底层是 WebFlux,classpath 上往往没有
spring-webmvc。你把 MVC 拦截器硬塞进去,不是「拦不到」,是网关进程直接启动失败
所以本文的主要目的是解决:
同一份 Starter,怎么认出宿主是普通 MVC 还是 Gateway,然后装不同的配置------既让 MVC 服务有拦截器,又别把网关搞崩。
上一篇讲的是自动装配那扇门(.imports 把入口配置推进容器)。
这篇讲门后面那句 @Import:真正「看菜下锅」的是 DeferredImportSelector。
这里先挖个坑:拦截器、WebFilter 怎么写,下一篇再展开说;这篇只关心装还是不装、装哪一套。
java
@Import(InsightAgentAutoConfigurationImportSelector.class)
一、先说会炸的那种写法
Spring MVC 的 HTTP 入口很好拦:写个 HandlerInterceptor,再实现 WebMvcConfigurer,addInterceptors 注册进去。Insight 就是这么干的:
java
@Configuration
@ConditionalOnWebApplication(type = SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
public class InsightAutoConfiguration implements WebMvcConfigurer {
// 注册 HttpRequestInterceptor
}
这在 spring-boot-starter-web 的服务里没毛病。WebMvcConfigurer 就在 classpath 上,拦截器正常干活。
把它原封不动丢给 Spring Cloud Gateway,就有问题了。
Gateway 走的是 WebFlux,classpath 上通常没有 spring-webmvc。
而 InsightAutoConfiguration 这个类声明了 implements WebMvcConfigurer------JVM 加载这个类的时候,就要去解析父接口。接口不在,直接:
text
NoClassDefFoundError: org/springframework/web/servlet/config/annotation/WebMvcConfigurer
注意:这不是「条件没过、安静跳过」,是类都加载不进来,应用起不来。

监测工具把业务服务搞挂了,这锅背不起。所以不能「看见配置类就 import」。得先看菜,再下锅。
二、@ConditionalOnClass 为啥来不及
有人会说:类上面不是已经写了 @ConditionalOnClass(WebMvcConfigurer.class) 吗?条件不满足不就跳过了?
对已经成功加载进 JVM 的配置类 ,这个注解很管用。Feign 那行配置就是靠它:classpath 没有 feign.Client,整段跳过。
但这次卡在更前面------条件还没机会跑,类已经加载失败了。
两个很容易踩的点:
1. implements 发生在类加载阶段
@ConditionalOnClass 是 Spring 的事。implements WebMvcConfigurer 是 JVM 的事。JVM 不管你注解怎么写,加载子类就要解析接口。接口没有,游戏结束。
2. 选择器里写 .class 也会提前爆
假如选择器里图省事这样写:
java
imports.add(InsightAutoConfiguration.class.getName());
看起来只是取了个名字,其实 .class 会让 JVM 立刻加载 InsightAutoConfiguration。Gateway 上,爆炸点从「import 之后」提前到「选择器自己加载的时候」。
所以 Insight 里 MVC 配置类不用 .class,用字符串:
java
private static final String INSIGHT_SERVLET_WEB_CONFIGURATION =
"io.github.iweidujiang.springinsight.agent.autoconfigure.InsightAutoConfiguration";
字符串只是一行字,不会触发类加载。真要 import 了,才轮到 Spring 去加载;不 import,Gateway 连这个类的面都见不到。
Spring Boot 官方还有一招:把
implements WebMvcConfigurer拆到内部静态类上,外层配置类保持「干净」,@ConditionalOnClass才来得及。Insight 选的是另一条路------判断不是 Servlet 栈,这个类根本不 import。 效果一样:别让 JVM 去碰那个接口。
三、DeferredImportSelector:晚点再点菜
@Import 后面可以跟一个普通配置类,也可以跟一个 ImportSelector:你自己返回「要 import 哪些类名」。
Insight 用的是它的延迟版:DeferredImportSelector。
ImportSelector |
DeferredImportSelector |
|
|---|---|---|
| 什么时候跑 | @Import 处理到就跑 |
等一波配置都处理完,再跑 |
| 适合干啥 | 立刻追加几个类 | 看环境、看 classpath,再决定菜单 |
晚点跑有个好处:Environment 已经在了,能读到 spring.main.web-application-type。选择器同时实现了 EnvironmentAware、BeanClassLoaderAware,就是为了拿这两样东西做判断。
Boot 自己的自动配置,走的也是延迟选择器这套节奏。Starter 里跟着用,不别扭。

四、对照选择器代码
Agent Starter 用的是 InsightAgentAutoConfigurationImportSelector。selectImports 很短:
java
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> imports = new ArrayList<>();
// 核心 Bean:异步上报、监听器、JVM、DB 切面、Http Sink
imports.add(InsightBeanConfiguration.class.getName());
if (shouldImportServlet()) {
imports.add(INSIGHT_SERVLET_WEB_CONFIGURATION); // 字符串,不是 .class
}
// 自己带 @ConditionalOnWebApplication(REACTIVE),Servlet 栈上不会生效
imports.add(InsightWebFluxAutoConfiguration.class.getName());
return imports.toArray(String[]::new);
}
三盘菜,职责分开:
| 导入项 | 里面有什么 | MVC 服务 | Gateway |
|---|---|---|---|
InsightBeanConfiguration |
上报器、Sink、DB 切面......不碰 WebMvcConfigurer |
装 | 装 |
InsightAutoConfiguration |
HandlerInterceptor + WebMvcConfigurer |
装 | 不装 |
InsightWebFluxAutoConfiguration |
ReactiveInsightWebFilter |
条件不满足,跳过 | 装 |
核心 Bean 用 .class 没问题:那个配置类不实现 MVC 接口,Gateway 上加载它是安全的。
危险的只有 MVC 那一份,所以才单独用字符串,并且套一层 if。
五、怎么判断「现在是不是 Gateway」
shouldImportServlet() 把脏活交给 InsightWebStackDetect:
java
public static boolean shouldImportServletWebInsight(String webApplicationTypeProperty,
ClassLoader... classLoaders) {
if (webApplicationTypeProperty != null && !webApplicationTypeProperty.isBlank()) {
return "servlet".equalsIgnoreCase(webApplicationTypeProperty.trim());
}
return !isSpringCloudGatewayPresent(classLoaders);
}
判断就两步,先看配置,再看 classpath:

配了 web-application-type,听配置的。 Gateway 一般是 reactive,直接否掉 MVC。你要是硬写成 servlet,那是你自己要走 MVC,选择器也尊重。
没配,就看 classpath 上有没有 org.springframework.cloud.gateway.filter.GlobalFilter。 有,当成 Gateway,跳过 MVC。探测用 Class.forName(..., false, classLoader),只问「类在不在」,不初始化。
探测时还多试了几个 ClassLoader(Bean 的、线程上下文的、默认的)。这不是炫技,是因为 Starter 跑在别人的应用里,类到底由谁加载,偶尔会对不上。试几个,总比漏判一次启动失败强。
WebFlux 那份配置类自己带着:
java
@ConditionalOnWebApplication(type = REACTIVE)
@ConditionalOnClass({WebFilter.class, Mono.class})
所以选择器里可以「无脑把它加进返回列表」:Servlet 应用上条件不满足,它自己躺平,不会去 new WebFilter。
六、两种栈,最终长这样
text
普通 Spring MVC 服务
└─ agent-starter
├─ InsightBeanConfiguration ✅ 上报、切面
├─ InsightAutoConfiguration ✅ HandlerInterceptor
└─ InsightWebFluxAutoConfiguration ⏭ 条件跳过
Spring Cloud Gateway
└─ agent-starter
├─ InsightBeanConfiguration ✅ 上报、切面
├─ InsightAutoConfiguration ❌ 根本不 import
└─ InsightWebFluxAutoConfiguration ✅ WebFilter
同一份依赖,两套入口。业务方不用写 exclusion,也不用在 Gateway 上专门关 MVC。
这就是「看菜下锅」:不是把后厨所有锅都架上,而是先问一句------今晚到底是炒菜还是煮汤。
七、自己做 Starter 时可以记住的三件事
- 可选 API 不要写在配置类的
implements上还指望条件注解来救。 要么拆内部类,要么像 Insight 这样:危险类用字符串引用,判断失败就不 import。 - 选择器里对「可能不存在的类」不要写
.class。 那不是取名字,那是立刻加载。 - 需要读
Environment、按运行形态分叉,用DeferredImportSelector更合适。 晚点点菜,菜单才齐。
八、小结
| 你以为的 | 实际是 |
|---|---|
@ConditionalOnClass 一定能拦住缺失的类 |
拦不住 implements 触发的类加载 |
@Import(Xxx.class) 只是登记名字 |
.class 会马上加载 Xxx |
| Starter 只能假设宿主是 MVC | 可以按 Web 栈返回不同配置类 |
| Gateway 要用 exclusion 才能加监测 | 选择器不 import MVC 类即可 |
上一篇的门是 .imports 推开的。这篇的门卫是 DeferredImportSelector:先看你是 Servlet 还是 Gateway,再决定把不把 WebMvcConfigurer 那份配置端上桌。
下一篇会把两套入口摊开讲:HandlerInterceptor 和 WebFilter 各拦什么、为啥 Gateway 不能共用那份 ThreadLocal 调用栈。
🌟 最后:欢迎围观 Spring Insight
如果你对「轻量监测 / Spring Boot Starter / 链路埋点」感兴趣,欢迎看看这个还在打磨的小项目:
🔗 GitHub :github.com/iweidujiang...
当前形态很朴素:业务服务加 spring-insight-agent-starter,旁边单独跑 insight-server 看拓扑和链路。能力有限,代码也还糙,适合当练手和对照。
你的 Star 是对我最大的鼓励。