文章目录

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学习记录 Day9(Spring Boot拓展)_xml


从图中可以看出,该文件同样也继承了一个文件: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

老办法,遇到不会的东西,就直接进入源文件查看。这里我们使用之前的方法,进入该文件查看,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_Spring Boot_02


可以看到,除了默认的四种注解(​​@Target({ElementType.TYPE})​​​,​​@Retention(RetentionPolicy.RUNTIME)​​​,​​@Documented​​​,​​@Inherited​​)之外,还有一些自定义的注解,说明如下:

  • ​@SpringBootConfiguration​​:标注在某个类上,表示这个类是一个Spring Boot的配置类
  • ​@EnableAutoConfiguration​​:开启自动配置功能,以前我们需要手动配置的东西,SpringBoot可以自动帮我们配置 ;该注解会告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
  • ​@ComponentScan​​:这个注解在Spring中很重要,它对应XML配置中的元素。该注解的功能就是自动扫描并加载符合条件的组件或者bean,并将这个bean定义加载到IOC容器中
  • ​@Filter​​:这个注解对应着过滤器的相关配置

接下来,我们将对其中几个注解进行更深入的研究。

探究:@SpringBootConfiguration

进入该注解的源文件进行查看,可以查看到如下的信息,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_Spring Boot_03


可以看到,除了默认的注解之外, 该注解上还标识着一个自定义的注解

​@Configuration​​:在配置类上来标注这个注解,用以配置类或者配置文件。

继续查看该注解的源代码,可以查看到如下的信息,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_配置文件_04


由图中可以看出,该注解上标识了一个我们在Spring中常见的注解​​@component​​,说明配置类也是容器中的一个组件

至此,我们探究完了​​@SpringBootConfiguration​​​,需要去探究另一个​​@EnableAutoConfiguration​

探究:@EnableAutoConfiguration

现在回到SpringBootApplication注解中,查看这个注解,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_配置文件_05


​@EnableAutoConfiguration​​ :开启自动配置功能

以前我们需要配置的东西,Spring Boot可以自动帮我们配置 ; ​​@EnableAutoConfiguration​​会告诉Spring Boot开启自动配置功能,这样自动配置才能生效;

按照老办法,点击这个注释,查看该注释文件的信息,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_Spring Boot_06


​@AutoConfigurationPackage​​ :自动配置包

继续点进去,会看到一个 ​​@Import({Registrar.class})​​,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_spring_07


​@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,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_Spring Boot_08


之后,还会发现对应的方法:该方法最终会读取一个配置文件: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”目录下可以找到这个文件,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_配置文件_09


通过以上的推论,我们可以得出大致的结论:

  • SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
  • 以前我们需要自己配置的东西 , 自动配置类都帮我们解决了
  • 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;

我们随意打开一个,例如WebMvcAutoConfiguration,可以看到该类具有​​@Configuration​​​和​​@Bean​​注释,如图所示:

Spring学习记录 Day9(Spring Boot拓展)_spring_10


总结

所以,真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中

分析路线

Spring学习记录 Day9(Spring Boot拓展)_xml_11


路线图:​​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的实例化

需要经历以下几个步骤:

  1. 推断应用的类型是普通的项目还是Web项目
  2. 查找并加载所有可用初始化器 , 设置到initializers属性中
  3. 找出所有的应用程序监听器,设置到listeners属性中
  4. 推断并设置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方法的执行

示意图如下:

Spring学习记录 Day9(Spring Boot拓展)_配置文件_12