前言

今天我们来聊聊springboot的starter,主要从starter的原理及如何自定义starter,springboot的启动流程等方面进行研究。

SpringBoot Starter机制

SpringBoot是由众多的Starter组成的(一系列自动化配置的starter插件),Springboot之所以流行,也是因为starter。

starter是springboot中非常重要的一部分,可以理解为一个可插拔式的插件,正是这些starter使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由springboot自动通过classpath路径下的类发现需要的Bean,并注入相应的bean。

比如,你想使用Redis插件,那么可以使用spring-boot-starter-redis;如果想使用MongoDB,那么可以使用spring-boot-starter-data-mongodb。

为什么要自定义starter

开发过程中,经常会有一些独立于业务之外的配置模块。如果我们能将这些可独立月业务代码之外的功能配置模块封装成一个个starter,复用的时候只需要将其在pom中引用依赖即可,springboot为我们完成自动装配。

自定义starter的命名规则

Springboot提供的starter是以spring-boot-starter-xxx的方式命名的。官方建议自定义的starter使用xxx-spring-boot-starter命名规则,以区分springboot生态提供的starter。

整个过程分为两个部分:1.自定义starter;2.使用starter。

下面我们来完成这两个过程。

自定义starter的具体步骤

1.自定义starter

第一步,新建maven工程,工程名为zdy-spring-boot-starter,导入依赖:

1
2
3
4
5
6
7
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
</dependencies>

第二步,新建JavaBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@EnableConfigurationProperties(SimpleBean.class)
@ConfigurationProperties(prefix = "simplebean")
public class SimpleBean {
private int id;
private String name;

public int getId() {
return id;
}

public SimpleBean setId(int id) {
this.id = id;
return this;
}

public String getName() {
return name;
}

public SimpleBean setName(String name) {
this.name = name;
return this;
}

@Override
public String toString() {
return "SimpleBean{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

第三步,编写配置类MyAutoConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ConditionalOnClass //当类路径classpath下有指定的类的情况下进行自动配置
public class MyAutoConfiguration {

static {
System.out.println("MyAutoConfiguration init ...");
}
@Bean
public SimpleBean simpleBean(){
return new SimpleBean();
}
}

第四步,在resources下创建/META-INF/spring.factories:

说明:META是自己手动创建的目录,spring.factories是手动创建的文件,在该文件中配置自己的自动配置类:

源码地址: 自定义starter源码

2.使用starter

在自己本地的项目中导入自定义starter依赖:

1
2
3
4
5
        <dependency>
<groupId>com.lagou</groupId>
<artifactId>zdy-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

在全局配置文件中配置属性值:

1
2
simplebean.id=1
simplebean.name=自定义starter

我的是application.yml文件格式,配置如下:

编写测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example;

import com.lagou.SimpleBean;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

//测试自定义的starter
@Autowired
private SimpleBean simpleBean;


@Test
void contextLoads() {
System.out.println(simpleBean);
}

}

运行测试方法,结果如下:

SpringBoot的执行原理

每个springboot项目都有个主程序启动类,在主程序启动类中有一个启动项目的main()方法,在该方法中通过执行SpringApplication.run()即可启动整个springboot程序。

1
2
3
4
5
6
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

下面我们通过这个入口的run方法来一步步深入,看springboot是如何启动的呢?

从源码看出,run方法主要干了两件事,一个是SpringApplication实例对象的创建,一个是又调用了run方法启动项目。下面我们就从这两个部分来看。

1.SpringApplication实例对象的创建

springApplication实例对象创建的核心源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	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));

//设置监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

//用于推断并设置项目main()方法启动的主程序类
this.mainApplicationClass = deduceMainApplicationClass();
}

打个断点看一下其中变量的值:

从源码中看出来,springapplication初始化的过程主要包括四个部分,具体如下:

1. this.webApplicationType = WebApplicationType.deduceFromClasspath();

用于判断当前webApplicationType应用的类型。deduceFromClasspath()方法的源码如下:

该方法主要是通过查看classpath路径下是否存在某个特征类,从而判断当前的webApplicationType是SERVLET应用(Spring5之前的传统MVC应用),还是REACTIVE应用(Spring5开始出现的WebFlux交互式应用)。

2. setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

用于SpringApplication应用的初始化器设置。在初始化器设置的过程中,会使用Spring类加载器SpringFactoriesLoader从类路径下的META-INF/spring.factories文件中获取所有可用的应用初始化器类ApplicationContextInitializer 。

3. setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

用于SpringApplication应用的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样,会使用Spring类加载器SpringFactoriesLoader从类路径下的META-INF/spring.factories文件中获取所有可用的应用监听器类ApplicationListener 。

4. this.mainApplicationClass = deduceMainApplicationClass();

用于推断并设置项目main()方法启动的主程序类。

2.项目的初始化启动

分析完new SpringApplication(primarySources).run(args);这段源码的**new SpringApplication(primarySources)**即SpringApplication实例对象的初始化创建之后,我们继续分析run(args)方法执行的项目初始化启动过程,核心源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();

//第一步:获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

//第二步,根据SpringApplicationRunListeners以及参数来准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);

//准备banner打印器,也就是启动springboot时候打印在控制台上的ASCII艺术字体
Banner printedBanner = printBanner(environment);

//第三步,创建Spring容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);

//第四步,spring容器前置处理
prepareContext(context, environment, listeners, applicationArguments, printedBanner);

//第五步,刷新容器
refreshContext(context);

//第六步,spring容器后置处理
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

//第七步,发出结束执行的事件
listeners.started(context);

//返回容器
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}

从上述源码可以看出,项目初始化启动过程大致包含以下几个部分:

第一步:获取并启动监听器:

1
	getRunListeners(args);和listeners.starting();主要用于获取SpringApplication实例初始化过程中初始化的SpringApplicationRunListener监听器并运行

第二步:根据SpringApplicationRunListener以及参数来准备环境:

1
 prepareEnvironment(listeners, applicationArguments);方法主要用于对项目运行环境进行预设置,同时通过configureIgnoreBeanInfo(environment);方法排除一些不需要的运行环境

第三步:创建Spring容器:

createApplicationContext()方法是创建spring容器的。根据webApplicationType进行判断,确定容器类型,如果该类型是SERVLET类型,会通过反射装载对应的字节码,也就是AnnotationConfigServletWebServerApplicationContext,如下:

接着使用之前初始化设置的context(应用上下文)、environment(项目运行环境)、listeners(运行监听器)、applicationArguments(项目参数)和printerBanner(项目图标信息)进行应用上下文的组装配置,并刷新配置。

第四步,Spring容器前置处理:

1
prepareEnvironment(listeners, applicationArguments);

这一步主要是在容器刷新之前的准备动作。设置容器环境,包括各种变量等,其中非常关键的一个操作是:将启动类注入容器,为后续开启自动化配置奠定基础。

第五步,刷新容器:

开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bean资源的定位,解析,注册等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭。

第六步,spring容器后置处理:

扩展接口,设计模式中的模板方法,默认是空实现:

1
2
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}

如果有自定义需求,可以重写该方法,比如打印一些启动结束log,或者其他一些后置处理。

第七步,发出结束执行的事件:

listeners.started(context);

方法中部分核心源码如下:

1
2
3
4
5
void started(ConfigurableApplicationContext context) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.started(context);
}
}
1
2
3
4
@Override
public void started(ConfigurableApplicationContext context) {
context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
}

获取EventPublishingRunListener监听器,并执行started方法,并且将创建的spring容器传进去了,创建了一个ApplicationStartedEvent 事件,并执行ConfigurableApplicationContext的publishEvent方法,也就是说这里是在spring容器中发布事件,并不是在SpringApplication 中发布事件,和前面的starting不同,前面的starting是直接向SpringApplication 中的监听器发布启动事件。

第八步,执行Runners:

1
callRunners(context, applicationArguments);

用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序。其中,springboot提供的执行器接口有ApplicationRunner和CommandLineRunner两种,在使用时只需要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后springboot项目启动后会立即执行这些特定程序。

Springboot执行流程图

用一张springboot的执行流程图来梳理下整个的执行过程:

总结

主要从自定义starter开始,分析了springboot的执行流程,也看了部分的核心源码,对springboot的整体流程有了认识。