文章目录
Day 9
2019年8月9日。
这是我学习Spring的第九天。
这一天,我学到了以下的知识。
Spring Boot运行原理探究
前言
编写的“Hello,SpringBoot!”,我们没有像以前写Spring MVC一样,写过任何的配置文件。仅仅是写了一个简单的控制类,便可以实现相等的效果,这是如何实现的呢?
探究
探究:pom.xml
在Maven项目中,存在有一个特殊的配置文件:pom.xml。该文件保存有项目的相关配置,包括项目的信息、依赖的注入、构建的环境等。
为了研究Spring Boot的运行原理,第一步应该就从这个文件开始。默认的pom.xml文件代码如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
其中,最重要的信息当属<parent.>标签中的信息。该标签类似于Java中的继承,而该项目便是继承名为spring-boot-starter-parent文件的信息,代码如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
查看该文件(将光标悬停在该行上,按下ctrl+鼠标左键即可进入该文件),可以进入到spring-boot-starter-parent-2.1.7.RELEASE.pom文件中,如图所示:
从图中可以看出,该文件同样也继承了一个文件:spring-boot-dependencies。可以进入该文件查看,发现该文件正如其名:配置了多个注入依赖的信息。
由此可知,spring-boot-starter-parent就是真正管理Spring Boot应用里面所有依赖版本的地方,同时也是Spring Boot的版本控制中心
也就是说,以后需要导入依赖的时候,默认不需要再添加版本。但是如果导入的包没有在依赖中管理,仍然需要手动配置版本
探究:启动器
我们继续观察pom.xml代码,可以找到从这个文件中注入的依赖,代码如下:
<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>
</dependency>
</dependencies>
见字知意,由该段代码可以得知:
- springboot-boot-starter-web:spring-boot的场景启动器
- springboot-boot-starter-test:测试spring-boot的场景启动器
其中,spring-boot-starter-web 还帮我们导入了web模块正常运行所依赖的组件;
总结:SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能,就导入什么样的场景启动器即可 ;
探究:主程序
研究完pom.xml后,我们将目光投向项目创建时默认生成的类(启动类)。这里因为创建项目时用的都是默认信息,所以该类在我的项目中的名字是DemoApplication.java(每个人的类名可能会有不同),该类代码如下:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
其中:
-
@SpringBootApplication
:来标注一个主程序类 , 说明这是一个Spring Boot应用 -
SpringApplication.run(DemoApplication.class, args)
:用于启动Spring应用
这个表面上看起来很简单的类,竟然只用短短一行代码就可以完成Spring应用的配置以及启动,那它到底是怎么做到的呢?这就需要我们继续去探究下去。
探究:@SpringBootApplication
老办法,遇到不会的东西,就直接进入源文件查看。这里我们使用之前的方法,进入该文件查看,如图所示:
可以看到,除了默认的四种注解(@Target({ElementType.TYPE})
,@Retention(RetentionPolicy.RUNTIME)
,@Documented
,@Inherited
)之外,还有一些自定义的注解,说明如下:
-
@SpringBootConfiguration
:标注在某个类上,表示这个类是一个Spring Boot的配置类 -
@EnableAutoConfiguration
:开启自动配置功能,以前我们需要手动配置的东西,SpringBoot可以自动帮我们配置 ;该注解会告诉SpringBoot开启自动配置功能,这样自动配置才能生效; -
@ComponentScan
:这个注解在Spring中很重要,它对应XML配置中的元素。该注解的功能就是自动扫描并加载符合条件的组件或者bean,并将这个bean定义加载到IOC容器中 -
@Filter
:这个注解对应着过滤器的相关配置
接下来,我们将对其中几个注解进行更深入的研究。
探究:@SpringBootConfiguration
进入该注解的源文件进行查看,可以查看到如下的信息,如图所示:
可以看到,除了默认的注解之外, 该注解上还标识着一个自定义的注解
@Configuration
:在配置类上来标注这个注解,用以配置类或者配置文件。
继续查看该注解的源代码,可以查看到如下的信息,如图所示:
由图中可以看出,该注解上标识了一个我们在Spring中常见的注解@component
,说明配置类也是容器中的一个组件
至此,我们探究完了@SpringBootConfiguration
,需要去探究另一个@EnableAutoConfiguration
探究:@EnableAutoConfiguration
现在回到SpringBootApplication注解中,查看这个注解,如图所示:
@EnableAutoConfiguration
:开启自动配置功能
以前我们需要配置的东西,Spring Boot可以自动帮我们配置 ; @EnableAutoConfiguration
会告诉Spring Boot开启自动配置功能,这样自动配置才能生效;
按照老办法,点击这个注释,查看该注释文件的信息,如图所示:
@AutoConfigurationPackage
:自动配置包
继续点进去,会看到一个 @Import({Registrar.class})
,如图所示:
@import
:Spring底层注解@import,给容器中导入一个组件,导入的组件由 {Registrar.class} 将主配置类 【即@SpringBootApplication
标注的类】的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;
现在回到EnableAutoConfiguration中,对其他两个注释作出说明:
-
@Import({AutoConfigurationImportSelector.class})
:给容器导入组件 -
AutoConfigurationImportSelector
: 导入哪些组件的选择器
它将所有需要导入的组件以全类名的方式返回 , 这些组件就会被添加到容器中 ;
它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件,并配置好这些组件 ;
有了自动配置类,就免去了我们手动编写配置注入功能组件等的工作;
为了获悉底层更多的细节,需要我们更进一步去AutoConfigurationImportSelector类中查看相应的代码
探究:AutoConfigurationImportSelector
进入AutoConfigurationImportSelector类,可以在该类中找到以下的代码:
//AutoConfigurationImportSelector中获取配置文件信息;
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;
}
我们将注意力投到该方法中的:
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
这里的getSpringFactoriesLoaderFactoryClass()方法返回的就是我们最开始看的启动自动导入配置文件的注解类,代码如下:
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
这就说明了这个SpringFactoriesLoader类与底层实现有重大联系,我们现在点进这个类查看,可以发现这个类的一个特殊变量String FACTORIES_RESOURCE_LOCATION,如图所示:
之后,还会发现对应的方法:该方法最终会读取一个配置文件:META-INF/Spring.factories,代码如下:
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryClassName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
终于,我们找到了一切的源头:即META-INF/Spring.factories
探究:Spring.factories
我们在项目目录下的“External Libraries”目录下可以找到这个文件,如图所示:
通过以上的推论,我们可以得出大致的结论:
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
- 以前我们需要自己配置的东西 , 自动配置类都帮我们解决了
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
我们随意打开一个,例如WebMvcAutoConfiguration,可以看到该类具有@Configuration
和@Bean
注释,如图所示:
总结
所以,真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中
分析路线
路线图:https://www.processon.com/view/link/5d4cedc3e4b08b95b817a312#map
Run
分析完了Spring Boot的运行原理,现在需要进一步理解Spring Boot是如何执行的,我们将目光放到默认创建出来的启动类,代码如下:
@SpringBootApplication
public class SpringbootDemo02Application {
public static void main(String[] args) {
//该方法返回一个ConfigurableApplicationContext对象
//参数一:应用入口的类 参数类:命令行参数
SpringApplication.run(SpringbootDemo02Application.class, args);
}
}
其中,需要对SpringApplication.run(SpringbootDemo02Application.class, args);
这个方法进行分析,而分析这个方法分为两部分:
- SpringApplication的实例化
- run方法的执行
SpringApplication的实例化
需要经历以下几个步骤:
- 推断应用的类型是普通的项目还是Web项目
- 查找并加载所有可用初始化器 , 设置到initializers属性中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并设置main方法的定义类,找到运行的主类
对应代码如下:
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
run方法的执行
示意图如下: