Spring Boot自动装配源码剖析

什么是Spring Boot的自动装配?

自动装配是Spring Boot的核心功能,它能够根据应用程序的依赖和配置自动配置Spring。这意味着我们只需要添加大量的依赖,Spring Boot就能自动完成配置,减少了人工配置的工作量。

自动装配的核心注解

在我们Spring Boot项目启动的时候,都会有一个@SpringBootApplication注解 ,这个注解里面包含了@EnableAutoConfigration注解、@SpringBootConfiguration 和 @ComponentScan注解。

  1. @EnableAutoConfigration:启动Spring Boot自动装配机制
  2. @SpringBootConfiguration 标识这是一个Spring Boot的配置类,和@Configration差不多
  3. @ComponentScan 扫描当前路径的所有包 及其组件

自动装配的实现流程

1. 启动Spring Boot应用程序

2.解析SpringBootApplication注解(通过反射获取,解析)

java 复制代码
//....

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
)
public @interface SpringBootApplication {
    //是否启用自动装配的key,值为ture则启用,false则不启用
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    //要排除加载的配置类
    Class<?>[] exclude() default {};
    String[] excludeName() default {}; 

}

3.解析EnableAutoConfiration注解

java 复制代码
//...
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    //配置键,标识是否启用自动装配,值从配置文件中获取
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    //排除加载的类
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

4.通过@Import(AutoConfirationImportsSelector.class) 引入自动装配选择器,这个类会在Spring启动的时候被处理。

java 复制代码
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
   //.....
    //环境变量
    private Environment environment;
    
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //判断EnableAutoConfiration是否被启用
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            //被启用了,通过getAutoConfigurationEntry获取所有的自动装配类
            AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }
    //判断是否启用了自动装配机制
   protected boolean isEnabled(AnnotationMetadata metadata) {
    //从配置文件中获取  spring.boot.enableautoconfiguration : value(false or ture) 的值,来判断是否启动自动装配机制,如果没有配置,默认为ture 
    //如果当前类是AutoConfigurationImportSelector的子类,直接返回ture
    return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }
//....
}

5.通过调用 getAutoConfigurationEntry方法加载所有配置类。

java 复制代码
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    //.....
    //一个空的配置类集合,内部有个configurations 和 exclusions数组
    //configurations代表要加载的配置类 
    //exclusions代表被排除加载的配置类
    private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            //自动装配机制未启用,返回空的配置列表
            return EMPTY_ENTRY;
        } else {
            // 获取启动类上的@EnableAutoConfiguration注解的属性,这可能包括对特定自动配置类的排除
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            //获取所有的候选配置类
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            //为了防止重复注入,对获取到的配置类进行一个去重
            configurations = this.removeDuplicates(configurations);

            //获取所有被排除的配置类,被排除的配置类不加载
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            // 检查排除的类是否存在于候选配置中或在路径中,如果不在则抛出异常,这个方法的作用是防止用户错误地排除不存在或不是自动配置类的类
            this.checkExcludedClasses(configurations, exclusions);
            //从加载的配置集合中移除被排除的配置类
            configurations.removeAll(exclusions);
            // 进一步筛选自动配置类。比如基于条件注解@ConditionalOnBean等来排除特定的配置类
            configurations = this.getConfigurationClassFilter().filter(configurations);
            // 触发自动配置导入事件,允许监听器对自动配置过程进行干预。
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            //返回最终选择加载的配置类集合
            return new AutoConfigurationEntry(configurations, exclusions);
        }
    }

    //加载META-INF/spring/%s.imports配置类
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

    //排除配置类
        protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        Set<String> excluded = new LinkedHashSet();
        excluded.addAll(this.asList(attributes, "exclude"));
        excluded.addAll(this.asList(attributes, "excludeName"));
        excluded.addAll(this.getExcludeAutoConfigurationsProperty());
        return excluded;
    }
    //....
}

ImportCandidates.load方法加载配置类

java 复制代码
public final class ImportCandidates implements Iterable<String> {
    //配置文件所在路径
    private static final String LOCATION = "META-INF/spring/%s.imports";
    private static final String COMMENT_START = "#";
    //存储加载到的所有配置类url
    private final List<String> candidates;

    private ImportCandidates(List<String> candidates) {
        Assert.notNull(candidates, "'candidates' must not be null");
        this.candidates = Collections.unmodifiableList(candidates);
    }


