传统的Spring框架实现一个Web服务,需要导入各种依赖Jar包,然后编写对应的XML配置文件等,相较而言,Spring Boot显得更加方便、快捷和高效。那么,Spring Boot究竟如何做到这些的呢?接下来分别针对Spring Boot框架的依赖管理、自动配置和执行流程进行深入分析。
一、依赖管理
首先我们创建一个Spring Boot项目,引入spring-boot-starter-web依赖
问题1:为什么导入dependency时不需要指定版本?
我们在创建Spring Boot项目后,在项目 pom.xml 文件中有两个核心依赖,分别是 spring-boot-starter-parent 和 spring-boot-starter-web,关于这两个依赖的相关介绍具体如下:
1.1 spring-boot-starter-parent 依赖
在上面pom.xml文件中找到 spring-boot-starter-parent 依赖,示例代码如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
上述代码中,将spring-boot-starter-parent 依赖作为Spring Boot项目的统一父项目依赖管理,并将项目版本号统一为2.2.2.RELEASE,该版本号根据实际开发需求可以修改的。
使用“Ctrl + 鼠标左键” 进入 spring-boot-starter-parent 底层源文件,发现 spring-boot-starter-parent 的底层有一个父依赖 spring-boot-dependencies,核心代码具体如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
继续查询 spring-boot-dependencies 底层源文件,核心代码具体如下:
<properties>
<activemq.version>5.15.11</activemq.version>
......
<commons-codec.version>1.13</commons-codec.version>
<commons-dbcp2.version>2.7.0</commons-dbcp2.version>
<commons-lang3.version>3.9</commons-lang3.version>
<commons-pool.version>1.6</commons-pool.version>
<commons-pool2.version>2.7.0</commons-pool2.version>
......
<solr.version>8.2.0</solr.version>
<spring-amqp.version>2.2.2.RELEASE</spring-amqp.version>
<spring-batch.version>4.2.1.RELEASE</spring-batch.version>
<spring-cloud-connectors.version>2.0.7.RELEASE</spring-cloud-connectors.version>
<spring-data-releasetrain.version>Moore-SR3</spring-data-releasetrain.version>
<spring-framework.version>5.2.2.RELEASE</spring-framework.version>
<spring-hateoas.version>1.0.2.RELEASE</spring-hateoas.version>
<spring-integration.version>5.2.2.RELEASE</spring-integration.version>
<spring-kafka.version>2.3.4.RELEASE</spring-kafka.version>
<spring-ldap.version>2.3.2.RELEASE</spring-ldap.version>
<spring-restdocs.version>2.0.4.RELEASE</spring-restdocs.version>
<spring-retry.version>1.2.4.RELEASE</spring-retry.version>
<spring-security.version>5.2.1.RELEASE</spring-security.version>
<spring-session-bom.version>Corn-RELEASE</spring-session-bom.version>
<spring-ws.version>3.0.8.RELEASE</spring-ws.version>
<sqlite-jdbc.version>3.28.0</sqlite-jdbc.version>
<sun-mail.version>${jakarta-mail.version}</sun-mail.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-extras-data-attribute.version>2.0.1</thymeleaf-extras-data-attribute.version>
<thymeleaf-extras-java8time.version>3.0.4.RELEASE</thymeleaf-extras-java8time.version>
<thymeleaf-extras-springsecurity.version>3.0.4.RELEASE</thymeleaf-extras-springsecurity.version>
<thymeleaf-layout-dialect.version>2.4.1</thymeleaf-layout-dialect.version>
<tomcat.version>9.0.29</tomcat.version>
......
</properties>
从 spring-boot-denpendencies 底层源文件可以看出,该文件通过标签对一些常用的技术框架的依赖文件进行统一版本号管理,例如activemq、spring、tomcat等,都有Spring Boot 2.2.2版本相匹配的版本,这也是 pom.xml 引入依赖文件不需要标注依赖文件版本号的原因。
需要说明的是,如果 pom.xml 引入的依赖文件不是 spring-boot-starter-parent 管理的,那么在 pom.xml 引入依赖文件时,需要使用version标签指定依赖文件的版本号。
问题2:spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的?
1.2 spring-boot-starter-web依赖
查看spring-boot-starter-web依赖文件源码,核心代码具体如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>tomcat-embed-el</artifactId>
<groupId>org.apache.tomcat.embed</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
从上述代码可以发现,spring-boot-starter-web依赖启动器的主要作用是提供Web开发场景所需的底层所有依赖。
正是如此,在pom.xml中引入spring-boot-starter-web依赖启动器时,就可以实现Web场景开发,而不需要额外导入Tomcat服务器以及其他Web依赖文件等。当然,这些引入的依赖文件的版本号还是由spring-boot-starter-parent父依赖进行的统一管理。
Spring Boot 除了提供有上述介绍的Web依赖启动器外,还提供了其它许多开发场景的相关依赖,我们可以打开Spring Boot官方文档,搜索 “Starters” 关键字查询场景依赖启动器。
列出了Spring Boot官方提供的部分场景依赖启动器,这些依赖启动器适用于不同的开放场景,使用时只需要在pom.xml文件中导入对应的依赖启动器即可。
需要说明的是,Spring Boot官方并不是针对所有场景开发的技术框架都提供了场景启动器,例如数据库操作框架MyBatis、阿里巴巴的Druid数据源等,Spring Boot官方就没有提供对应的依赖启动器。为了充分利用Spring Boot框架的优势,在Spring Boot官方没有整合这些技术的情况下,MyBatis、Druid等技术框架所在的开发团队主动与Spring Boot框架进行了整合,实现了各自的依赖启动器,例如mybatis-spring-boot-starter、druid-spring-boot-starter等。我们在pom.xml文件中引入这些第三方的依赖启动器时,切记要配置对应的版本号。
二、自动配置
概念:能够在我们添加 jar 包依赖的时候,自动为我们配置一些组件的相关配置,我们无需配置或者只需要少量配置就能运行编写的项目。
问题:Spring Boot到底是如何进行自动配置的呢?都把哪些组件进行了自动配置?
Spring Boot应用的启动入口是 @SpringBootApplication 注解标注的类中的 main() 方法,@SpringBootApplication能够扫描该注解标注的类的包以及子包底下的所有组件并自动配置
2.1 @SpringBootApplication注解
从上述源码可以看出,@SpringBootApplication注解是一个组合注解,前面4个是注解的元数据信息,我们主要看后面3个注解:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个核心注解,关于这是哪个核心注解的相关说明具体如下:
2.1.1 @SpringBootConfiguration注解
@SpringBootConfiguration注解表示Spring Boot配置类。查看@SpringBootConfiguration注解源码,核心代码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 配置IOC容器,标注该注解的类表示是一个配置类
public @interface SpringBootConfiguration {
}
2.1.2 @EnableAutoConfiguration注解
@EnableAutoConfiguration注解表示开启自动配置功能,该注解是Spring Boot框架最重要的注解,也是实现自动化配置的注解。同样,查看该注解内部查看源码信息,核心代码具体如下:
可以发现它是一个组合注解,Spring中有很多以 Enable 开头的注解,其作用就是借助 @Import 来收集并注册特定场景相关的 Bean,并加载到 IOC 容器。@EnableAutoConfiguration 就是借助 @Import 来收集所有符合自动配置条件的 Bean 定义,并加载到 IOC 容器。
2.1.2.1 @AutoConfigurationPackage 注解
查看 @AutoConfigurationPackage 注解内部源码信息,核心代码具体如下:
从上述源码可以看出,@AutoConfigurationPackage 注解的功能是由 @Import 注解实现的,它是Spring框架的底层注解,它的作用就是给容器中导入某个组件类,例如@Import(AutoConfigurationPackages.Registrar.class),它就是将 Registrar 这个组件类导入到容器中,可查看 Registrar 类中的 registerBeanDefinitions 方法,这个方法就是导入组件类的具体实现:
从上述源码可以看出,在Registrar类中有一个 registerBeanDefinitions()方法,使用Debug模式启动项目,可以看到选中的部分就是 com.cyd。也就是说,@AutoConfigurationPackage注解的主要作用就是将主程序类所在包及所有子包下的组件扫描到Spring容器中。
因此,在定义项目包结构时,要求定义的包结构非常规范,项目主程序启动类要定义在最外层的根目录位置,然后在根目录位置内部建立子包和类进行业务开发,这样才能够保证定义的类能够被组件扫描器扫描。
2.1.2.2 @Import(AutoConfigurationImportSelector.class)
@Import(AutoConfigurationImportSelector.class):将AutoConfigurationImportSelector这个类导入到Spring容器中,AutoConfigurationImportSelector可以帮助SpringBoot 应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IOC容器(ApplicationContext)中。
继续研究AUtoConfigurationImportSelector 这个类,通过源码分析这个类中是通过selectImports 这个方法告诉SpringBoot 都需要导入哪些组件:
(1)深入研究 AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader) 方法
/**
* Internal utility used to load {@link AutoConfigurationMetadata}.
*
* @author Phillip Webb
*/
final class AutoConfigurationMetadataLoader {
protected static final String PATH = "META-INF/" + "spring-autoconfigure-metadata.properties";
private AutoConfigurationMetadataLoader() {
}
public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
// 获得 PATH 对应的 URL 们
Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
// 遍历 URL 数组,读取到 properties 中
Properties properties = new Properties();
while (urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
// 将 properties 转换成 PropertiesAutoConfigurationMetadata 对象
return loadMetadata(properties);
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
}
}
static AutoConfigurationMetadata loadMetadata(Properties properties) {
return new PropertiesAutoConfigurationMetadata(properties);
}
/**
* {@link AutoConfigurationMetadata} implementation backed by a properties file.
*/
private static class PropertiesAutoConfigurationMetadata implements AutoConfigurationMetadata {
/**
* Properties 对象
*/
private final Properties properties;
PropertiesAutoConfigurationMetadata(Properties properties) {
this.properties = properties;
}
@Override
public boolean wasProcessed(String className) {
return this.properties.containsKey(className);
}
@Override
public Integer getInteger(String className, String key) {
return getInteger(className, key, null);
}
@Override
public Integer getInteger(String className, String key, Integer defaultValue) {
String value = get(className, key);
return (value != null) ? Integer.valueOf(value) : defaultValue;
}
@Override
public Set<String> getSet(String className, String key) {
return getSet(className, key, null);
}
@Override
public Set<String> getSet(String className, String key, Set<String> defaultValue) {
String value = get(className, key);
return (value != null) ? StringUtils.commaDelimitedListToSet(value) : defaultValue;
}
@Override
public String get(String className, String key) {
return get(className, key, null);
}
@Override
public String get(String className, String key, String defaultValue) {
String value = this.properties.getProperty(className + "." + key);
return (value != null) ? value : defaultValue;
}
}
}
总结:该方法将 spring-boot-autoconfigure-2.2.2.RELEASE.jar 下 META-INF 底下的 spring-autoconfigure-metadata.properties
的键值对配置载入到PropertiesAutoConfigurationMetadata 对象中并返回。
(2)深入研究 getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata)方法
/**
* 获得 AutoConfigurationEntry 对象
*
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param autoConfigurationMetadata the auto-configuration metadata
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
// 1. 判断是否开启。如未开启,返回空串
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 2. 获得注解的属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 3. 获得符合条件的配置类的数组
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 3.1 移除重复的配置类
configurations = removeDuplicates(configurations);
// 4. 获得需要排除的配置类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 4.1 校验排除的配置类是否合法
checkExcludedClasses(configurations, exclusions);
// 4.2 从 configurations 中,移除需要排除的配置类
configurations.removeAll(exclusions);
// 5. 根据条件(Condition),过滤掉不符合条件的配置类
configurations = filter(configurations, autoConfigurationMetadata);
// 6. 触发自动配置类引入完成的事件
fireAutoConfigurationImportEvents(configurations, exclusions);
// 7. 创建 AutoConfigurationEntry 对象
return new AutoConfigurationEntry(configurations, exclusions);
}
该方法中有个非常重要的方法 getCandidateConfigurations(annotationMetadata, attributes),现在我们深入研究一下该方法。
/**
* 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) {
// 加载指定类型 EnableAutoConfiguration 对应的,在 `META-INF/spring.factories` 里的类名的数组
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;
}
/**
* Return the class used by {@link SpringFactoriesLoader} to load configuration
* candidates.
* @return the factory class
*/
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
protected ClassLoader getBeanClassLoader() {
return this.beanClassLoader;
}
这个方法中调用SpringFactoriesLoader 的 loadFactoryNames 方法加载指定类型 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的在 META-INF/spring.factories
里的类名的数组。
进入SpringFactoriesLoader的loadFactoryNames 方法:
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
// 键值key名称
String factoryClassName = factoryClass.getName();
// 根据key从spring.factories加载后的Map中获取对应的类列表
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
/**
* 该方法其实是去加载一个外部的文件,文件名称是spring.factories
*/
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 {
// 如果类加载器不为null,则加载类路径下spring.factories文件,将其中设置的配置类的全路径信息封装为Enumeration类对象
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
// 循环Enumeration类对象,根据相应的节点信息生成Properties对象,通过传入的键获取值,在将值切割为一个个小的字符串转化为Array,放入result集合中
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);
}
}
}
loadSpringFactories(@Nullable ClassLoader classLoader) 该方法其实是去加载一个外部的文件,文件名称是spring.factories,该文件是在:
@EnableAutoConfiguration 就是从classpath中搜寻META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableautoConfiguration 对应的配置项通过反射实例化为对应的标注了@Configuration的JavaConfig形式的配置类,并加载到IOC容器中。
2.1.3 @ComponentScan注解
@ComponentScan注解具体扫描的包的根路径由Spring Boot项目主程序启动类所在包位置决定,在扫描过程中由前面介绍的@AutoConfigurationPackage 注解进行解析,从而得到Spring Boot项目主程序启动类所在包的具体位置。
三、自定义starter
3.1 SpringBoot starter机制
SpringBoot由众多的starter组成(一系列的自动化配置的starter插件),SpringBoot之所以流行,也是因为starter。
starter是SpringBoot非常重要的一部分,可以理解为一个可拔插的插件,正是因为这些starter使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由Spring Boot自动通过classpath路径下的类发现需要的Bean,并织入相应的Bean。
例如,你想使用Redis插件,那么可以使用spring-boot-starter-redis;如果想使用MongoDB,可以使用spring-boot-starter-data-mongodb。
3.2 为什么要自定义starter
开发过程中,经常会有一些独立于业务之外的配置模块。如果我们将这些课独立于业务之外的功能配置模块封装成一个starter,复用的时候只需要将其在pom中引用依赖即可,SpringBoot为我们完成自动装配。
3.3 自定义starter的命名规则
SpringBoot提供的starter以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的starter使用
xxx-spring-boot-starter 命名规则,以区分SpringBoot生态提供的starter。
3.4 自定义starter案例
(1)新建maven jar工程,工程名为zdy-spring-boot-starter,导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
</dependencies>
(2)编写 JavaBean
@EnableConfigurationProperties(SimpleBean.class)
@ConfigurationProperties(prefix = "simplebean")
public class SimpleBean {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "SimpleBean{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
(3)编写配置类MyAutoConfiguration
@Configuration
@ConditionalOnClass //@ConditionalOnClass:当类路径classpath下有指定的类的情况下进行自动配置
public class MyAutoConfiguration {
static {
System.out.println("MyAutoConfiguration init....");
}
@Bean
public SimpleBean simpleBean(){
return new SimpleBean();
}
}
(4)在resources下创建/META-INF/spring.factories
注意:META-INF 是自己手动创建的目录,spring.factories也是手动创建的文件,在该文件中配置自己的自动配置类。
3.5 使用自定义starter
(1)导入自定义starter的依赖
<dependency>
<groupId>com.lagou</groupId>
<artifactId>zdy-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)在全局配置文件中配置属性值
simplebean.id=1
simplebean.name=自定义starter
(3)编写测试方法
//测试自定义starter
@Autowired
private SimpleBean simpleBean;
@Test
public void zdyStarterTest(){
System.out.println(simpleBean);
}
四、run()方法执行原理
每个Spring Boot项目都有一个主程序启动类,在主程序启动类中有一个启动项目的main()方法,在该方法中通过执行SpringApplication.run()即可启动整个Spring Boot程序。
问题:那么SpringApplication.run()方法到底是如何做到启动Spring Boot项目的呢?
下面我们查看run()方法内部的源码,核心代码具体如下:
SpringApplication
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
从上述源码可以看出,SpringApplication.run()方法内部执行了两个操作,分别是SpringApplication实例的初始化创建和调用run()启动项目,这两个阶段的实现具体说明如下:
4.1 SpringApplication实例的初始化创建
查看SpringApplication实例对象初始化创建的源码信息,核心代码具体如下
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 把项目启动类.class设置为属性存储起来
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判断当前webApplicationType应用的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 设置初始化器(Initializer),最后会调用这些初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 设置监听器(Listener)
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 初始化 mainApplicationClass 属性,/用于推断并设置项目main()方法启动的主程序启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下。
- this.webApplicationType = WebApplicationType.deduceFromClasspath()
用于判断当前webApplicationType应用的类型。deduceFromClasspath()方法用于查看Classpath类路径下是否存在某个特征类,从而判断当前webApplicationType类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用) - setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class))
用于SpringApplication应用的初始化器设置。在初始化器设置过程中,会使用Spring类加载器SpringFactoriesLoader从类路径下的META-INF/spring.factories文件中获取所有可用的应用初始化器类ApplicationContextInitializer。见下图: - setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))
用于SpringApplication应用的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样,也是使用SpringFactoriesLoader从类路径下的META-INF下的spring.factores文件中获取所有可用的监听器类ApplicationListener。见下图: - this.mainApplicationClass = deduceMainApplicationClass()
用于推断并设置项目main()方法启动的主程序启动类
4.2 项目的初始化启动
分析完(new SpringApplication(primarySources)).run(args)源码前一部分SpringApplication实例对象的初始化创建后,查看run(args)方法执行的项目初始化启动过程,核心代码具体如下:
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
// 创建 StopWatch 对象,并启动。StopWatch 主要用于简单统计 run 启动过程的时长。
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 配置 headless 属性
this.configureHeadlessProperty();
// 第一步:获得 SpringApplicationRunListener 的数组,并启动监听器
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();
try {
// 创建 ApplicationArguments 对象
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 第二步:加载属性配置。执行完成后,所有的 environment 的属性都会加载进来,包括 application.properties 和外部的属性配置。
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
// 打印 Spring Banner
Banner printedBanner = printBanner(environment);
// 第三步:创建 Spring 容器。
context = createApplicationContext();
// 获得异常报告器 SpringBootExceptionReporter 数组
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 第四步:Spring容器前置处理,主要是调用所有初始化类的 initialize 方法
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 第五步:刷新容器
refreshContext(context);
// 第六步:Spring容器后置处理,执行 Spring 容器的初始化的后置逻辑。默认实现为空。
this.afterRefresh(context, applicationArguments);
// 停止 StopWatch 统计时长
stopWatch.stop();
// 打印 Spring Boot 启动的时长日志。
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 第七步:发出结束执行的事件,通知 SpringApplicationRunListener 的数组,Spring 容器启动完成。
listeners.started(context);
// 调用 ApplicationRunner 或者 CommandLineRunner 的运行方法。
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
// 通知 SpringApplicationRunListener 的数组,Spring 容器运行中。
try {
listeners.running(context);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
从上述源码可以看出,项目初始化启动过程大致包括以下部分:
4.2.1 第一步:获取并启动监听器
this.getRunListeners(args) 和 listeners.starting() 方法主要用于获取SpringApplication实例初始化过程中初始化的SpringApplicationRunListener监听器并运行。
SpringApplication
使用SpringFactoriesLoader从类路径下的META-INF下的spring.factores文件中获取所有可用的监听器类SpringApplicationRunListener。
4.2.2 第二步:根据SpringApplicationRunListeners以及参数来准备环境
this.prepareEnvironment(listeners, applicationArguments) 方法主要用于对项目运行环境进行预设置,同时通过this.configureIgnoreBeanInfo(environment) 方法排除一些不需要的运行环境。
SpringApplication
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
// Create and configure the environment
// 创建 ConfigurableEnvironment 对象,并进行配置
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 通知 SpringApplicationRunListener 的数组,环境变量已经准备完成。
listeners.environmentPrepared(environment);
// 绑定 environment 到 SpringApplication 上
bindToSpringApplication(environment);
// 如果非自定义 environment ,则根据条件转换
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
// 如果有 attach 到 environment 上的 MutablePropertySources ,则添加到 environment 的 PropertySource 中。
ConfigurationPropertySources.attach(environment);
return environment;
}
4.2.3 第三步:创建Spring容器
根据webApplicationType进行判断, 确定容器类型,如果该类型为SERVLET类型,会通过反射装载对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,接着使用之前初始化设置的context(应用上下文环境)、environment(项目运行环境)、listeners(运行监听器)、applicationArguments(项目参数)和printedBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置
SpringApplication
/**
* Strategy method used to create the {@link ApplicationContext}. By default this
* method will respect any explicitly set application context or application context
* class before falling back to a suitable default.
* @return the application context (not yet refreshed)
* @see #setApplicationContextClass(Class)
*/
protected ConfigurableApplicationContext createApplicationContext() {
// 根据 webApplicationType 类型,获得 ApplicationContext 类型
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass", ex);
}
}
// 创建 ApplicationContext 对象
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
4.2.4 第四步:Spring容器前置处理
这一步主要是在容器刷新之前的准备动作。设置容器环境,包括各种变量等等,其中包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。
SpringApplication
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置 context 的 environment 属性
context.setEnvironment(environment);
// 设置 context 的一些属性
postProcessApplicationContext(context);
// 初始化 ApplicationContextInitializer
applyInitializers(context);
// 通知 SpringApplicationRunListener 的数组,Spring 容器准备完成。
listeners.contextPrepared(context);
// 打印日志
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
// 设置 beanFactory 的属性
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// Load the sources
// 加载 BeanDefinition 们
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
// 通知 SpringApplicationRunListener 的数组,Spring 容器加载完成。
listeners.contextLoaded(context);
}
4.2.5 第五步:刷新容器
开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bean资源的定位,解析,注册等等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭。
SpringApplication
private void refreshContext(ConfigurableApplicationContext context) {
// 开启(刷新)Spring 容器
refresh(context);
// 注册 ShutdownHook 钩子
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
} catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
/**
* Refresh the underlying {@link ApplicationContext}.
* @param applicationContext the application context to refresh
*/
protected void refresh(ApplicationContext applicationContext) {
// 断言,判断 applicationContext 是 AbstractApplicationContext 的子类
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
// 启动(刷新) AbstractApplicationContext
((AbstractApplicationContext) applicationContext).refresh();
}
4.2.6 第六步:Spring容器后置处理
扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理。
SpringApplication
/**
* Called after the context has been refreshed.
* @param context the application context
* @param args the application arguments
*/
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
4.2.7 第七步:发出结束执行的事件
获取EventPublishingRunListener监听器,并执行其started方法,并且将创建的Spring容器传进去了,创建一个ApplicationStartedEvent事件,并执行ConfigurableApplicationContext 的publishEvent方法,也就是说这里是在Spring容器中发布事件,并不是在SpringApplication中发布事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布启动事件。
SpringApplicationRunListeners
public void started(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.started(context);
}
}
EventPublishingRunListener
@Override // ApplicationStartedEvent
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
}
4.2.8 第八步:执行Runners
用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序。其中,Spring Boot提供的执行器接口有ApplicationRunner 和CommandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后Spring Boot项目启动后会立即执行这些特定程序。
SpringApplication
private void callRunners(ApplicationContext context, ApplicationArguments args) {
// 获得所有 Runner 们
List<Object> runners = new ArrayList<>();
// 获得所有 ApplicationRunner Bean 们
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 获得所有 CommandLineRunner Bean 们
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
// 排序 runners
AnnotationAwareOrderComparator.sort(runners);
// 遍历 Runner 数组,执行逻辑
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
下面,通过一个Spring Boot执行流程图,让大家更清晰的知道Spring Boot的整体执行流程和主要启动阶段: