SpringBoot(二)自动配置底层原理及自定义starter
在上节里面多少已经提到了自动装配的内容,比如DispatcherServletAutoConfiguration配置类中定义DispatcherServlet并把它加载到spring容器中。
但是这个类是配置在autoconfigure的jar包中的spring.factories中的,它是怎么生效的呢?这就涉及到springboot的自动装配的原理。
自动装配是通过SPI机制实现的。
在spring中也有一种SPI机制。但是和tomcat中使用SPI机制有一点不同。
在tomcat中SPI机制是扫描 META-INF/service/接口全限定类名的文件 来实现的。
但是在spring中的SPI是扫描 META-INF/spring.factories 文件中的实现类。我们看spring中的这个文件里面存的都是key=value的那种键值对,所以可以指定key来读取实现类进行加载。
对于一个springboot的项目入口类:
@ComponentScan("com.indigo") @SpringBootApplication public class ApplicationStart { public static void main(String[] args) { SpringApplication.run(ApplicationStart.class); } }
启动就是运行的这个main方法,但是这个类上面有两个注解:第一个注解应该都是知道是spring中扫描包路径的,第二个注解是个合成注解
这个@SpringBootApplication 其实也表示了当前这个启动类也是一个配置类,也就是说在这个启动类里面定义@Bean方法也是会生效的。但是我们知道这种只有在当前类也能被spring管理的情况下才能生效,当前类是个启动入口类,那它是什么时候注册到spring容器中的呢?这就要看main方法中的唯一一行代码了。猜测就是run方法里面把当前类注册到容器中的。
当前类传递进去。
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { return run(new Class[]{primarySource}, args); }
在prepareContext中,断点那一行的sources就是入口类。
load方法中就把入口类注册到spring容器中了。熟悉spring的应该就知道下面的操作了。
入口类注册到spring容器中,它上面的注解就可以生效了。主要看下:@SpringBootApplication
它是一个复合注解:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication { @AliasFor( annotation = EnableAutoConfiguration.class ) Class<?>[] exclude() default {}; @AliasFor( annotation = EnableAutoConfiguration.class ) String[] excludeName() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackages" ) String[] scanBasePackages() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" ) Class<?>[] scanBasePackageClasses() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "nameGenerator" ) Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true; }
这个注解里面的属性呢都是一些其他注解里面的属性的别名。没什么特别的。
@Target(ElementType.TYPE)//修饰自定义注解,指定该自定义注解的注解位置,类还是方法,或者属性 @Retention(RetentionPolicy.RUNTIME)//被它所注解的注解保留多久,注解的生命周期。可选的参数值在枚举类型 RetentionPolicy 中,一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。 @Documented//将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see(后面可以跟类路径等参数实现链接跳转 Ctrl跳转),@param(注释参数) 等。 @Inherited//修饰自定义注解,该自定义注解注解的类,被继承时,子类也会拥有该自定义注解 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
@ComponentScan 呢主要过滤了一些过滤器,并没有实质的包路径扫描。如果在启动类上自己写上了这个注解就覆盖了。
后面就剩下:@SpringBootConfiguration @EnableAutoConfiguration
这两个注解了。
而 @SpringBootConfiguration 这个注解呢也是一个复合注解,它上面就一个关键的@Configuration注解,然后属性也是别名的作用。所以它实际上就等同于@Configuration注解了。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration @Indexed public @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true; }
最后@EnableAutoConfiguration 才是最关键的注解。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
这个上面主要有两个注解
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
对于@Import的用法这里再提出一下。有三种用法,对应于它有三种参数。
- 普通类,直接导入到IOC容器管理
- ImportBeanDefinitionRegistrar接口实现类,支持手动注册bean
- ImportSelector实现,将selectImports方法返回的数组,数组里面放的是类的全限定类名。把他们加载到IOC
而上面的 @Import({AutoConfigurationImportSelector.class}) 就是第三种。
对于@AutoConfigurationPackage 它就使用了第二种。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({Registrar.class}) public @interface AutoConfigurationPackage { String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; }
而它导入了Registrar类中做的事情就是向容器中注册了一个 BasePackagesBeanDefinition 的bean,
这个BasePackagesBeanDefinition 里面存着SpringBoot应用启动时默认将启动类所在的package作为自动配置的package。
然后通过AutoConfigurationPackages#get(BeanFactory beanFactory) 方法就可以得到当前应用程序管理的package有那些了。也就是说提供了一种查询管理包路径的接口。这个再spring-data-jpa会有用。
看来@AutoConfigurationPackage 注解也和自动配置没太多关系,就剩下@Import({AutoConfigurationImportSelector.class}) 这一个了。
AutoConfigurationImportSelector 中有一个 selectImports 方法。
public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } }
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else {
// 这里得到注解的属性 AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 它是找到候选者bean的。Spring SPI使用的地方 就是找 classpath下面 META-INF/spring.factories文件中的 EnableAutoConfiguration.calss对应
//的value List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 如果其他jar包中也有spring.factories文件 加载的时候里面的value可能有重复 这里进行去重 configurations = this.removeDuplicates(configurations);
// 这里获取那些exclude属性 Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
// 为什么做检查? 因为exclude的值,必须是spring.factories中的某些值 不然就直接抛出异常 this.checkExcludedClasses(configurations, exclusions);
// remove configurations.removeAll(exclusions);
// 做过滤 这里还会加载另外一个AutoConfigurationImportFilter.class对应的 value值 ,就是加载那些过滤器,过滤掉不符合条件的 剩下的就是需要自动装配的beanName了 configurations = this.getConfigurationClassFilter().filter(configurations);
// 最后一步 就是触发事件驱动机制(基于java的监听机制做了一层封装,因为java中一个监听器只能监听一个事件)
// 从spring.factories中加载 AutoConfigurationImportListener.class对应的value ,但是这一次不止是加载到对应的value还是进行反射实例化,因为要马上使用
// 用到了下面SPI图解的右边的API this.fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } }
this.getCandidateConfigurations(annotationMetadata, attributes);
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 这里就是spring SPI机制加载的地方,这里首先加载的key就是 EnableAutoConfiguration.class List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; }
spring SPI的流程如下:META-INF/spring.factories就是靠SpringFactoriesLoader来加载的,spring.factories中的内容是key,value的形式,但是这里key,value不一定是接口和接口实现类的关系,但是value一定是key功能的一个实现。下面的factoryType就是key。
EnableAutoConfiguration.class 在spring.factories中的value就是下面的一堆:当然没有显示全。
看下 SpringFactoriesLoader.loadFactoryNames中的逻辑:就是通过classLoader加载classpath路径下的META-INF/spring.factories文件。然后把文件的内容读取出来到Properties中
this.getConfigurationClassFilter().filter(configurations);
加载spring.factoies中AutoConfigurationImportFilter.class 对应的value值。这三个都是过滤器。
this.fireAutoConfigurationImportEvents(configurations, exclusions); 中加载的监听器



