最近在帮团队面试 Java 后端同学时发现一个挺有意思的现象很多同学对 Spring 框架的常用注解、配置、甚至一些源码细节都能说上几句但当我问到“Spring 框架内部是如何发现和加载那些扩展组件的比如你引入一个spring-boot-starter-data-redisSpring Boot 怎么就知道要去初始化RedisTemplate”时能清晰回答的人就少了很多。如果再追问一句“这和 JDK 的 SPI 机制有什么区别和联系”场面往往就安静了。这其实反映了一个问题我们学习框架时容易停留在“怎么用”的层面而忽略了框架设计者为了解决“怎么灵活组装”这个核心问题所构建的底层机制。SPIService Provider Interface就是这样一个机制。它听起来像是一个简单的“服务发现”模式但在 Spring 这种庞大的生态体系中它的实现和演化远比我们想象的要深入和巧妙。理解它不仅能帮你回答“Spring 中的 SPI 是如何实现的”这类面试题更能让你看清一个优秀框架是如何保持核心稳定又能无限扩展的。今天我们就结合源码把这件事聊透。1. 先别急着看 Spring从 JDK SPI 理解“约定大于配置”的起点要理解 Spring 的 SPI我们必须先回到源头看看 Java 标准库提供的 SPI 机制。它的核心思想极其简单“约定大于配置”。我不在代码里硬编码实现类而是约定一个规则让程序在运行时去发现。1.1 JDK SPI 的基本玩法与局限JDK 的 SPI 定义非常清晰。假设我们有一个服务接口com.example.Encoder它的实现类FastEncoder和StandardEncoder分别在两个不同的 Jar 包里。作为服务的使用方我只需要依赖接口包。那么服务提供方如何告知使用方“我在这里”呢答案就在META-INF/services/目录下。提供方需要在这个目录下创建一个以服务接口全限定名命名的文件例如META-INF/services/com.example.Encoder文件内容就是该接口实现类的全限定名一行一个。# 文件META-INF/services/com.example.Encoder com.provider.fast.FastEncoder com.provider.standard.StandardEncoder使用时通过java.util.ServiceLoader来加载ServiceLoaderEncoder loader ServiceLoader.load(Encoder.class); for (Encoder encoder : loader) { // 遍历所有找到的实现 System.out.println(encoder.encode(Hello)); }这个过程完全由ServiceLoader在类路径下扫描完成实现了接口与实现的解耦。然而在实际的、复杂的生产框架中JDK SPI 暴露出几个明显的短板一次性加载全部实现ServiceLoader会实例化配置文件中所有的实现类。如果实现类很多或者某些实现类初始化成本很高会造成不必要的资源浪费和启动延迟。缺乏精细化的筛选能力它只能拿到所有实现无法根据某个条件如注解、属性进行过滤或排序。框架通常需要“按需加载”。单例与生命周期管理缺失ServiceLoader每次调用load都会返回新的迭代器实例化新的对象。它不关心对象是单例还是原型也不管理对象的销毁。而在 Spring 的 IoC 容器里Bean 的生命周期是核心。与容器环境脱节实现类无法自然地享受 Spring 容器的依赖注入、AOP 等能力。它们只是被简单new出来的普通对象。正是这些局限促使 Spring 框架在借鉴 SPI 思想的同时必须设计一套更强大、与自身容器深度集成的扩展机制。1.2 思想迁移从“文件发现”到“容器内注册”理解 JDK SPI 后再看 Spring 的 SPI你会发现核心思想一脉相承——都是通过外部配置来发现实现。但 Spring 将其升华了它把“发现服务实现”这个过程整合进了自己的 Bean 定义加载和生命周期管理流程中。Spring 不再仅仅读取一个文本文件然后反射创建对象。它的 SPI 机制本质上是在容器启动的某个特定阶段主动扫描某些特定的路径或注解将找到的类转换为 Spring 所理解的BeanDefinition然后注册到容器里使其成为一个真正的 Spring Bean后续便能享受容器的一切服务。所以Spring SPI 不是一个孤立的ServiceLoader调用而是一套基于事件、阶段、条件判断的扩展点加载体系。这是理解后续所有内容的基础。2. Spring SPI 的核心实现SpringFactoriesLoader与META-INF/spring.factoriesSpring 框架自己实现了一个增强版的 SPI 加载器org.springframework.core.io.support.SpringFactoriesLoader。它是 Spring Boot 自动化配置的基石也是 Spring 框架内部模块化扩展的关键。2.1spring.factories文件的格式与加载与 JDK SPI 类似它也是基于类路径下META-INF/spring.factories文件的约定。但它的格式更灵活是一个 Properties 文件支持一个键对应多个值。# 文件META-INF/spring.factories # 键是接口或抽象类的全限定名值是实现类的全限定名用逗号分隔 org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.MyAutoConfiguration,\ com.example.AnotherAutoConfiguration org.springframework.context.ApplicationListener\ com.example.MyApplicationListener org.springframework.boot.env.EnvironmentPostProcessor\ com.example.MyEnvironmentPostProcessor加载这些配置的核心方法是SpringFactoriesLoader.loadFactoryNames()。我们可以看一下它的简化逻辑// 简化版逻辑非完整源码 public final class SpringFactoriesLoader { public static final String FACTORIES_RESOURCE_LOCATION META-INF/spring.factories; public static ListString loadFactoryNames(Class? factoryType, Nullable ClassLoader classLoader) { // 1. 获取工厂接口的全限定名 String factoryTypeName factoryType.getName(); // 2. 遍历所有jar包的 META-INF/spring.factories 文件 // 3. 获取对应 factoryTypeName 键下的所有值类名 // 4. 返回去重后的列表 return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); } private static MapString, ListString loadSpringFactories(Nullable ClassLoader classLoader) { // 关键使用 ClassLoader.getResources() 获取所有同名资源 EnumerationURL urls classLoader.getResources(FACTORIES_RESOURCE_LOCATION); // ... 解析每个URL对应的properties文件合并结果 } }这个方法的核心是ClassLoader.getResources()它能从所有已加载的 Jar 包中找到所有同名的spring.factories文件然后进行合并。这保证了不同模块、不同 Starter 提供的扩展配置都能被收集到。2.2 与 JDK SPI 的关键增强点通过SpringFactoriesLoaderSpring 实现了对 JDK SPI 的几点重要增强按需加载框架通常只在特定的、需要的时候才调用loadFactoryNames加载某一类扩展如ApplicationListener而不是启动时就加载全部。成为 Bean 定义的前置步骤加载到的类名往往不是直接被实例化而是作为候选者交给后续的流程如BeanDefinitionLoader去处理最终变成 Bean。这为生命周期管理打下了基础。支持排序虽然spring.factories文件本身不定义顺序但 Spring Boot 在后续处理如自动配置类加载时引入了AutoConfigureOrder,Order等注解进行排序实现了更精细的控制。这里有一个非常重要的实践认知SpringFactoriesLoader本身只是一个“类名发现器”。它只负责找到字符串形式的全限定类名。至于这些类是被实例化、被注册为 Bean还是被用作其他用途完全取决于调用它的上层框架代码。这是理解 Spring SPI 灵活性的关键。3. 典型应用场景一Spring Boot 自动配置的引擎SpringFactoriesLoader最广为人知的应用就是 Spring Boot 的自动配置。这也是面试中最常被问到的点。3.1 从EnableAutoConfiguration到自动配置类的加载当我们使用SpringBootApplication注解时它元注解了EnableAutoConfiguration。这个注解的核心是导入了一个AutoConfigurationImportSelector。AutoConfigurationImportSelector的关键方法selectImports()最终会调用getCandidateConfigurations()// 摘自 AutoConfigurationImportSelector protected ListString getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 关键调用使用 SpringFactoriesLoader 加载 key 为 EnableAutoConfiguration 的类 ListString configurations SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); // ... return configurations; } protected Class? getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }你看这里明确使用了SpringFactoriesLoader.loadFactoryNames()并指定了工厂类型为EnableAutoConfiguration.class。它会去所有 Jar 包的META-INF/spring.factories文件中寻找org.springframework.boot.autoconfigure.EnableAutoConfiguration这个键对应的值。例如在spring-boot-autoconfigure包的spring.factories中就有上百个自动配置类org.springframework.boot.autoconfigure.EnableAutoConfiguration\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\ # ... 非常多3.2 自动配置类的筛选与生效条件拿到所有候选配置类后Spring Boot 并不会全部启用。它会经过一系列严格的筛选去重排除重复的类。排除根据spring.autoconfigure.exclude配置或EnableAutoConfiguration的exclude属性排除指定类。条件注解过滤这是最核心的一步。每个自动配置类上都标有大量的ConditionalOnClass,ConditionalOnBean,ConditionalOnProperty等条件注解。Spring Boot 会判断当前项目的类路径、已有的 Bean、配置属性等是否满足这些条件。只有全部满足这个配置类才会被真正解析其内部定义的 Bean 才会注册到容器中。这就是 Spring Boot “开箱即用” 的秘密通过 SPI 机制“发现”所有可能的配置再通过条件注解“按需启用”真正匹配的配置。整个过程高度自动化且对用户透明。4. 典型应用场景二框架内部的事件监听器与后置处理器除了自动配置spring.factories机制还被广泛用于注册 Spring 框架内部的各种扩展点。这些扩展点通常不是普通的业务 Bean而是用于框架自身启动、初始化和扩展的组件。4.1 应用事件监听器 (ApplicationListener)Spring 的事件驱动模型是一个强大的特性。除了我们自己在代码中用EventListener注解的方法框架本身也需要监听一些内部事件。例如ConfigFileApplicationListener用来监听ApplicationEnvironmentPreparedEvent事件从而加载application.properties文件。它就是在spring-boot包的spring.factories中声明的org.springframework.context.ApplicationListener\ org.springframework.boot.context.config.ConfigFileApplicationListener,\ org.springframework.boot.ClearCachesApplicationListener,\ # ... 其他监听器在 Spring Boot 应用的SpringApplication.run()方法中会通过SpringFactoriesLoader加载这些监听器并将它们添加到SpringApplication的监听器列表中从而在应用生命周期的各个阶段接收到相应的事件。4.2 环境后置处理器 (EnvironmentPostProcessor) 与 应用上下文初始化器 (ApplicationContextInitializer)这两个扩展点允许我们在 Spring 容器完全刷新之前对环境和上下文进行定制。EnvironmentPostProcessor: 用于在Environment对象包含所有配置属性被用于容器之前对其进行修改或增强。例如CloudFoundryVcapEnvironmentPostProcessor用来解析 Cloud Foundry 的 VCAP 服务信息。ApplicationContextInitializer: 用于在ConfigurableApplicationContext刷新之前对其进行编程式配置。它们的注册方式同样是通过spring.factoriesorg.springframework.boot.env.EnvironmentPostProcessor\ org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor org.springframework.context.ApplicationContextInitializer\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer这些扩展点的共同特点是它们需要在 Spring 容器生命周期的极早期介入其注册必须独立于常规的组件扫描和Bean定义。spring.factoriesSPI 机制完美地满足了这一需求为框架提供了“自举”和“插件化”的能力。5. 演进与替代META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports随着 Spring Boot 2.7 的发布官方开始推动一种新的自动配置注册方式并在 Spring Boot 3.0 中将spring.factories中关于自动配置的条目标记为废弃转向了META-INF/spring/目录下的新文件。5.1 新老机制对比特性老机制 (spring.factories)新机制 (spring/目录)文件位置META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件格式Properties 格式一个键对应多行值文本格式每行一个全限定类名可读性相对较差所有扩展点混在一个文件更好按功能分文件如还有org.springframework.context.ApplicationContextInitializer文件加载方式由SpringFactoriesLoader统一加载由AutoConfigurationLoader等专用加载器读取设计意图通用的 SPI 注册表承载多种扩展点更专注、类型安全的配置加载减少冗余解析5.2 为什么需要改变这个变化背后有几点考虑关注点分离将不同目的的配置自动配置、监听器、初始化器拆分到不同文件结构更清晰。性能优化专用加载器可以更高效地读取和处理特定类型的配置避免解析不需要的键值对。向前演进为未来可能引入的更丰富的配置方式如基于注解的索引留出空间。对于开发者而言这意味着什么如果你在开发自己的 Spring Boot Starter从 Spring Boot 2.7 开始推荐使用新的spring/目录方式注册自动配置类但同时为了向后兼容可以在过渡期保留spring.factories中的条目。Spring Boot 3.x 已完全转向新方式。6. 实践启示如何设计自己的 SPI 扩展点理解了 Spring 的 SPI 实现我们可以在自己的项目或中间件设计中借鉴这种模式。关键在于想清楚你的扩展点是需要在容器外被简单发现还是需要深度集成到容器的生命周期中6.1 场景一轻量级插件发现仿 JDK SPI如果你的模块相对独立扩展实现不需要成为 Spring Bean或者可以自己管理生命周期那么模仿 JDK SPI 或直接使用SpringFactoriesLoader是简洁有效的。步骤定义扩展接口。约定配置文件位置和格式如META-INF/services/my.interface或META-INF/my-module/extensions。在框架启动时使用ClassLoader.getResources()或SpringFactoriesLoader扫描并加载所有实现类。实例化并使用它们。适用场景日志门面适配器、编解码器插件、简单的策略模式实现。6.2 场景二需要容器集成的扩展仿 Spring Boot Starter如果你的扩展实现需要依赖其他 Spring Bean或者需要被注入或者其本身需要复杂的生命周期管理那么最好的方式是将其设计成一个 Spring Boot Starter 的自动配置。步骤定义你的核心业务接口和至少一个默认实现。创建一个Configuration配置类使用ConditionalOnXxx系列注解控制其生效条件。在该配置类中通过Bean方法将你的实现类暴露为 Spring Bean。可以考虑使用ConditionalOnMissingBean来提供默认实现同时允许用户自定义。在src/main/resources/META-INF/spring/目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件写入你的配置类全名。可选提供额外的EnvironmentPostProcessor或ApplicationContextInitializer来处理更早阶段的定制。适用场景连接池 Starter、消息队列客户端 Starter、分布式锁 Starter、监控上报 SDK 等。6.3 核心设计要点无论采用哪种方式在设计自己的 SPI 时都要注意以下几点明确的契约接口定义要稳定向后兼容。良好的文档清晰说明扩展点的作用、何时被调用、以及实现者的职责。合理的默认值提供“开箱即用”的体验就像 Spring Boot 做的那样。失败友好扩展点加载失败不应导致主流程崩溃应有合理的降级或错误日志。避免循环依赖特别是在 Spring 容器内要小心自动配置类之间的依赖关系。回过头看文章开头的问题Spring 中的 SPI 实现远不止是一个“服务发现”的工具类。它是 Spring 框架模块化、可插拔架构的神经系统。从SpringFactoriesLoader到spring.factories再到新的spring/目录结构其演进始终围绕着同一个目标在保持核心容器简洁稳定的前提下为海量的第三方集成和用户自定义扩展提供一套优雅、统一、强大的接入规范。下次当你引入一个 Starter 就能神奇地使用某个功能时可以想想背后这套精密的 SPI 机制是如何工作的。这不仅是一个面试考点更是理解现代框架设计哲学的一把钥匙。在技术选型和架构设计时当你也面临“如何让系统核心保持稳定又能灵活扩展”的命题时Spring SPI 的设计思路或许能给你带来直接的启发。