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配置类,使用自定义组件的启动类上面添加该注解。

 

 
 

 

 

 

 

posted @   蒙恬括  阅读(648)  评论(0编辑  收藏  举报
编辑推荐:
· 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框架的用法!
点击右上角即可分享
微信分享提示