SpringBoot 自动装配原理

零、前言

Spring简直是java企业级应用开发人员的春天,我们可以通过Spring提供的ioc容器,避免硬编码带来的程序过度耦合。但是,启动一个Spring应用程序也绝非易事,他需要大量且繁琐的xml配置,开发人员压根不能全身心的投入到业务中去。

因此,SpringBoot诞生了,虽然本质上还是属于Spring,但是SpringBoot的优势在于以下两个特点:

  1. 约定大于配置

SpringBoot定义了项目的基本骨架,例如各个环境的配置文件统一放到resource中,使用active来启用其中一个。配置文件默认为application.properties,或者yaml、yml都可以。

  1. 自动装配

以前在Spring使用到某个组件的时候,需要在xml中对配置好各个属性,之后被Spring扫描后注入进容器。

而有了SpringBoot后,我们仅仅需要引入一个starter,就可以直接使用该组件,如此方便、快捷,得益于自动装配机制。

一、定义

什么是SpringBoot的自动装配?

SpringBoot 自动装配,英文是 Auto-Configuration ,是指在Spring Boot启动时,通过一系列机制和注解,将第三方组件或配置类加载到Spring容器中,并生成相应的Bean对象供使用。这一过程极大地简化了开发工作,提高了效率,为 SpringBoot 框架的 "开箱即用"提供了基础支撑;

具体来说,SpringBoot的自动装配主要依赖以下几个关键点

  1. 注解 :SpringBoot提供了多个注解来实现自动装配,例如@SpringBootApplication@EnableAutoConfiguration@ComponentScan等。这些注解帮助SpringBoot识别并加载需要自动配置的类。
  2. 条件化配置 :SpringBoot利用条件化配置(@Conditional注解)来决定是否进行某些配置。例如,如果类路径下存在某个特定的jar包,则自动配置相应的功能。
  3. 元数据文件 :SpringBoot会在启动时扫描META-INF/spring.factories 文件,从中获取需要进行自动装配的类,并执行这些类中的配置操作。
  4. 场景启动器:SpringBoot还引入了场景启动器(如WebMvcAutoConfiguration),根据项目中添加的依赖自动配置相应的功能。
  5. 约定大于配置:SpringBoot遵循"约定大于配置"的原则,通过默认配置减少开发者手动编写配置的必要性。例如,在没有明确配置的情况下,SpringBoot会自动配置数据库连接、视图解析器等。
  6. SPI机制 :自动装配与Spring的SPI(Service Provider Interface)机制相似,都是通过反射机制动态加载和实例化组件。更多SPI讲解:深入理解 Java SPI - 概念、原理、应用

二、启动流程

SpringBoot启动流程的简化版代码

java 复制代码
public static void run(Class<?> primaryClass) {
    //1.创建一个 ApplicationContext 实例,即我们常说的IoC容器
    ApplicationContext context = createApplicationContext();
    // 2.将主类(primaryClass)注册到IoC容器中(简单但很重要的一步)
    loadSourceCLass(context, primaryClass);
    // 3.递归加载并处理所有的配置类
    processConfigurationClasses(context);
    // 4.实例化所有的单例Bean(Singleton Bean)
    instantiateSingletonBeans(context);
    // 5.如果时web应用,则启动web服务器(如Tomcat)
    startWebServer(context);
}

图解:

详解第三步核心流程:

图解:

三、原理剖析

一切都从注解 @SpringBootApplication 说起:

java 复制代码
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@SpringBootApplication

**@SpringBootApplication **注解又是一个组合注解

java 复制代码
@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 {...}

其中**@Target**、** @Retention**、@Documented 与**@Inherited**是元注解,即对注解的注解;

还包含了@SpringBootConfiguration与@EnableAutoConfiguration

以下注解都将省略这些元注解

@SpringBootConfiguration

java 复制代码
@Configuration
public @interface SpringBootConfiguration {
}
 
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

可以看到,@SpringBootConfiguration 注解本质上是一个 @Configuration 注解,表明该类是一个配置类。

@Configuration 又被 @Component 注解修饰,代表任何加了**@Configuration** 注解的配置类,都会被注入进Spring容器中。

@EnableAutoConfiguration

该注解开启了自动配置的功能

java 复制代码
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

本身又包含了以下两个注解
@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage

以前我们直接使用Spring的时候,需要在xml中的 context:component-scan 中定义好base-package,那么 Spring 在启动的时候,就会扫描该包下及其子包下被@Controller、@Service与@Component标注的类,并将这些类注入到容器中。
@AutoConfigurationPackage 则会将被注解标注的类,即主配置类,将主配置类所在的包当作base-package,而不用我们自己去手动配置了。

这也就是为什么我们需要将主配置类放在项目的最外层目录中 的原因。

那么容器是怎么知道主配置当前所在的包呢?

我们注意到,@AutoConfigurationPackage 中使用到了@Import注解

@Import注解会直接向容器中注入指定的组件

引入了AutoConfigurationPackages类中内部类 Registrar

java 复制代码
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
 
		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata,
				BeanDefinitionRegistry registry) {
			register(registry, new PackageImport(metadata).getPackageName());
		}
 
		@Override
		public Set<Object> determineImports(AnnotationMetadata metadata) {
			return Collections.singleton(new PackageImport(metadata));
		}
}

而getName将会返回主配置类所在的包路径;这样,容器就知道了主配置类所在的包,之后就会扫描该包及其子包。

@Import(AutoConfigurationImportSelector.class)

该注解又引入了AutoConfigurationImportSelector类,而AutoConfigurationImportSelector中有一个可以获取候选配置的方法,即getCandidateConfigurations

java 复制代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
        AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            getSpringFactoriesLoaderFactoryClass(), 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;
}

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}

其中核心方法SpringFactoriesLoader.loadFactoryNames ,第一个参数是EnableAutoConfiguration.class

loadFactoryNames方法
java 复制代码
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }
    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

可以看得出,loadSpringFactorie方法,会从META-INF/spring.factories文件中读取配置,将其封装为Properties对象,将每个key作为返回的map的key,将key对应的配置集合作为该map的value。

而loadFactoryNames则是取出key为EnableAutoConfiguration.class的配置集合

我们查看META-INF/spring.factories的内容(完整路径:\org\springframework\boot\spring-boot\2.7.17\spring-boot-2.7.17.jar!\META-INF\spring.factories)

可以看到,EnableAutoConfiguration对应的value,则是我们在开发中经常用到的组件,比如Rabbit、Elasticsearch与Redis等中间件。

到这里,我们可以知道getCandidateConfigurations方法会从META-INF/spring.factories中获取各个组件的自动配置类的全限定名。
图解

总结一下

参考

SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽
目前讲的最透彻的SpringBoot自动配置

相关推荐
Asthenia04125 分钟前
理解词法分析与LEX:编译器的守门人
后端
uhakadotcom6 分钟前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04122 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04123 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端