SpringBoot看过很多次,但是一直理解不深,只停留在了解的阶段。直到最近公司项目做拆分,需要用到SpringBoot启动时的一些特性,于是把启动过程看了一遍。举个例子
测试对Spring启动原理的理解
- Rpc框架和Spring的集成问题。Rpc框架何时注册暴露服务,在哪个Spring扩展点注册呢?init-method 中行不行?
- MQ 消费组和Spring的集成问题。MQ消费者何时开始消费,在哪个Spring扩展点”注册“自己?init-method 中行不行?
- SpringBoot 集成Tomcat问题。如果出现已开启Http流量,Spring还未启动完成,怎么办?Tomcat何时开启端口,对外服务?
SpringBoot项目常见的流量入口无外乎 Rpc、Http、MQ 三种方式。一名合格的架构师必须精通服务的入口流量何时开启,如何正确开启?
SpringBoot是什么
我们日常开发对SpringBoot的使用,可谓非常熟练了。但是很多SpringBoot的很多东西对我们来说依然如同一个黑盒。例如SpringBoot的自动装配,有注解可以实现注入为什么还需要SPI。springboot是依赖于spring的,比起spring,除了拥有spring的全部功能以外,springboot无需繁琐的xml配置,这取决于它自身强大的自动装配功能;并且自身已嵌入Tomcat、Jetty等web容器,集成了springmvc,使得springboot可以直接运行,不需要额外的容器,提供了一些大型项目中常见的非功能性特性、指标、健康检测、外部配置等,
其实spring大家都知道,boot是启动的意思。所以,spring boot其实就是一个启动spring项目的一个工具而已,总而言之,springboot 是一个服务于框架的框架;也可以说springboot是一个工具,这个工具简化了spring的配置;
SpringBoot的启动过程
一个spring应用的启动从run方法开始
@SpringBootApplication
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}
点进源来,SpringApplication类中有个静态run()方法,最终执行如下:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return (new SpringApplication(primarySources)).run(args);
}
显然到这里分为两步,下面分析一下这两步。其中第一步是一些准备工作,Spring的核心启动流程在第二步
- 实例化new SpringApplication()。
- 执行SpringApplication的run()方法。
1.SpringApplication构造函数
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 = Collections.emptySet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
this.applicationStartup = ApplicationStartup.DEFAULT;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories();
// 加载初始器,配置应用程序启动前的初始化对象
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 加载监听器
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
构造函数中重要的两个方法就是初始化和见监听器,初始化器以及监听器 调用的方法都是 getSpringFactoriesInstances
最终逻辑是从类路径META-INF/spring.factories中 加载 初始化器以及监听器 。
- 初始化器(ApplicationContextInitializer): 初始化某些 IOC容器刷新之前的组件。
- 监听器(ApplicationListener): 监听特定的事件 比如IOC容器刷新、容器关闭等。
getSpringFactoriesInstances() 方法根据指定的Class类型从META-INF/spring.factories 中获取相应的自动配置类。
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = this.getClassLoader();
// 从 META-INF/spring.factories 中获取 (loadFactoryNames方法)
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 根据名称实例化
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
SpringApplication的构建都是为了run()方法启动做铺垫,最重要的部分就是设置应用类型、设置初始化器、设置监听器
注意: 初始化器和这里的监听器都要放置在spring.factories文件中才能在这一步骤加载,否则不会生效。因为此时IOC容器还未创建,所以即使将其注入到IOC容器中也是不会生效的。
2.SpringApplication的run方法
SpringApplication构建完成,一切都做好了铺垫,现在到了启动的过程了。下面是run方法的源码,我这里用的是spring-boot-starter-parent的 2.5.3 版本,不同版本源码差异可能会稍有差别
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
this.configureHeadlessProperty();
// 获取监听器,就是在第一步SpringApplication构造函数中加载的监听器
SpringApplicationRunListeners listeners = this.getRunListeners(args);
// 通知监听者Spring启动开始
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 创建应用程序环境 配置文件在此处读取(application.properties application.yml)
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
this.configureIgnoreBeanInfo(environment);
// 控制台打印 Banner
Banner printedBanner = this.printBanner(environment);
// 2.创建应用程序上下文...此处创建了beanfactory,创建IoC容器
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 准备ApplicationContext IOC容器基本信息
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 3.刷新上下文(spring启动核心,刷新IOC容器refresh,tomcat,bean组件的初始化都在这一步完成)
this.refreshContext(context);
// 刷新IOC容器的后置处理,源码是空方法,如果有自定义需求可以重写,比如打印一些启动结束日志等。
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
// 4.启动完成通知
listeners.started(context);
// 执行自定义的run方法,源码是空方法,是一个扩展功能
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, listeners);
throw new IllegalStateException(var10);
}
try {
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}
可以看到源码中有很多空方法,是Spring给我们留的口子方便我们自定义扩展功能。总的流程大致如下
1、StopWatch保存一些信息,应用名字,当前启动时间
2、this.createBootstrapContext(),创建引导上下文
2.1:获取到所有之前创建的bootstrapers,挨个执行initialize方法,来完成对引导启动器上下文环境设置。
3、this.configureHeadlessProperty(),让当前应用进入headless模式(用于在缺失显示屏、鼠标、键盘时候的系统配置)
4、this.getRunListeners(args),获取所有RunListener运行时监听器(为了方便所有Listener进行事件感知,项目在启动)
4.1 getSpringFactoriesInstances去Spring.factories找SpringApplicationRunListener
4.2 遍历所有SpringApplicationRunListener,调用starting方法,监听所有项目运行的状态
4.3 保存命令行参数;
4.4 this.prepareEnvironment(…),准备环境
4.4.1返回创建基础环境信息。StandardServletEnviroment
4.4.2配置环境信息对象(properties等)
4.4.2.1 读取所有配置元的配置属性
4.4.3绑定环境信息
4.4.4监听器调用environmentPrepared;通知所有监听器,当前环境准备完成。
4.5 this.createApplicationContext(),创建IoC容器
4.5.1根据项目类型(Servlet)创建容器
4.5.2当前会创建AnnotationConfigServletWebServerApplicationContext;
4.6 this.prepareContext(…)准备ApplicationContext IOC容器基本信息 prepareContext方法)
4.6.1 保存环境信息
4.6.2 IOC容器的后置处理流程
4.6.3 应用初始化;applyInitializers
4.6.3.1 遍历所有的ApplicationContxtInitializer,调用initialize,来对IOC容器进行初始化扩展
4.7.4 遍历所有的Listener调用ContextPrepared。EventPublishListener;通知所有的监听器ContextPrepared
4.7.5 所有监听器 调用contextLoaded,通知上下文IOC容器加载完成。
4.7 this.refreshContext(context),刷新IOC容器refresh(核心源码)
4.7.1创建容器中的所有组件
4.8 this.afterRefresh(…),刷新容器后的工作
4.8.1所有监听器调用started方法,当前项目启动了
4.9 this.callRunners(…),调用runner方法
4.9.1获取容器中的ApplicationRunner
4.9.2获取容器中CommandLineRunner
4.9.3合并所有runner,并且按照@order进行排序
4.9.4遍历所有的runner调用run方法。
4.10 this.handleRunFailure(…),如果有异常会调用Listener的failed方法
4.11 listeners.running(context),调用所有监听器running方法Listener.running方法
4.12 this.handleRunFailure(…),方法如果有异常,还是调用Listener的failed方法。
下面着重看一下核心方法this.refreshContext(context),中间件,tomcat,MQ,还有自己定义的Bean,依赖注入都是在这一步完成的。即使点开这个方法也只有几行。但里面做了很多事情
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
this.refresh(context);
}
我们来看下这么几行代码,Spring是如何优雅的把容器Tomcat,中间件,自定义组件这么复杂的应用启起来的,这就是SpringBoot的精髓,自动装配
SpringBoot的自动装配
在讲之前先了解一下,手动装配的流程。在SpringMVC的时代,首先你需要引入Spring框架的相关依赖,包括核心容器(spring-context)、AOP(spring-aop)、数据访问(spring-jdbc、spring-orm)、事务管理(spring-tx)、Web(spring-web)等
<!-- Spring Core Container -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10.RELEASE</version> <!-- 请替换为最新版本 -->
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.10.RELEASE</version>
</dependency>
<!-- 其他Spring相关依赖... -->
然后需要手动创建Spring的配置文件(例如XML配置文件)来定义和配置Spring容器中的Bean、数据源、事务管理等。
如果你的项目是一个Web应用,你需要手动配置Servlet、Filter、Listener等。
<!-- web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- 配置Servlet、Filter、Listener等... -->
</web-app>
最最麻烦的是,当项目比较复杂,要引入很多第三方组件时,XML中要配置的Bean也很多,而且有些Bean还有依赖关系,这就要求Bean配置的顺序不是随意的,而是有先后的。这对于项目维护是灾难。那SpringBoot是怎么解决这些问题的
组件扫描
SpringBoot中,不需要再配置XML文件来指定要注入那些Bean了,取而代之的是注解。那么在服务启动时,要把我们定义的组件扫描并加载到IOC容器中,那么就需要配置扫描路径。但是我们在启动SpringBoot项目时并没有配置扫描路径,它是怎么扫描到Bean然后注入到容器中的呢。
在SpringBoot中有两种类型的类路径需要扫描,一是项目根路径下的类,二是第三方jar包路径下的类。如下图
SpringBoot默认会扫描启动类下的包和子包中定义的Bean,例如被 @Controller,@Service,@Configuration注解的类,并加载到IOC容器中,在SpringBoot启动流程中,SpringApplication构造函数中,就获取了当前启动类的路径。
但是第三方的jar包,SpringBoot是怎么扫描到的呢,比如我们依赖了 spring-boot-starter-data-redis ,由于这个是第三方的 jar包,SpringBoot显然是不可能提前知道 redis相关 Bean的类路径的,而且我们也没有配置任何xml文件或是其它配置来告诉SpringBoot第三方 redis的类路径。来看看SpringBoot是怎么做到的
假设我们自己项目中开发了一个公共模块,这个模块是给其他业务系统使用的,比如编写了一个Robot用来监控应用是否健康。我们通常会有一些业务类。然后新建一个RobotAutoConfiguration类
@Import({RobotService.class, User.class, RobotController.class})
@Configuration
public class RobotAutoConfiguration {
}
然后我们在启动类中显示的引入这个配置类,就可以了
@SpringBootApplication
@Import(RobotAutoConfiguration.class)
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}
这样就达成了我们目的,使用者只需要一个@Import(RobotAutoConfiguration.class)就能正常使用我们的Starter了。但是我们看SpringBoot官方的第三方 starter时并不需要显示注入类,只需要pom中依赖 jar包,就可以实现自动注入
如何全自动注入?
这就需要SPI登场了,META-INF/spring.factories这个文件几乎在所有的自动装配的starter中都有,它是实现自动装配注入的核心文件。在这个文件中指定了要扫描的类路径,SpringBoot会读取这个文件中配置的类路径,加载并注入到IOC中。说白了,就是与前面提到的以注解的方式注入Bean,SpringBoot还支持以文件配置的方式注入Bean。