Spring Boot里的spring.factories,很多同学第一眼看到会有点懵:一个放在META-INF下的properties文件,到底凭什么撑起自动配置、starter、各种扩展机制?其实它就是框架和业务代码之间的一份“通讯录”,告诉Spring容器“我有这些扩展点实现类,你启动的时候记得加载”。这篇文章我从原理、源码、实操到迁移,把spring.factories彻底讲透。不管你是刚学Spring Boot的新手,还是正在封装公司内部starter的开发者,都能从中拿到能直接用的东西。
1. 认识spring.factories:它是自动配置的“通讯录”
1.1 文件路径与基本格式
spring.factories必须放在classpath下的META-INF/spring.factories,注意是META-INF,不是META_INF,也不是META-INFO。这个路径是Spring Boot约定好的,改错一个字母,你的配置就无声无息地消失。
文件格式是标准的Java Properties格式,每一行的结构是:
完整接口或注解类名=实现类1,实现类2,实现类3等号左边是“扩展点的类型”,等号右边是“你要注册的具体类”。多个实现类用英文逗号分隔,如果只有一个实现类就不需要逗号。看一个Spring Boot内置的真实片段:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration行尾的反斜杠是Properties格式的续行符,代码里常见。注意这里等号虽然是EnableAutoConfiguration,但它其实是一个注解的完整类名,Spring Boot把“自动配置类”统一挂在这个key下面。
1.2 它到底解决了什么问题
你可以把Spring Boot想象成一个酒店前台,spring.factories就是前台的电话本。酒店有很多服务,比如送餐、保洁、维修,前台不可能提前知道每位客人需要什么,但客人一打电话,前台就能根据电话本找到对应的服务人员。spring.factories就是这张电话本:Spring Boot启动时,不知道项目里有哪些自动配置类、有哪些监听器、有哪些初始化器,但它会去扫描所有jar包里的spring.factories,把注册过的实现类全部读出来,再根据你的依赖和你加的条件判断要不要真的实例化。
所以它解决的是一个很基础也很关键的问题:在Spring Boot自己的代码启动之前,如何发现并加载那些“不属于Spring Boot核心,但又必须被Spring管理”的类。没有这个机制,你要么只能手动@Import,要么只能要求用户把类写进@ComponentScan的扫描路径里,这会让starter和框架的扩展性大打折扣。
1.3 spring.factories与SPI机制的关系
SPI的全称是Service Provider Interface,Java原生的SPI通过META-INF/services/接口全限定名文件来注册实现。spring.factories本质上就是Spring自己改造后的SPI,区别在于:
- 原生SPI一个接口对应一个文件,
spring.factories一个文件可以写多个接口。 - 原生SPI通常只做“发现”,Spring Boot的
SpringFactoriesLoader还负责“实例化”和“排序”。 spring.factories由Spring Boot统一管理,加载时带缓存,性能没问题。
我把关系总结成一张表:
| 对比项 | Java原生SPI | Spring Boot的spring.factories |
|---|---|---|
| 配置位置 | META-INF/services/ | META-INF/spring.factories |
| 配置粒度 | 一个接口一个文件 | 一个文件多个接口 |
| 自动实例化 | 留给调用方处理 | SpringFactoriesLoader自动实例化 |
| 排序支持 | 没有内置排序 | 支持Ordered和@Order |
| 常见应用 | JDBC Driver、日志门面 | Spring Boot自动配置、starter扩展 |
理解这一点,再看后面的源码就顺了。你不需要重写一套SPI,只需要知道Spring Boot在哪一步、怎么读这个文件,就能掌控自动配置的加载过程。
2. 核心机制拆解:SpringFactoriesLoader如何读取和实例化
2.1 SpringFactoriesLoader的加载逻辑
SpringFactoriesLoader是Spring Framework里提供的核心类,虽然名字里带Spring Boot,但它其实在spring-core包里,Spring Boot在它基础上做了一层封装。它有两个核心方法:
loadFactoryNames(Class<?> factoryType, ClassLoader classLoader):只返回类名的列表。loadFactories(Class<T> factoryType, ClassLoader classLoader):返回实例化的对象列表。
加载流程大致是:遍历classpath下的所有jar包,找到每个jar包里的META-INF/spring.factories,解析Properties,把key匹配的value拆成多个类名,然后用反射实例化。为了性能,SpringFactoriesLoader会使用ConcurrentReferenceHashMap做缓存,同一个ClassLoader下,同一个接口的类名结果只会加载一次。
这里有个容易被忽略的细节:loadFactories在拿到类名并实例化之后,如果对象实现了Ordered,或者标注了@Order,会按照顺序排序。也就是说spring.factories里写的类顺序,不一定等于实际加载顺序,最终顺序由排序注解决定。这一点在多个自动配置类互相依赖时尤其重要。
2.2 关键方法loadFactories与loadFactoryNames
看一个简化版的使用方法,理解起来更直观:
List<ApplicationListener> listeners = SpringFactoriesLoader.loadFactories( ApplicationListener.class, getClass().getClassLoader() );这段代码会自动去所有spring.factories里找org.springframework.context.ApplicationListener这个key对应的所有实现类,并把它们实例化出来。Spring Boot启动时,做监听器加载用的就是这个方法。
loadFactoryNames则更轻量,只拿类名,不实例化。比如Spring Boot在判断自动配置类时,会先拿到所有类名,然后逐一解析@ConditionalOnClass等条件,不满足条件的类根本不会创建对象。
2.3 类加载器与缓存机制
类加载器的问题特别值得说。loadFactories接收的ClassLoader,决定了它能读到哪些META-INF/spring.factories。在Spring Boot里,如果用到了自定义类加载器,加载不到某个starter的配置,别急着怀疑spring.factories文件,先检查是不是类加载器不对。实际排查时,可以用下面这段代码验证当前线程的类加载器能不能读到一个jar包里的spring.factories:
ClassLoader cl = Thread.currentThread().getContextClassLoader(); Enumeration<URL> urls = cl.getResources("META-INF/spring.factories"); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url); }缓存也是很多人踩坑的点。SpringFactoriesLoader的缓存是静态的,加载过的类名会留在缓存里。在IDE里反复热部署、热加载时,如果类名列表没变,可能读到的是旧配置;如果改了spring.factories里的类名,必须重启进程才能真正生效。
2.4 为什么适合做框架扩展点
因为spring.factories有三个特点:集中、批量、轻量。集中是说我可以在一个文件里注册多类扩展点,比如既注册ApplicationListener,又注册EnvironmentPostProcessor;批量是说只要jar在classpath里,Spring启动时会自动发现,不需要用户写任何注解;轻量是说它不依赖base package扫描,不会被项目自己的@ComponentScan范围影响。
这三个特点决定了它适合做底层框架的扩展口。你在项目里见过的一些组件,比如配置中心客户端、分布式锁starter、接口幂等框架,很多都是靠spring.factories注册自己的核心处理器的。知道这一点,你自己封装组件时就能用同样的方式设计公共入口。
3. 实战:自定义starter并正确配置spring.factories
3.1 创建starter项目的目录结构
要真正理解spring.factories,自己手写一个starter是最快的方式。我先说下标准的starter拆法:通常拆成两个模块,xxx-spring-boot-starter和xxx-spring-boot-autoconfigure。其中starter模块里一般只放依赖配置,自动配置逻辑放在autoconfigure模块里。也可以不拆,但在团队内部做沉淀时,拆开能避免把业务代码和配置逻辑混在一起。
我以“问候服务starter”为例,目录结构如下:
hello-spring-boot-starter/ ├── pom.xml └── src/main/resources/META-INF/spring.factories hello-spring-boot-autoconfigure/ ├── pom.xml └── src/main/java/com/example/hello/ ├── HelloAutoConfiguration.java ├── HelloProperties.java ├── HelloService.java为了让这个例子能跑起来,我们做一个最简单的功能:如果classpath里存在某个标记类,就自动创建一个HelloServiceBean,同时读取配置hello.prefix和hello.suffix。
3.2 编写自动配置类
自动配置类本质上就是一个带@Configuration的类,但因为要支持条件加载,所以会配合多个@Conditional注解使用。看一个完整的例子:
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(HelloProperties.class) @ConditionalOnClass(name = "com.example.some.Marker") public class HelloAutoConfiguration { @Bean @ConditionalOnMissingBean public HelloService helloService() { return new HelloService(properties.getPrefix(), properties.getSuffix()); } }@Configuration(proxyBeanMethods = false)是从Spring Boot 2.2开始官方推荐的写法,因为自动配置类里的@Bean方法之间很少互相直接调用,关闭CGLIB代理能减少启动时间。@ConditionalOnClass(name = "com.example.some.Marker")表示当classpath里有Marker类时才生效,这个类通常放在被集成的第三方库里,起到“探测依赖是否引入”的作用。@ConditionalOnMissingBean则保护用户自定义的HelloService,如果用户自己已经定义了一个,自动配置的就不会覆盖。
对应的属性类:
@ConfigurationProperties(prefix = "hello") public class HelloProperties { private String prefix = "Hello"; private String suffix = "!"; // getter setter 省略 }3.3 spring.factories中的标准Key汇总
接下来是重头戏:把自动配置类写进spring.factories。在Spring Boot 2.6及更早版本,你需要这样写:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.hello.HelloAutoConfiguration如果你还想注册一些非自动配置的扩展点,可以在这个文件里追加:
org.springframework.context.ApplicationListener=\ com.example.hello.MyApplicationListener我把Spring Boot中常用的spring.factoriesKey整理在下面,这些Key在启动时都会被SpringFactoriesLoader读取:
| Key | 作用 |
|---|---|
org.springframework.boot.autoconfigure.EnableAutoConfiguration | 注册自动配置类 |
org.springframework.context.ApplicationContextInitializer | 注册ApplicationContext初始化器 |
org.springframework.context.ApplicationListener | 注册监听器 |
org.springframework.boot.env.EnvironmentPostProcessor | 处理Environment,可在启动早期修改配置 |
org.springframework.boot.diagnostics.FailureAnalyzer | 自定义启动失败分析 |
org.springframework.boot.SpringApplicationRunListener | 监听SpringApplication运行过程 |
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider | 模板可用性判断 |
需要特别提醒:spring.factories里注册的类,Spring Boot默认是会直接实例化的。所以这些类最好都提供无参构造,不要在里面做太重的事情。条件判断只能用在自动配置场景,其他扩展点没有那么多@Conditional保护,写错了会在启动时报错。
3.4 多配置类的加载顺序控制
一个starter可能不止一个自动配置类。当spring.factories里写了多个类时,顺序不能靠文件里写的先后顺序保证。Spring Boot提供了三个注解:
@AutoConfigureOrder(order = N):数字越小越靠前。@AutoConfigureBefore(SomeClass.class):在指定配置类之前。@AutoConfigureAfter(SomeClass.class):在指定配置类之后。
这三个注解专门用来控制自动配置类的加载顺序,优先级高于spring.factories里的声明顺序。注意,@AutoConfigureBefore和@AutoConfigureAfter接受的是类对象数组,类必须能被安全引用。如果你不想直接依赖那个类的真实类型,但在同一个模块里,没问题;如果跨模块,尽量用@AutoConfigureOrder避免编译期硬依赖。
3.5 条件装配:@Conditional在自动配置中的重要性
自动配置类不是加载就一定要生效,它更像“候选人”。Spring Boot通过条件注解来决定最终“谁上岗”。最常见的条件注解包括:
@ConditionalOnClass:classpath中存在指定类才生效。@ConditionalOnMissingClass:相反,classpath中不存在指定类才生效。@ConditionalOnBean/@ConditionalOnMissingBean:容器中是否存在指定Bean。@ConditionalOnProperty:配置项是否等于某个值。@ConditionalOnWebApplication:是否是Web环境。
实际中,@ConditionalOnClass是使用频率最高的。比如你的starter想集成Redis,但又不想强制用户必须引入Redis依赖,你就可以在自动配置类上写@ConditionalOnClass(RedisConnectionFactory.class)。依赖没引,条件不成立,自动配置自然跳过。这样用户能灵活选择是否引入对应功能。
要注意@ConditionalOnClass的解析时机:它是在注解元数据解析阶段读取的,靠的是ClassLoader扫描,不是容器实例化阶段。所以即使RedisConnectionFactory在模块里没有编译依赖,用一个字符串name属性也可以安全判断,不要直接写RedisConnectionFactory.class,否则类加载时就会抛NoClassDefFoundError。
4. 基于spring.factories的其他玩法与坑
4.1 注册ApplicationListener与EnvironmentPostProcessor
除了自动配置,spring.factories还能注册很多“启动钩子”。以ApplicationListener为例,你可以自己写一个监听器:
public class HelloApplicationListener implements ApplicationListener<ApplicationStartedEvent> { @Override public void onApplicationEvent(ApplicationStartedEvent event) { // 应用启动完成后执行 } }然后在spring.factories里配置:
org.springframework.context.ApplicationListener=\ com.example.hello.HelloApplicationListenerSpring启动过程中会通过SpringFactoriesLoader.loadFactories加载所有ApplicationListener实现类,并放入事件广播器。
EnvironmentPostProcessor更高阶一点,它可以在Spring容器创建之前修改Environment。比如你想强行给配置项注入默认值,就可以实现EnvironmentPostProcessor:
public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { Map<String, Object> map = new HashMap<>(); map.put("hello.prefix", "Bonjour"); environment.getPropertySources().addLast(new MapPropertySource("hello-defaults", map)); } }这类扩展点非常适合做云上部署时的默认配置,或者打印启动降级日志。前提是记得在spring.factories里注册,否则Spring不会主动发现它。
4.2 注册Initializer、FailureAnalyzer等扩展点
说到扩展点,很多人只知道自动配置,其实spring.factories里可以挂载的扩展比想象中多:
ApplicationContextInitializer在Spring容器刷新之前执行,可以用来注册初始化逻辑;FailureAnalyzer可以在启动失败时输出友好提示。比如你可以自定义一个失败分析器,把某个异常翻译成“请检查你的Redis地址”这样的提示,比堆栈舒服多了。示例:
public class MyFailureAnalyzer implements FailureAnalyzer<SomeException> { @Override public FailureAnalysis analyze(Throwable failure, Description description) { return new FailureAnalysis("连接第三方服务失败,请检查网络", "检查配置项 xxx.xxx", failure); } }在spring.factories里加上:
org.springframework.boot.diagnostics.FailureAnalyzer=\ com.example.hello.MyFailureAnalyzer这个机制在Spring Boot 1.3之后就存在了,到现在还在用。如果你的脚手架要给团队做统一错误提示,用它特别合适。
4.3 常见坑:IDE不提示、文件名拼写、META-INF路径错误
实战中我先踩过的坑主要有这几个:
第一,文件名拼写错误。spring.factories不是spring-factories,不是spring.factory,必须一字不差。IDE不会帮你检查这个文件,所以错了也不报错,只是配置静默失效。
第二,文件位置错误。必须是src/main/resources/META-INF/spring.factories。很多人习惯复制,结果把文件放到了src/main/java下面,导致资源没进classpath。IDE里可以用Ctrl+Shift+N搜文件名,确认是不是在目标模块的resources目录下。
第三,Properties编码问题。spring.factories本质上走的Java Properties解析,默认编码是ISO-8859-1,如果你的类名或者注释里有中文,最好用\uXXXX转义,不然读出来是乱码。日常写注释尽量避免中文,或者把注释去掉。
第四,IDEA不提示类名。spring.factories里写实现类时,IDEA有时不会自动补全类路径。这很正常,因为IDEA没有把Properties文件的value识别为类引用。写完后你要么自己确认全限定名,要么用“Go To Class”复制完整类名。推荐后者,手敲最容易错。
4.4 多个jar/factories冲突处理
你的应用依赖了很多starter,每个jar包里都有META-INF/spring.factories,Spring Boot会把所有文件的内容合并。比如多个jar包里都注册了同一个Key,最终结果是所有实现类都会被加载,而不是后者覆盖前者。
这种机制的好处是可以组合扩展,坏处是如果有两个自动配置类逻辑上有冲突,你很难在文件级别“删除”其中一个。解决办法只能靠条件注解。如果你不想让某个自动配置生效,可以:
- 排除依赖,不让那个jar进入classpath。
- 使用
@SpringBootApplication(exclude = XxxAutoConfiguration.class)排除指定自动配置类。 - 在
application.properties里设置spring.autoconfigure.exclude=com.example.xxx.XxxAutoConfiguration。
注意,排除自动配置类时,类路径必须写对,否则Spring在解析时找不到类名,会启动失败。
4.5 调试技巧:如何查看实际加载了哪些自动配置
遇到“我的配置为什么没生效”这种问题,先别急着改代码,先看自动配置报告。在application.properties里加:
debug=true启动后控制台会打印一个“Positive matches”和“Negative matches”列表,分别列出哪些自动配置生效了、哪些条件没满足而没生效。比如你写了@ConditionalOnProperty,但配置项名打错了,就会在Negative matches里看到原因,非常直观。
也可以打开AutoConfigurationReport,或者直接使用Spring Boot的Actuator端点/actuator/conditions查看。如果是开发阶段,debug=true就够用了。这个方法能帮助你判断spring.factories到底有没有把这个类注册进去。如果列表里压根没有这个类,那问题多半出在spring.factories的路径、文件名或者Key写错上。
5. Spring Boot 2.7+及3.x的变化:spring.factories逐渐退居二线
5.1 导入AutoConfiguration.imports文件
从Spring Boot 2.7开始,官方推荐不再用spring.factories注册自动配置类,而是用一个新的文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports注意这个路径比较长,但它是固定的。文件格式很简单,每行写一个自动配置类的全限定名,不需要Key:
com.example.hello.HelloAutoConfiguration com.example.hello.HelloSecurityAutoConfiguration这个改动背后的原因,主要是让自动配置的声明更直观,也避免spring.factories里所有扩展点挤在一起。同时AutoConfiguration.imports支持“自动配置类的加载必须有直接依赖关系”,让条件判断更可控。
这里需要特别说明:在AutoConfiguration.imports里的类,官方建议标注@AutoConfiguration注解。它是@Configuration的增强版,可以带after、before等属性来声明顺序:
@AutoConfiguration(after = DataSourceAutoConfiguration.class) public class HelloAutoConfiguration { }等价于之前的@AutoConfigureAfter(DataSourceAutoConfiguration.class)。
5.2 从spring.factories迁移到META-INF/spring/...
如果你维护的starter从Spring Boot 2.6升级到2.7或3.x,一个核心动作就是把自动配置注册迁移到AutoConfiguration.imports。迁移步骤直接说:
- 新建文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。 - 把原本写在
spring.factories里EnableAutoConfigurationkey下的所有类名,逐行粘到新文件。 - 给每个自动配置类加上
@AutoConfiguration注解(或者至少确保它是@Configuration类)。 - 删除
spring.factories里的EnableAutoConfiguration项。其他key,如ApplicationListener、EnvironmentPostProcessor,仍然保留在spring.factories里,不用迁移。 - 在
spring.factories和imports文件同时存在的过渡期,Spring Boot 2.7会输出一条兼容性提示。建议尽快完成迁移。
这里要留心:AutoConfiguration.imports文件里每行一个类名,不能写续行符,结尾不要留多余的空格。类名也不能写通配符,不支持*。
5.3 新旧兼容写法与选择建议
很多老项目还在Spring Boot 2.3/2.4/2.5上,如果你想封装的starter同时兼容2.x和3.x,怎么办?
比较稳妥的做法是:在META-INF/spring.factories和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里同时注册。Spring Boot 2.7以下版本不认识imports文件,会读spring.factories;Spring Boot 2.7及以上版本优先读imports,对spring.factories里的EnableAutoConfiguration仍会兼容读,但会有警告。Spring Boot 3.x彻底移除了从spring.factories加载自动配置的能力,所以如果你要支持3.x,必须要有imports文件。
我在维护公司的公共starter时,采用的方案是:保留一份spring.factories用于旧版本和监听器等扩展点,同时新增AutoConfiguration.imports用于自动配置类。这样做虽然多维护一个文件,但兼容面最广。如果你只支持Boot 2.7+和3.x,就大胆删掉EnableAutoConfigurationkey,不要两边留重复配置,避免你排查时不知道哪边生效。
6. 几个我踩过的坑和最后的小技巧
说点实际的。在我早期封装starter时,有一次怎么配spring.factories都不生效,后来发现是maven构建时没有把META-INF/spring.factories打进jar包。原因很简单:pom.xml里配置了资源过滤,把src/main/resources下的文件都按二进制文件重新编码,导致生成的文件名变成了spring.factories.txt。从那以后,凡是有资源过滤的模块,我都会显式排除配置文件:
<resources> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <excludes> <exclude>META-INF/**</exclude> </excludes> </resource> </resources>还有一个经验:不要过度依赖spring.factories注册“所有东西”。有些组件你用@Bean定义就够了,没必要硬塞进EnableAutoConfiguration。自动配置的定位是“可选的、可替换的”,它要尊重使用者的覆盖权。如果只是项目内部的一个普通配置类,让它在base package下被@ComponentScan扫描到就完事了,引入spring.factories反而增加了排查成本。
最后推荐一个小操作:在构建时生成自动配置元数据文件。在autoconfigure模块的pom.xml里加上spring-boot-autoconfigure-processor依赖,可以自动生成META-INF/spring-configuration-metadata.json,这样使用者在IDE里写配置时会有提示:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency>这是我个人很推荐的一个细节。很多团队宁可花时间手撸文档,也不加这个依赖,结果使用者配置老打错。自动生成元数据后,IDE里补全配置项,错误率能降一大截。
spring.factories本身不复杂,复杂的是它背后那套“约定优于配置”的体系。你把文件的路径、Key、类名、条件注解、加载顺序都捋清楚了,之后再看任何源码里的ImportSelector、@EnableAutoConfiguration,都会有一条清晰的主线。以后遇到“配置不生效”,也可以先用debug=true看匹配报告,再回头看spring.factories,基本十分钟内能定位问题。