Detailed explanation of SpringBoot automatic configuration principle

1 Introduction

I wrote a similar article before, but I didn't understand it very deeply at the time, so I always wanted to write it again, but I didn't have time, so I dragged it until now. This article may be very long, because some other important knowledge points will be derived in the process of explaining automatic configuration, and I will also introduce them.

2 warm-up case

To talk about SpringBoot's automatic assembly, the following code is definitely inseparable.insert image description here

In fact, when I was learning SpringBoot, I didn't think about this code at all. I only knew that this was the startup class of the SpringBoot program. I had to ensure that it could cover all the packages to be scanned, and then I went to write the code.

Let's take a look at this code first:

ConfigurableApplicationContext run = SpringApplication.run(AppRunApplication.class, args);
复制代码

The purpose of this code is to start the SpringBoot application and return an instance of a ConfigurableApplicationContext object.

The SpringApplication.run() method returns an object of type ConfigurableApplicationContext, representing the context of the Spring application. This context saves the references of all beans in the Spring container. For other components in the application, this context is a globally shared container.

The meaning of the SpringApplication.run(AppRunApplication.class, args) statement is to start the SpringBoot application based on the AppRunApplication class and pass the command line parameters to the application. After running this statement, Spring will automatically initialize the application, including creating the ApplicationContext, registering all bean definitions, starting the embedded Web container, loading the configuration file of the application, and so on.

In addition, the instance of ConfigurableApplicationContext can also be obtained through this method, and the operation of the application can be further controlled by using this object, such as manually closing the application, obtaining the environment variables of the application, adding custom beans, and so on.

只看文字实在时太枯燥了,我们还是看看代码吧。我们在启动类,创建了一个 Test 类,然后生命了一个 Bean 方法,它的作用就是返回一个 Test 类,然后我们进行断点调试。 insert image description here

太神奇了,我们通过 run 拿到了 Test 的 Bean 实例 insert image description here

如果我们试图获取一个没有使用 Bean 方法注册的类,就会抛出异常。 insert image description here

这个 run 其实 可以简单地理解为 Spring 的 IOC 容器,SpringBoot 启动时会自动帮我们配置程序运行需要的使用的 Bean 对象放到 IOC 容器中,我们在其他类需要使用时只需要使用 @Autowire 或者 @Resource 注解进行依赖注入即可。

Spring Boot 自动配置是 Spring Boot 框架的一项核心特性,它可以基于应用程序的依赖关系和配置信息,
自动配置应用程序所需的 Spring Bean。Spring Boot 自动配置是通过条件化配置实现的,
这意味着只有在特定条件下才会应用这些配置。这些条件可以是应用程序的依赖关系、配置值、环境变量等等。

Spring Boot 提供了许多 Starter 包,这些 Starter 包为应用程序添加了一组默认的依赖关系和配置信息,
以便应用程序能够正常运行。例如,Spring Boot Starter Web 包为应用程序添加了 
Spring MVC、Tomcat 等 Web 相关的依赖关系和配置信息。当应用程序添加了 Spring Boot Starter Web 包时,
Spring Boot 会自动配置应用程序的 Web 相关配置。

自动装配是指Spring Boot的依赖注入机制,它会根据需要自动为应用程序中的Bean注入依赖关系。
例如,当一个类需要使用JdbcTemplate来访问数据库时,Spring Boot会自动将JdbcTemplate对象注入到这个类中,
而不需要程序员手动编写任何配置代码。
复制代码

如下图,我们在项目中要使用 Redis ,只需要在 pom 文件中引入相关依赖,然后在 application.yml 文件中配置相关连接参数,之后的累活就全部交给 SpringBoot 自动进行配置,我们使用时只需要使用 @Autowire 或者 @Resource 注解进行依赖注入即可 insert image description here

通过上面的讲解,我们对 SpringBoot 自动配置有了一个大概的认识——我们需要什么,就在 pom 文件中引入相关依赖,然后在 application.yml 文件中配置相关配置信息,然后 SpringBoot 会帮我们把需要的 Bean 都自动配置到 Spring IOC 容器中,之后我们使用时只需要通过依赖注入机制将需要的 Bean 对象注入即可。

到现在,我们应该知道了 SpringBoot 自动配置是什么,但是这样我们只是知其所以然,所以我们还需要继续了解它的底层实现。

3 源码解读

我们把目光来到今天的主角,@SpringBootApplication 注解。 insert image description here 我们进入这个注解,好嘛,它的头上怎么顶着这么多注解,不过真正重要的只有三个注解,我们接下来会一一介绍。 insert image description here

3.1 @SpringBootConfiguration

点进@SpringBootConfiguration 注解,可以发现其核心注解为@Configuration注解:

insert image description here @Configuration注解是Spring框架的注解之一,用于标记配置类。

在Spring Boot中,使用@Configuration注解可以将该类作为配置类,从而使该类中的Bean可以被Spring IoC容器管理和使用。

在配置类中,我们可以使用另外两个注解@Bean和@Scope来定义Bean,其中@Bean注解用于定义Bean对象,而@Scope注解用来指定Bean对象的作用域。

除此之外,在配置类中,我们还可以定义一些常量,并使用@Value注解来注入应用程序的属性。

举例来说,一个简单的配置类可以被定义如下:

@Configuration
public class MyConfiguration {
    
    @Value("${myapp.something}")
    private String something;

    @Bean
    public MyBean myBean() {
        return new MyBean(something);
    }
}
复制代码

在上述代码中,我们使用@Configuration注解来标记MyConfiguration类为配置类。使用@Value注解来注入myapp.something属性到该类中的something变量中。

同时,我们使用@Bean注解来定义一个名为myBean的Bean对象,并在Bean方法中返回一个新创建的MyBean对象,将something参数作为其构造函数的参数进行传递。

@Configuration 注解还可以与 @Import 注解一起使用,@Import 注解用于导入其他的配置类,从而组合多个配置类,形成一个完整的应用程序配置。这样,应用程序可以分而治之,将配置信息分散到不同的配置类中,从而使得配置更加灵活和可维护。

总的来说,@Configuration注解能够将一个类定义为Spring Boot应用程序中的配置类,从而使该类中的Bean对象能够被Spring IoC容器进行自动管理和装配。这让应用开发者能够更加专注于应用逻辑的实现,而不必花费精力在繁琐的配置上。

所以@SpringBootConfiguration 注解本质上就是一个@Configuration注解,用来标注某个类为 JavaConfig 配置类,有了这个注解就可以在 SpringBoot 启动类中使用```@Bean``标签配置类了,如下图所示。

insert image description here

3.2 @ComponentScan

@ComponentScan 是 Spring Framework 中的一个注解,它用于指定 Spring 容器需要扫描和管理的组件。组件是 Spring 中的一个抽象概念,它包括了 Spring Bean、Controller、Service、Repository 等等。通过 @ComponentScan 注解,可以让 Spring 容器自动扫描和管理这些组件,从而简化应用程序的配置和管理。

@ComponentScan 注解有多个参数,可以用于指定要扫描的组件的位置、排除不需要扫描的组件、指定要排除扫描的组件等等。

默认情况下,Spring Boot会自动扫描主应用程序下的所有组件(@Configuration, @Controller, @Service, @Repository等),但是如果你将组件放在其他包下,那么就需要显式地配置扫描目录。

举个例子,假设我们有以下目录结构:

com
|-- myapp
|   |-- Application.java
|   +-- config
|       +-- MyConfiguration.java
+-- other
    +-- MyComponent.java
复制代码

可以在主应用程序中添加@ComponentScan注解,来指定Spring应该扫描的包位置:

@SpringBootApplication
@ComponentScan(basePackages = { "com.myapp", "com.other" })
public class Application {
    // ...
}
复制代码

在上述代码中,我们使用@ComponentScan注解,并指定两个基本包路径com.myapp和com.other以进行扫描。这两个路径下的组件都会被自动扫描到并加载入Spring IoC容器中。

除了basePackages参数以外,@ComponentScan注解还有一些其他可选参数:

除了basePackages参数以外,@ComponentScan注解还有一些其他可选参数:

  1. basePackageClasses:可以使用一个或多个类作为基础包来指定要扫描的根目录。比如:@ComponentScan(basePackageClasses = {MyComponent.class, MyService.class})。

  2. excludeFilters:可以指定过滤器来排除带有某些注解或实现某些接口的组件。

  3. includeFilters:可以指定过滤器来仅包含带有某些注解或实现某些接口的组件,可能的值有@Component, @Repository, @Service, @Controller等。

使用这些参数,可以更加精细的控制扫描范围。

3.3 @EnableAutoConfiguration 注解

这是今天的主角中的主角,自动配置实现的核心注解。

点进这个注解可以发现,如下图所示。

insert image description here

我们重点来看 @Import(AutoConfigurationImportSelector.class)这个注解。

@Import 注解是 它用于将一个或多个类导入到 Spring 容器中,以便于在应用程序中使用。通过 @Import 注解,我们可以将一些非 Spring 管理的类实例化并注册到 Spring 容器中,或者将一些 Spring 管理的配置类导入到当前配置类中,以便于在应用程序中进行统一的配置和管理。

@Import是Spring Framework 中的一个注解,用于在配置类中导入其他配置类或者普通的Java类。

通过@Impor注解,它用于将一个或多个类导入到 Spring 容器中,以便于在应用程序中使用。通过 @Import 注解,我们可以将一些非 Spring 管理的类实例化并注册到 Spring 容器中,或者将一些 Spring 管理的配置类导入到当前配置类中,以便于在应用程序中进行统一的配置和管理。

说白了在这里@Import注解的作用就是将 AutoConfigurationImportSelector 这个类导入当前类,这个类就是实现自动配置的核心。

我们继续进入到 AutoConfigurationImportSelector 类:

insert image description here

insert image description here insert image description here 最后,我们发现, AutoConfigurationImportSelector 实际上是实现了 ImportSelector 接口,这个接口只有两个方法,其中我们需要重点关注 selectImports() 方法。

ImportSelector 接口是 Spring Framework 中的一个接口,它可以用于在 Spring 容器启动时动态地导入一些类到 Spring 容器中。通过实现 ImportSelector 接口,并重写其中的 selectImports 方法,我们可以自定义逻辑来确定需要导入的类,从而实现更加灵活的配置和管理。

selectImports 方法是 ImportSelector 接口中的一个方法,用于返回需要导入的类的全限定类名数组。在 Spring 容器启动时,Spring 会扫描所有实现了 ImportSelector 接口的类,并调用其中的 selectImports 方法来确定需要导入的类。在 selectImports 方法中,我们可以自定义逻辑来确定需要导入的类,例如根据某些条件来动态地确定需要导入的类。

好嘛,搞了半天,关键点在这里,通过 selectImports 方法,我们就可以得到需要自动配置的类的全限定类名数组,那我们来看一下这个方法。

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
复制代码

insert image description here 既然我们需要自动配置的类的全限定类名数组,那么这个方法必然通过某个方法获取到这个数组,我们看一下这个方法getAutoConfigurationEntry(annotationMetadata),单看这个名字它的嫌疑就非常大。

getAutoConfigurationEntry 方法可以用于获取自动配置类的元数据,以便于分析和调试自动配置机制。
它接受一个 AnnotationMetadata 对象作为参数,该对象表示使用了 @EnableAutoConfiguration 注解的配置类的元数据。
通过调用该方法,我们可以获取到所有已经配置的自动配置类的全限定类名,以及这些自动配置类的条件注解和优先级信息等。
复制代码

我们继续进入到 getAutoConfigurationEntry() 方法:

insert image description here 说实话这个方法我现在看还是感觉眼花缭乱,哈哈,不过不影响我们分析,我们先看方法返回值,返回值是一个 AutoConfigurationEntry 对象,再看看 return 语句:

return new AutoConfigurationEntry(configurations, exclusions);
复制代码

果然是通过构造函数创建一个 AutoConfigurationEntry 对象并返回,我们再看看它的构造参数:

configurations, exclusions
复制代码

再结合我们之前的分析,这个方法的作用是返回自动配置类的元数据,不难推断出 configurations 就是我们需要的自动配置类的元数据,那exclusions 参数呢,这个从名字上来看,它应该是需要排除的类的元数据。

类似上面 @ComponentScan注解 中的 excludeFilter 参数,可以指定过滤器来排除带有某些注解或实现某些接口的组件。

那我们现在要做的就是分析 configurations 是怎么来的:

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
复制代码

我们继续进入到 getCandidateConfigurations() 方法:insert image description here

这个方法的组成还是非常简单的,它只调用了 SpringFactoriesLoader 的静态方法 loadFactoryNames(),还有就是一个断言。

Java 断言是一种调试工具,它用于在程序运行时检查一个条件是否为 true。
可以使用 assert 关键字来编写断言语句,如果条件为 false,则会抛出 AssertionError 异常。
复制代码
getCandidateConfigurations 方法是 Spring Boot 中的一个方法,它用于获取所有候选的自动配置类。
在 Spring Boot 应用程序中,自动配置是一种约定俗成的机制,它可以根据应用程序的依赖和配置来自动配置 Spring 应用程序上下文。
Spring Boot 会在 classpath 下扫描 META-INF/spring.factories 文件,该文件中定义了一些自动配置类,
这些自动配置类会在应用程序启动时被自动加载和配置。
复制代码

我们先来了解一下 SpringFactoriesLoader :

SpringFactoriesLoader 是 Spring 框架中的一个工具类,用于加载 META-INF/spring.factories 文件中定义的类。
在 Spring Boot 应用程序中,META-INF/spring.factories 文件中定义了一些自动配置类,
这些自动配置类会在应用程序启动时被自动加载和配置。
SpringFactoriesLoader 可以用于加载这些自动配置类,从而实现自动配置机制。
复制代码

接下来我们继续进入 loadFactoryNames() 方法:

insert image description here

SpringFactoriesLoader 类中的 loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) 方法用于加载指定类型的工厂实现类。该方法接受两个参数:

  1. factoryType: 要加载的工厂类型,必须是一个接口或者抽象类。该参数是必须的,因为 SpringFactoriesLoader 会在 META-INF/spring.factories 文件中查找该工厂类型对应的实现类。

  2. classLoader: 类加载器,用于加载 META-INF/spring.factories 文件中定义的类。如果该参数为 null,则使用当前线程的上下文类加载器。 该方法会返回一个 List 类型的对象,其中包含了所有的候选工厂实现类的全限定类名。在加载工厂实现类时,SpringFactoriesLoader 会使用反射机制创建实例,并调用工厂方法生成对应的工厂对象。

了解了 loadFactoryNames()方法后,我们先把目光回到 getCandidateConfigurations() 方法,它在调用 loadFactoryNames()方法时传递了两个参数:

SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
复制代码

insert image description here

这两个参数是两个方法:

insert image description here

insert image description here

结合传递的参数进行分析,这里 loadFactoryNames() 方法的作用是:

加载所有使用了 @EnableAutoConfiguration 注解的自动配置类的全限定类名,并返回一个 List 类型的对象,其中包含了所有的候选自动配置类的全限定类名。

我们把目光回到 loadFactoryNames() 方法,不难看出,实际的加载功能使用最后方法返回处调用的 :

loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList())
复制代码

完成的。 insert image description here

我们先来看一下 getOrDefault()方法:

getOrDefault(Object key, V defaultValue) 是 Map 接口中的一个方法,
用于获取指定 key 对应的 value。如果该 key 存在,则返回对应的 value;否则,返回 defaultValue。
复制代码

也就是说 loadSpringFactories(classLoaderToUse) 方法,返回的是一个 Map 类型的数据,而 getOrDefault()方法的 key 为:

String factoryTypeName = factoryType.getName();
复制代码

insert image description here 显然,这个 factoryTypeName 是 EnableAutoConfiguration。

所以 loadSpringFactories(classLoaderToUse) 方法会返回的是一个 Map 类型的数据,并且结合 getOrDefault()传递的参数 key 可知,这个 Map 数据的 key 应该是 EnableAutoConfiguration,而 value 是一个 List<String>集合,所以这个 Map 类型的数据为 Map<String, List<String>>

原来 SpringBoot 通过 loadSpringFactories 方法获得了 Map<String, List<String>>数据结构的数据然后再通过 getOrDefault 方法将其转化成 List<String>数据结构。

分析到这里,我们再来看一下 loadSpringFactories(ClassLoader classLoader) 方法:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
		
		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		result = new HashMap<>();
		try {
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}

			// Replace all lists with unmodifiable lists containing unique elements
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}
复制代码

这个方法代码很多,我们一点一点的分析。

insert image description here 方法开始,先尝试从缓存中获取 Map<String, List<String>>类型的返回值 result,如果缓存命中就直接返回,如果缓存中没有,就继续往下执行。

insert image description here 到了这里,我们已经来到了这个方法的核心,简单分析一下这段代码的作用:

这段代码的作用是加载指定位置的资源并解析其中的属性,获取工厂类型和对应的实现类名,然后将它们存储在一个 Map 中。
具体来说,这段代码的实现过程如下:


1.获取指定位置 FACTORIES_RESOURCE_LOCATION 的所有资源 URL。

2.遍历所有获取到的 URL,对每一个 URL 进行如下操作:

	a.将 URL 封装成一个 UrlResource 对象,用于访问该 URL 资源。

	b.使用 PropertiesLoaderUtils 工具类加载 UrlResource 对象中的属性,获取工厂类型和对应的实现类名。

	c.遍历工厂类型对应的实现类名数组,将每个实现类名添加到一个 Map 对象中,以工厂类型为键,以实现类名列表为值。
	  这里使用了 computeIfAbsent 方法,如果该工厂类型在 Map 对象中不存在,则会创建一个新的键值对,
	  否则会将实现类名添加到该工厂类型对应的实现类名列表中。


3.返回包含工厂类型和对应的实现类名的 Map 对象。
  
该段代码通常用于 Spring Boot 应用程序中的自动配置,主要目的是在启动时自动加载并配置一些自动配置类,
以减少手动配置的工作量。在 Spring Boot 应用程序中,这段代码通常会在 AutoConfigurationImportSelector 类中被调用,
用于加载并解析 META-INF/spring.factories 文件中定义的自动配置类。
复制代码

在这个方法中 FACTORIES_RESOURCE_LOCATION :

insert image description here

搞了这么久,终于破案了,这个 loadSpringFactories 就是根据配置信息的 url 加载配置文件的内容,接下来我们进行断点调试,验证我们的猜想。

insert image description here

看,我们从 result 中找到了 key 为 EnableAutoConfiguration ,value 为 List<String> 的 Map 类型的数据。 insert image description here 再看看 spring.factories 文件中的内容,进一步印证了我们的推测。 insert image description here

最后,我们再次将目光会到 loadSpringFactories() 方法,这个方法先获取 loadSpringFactories() 方法返回的 Map<String, List<String>>数据结构的数据,这里封装了从 spring.factories 文件中获取的类的全限定名信息。

然后再通过 getOrDefault(factoryTypeName, Collections.emptyList()) 方法获取 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration
List<String>类型的数据。这个 list 集合里面就是 SpringBoot 自动配置的类的全限定名信息。

insert image description here

4 SpringFactories 机制

The SpringFactories mechanism is an extension mechanism provided by the Spring framework, which is used to automatically load and configure some extension classes when the application starts. Specifically, the mechanism defines the fully qualified class names of some extension classes in the META-INF/spring.factories file under the class path, and then automatically scans the file when the application starts, and loads the extension classes in it. The implementation process of the SpringFactories mechanism is as follows:

  1. Define the fully qualified class names of some extension classes in the META-INF/spring.factories file under the classpath.

  2. At application startup, use the ClassLoader to load the META-INF/spring.factories file and resolve the extension class names defined therein.

  3. Use the reflection mechanism to dynamically create an instance of the extended class according to the name of the extended class, and register it in the corresponding container. For example, in a Spring Boot application, auto-configuration classes are registered with the Spring container and auto-configured when the application starts.

The advantage of the SpringFactories mechanism is that it can greatly reduce the configuration difficulty of the application and improve the development efficiency. In Spring Boot applications, this mechanism is widely used in automatic configuration, custom Starter, plug-ins and other fields.

Summarize

That's right, the loadSpringFactories() method we analyzed above is implemented based on the SpringFactories mechanism.

So if the interviewer asks, talk about your understanding of SpringBoot automatic configuration, how should we answer?

SpringBoot automatic configuration is based on the SpringFactories mechanism to obtain the fully qualified name information of the classes that need to be automatically configured in the spring.factories file that depends on the META-INF directory, and then put the Bean objects we need into the IOC container based on this information , when we need to use it, we can directly inject and use it through the dependency injection mechanism. Of course, if you ask about the specific implementation details, you can talk about the specific code implementation according to our analysis process.

The above text is written by myself based on my own understanding, so it is inevitable that there are mistakes, any questions, or any mistakes in the article, please @我 in the comment area.

Guess you like

Origin juejin.im/post/7228780174312751161