目录
@Import(AutoConfigurationImportSelector.class)
SpringBoot的搭建十分快速,我们知道SpringBoot为我们做了很多事情,省去了大量的繁琐配置。它是如何做到的?
环境前提:我的SpringBoot导入了spring-web的依赖,SpringBoot的版本为2.2.5
Pom.XML的分析
我们用Maven构建整个工程,首先我们得去看看pom.xml,它主要包含了四个部分:
- 项目的元素信息:也就是Maven的基本元素,包括了groupId,artifactId,version,name,description等。
- parent:继承了父类的依赖管理,控制版本和打包内容(重点)
- dependencies:项目的具体依赖,这里包含了spring-boot-starter-web 和 spring-boot-starter-test,这些是我们一开始选择创建第一个SpringBoot项目时选择的spring-web依赖时为我们导入的。(重点)
- spring-boot-starter-web用于实现了HTTP接口,该依赖包含了SpringMVC,它使用了Tomcat作为默认嵌入式容器
- spring-boot-starter-test用于编写单元测试的依赖包。
- build:构建配置部分,默认使用了pring-boot-maven-plugin,配合spring-boot-starter-parent 就可以把Spring Boot应用打包成JAR来直接运行。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--继承了父类<spring-boot-starter-parent>的依赖管理,控制版本与打包等内容。-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--Maven项目的基本元素 groupId artifactId version name description-->
<groupId>com.claw</groupId>
<artifactId>springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<!--SpringBoot启动器-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!--构建配置部分-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这个pom.xml中有两个重点,一个父类的依赖,一个就是本项目的具体依赖,也就是启动器。
从它点击进去就能看见父类的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
它继承了 spring-boot-dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
spring-boot-dependencies里有什么?我们点击进去可以看见它集中管理了依赖和版本,我们可以从里面找到一些我们认识的依赖,比如mysql,jdbc,junit,这就是SpringBoot应用里所有依赖版本的地方,SpringBoot的版本控制中心。我们在导入依赖默认是可以不用写版本了。
还有一个就是启动器。SpringBoot将所有的功能场景抽取出来,做成了一个个的starter,只要在项目中引入这些starter,相关的依赖都会导入进来,我们需要什么功能,就导入什么样的启动器。
spring-boot-starter-web启动器里就涵盖了,spring-web,spring-webmvc等依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
SpringBootApplication的分析
我们的启动类简简单单,却做了很多事情,它到底是如何启动应用的?
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
首先我们要从注解上来分析
@SpringBootApplication
@SpringBootApplication 它表明了这个类是SpringBoot的主配置类。
点进去可以看到这个注解上还有很多注解,除了四个元注解,还有三个重要的注解:
- @SpringBootConfiguration 作用:表明它是SpringBoot的配置类。
- @EnableAutoConfiguration 作用:开启自动配置功能
- @ComponentScan 作用:通过它可以自动扫描并加载符合条件组件的Bean,并且将这个Bean加载到IOC容器中
@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) })
可以想到SpringBoot能够自动配置,一定是跟这个@EnableAutoConfiguration这个注解有很大关系。
@EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//添加该注解的类所在的package作为自动配置的pakage进行管理。
@AutoConfigurationPackage
//引入了 AutoConfigurationImportSelector 这个配置类
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
....
}
点击进入@EnableAutoConfiguration这个注解可以看到它有两个注解
- @AutoConfigurationPackage 作用:添加该注解的类所在的package作为自动配置的pakage进行管理。
- @Import(AutoConfigurationImportSelector.class) :引入了AutoConfigurationImportSelector这个配置类
我们一一分析这两个注解
@AutoConfigurationPackage
点击进入@AutoConfigurationPackage,可以看到它给容器导入了一个AutoConfigurationPackages.Registrar.class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//引入了 AutoConfigurationPackages.Registrar 这个配置类
@Import(AutoConfigurationPackages.Registrar.class)
这个AutoConfigurationPackages.Registrar.class又做了什么呢?它将主配置类的所在包及包下面所有的子包里面的所有组件都扫描到IOC容器里了。
这也是为什么我们的新创建的包要和SpringbootApplication主配置类在同一个包下的原因
@Import(AutoConfigurationImportSelector.class)
它导入了AutoConfigurationImportSelector这个类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
//它导入了AutoConfigurationImportSelector这个类
@Import(AutoConfigurationImportSelector.class)
这个AutoConfigurationImportSelector类通过getAutoConfigurationEntry()方法,获取到了候选的配置类。这个候选配置类是通过调用getCandidateConfigurations()方法得到的。
//AutoConfigurationImportSelector
//得到自动配置实体
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//得到符合条件的候选配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
//排除不符合条件的配置类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
那么getCandidfanateConfigurations()里做了什么呢?它调用了SpringFactoriesLoader类的静态方法loadFactoryNames()
//AutoConfigurationImportSelector 类
//获取候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//getSpringFactoriesLoaderFactoryClass返回的就是启动自动导入配置文件的注解类,EnableAutoConfiguration
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;
}
loadFactoryNames()把标注了@EnableAutoConfiguration注解的类作为了参数又去调用了loadSpringFactories()方法。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
loadSpringFactories()得到了@EnableAutoConfiguration标注的类的类加载器,然后读取META-INF/spring.factories下的数据。
它读取到spring.factories里的数据后,会做一个遍历,把资源加载到properties里。
在这里回顾一下,标注了@EnableAutoConfiguration类是哪一个?就是最开始的@SpringBootApplication这个注解!它集成了@EnableAutoConfiguration
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//得到ClassLoader,就是@EnableAutoConfiguration标注的类
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
//去获取META-INF/spring.factories这个文件里的数据
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
//将读取到的数据遍历,封装成一个Properties
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();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
从这里追根朔源,我们找到META-INF/spring.factories 这个文件,这里面有大量的自动配置文件,这就是自动配置的根源所在!
也就是说,@SpringBootApplication最终的目的就是让启动类下所有的资源被导入
spring.factories里都是一个个自动配置类的全限定名构成,选一个我们认识的类看看 ,比如WebMvcAutoConfiguration,可以看出来这些自动配置类就是一个个javaConfig配置类,并注入了一些@Bean
自动配置的真正实现,是从classpath中搜寻所有META-INF/spring.factories配置文件,并将其中对应的spring-boot-autoconfigure包下的配置项,通过反射实例化为对应标注了@Configuation的javaConfig形式的IOC容器配置类,然后将这些汇总成了一个个实例并加载到IOC容器中。
@ConditionalOnClass
这么多自动配置的文件,为什么只生效了一部分?loadSpringFactories()方法把spring.factories里所有的自动配置类都遍历了一遍, 但在SpringBoot启动后,只有mvc的文件自动加载了,这是为什么?
这就不得不说@ConditionalOnClass注解了,可以随便在spring.factories里找一个没有自动配置的类,点开可以发现在@ConditionalOnClass注解里,未自动配置的类是爆红的,类不存在。@ConditionOnClass就相当于判定条件,如果里面的条件都满足,则自动装配,不存在就不自动装配。
总结
SpringBoot自动配置的原理,其实是依靠了@EnableAutoConfiguation注解里的@Import(AutoConfigurationImportSelector.Class),通过AutoConfigurationImportSelector这个类的getAutoConfiguationEntry的方法获取了SpringBoot项目中需要自动配置的Bean,getAutoConfiguationEntry方法通过调用getCandidfanateConfigurations获取了可能参与到项目的候选配置bean,这些可能参与到项目的候选配置bean就是META-INF/spring.factories里的内容。getCandidfanateConfigurations获取了需要自动配置的类,除去了需要排除的配置类,将它们注册在了Spring的IOC容器中。
简单来说,SpringBoot的自动配置原理,就是去扫描了META-INF/spring.factories里的内容,将里面符合条件的(有启动器的)的自动配置类当作候选配置类,加载到了IOC容器中。
参考:SpringBoot之@EnableAutoConfiguration注解
参考:SpringBoot:快速入门及原理探究【狂神说Java系列】