學(xué)習(xí)內(nèi)容主要參考《Spring Boot 實(shí)戰(zhàn)》,本篇內(nèi)容結(jié)合項(xiàng)目實(shí)例,由淺入深,源碼級(jí)別了解自動(dòng)配置的原理
一、Spring Boot簡(jiǎn)介
Spring Boot背景
-
痛點(diǎn)1:Spring 配置復(fù)雜:雖然Spring的組件代碼是輕量級(jí)的,但它的配置卻是重量級(jí)的。一開始,Spring用XML配置,而且是很多XML配置。Spring 2.5引入了基于注解的組件掃描,這消除了大量針對(duì)應(yīng)用程序自身組件的顯式XML配置。Spring 3.0引入了基于Java的配置,這是一種類型安全的可重構(gòu)配置方式,可以代替XML。
組件掃描減少了配置量,Java配置讓它看上去簡(jiǎn)潔不少,但Spring還是需要不少配置。 - 痛點(diǎn)2:依賴管理復(fù)雜:實(shí)現(xiàn)一個(gè)功能要考慮使用哪些依賴,依賴版本管理等,是否會(huì)有依賴沖突。
Spring Boot精要
- 自動(dòng)配置能力(核心):針對(duì)很多Spring應(yīng)用程序常見(jiàn)的應(yīng)用功能,Spring Boot能自動(dòng)提供相關(guān)配置。
- 起步依賴(核心):告訴Spring Boot需要什么功能,它就能引入需要的庫(kù)。
- Actuator(核心):讓你能夠深入運(yùn)行中的Spring Boot應(yīng)用程序,一探究竟。
- 命令行界面:這是Spring Boot的可選特性,借此你只需寫代碼就能完成完整的應(yīng)用程序,無(wú)需傳統(tǒng)項(xiàng)目構(gòu)建。
二、由淺入深
快速搭建Spring Boot項(xiàng)目
作者習(xí)慣使用Idea + maven方式
-
idea -> File -> New -> Project,選擇Spring Initializr
image.png -
創(chuàng)建項(xiàng)目基本信息
image.png -
選擇Starters
image.png -
查看項(xiàng)目結(jié)構(gòu)
image.png - 啟動(dòng)類
package com.example.readinglist;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ReadingListApplication {
public static void main(String[] args) {
SpringApplication.run(ReadingListApplication.class, args);
}
}
@SpringBootApplication注解
注解定義
@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 {
```
}
從定義可以看到該注解開啟了Spring的組件掃描和Spring Boot的自動(dòng)配置功能。實(shí)際上,@SpringBootApplication將三個(gè)有用的注解組合在了一起。
- Spring的@Configuration:標(biāo)明該類使用Spring基于Java的配置。我們會(huì)更傾向于使用基于Java而不是XML的配置。
- Spring的@ComponentScan:啟用組件掃描,這樣你寫的Web控制器類和其他組件才能被自動(dòng)發(fā)現(xiàn)并注冊(cè)為Spring應(yīng)用程序上下文里的Bean。本章稍后會(huì)寫一個(gè)簡(jiǎn)單的Spring MVC控制器,使用@Controller進(jìn)行注解,這樣組件掃描才能找到它。
- Spring Boot的@EnableAutoConfiguration:就是這個(gè)配置
,讓你不用再寫成篇的配置了。
Spring Boot生成的jar包
我們先繼續(xù)由淺入深的看一下Spring boot生成的jar包
pom.xml中會(huì)自動(dòng)引入插件:spring-boot-maven-plugin
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
mvn clean install 會(huì)在項(xiàng)目的target目錄生成對(duì)應(yīng)的jar包(todo:上傳項(xiàng)目源碼)
jar xvf xxx.jar解壓生成的jar包,查看目錄結(jié)構(gòu)如下

cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: ReadingList
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.readinglist.ReadingListApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.2
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
todo:啟動(dòng)類入口分析:org.springframework.boot.loader.JarLauncher
三、自動(dòng)配置能力分析
- 注解@ EnableAutoConfiguration定義
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
/**
* Environment property that can be used to override when auto-configuration is
* enabled.
*/
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
其中最重要的是 @Import(AutoConfigurationImportSelector.class)注解。借助AutoConfigurationImportSelector ,@EnableAutoConfiguration 幫助Spring Boot 應(yīng)用將所有符合條件的 @Configuration 配置加載到當(dāng)前IoC容器中。而最主要的還是借助于 Spring 框架一的一個(gè)工具類:SpringFactoriesLoader 將 META-INF/spring.factories加載配置,spring.factories 文件是一個(gè)典型的properties配置文件,配置的格式仍然是Key = Value 的形式,中不過(guò) Key 和 Value 都是Java的完整類名。比如:
org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.jpa.repository.support.JpaRepositoryFactory
- AutoConfigurationImportSelector源碼分析
核心方法selectImports
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);//關(guān)鍵方法
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
getAutoConfigurationEntry
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);//獲取注解屬性
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);//關(guān)鍵方法,從META-INF/spring.factories加載配置
configurations = removeDuplicates(configurations);//去重
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);//去掉exclusions
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
getCandidateConfigurations
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());//關(guān)鍵方法
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;
}
/**
* Return the class used by {@link SpringFactoriesLoader} to load configuration
* candidates.
* @return the factory class
*/
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
loadFactoryNames
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* <p>As of Spring Framework 5.3, if a particular implementation class name
* is discovered more than once for the given factory type, duplicates will
* be ignored.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories
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);//META-INF/spring.factories
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;
}
四、SpringBoot默認(rèn)提供了哪些自動(dòng)配置類
在向應(yīng)用程序加入Spring Boot時(shí),有個(gè)名為spring-boot-autoconfigure.jar文件,其中包含了很多配置類。

這些配置類里有用于Thymeleaf的配置,有用于Spring Data JPA的配置,有用于Spiring MVC的配置,還有很多其他東西的配置,你可以自己選擇是否在Spring應(yīng)用程序里使用它們。共計(jì)130個(gè)左右

所有這些配置如此與眾不同,原因在于它們利用了Spring的條件化配置,這是Spring 4.0引入的新特性。
這里貼一個(gè)demo示例,講解了如何自動(dòng)將configure類引入到容器。
- Conditional
上面說(shuō)了SpringBoot如何引入Configuration,但是如何做到智能的引入對(duì)應(yīng)的bean呢?這里就要講解一下Conditional了。
讓我們以 ElasticsearchAutoConfiguration 這個(gè)配置類為例一探究竟:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class })
@ConditionalOnProperty(prefix = "spring.data.elasticsearch", name = "cluster-nodes", matchIfMissing = false)
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration {
private final ElasticsearchProperties properties;
public ElasticsearchAutoConfiguration(ElasticsearchProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public TransportClient elasticsearchClient() throws Exception {
TransportClientFactoryBean factory = new TransportClientFactoryBean();
factory.setClusterNodes(this.properties.getClusterNodes());
factory.setProperties(createProperties());
factory.afterPropertiesSet();
return factory.getObject();
}
private Properties createProperties() {
Properties properties = new Properties();
properties.put("cluster.name", this.properties.getClusterName());
properties.putAll(this.properties.getProperties());
return properties;
}
}
不難注意到上述代碼中出現(xiàn)的 @ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean 等注解。它們都屬于 Spring 4.0 的新特性——Conditional 接口。條件化配置允許配置存在于應(yīng)用程序中,但在滿足某些特定條件之前都忽略這個(gè)配置。 Conditional 接口又有很多衍生條件,以上面的 @ConditionalOnClass 為例,代碼如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
/**
* The classes that must be present. Since this annotation is parsed by loading class
* bytecode, it is safe to specify classes here that may ultimately not be on the
* classpath, only if this annotation is directly on the affected component and
* <b>not</b> if this annotation is used as a composed, meta-annotation. In order to
* use this annotation as a meta-annotation, only use the {@link #name} attribute.
* @return the classes that must be present
*/
Class<?>[] value() default {};
/**
* The classes names that must be present.
* @return the class names that must be present.
*/
String[] name() default {};
}
它關(guān)聯(lián)到 OnClassCondition 去返回條件匹配結(jié)果。結(jié)合上文 ElasticsearchAutoConfiguration 的源碼來(lái)看,該配置只有在 classpath 中有 org.elasticsearch.client.Client 及 org.elasticsearch.client.transport.TransportClient 時(shí)才會(huì)生效。
五、結(jié)語(yǔ)
本篇由淺入深逐漸了解Spring Boot的內(nèi)部機(jī)制,從項(xiàng)目創(chuàng)建、jar包分析、自動(dòng)配置源碼講解以及如何智能的加載bean到IoC中。整篇梳理有對(duì)應(yīng)的代碼示例,方便理解。



