1 前言
之前也写过一篇类似的文章,但是当时理解的并不是很深入,所以一直想重新写,但是一直没有时间,就拖到了现在。这篇文章可能会很长,因为在讲解自动配置的过程中还会衍生出其他一些重要的知识点,我也会进行介绍。
2 热身案例
要谈 SpringBoot 的自动装配,肯定离不开下面这段代码。
其实我学习 SpringBoot 时,根本没有思考过这段代码,只知道这是 SpringBoot 程序的启动类,要保证他能够覆盖所有要扫描的包,然后就去写代码了。
我们先来看看这段代码:
ConfigurableApplicationContext run = SpringApplication.run(AppRunApplication.class, args);
这段代码的作用是启动 SpringBoot 应用程序,并返回一个 ConfigurableApplicationContext 对象的实例。
SpringApplication.run() 方法返回的是 ConfigurableApplicationContext 类型的对象,表示 Spring 应用程序的上下文。这个上下文保存了Spring 容器中所有 bean 的引用,对于应用程序中的其他组件来说,这个上下文就是一个全局共享的容器。
SpringApplication.run(AppRunApplication.class, args) 语句的意义是基于 AppRunApplication 这个类启动 SpringBoot 应用程序,并将命令行参数传递给应用程序。运行这个语句后,Spring会自动进行应用程序的初始化工作,包括创建ApplicationContext、注册所有的bean定义、启动嵌入式Web容器、加载应用程序的配置文件等。
此外,通过这个方法还可以获取到 ConfigurableApplicationContext 的实例,使用这个对象可以进一步控制应用程序的运行,如手动关闭应用程序、获取应用程序的环境变量、添加自定义的bean等。
只看文字实在时太枯燥了,我们还是看看代码吧。我们在启动类,创建了一个 Test 类,然后生命了一个 Bean 方法,它的作用就是返回一个 Test 类,然后我们进行断点调试。
太神奇了,我们通过 run 拿到了 Test 的 Bean 实例
如果我们试图获取一个没有使用 Bean 方法注册的类,就会抛出异常。
这个 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 注解进行依赖注入即可
通过上面的讲解,我们对 SpringBoot 自动配置有了一个大概的认识——我们需要什么,就在 pom 文件中引入相关依赖,然后在 application.yml 文件中配置相关配置信息,然后 SpringBoot 会帮我们把需要的 Bean 都自动配置到 Spring IOC 容器中,之后我们使用时只需要通过依赖注入机制将需要的 Bean 对象注入即可。
到现在,我们应该知道了 SpringBoot 自动配置是什么,但是这样我们只是知其所以然,所以我们还需要继续了解它的底层实现。
3 源码解读
我们把目光来到今天的主角,@SpringBootApplication 注解。
我们进入这个注解,好嘛,它的头上怎么顶着这么多注解,不过真正重要的只有三个注解,我们接下来会一一介绍。
3.1 @SpringBootConfiguration
点进@SpringBootConfiguration
注解,可以发现其核心注解为@Configuration
注解:
@Configuration注解是Spring框架的注解之一,用于标记配置类。
在Spring Boot中,使用@Configuration注解可以将该类作为配置类,从而使该类中的Bean可以被Spring IoC容器管理和使用。
在配置类中,我们可以使用另外两个注解@Bean和@Scope来定义Bean,其中@Bean注解用于定义Bean对象,而@Scope注解用来指定Bean对象的作用域。
除此之外,在配置类中,我们还可以定义一些常量,并使用@Value注解来注入应用程序的属性。
举例来说,一个简单的配置类可以被定义如下:
@Configurationpublic class MyConfiguration {@Value("${myapp.something}")private String something;@Beanpublic 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“标签配置类了,如下图所示。
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注解还有一些其他可选参数:
basePackageClasses:可以使用一个或多个类作为基础包来指定要扫描的根目录。比如:@ComponentScan(basePackageClasses = {MyComponent.class, MyService.class})。
excludeFilters:可以指定过滤器来排除带有某些注解或实现某些接口的组件。
includeFilters:可以指定过滤器来仅包含带有某些注解或实现某些接口的组件,可能的值有@Component, @Repository, @Service, @Controller等。
使用这些参数,可以更加精细的控制扫描范围。
3.3 @EnableAutoConfiguration 注解
这是今天的主角中的主角,自动配置实现的核心注解。
点进这个注解可以发现,如下图所示。
我们重点来看 @Import(AutoConfigurationImportSelector.class)
这个注解。
@Import 注解是 它用于将一个或多个类导入到 Spring 容器中,以便于在应用程序中使用。通过 @Import 注解,我们可以将一些非 Spring 管理的类实例化并注册到 Spring 容器中,或者将一些 Spring 管理的配置类导入到当前配置类中,以便于在应用程序中进行统一的配置和管理。
@Import
是Spring Framework 中的一个注解,用于在配置类中导入其他配置类或者普通的Java类。
通过@Impor
注解,它用于将一个或多个类导入到 Spring 容器中,以便于在应用程序中使用。通过 @Import 注解,我们可以将一些非 Spring 管理的类实例化并注册到 Spring 容器中,或者将一些 Spring 管理的配置类导入到当前配置类中,以便于在应用程序中进行统一的配置和管理。
说白了在这里@Import
注解的作用就是将 AutoConfigurationImportSelector 这个类导入当前类,这个类就是实现自动配置的核心。
我们继续进入到 AutoConfigurationImportSelector 类:
最后,我们发现, AutoConfigurationImportSelector 实际上是实现了 ImportSelector 接口,这个接口只有两个方法,其中我们需要重点关注 selectImports() 方法。
ImportSelector 接口是 Spring Framework 中的一个接口,它可以用于在 Spring 容器启动时动态地导入一些类到 Spring 容器中。通过实现 ImportSelector 接口,并重写其中的 selectImports 方法,我们可以自定义逻辑来确定需要导入的类,从而实现更加灵活的配置和管理。
selectImports 方法是 ImportSelector 接口中的一个方法,用于返回需要导入的类的全限定类名数组。在 Spring 容器启动时,Spring 会扫描所有实现了 ImportSelector 接口的类,并调用其中的 selectImports 方法来确定需要导入的类。在 selectImports 方法中,我们可以自定义逻辑来确定需要导入的类,例如根据某些条件来动态地确定需要导入的类。
好嘛,搞了半天,关键点在这里,通过 selectImports 方法,我们就可以得到需要自动配置的类的全限定类名数组,那我们来看一下这个方法。
@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}
既然我们需要自动配置的类的全限定类名数组,那么这个方法必然通过某个方法获取到这个数组,我们看一下这个方法getAutoConfigurationEntry(annotationMetadata),单看这个名字它的嫌疑就非常大。
getAutoConfigurationEntry 方法可以用于获取自动配置类的元数据,以便于分析和调试自动配置机制。它接受一个 AnnotationMetadata 对象作为参数,该对象表示使用了 @EnableAutoConfiguration 注解的配置类的元数据。通过调用该方法,我们可以获取到所有已经配置的自动配置类的全限定类名,以及这些自动配置类的条件注解和优先级信息等。
我们继续进入到 getAutoConfigurationEntry() 方法:
说实话这个方法我现在看还是感觉眼花缭乱,哈哈,不过不影响我们分析,我们先看方法返回值,返回值是一个 AutoConfigurationEntry 对象,再看看 return 语句:
return new AutoConfigurationEntry(configurations, exclusions);
果然是通过构造函数创建一个 AutoConfigurationEntry 对象并返回,我们再看看它的构造参数:
configurations, exclusions
再结合我们之前的分析,这个方法的作用是返回自动配置类的元数据,不难推断出 configurations 就是我们需要的自动配置类的元数据,那exclusions 参数呢,这个从名字上来看,它应该是需要排除的类的元数据。
类似上面 @ComponentScan注解 中的 excludeFilter 参数,可以指定过滤器来排除带有某些注解或实现某些接口的组件。
那我们现在要做的就是分析 configurations 是怎么来的:
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
我们继续进入到 getCandidateConfigurations() 方法:
这个方法的组成还是非常简单的,它只调用了 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() 方法:
SpringFactoriesLoader 类中的 loadFactoryNames(ClassSpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
这两个参数是两个方法:
结合传递的参数进行分析,这里 loadFactoryNames() 方法的作用是:
加载所有使用了 @EnableAutoConfiguration 注解的自动配置类的全限定类名,并返回一个 List 类型的对象,其中包含了所有的候选自动配置类的全限定类名。
我们把目光回到 loadFactoryNames() 方法,不难看出,实际的加载功能使用最后方法返回处调用的 :
loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList())
完成的。
我们先来看一下 getOrDefault()方法:
getOrDefault(Object key, V defaultValue) 是 Map 接口中的一个方法,用于获取指定 key 对应的 value。如果该 key 存在,则返回对应的 value;否则,返回 defaultValue。
也就是说 loadSpringFactories(classLoaderToUse) 方法,返回的是一个 Map 类型的数据,而 getOrDefault()方法的 key 为:
String factoryTypeName = factoryType.getName();
显然,这个 factoryTypeName 是 EnableAutoConfiguration。
所以 loadSpringFactories(classLoaderToUse) 方法会返回的是一个 Map 类型的数据,并且结合 getOrDefault()传递的参数 key 可知,这个 Map 数据的 key 应该是 EnableAutoConfiguration,而 value 是一个 List
集合,所以这个 Map 类型的数据为 Map<String, List>
。
原来 SpringBoot 通过 loadSpringFactories 方法获得了 Map<String, List>
数据结构的数据然后再通过 getOrDefault 方法将其转化成 List
数据结构。
分析到这里,我们再来看一下 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 elementsresult.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;}
这个方法代码很多,我们一点一点的分析。
方法开始,先尝试从缓存中获取 Map<String, List>
类型的返回值 result,如果缓存命中就直接返回,如果缓存中没有,就继续往下执行。
到了这里,我们已经来到了这个方法的核心,简单分析一下这段代码的作用:
这段代码的作用是加载指定位置的资源并解析其中的属性,获取工厂类型和对应的实现类名,然后将它们存储在一个 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 :
搞了这么久,终于破案了,这个 loadSpringFactories 就是根据配置信息的 url 加载配置文件的内容,接下来我们进行断点调试,验证我们的猜想。
看,我们从 result 中找到了 key 为 EnableAutoConfiguration ,value 为 List
的 Map 类型的数据。
再看看 spring.factories 文件中的内容,进一步印证了我们的推测。
最后,我们再次将目光会到 loadSpringFactories() 方法,这个方法先获取 loadSpringFactories() 方法返回的 Map<String, List>
数据结构的数据,这里封装了从 spring.factories 文件中获取的类的全限定名信息。
然后再通过 getOrDefault(factoryTypeName, Collections.emptyList()) 方法获取 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration
的 List
类型的数据。这个 list 集合里面就是 SpringBoot 自动配置的类的全限定名信息。
4 SpringFactories 机制
SpringFactories 机制是 Spring 框架提供的一种扩展机制,用于在应用程序启动时自动加载并配置一些扩展类。具体来说,该机制通过在类路径下的 META-INF/spring.factories 文件中定义一些扩展类的全限定类名,然后在应用程序启动时自动扫描该文件,并加载其中的扩展类。
SpringFactories 机制的实现过程如下:
在类路径下的 META-INF/spring.factories 文件中定义一些扩展类的全限定类名。
在应用程序启动时,使用 ClassLoader 加载 META-INF/spring.factories 文件,并解析其中定义的扩展类名。
根据扩展类名使用反射机制动态创建扩展类的实例,并将其注册到相应的容器中。例如,在 Spring Boot 应用程序中,自动配置类会被注册到 Spring 容器中,并在应用程序启动时自动配置。
SpringFactories 机制的优点是可以极大地降低应用程序的配置难度,提高开发效率。在 Spring Boot 应用程序中,该机制被广泛应用于自动配置、自定义 Starter、插件等领域。
总结
没错,我们上面分析的 loadSpringFactories() 方法就是基于 SpringFactories 机制实现的。
那如果面试官问,说说你对 SpringBoot 自动配置的理解,我们该怎么回答?
SpringBoot 自动配置就是基于SpringFactories 机制获取对应依赖META-INF目录下的 spring.factories 文件中的需要自动配置的类的全限定名信息,然后根据这些信息将我们需要的使用的 Bean 对象放到 IOC 容器中,当我们需要使用时,通过依赖注入机制直接注入使用即可。当然如果再追问具体的实现细节,可以根据我们的分析流程讲讲具体的代码实现。
以上文字都是我自己根据自己的理解写的,所以难免有错误的地方,有任何问题,或者文章有任何错误,请在评论区@我。