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自动配置

相关推荐
小李不想输啦11 分钟前
什么是微服务、微服务如何实现Eureka,网关是什么,nacos是什么
java·spring boot·微服务·eureka·架构
张铁铁是个小胖子12 分钟前
微服务学习
java·学习·微服务
ggs_and_ddu13 分钟前
Android--java实现手机亮度控制
android·java·智能手机
敲代码娶不了六花2 小时前
jsp | servlet | spring forEach读取不了对象List
java·spring·servlet·tomcat·list·jsp
Yhame.2 小时前
深入理解 Java 中的 ArrayList 和 List:泛型与动态数组
java·开发语言
是小崔啊3 小时前
开源轮子 - EasyExcel02(深入实践)
java·开源·excel
myNameGL4 小时前
linux安装idea
java·ide·intellij-idea
青春男大4 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.4 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
2401_882727574 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架