Gateway 上别硬塞 MVC:DeferredImportSelector 看菜下锅

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

本文要干什么

Spring Insight 要监测的是一整条微服务调用链 。链路上不只有 orderuser 这种普通 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,再实现 WebMvcConfigureraddInterceptors 注册进去。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。选择器同时实现了 EnvironmentAwareBeanClassLoaderAware,就是为了拿这两样东西做判断。

Boot 自己的自动配置,走的也是延迟选择器这套节奏。Starter 里跟着用,不别扭。

四、对照选择器代码

Agent Starter 用的是 InsightAgentAutoConfigurationImportSelectorselectImports 很短:

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 时可以记住的三件事

  1. 可选 API 不要写在配置类的 implements 上还指望条件注解来救。 要么拆内部类,要么像 Insight 这样:危险类用字符串引用,判断失败就不 import。
  2. 选择器里对「可能不存在的类」不要写 .class 那不是取名字,那是立刻加载。
  3. 需要读 Environment、按运行形态分叉,用 DeferredImportSelector 更合适。 晚点点菜,菜单才齐。

八、小结

你以为的 实际是
@ConditionalOnClass 一定能拦住缺失的类 拦不住 implements 触发的类加载
@Import(Xxx.class) 只是登记名字 .class 会马上加载 Xxx
Starter 只能假设宿主是 MVC 可以按 Web 栈返回不同配置类
Gateway 要用 exclusion 才能加监测 选择器不 import MVC 类即可

上一篇的门是 .imports 推开的。这篇的门卫是 DeferredImportSelector:先看你是 Servlet 还是 Gateway,再决定把不把 WebMvcConfigurer 那份配置端上桌。

下一篇会把两套入口摊开讲:HandlerInterceptorWebFilter 各拦什么、为啥 Gateway 不能共用那份 ThreadLocal 调用栈。

🌟 最后:欢迎围观 Spring Insight

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

🔗 GitHubgithub.com/iweidujiang...

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

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

相关推荐
名字还没想好☜1 小时前
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法
后端
十六年开源服务商2 小时前
2026年WordPress页面构建器终极选型指南
开源
IT_陈寒2 小时前
React Hooks闭包陷阱差点让我加班到凌晨
前端·人工智能·后端
黑马程序员毕设2 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设
码事漫谈2 小时前
本体论,彻底懂
后端
创新技术阁3 小时前
FastapiAdmin 系统日志体系与核心配置参数详解
前端·后端·fastapi
掘金者阿豪3 小时前
SQL Server 迁到金仓,真正麻烦的不是数据,而是这些 T-SQL
后端
xcl09253 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·spring boot·需求分析
Kyrie_kk3 小时前
Java--IO--Path文件访问
java·后端