ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot spring.factories全解:自动配置、SPI与自定义starter实战

Spring Boot spring.factories全解:自动配置、SPI与自定义starter实战 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 InterfaceJava原生的SPI通过META-INF/services/接口全限定名文件来注册实现。spring.factories本质上就是Spring自己改造后的SPI区别在于原生SPI一个接口对应一个文件spring.factories一个文件可以写多个接口。原生SPI通常只做“发现”Spring Boot的SpringFactoriesLoader还负责“实例化”和“排序”。spring.factories由Spring Boot统一管理加载时带缓存性能没问题。我把关系总结成一张表对比项Java原生SPISpring 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(ClassT 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看一个简化版的使用方法理解起来更直观ListApplicationListener 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.factoriesClassLoader cl Thread.currentThread().getContextClassLoader(); EnumerationURL 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.factories3.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通过条件注解来决定最终“谁上岗”。最常见的条件注解包括ConditionalOnClassclasspath中存在指定类才生效。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 ApplicationListenerApplicationStartedEvent { Override public void onApplicationEvent(ApplicationStartedEvent event) { // 应用启动完成后执行 } }然后在spring.factories里配置org.springframework.context.ApplicationListener\ com.example.hello.HelloApplicationListenerSpring启动过程中会通过SpringFactoriesLoader.loadFactories加载所有ApplicationListener实现类并放入事件广播器。EnvironmentPostProcessor更高阶一点它可以在Spring容器创建之前修改Environment。比如你想强行给配置项注入默认值就可以实现EnvironmentPostProcessorpublic class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { MapString, 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 FailureAnalyzerSomeException { 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里可以用CtrlShiftN搜文件名确认是不是在目标模块的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.factoriesSpring Boot会把所有文件的内容合并。比如多个jar包里都注册了同一个Key最终结果是所有实现类都会被加载而不是后者覆盖前者。这种机制的好处是可以组合扩展坏处是如果有两个自动配置类逻辑上有冲突你很难在文件级别“删除”其中一个。解决办法只能靠条件注解。如果你不想让某个自动配置生效可以排除依赖不让那个jar进入classpath。使用SpringBootApplication(exclude XxxAutoConfiguration.class)排除指定自动配置类。在application.properties里设置spring.autoconfigure.excludecom.example.xxx.XxxAutoConfiguration。注意排除自动配置类时类路径必须写对否则Spring在解析时找不到类名会启动失败。4.5 调试技巧如何查看实际加载了哪些自动配置遇到“我的配置为什么没生效”这种问题先别急着改代码先看自动配置报告。在application.properties里加debugtrue启动后控制台会打印一个“Positive matches”和“Negative matches”列表分别列出哪些自动配置生效了、哪些条件没满足而没生效。比如你写了ConditionalOnProperty但配置项名打错了就会在Negative matches里看到原因非常直观。也可以打开AutoConfigurationReport或者直接使用Spring Boot的Actuator端点/actuator/conditions查看。如果是开发阶段debugtrue就够用了。这个方法能帮助你判断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注意这个路径比较长但它是固定的。文件格式很简单每行写一个自动配置类的全限定名不需要Keycom.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.factoriesSpring 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 directorysrc/main/resources/directory filteringfalse/filtering excludes excludeMETA-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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure-processor/artifactId optionaltrue/optional /dependency这是我个人很推荐的一个细节。很多团队宁可花时间手撸文档也不加这个依赖结果使用者配置老打错。自动生成元数据后IDE里补全配置项错误率能降一大截。spring.factories本身不复杂复杂的是它背后那套“约定优于配置”的体系。你把文件的路径、Key、类名、条件注解、加载顺序都捋清楚了之后再看任何源码里的ImportSelector、EnableAutoConfiguration都会有一条清晰的主线。以后遇到“配置不生效”也可以先用debugtrue看匹配报告再回头看spring.factories基本十分钟内能定位问题。
返回列表