所以这个 org.springframework.boot.autoconfigure.AutoConfigurationImportListener 也是一个扩展点,因为自己可以实现一个 AutoConfigurationImportListener接口,重写里面的onAutoConfigurationImportEvent
方法,看上面的调用参数就知道
onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) 参数中的event中已经保有了所有的候选者bean的beanName,还有exlude的属性值, 可以根据这些做一些需要的事情。
上面的逻辑走完,spring.factories中的那些key对应的value值,就已经注册进IOC容器中了。自动装配也就生效了。拿上节DispatcherServlet举例子。它是靠spring.factories中
DispatcherServletAutoConfiguration 加载进来的。
它这里面有一些条件注解,它才会生效。这也是为什么我们有时候只要引入一些相关的jar包,配置就生效了。
但是这里的条件注解比如: @ConditionalOnClass 是springboot中自定义的条件注解。
条件注解上面必须加一个 @Conditional 注解,它的属性就是条件满足判断类。这个类要实现spring中的 Condition 接口,重写里面的matches方法。但是在springboot对它进行了一层包装。
判断类是继承了 FilteringSpringBootCondition 类。
对于spring中Condition接口如下,如果要自定义扩展,就基于它。
@FunctionalInterface public interface Condition { /** * Determine if the condition matches. * @param context the condition context * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class} * or {@link org.springframework.core.type.MethodMetadata method} being checked * @return {@code true} if the condition matches and the component can be registered, * or {@code false} to veto the annotated component's registration */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
第一个参数:ConditionContext context: 获取上下文,可以获取很多参数,比如容器上下文。
对应@ConditionalOnClass 的用法,还有一个条件注解@ConditionalOnBean 注解,他们有时候可以互换。
但是实际上 @ConditionalOnClass 是JVM中有某个class才生效。@ConditionalOnBean 是容器中有某个bean才生效。对于@ConditionalOnBean 用的时候,有时候可能会出现问题,因为如果有多个@Configuration,他们的加载顺序是不确定,
所以有时候在遇到@ConditionalOnBean 注解的时候,如果那个bean还没有被加载到,这个判断就失效了。
有一种解决办法就是如果可以用 @ConditionalOnClass 就用它。
还有一种解决方法就是使用 @AutoConfigureAfter ,@AutoConfigureBefore 注解。但是这两个注解是springboot中的,而且必须是要在自动装配的value中才有效果。所以只能让这依赖的两个bean放入到 EnableAutoConfiguration.class
对应的value中了。
上面就是自动装配的整理源码流程了。下面说下怎么写一个start模块。
自定义starter
1、新建两个模块:
命名规范,springboot自带的模块命名都为:spring-boot-starter-xxx,自定义模块命名一般为:xxx-spring-boot-starter
- xxx-spring-boot-autoconfigure:自动配置核心代码
- xxx-spring-boot-starter:管理依赖
如果不需要将自动配置代码和依赖项管理分离开来,则可以将它们组合到一个模块中。
2、使用@ConfigurationProperties注入需要的配置属性为配置的前缀。
3、@Configuration + @Bean注册需要的bean,使用@EnableConfigurationProperties开启配置注入
4、在自定义组件的resouces下面新建 META-INF/spring.factories文件,写入
org.springframework.boot.autoconfigure.EnableAutoConfiguration=配置类入口@Configuration配置类(完全无侵入,但缺乏灵活性)
4.1、或者使用自定义组件的地方,在启动类使用@Import注解导入自定义组件中的@Configuration配置类。但是这种耦合性更大写。
4.1.1、或者自定义注解EnableXXX
,自定义注解上面使用@Import注解导入组件的@Configuration配置类,使用自定义组件的启动类上面添加该注解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!