    public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
        Assert.notNull(annotation, "'annotation' must not be null");
        //获取类加载器
        ClassLoader classLoaderToUse = decideClassloader(classLoader);
        //做一个字段串拼接,获取配置文件的名称,高版本的Spring Boot装配文件为AutoConfiguration.imports
        String location = String.format("META-INF/spring/%s.imports", annotation.getName());
        //读取配置文件内的url
        Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);

        //保存读取到的所有URL
        List<String> importCandidates = new ArrayList();

        while(urls.hasMoreElements()) {
            //迭代器进行迭代
            URL url = (URL)urls.nextElement();
            //添加url到集合中
            importCandidates.addAll(readCandidateConfigurations(url));
        }
        //构建ImportCandidates对象返回给AutoConfigurationImportSelector
        return new ImportCandidates(importCandidates);
    }

    private static ClassLoader decideClassloader(ClassLoader classLoader) {
        return classLoader == null ? ImportCandidates.class.getClassLoader() : classLoader;
    }

    private static Enumeration<URL> findUrlsInClasspath(ClassLoader classLoader, String location) {
        try {
            return classLoader.getResources(location);
        } catch (IOException var3) {
            IOException ex = var3;
            throw new IllegalArgumentException("Failed to load configurations from location [" + location + "]", ex);
        }
    }
    //...

}

类加载器加载的文件所在的位置。

而AutoConfigration.imports配置文件内存储的是要自动装配类的URL路径。

这些路径会被读取保存进ImportCandidates类内的candidates数组供AutoConfiguratioImportsSelector配置选择器进行筛选。最后通过Spring将要自动装配的类注入到容器中。

条件注解

在一些要装配的类中,会一些条件注解。例如

java 复制代码
@AutoConfiguration(
    after = {RedisAutoConfiguration.class}
)
@ConditionalOnClass({ReactiveRedisConnectionFactory.class, ReactiveRedisTemplate.class, Flux.class})
public class RedisReactiveAutoConfiguration {
    public RedisReactiveAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"reactiveRedisTemplate"}
    )
    @ConditionalOnBean({ReactiveRedisConnectionFactory.class})
    public ReactiveRedisTemplate<Object, Object> reactiveRedisTemplate(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory, ResourceLoader resourceLoader) {
        JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(resourceLoader.getClassLoader());
        RedisSerializationContext<Object, Object> serializationContext = RedisSerializationContext.newSerializationContext().key(jdkSerializer).value(jdkSerializer).hashKey(jdkSerializer).hashValue(jdkSerializer).build();
        return new ReactiveRedisTemplate(reactiveRedisConnectionFactory, serializationContext);
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"reactiveStringRedisTemplate"}
    )
    @ConditionalOnBean({ReactiveRedisConnectionFactory.class})
    public ReactiveStringRedisTemplate reactiveStringRedisTemplate(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
        return new ReactiveStringRedisTemplate(reactiveRedisConnectionFactory);
    }
}

在类上存在 @ConditionalOnClass 注解,用来路径是否存在该配置类,存在则加载配置类,不存在则不加载。

在方法上则存在 @ConditionalOnMissingBean注解 和 @Bean注解

@Bean注解就是把方法的返回值注入到Spring容器中

而@ConditionalOnBean 则是检查Bean是否被加载,如果未被加载则加载,如果已经加载过了,则不加载,保证Bean的单例性。name属性则是对应的Bean名称

@ConditionalOnClass: 当类路径中存在指定的类时,配置类才会生效。

@ConditionalOnBean: 当容器中存在指定的Bean时,配置类才会生效。

@ConditionalOnMissingBean: 当容器中不存在指定的Bean时,配置类才会生效。

@ConditionalOnProperty: 当配置文件中存在指定的属性且值满足要求时,配置类才会生效。

总结

Spring Boot自动装配流程:

  1. 获取@SpringBootApplication注解并解析
  2. 处理@@EnableAutoConfiguration注解,并通过@Import注解导入自动配置选择器AutoConfigurationImportsSelector
  3. 调用AutoConfigurationImportsSelector内getAutoConfigurationEntry加载配置类文件,加载过程如下
  4. 检查当前是否启动自动装配机制
  5. 通过调用getCandidateConfigurations方法获取所有加载的候选类,这个方法内会从Mate-INF/Spring目录下的autoConfiguration.imports中读取所有自动装配配置类的URL。
  6. 筛选重复的配置类
  7. 获取要排除的自动装配类
  8. 检查要出排除的配置类是否在候选集合中或在指定的路径中,如果不存在则报错。避免用户排除不需要自动装配的类。
  9. 移除候选集合中所有被排除的配置类。
  10. 进一步对加了条件注解的配置类进行过滤
  11. 返回最后筛选完的
相关推荐
Chenyiax12 分钟前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH13 分钟前
Koa和Express的区别
后端
MariaH19 分钟前
Koa框架的使用
后端
luckdewei1 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某3 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy3 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom3 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫7 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
用户1474853079747 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1237 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端