Spring boot启动源码之run方法
前言
1、run方法的大致流程概述
2、run方法源码
public ConfigurableApplicationContext run(String... args) {
//计时器,用于统计启动耗时,stopWatch.start()开始计时,stopWotch.stop()结束计时
StopWatch stopWatch = new StopWatch();
//开始计时
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
//设置java.awt.headless 属性是true 还是false。
configureHeadlessProperty();
//获取所有在构造器中读取加载的监听器。
SpringApplicationRunListeners listeners = getRunListeners(args);
//启动监听器。补充点⑥
listeners.starting();
try {
//将main方法中的args参数封装为ApplicationArguments类。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//顾名思义,准备环境(生成环境变量)
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
//打印banner
Banner printedBanner = printBanner(environment);
// 创建ApplicationContext,通过从上到下实例化,实例化了一个DefaultListableBeanFactory,作为Spring框架中很重要的一个类
context = createApplicationContext();
//从spring.factories中获取SpringBootExceptionReporter类型的异常解析器
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 用已有的数据准备上下文,为刷新做准备
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 刷新Context,最终会回到Spring中的核心流程AbstractApplicationContext#refresh
refreshContext(context);
//目前为空
afterRefresh(context, applicationArguments);
stopWatch.stop();
//输出日志记录执行主类名、时间信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//发布应用上下文启动完成事件
listeners.started(context);
//执行所有 Runner 运行器
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;
}
下面我们对源码进行逐行解析,并附带些自己的理解
3、关键代码:configureHeadlessProperty()
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
点开来看,发现有步迷之操作,先getProperties(),再setProperties(),取出来然后存进去,脑子瓦特了?
其实是由于System中getProperty()有2个重载方法,但却只有一个setProperty()方法,其中getProperty()有单参和双参两方法,单参就是简单的获取属性,有就有,没有就没有。
源码中使用的是双参,双参则聪明一点,在没有的时候会返回一个调用者指定的默认值,所以经过这样操作后,不管有没有那个属性,最终都能保证有。
java.awt.headless是J2SE的一种模式,用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole 需要将该值设置为true。
4、关键代码:getRunListeners(args)
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
看到这里的时候,我进行debug,发现在执行这行代码之前,this中已经加载了21个Listener,可能你会疑惑,纳尼,什么时候的事,这是在SpringApplication调用构造方法的时候就初始化了,具体可以看我以下文章:
Spring启动源码之SpringApplication构造器
这里又是我们熟悉的getSpringFactoriesInstances()方法,这个方法在Spring和Spring boot的源码中出镜率极高,因为Spring框架的大部分需要初始化的类都配置在了spring.factories文件中,所以每次要加载特定类型的类时,都得使用这个方法。
不过这里就是加载了一个EventPublishingRunListener,这是唯一实现了SpringApplicationRunListeners的实现类,该类定义了很多的方法用于在Spring启动过程中进行回调通知,具体的细节可以看我下面这篇文章:
代步车
5、关键代码:DefaultApplicationArguments(args)
public DefaultApplicationArguments(String... args) {
Assert.notNull(args, "Args must not be null");
this.source = new Source(args);
this.args = args;
}
看起来用处是不大,对吧,这样new一个包装类来包装args干嘛,多此一举吗?按照我的理解,在这里的使用只是附带作用,这个包装类DefaultApplicationArguments最主要的作用是为了为我们提供获取main方法中参数的途径,比如,我们想要获取main方法中的参数,我们可以这样做:
@Component
public class TestApplicationArguments {
@Resource
private ApplicationArguments arguments;
public void test() {
System.out.println("非选项参数数量: " + arguments.getNonOptionArgs().size());
System.out.println("选项参数数量: " + arguments.getOptionNames().size());
System.out.println("非选项参具参数:");
arguments.getNonOptionArgs().forEach(System.out::println);
}
}
可以在controller,service中获取,非常方便,虽然不常用,但是有没有就是另一回事了。这也是spring boot强大的一种体现。
6、关键代码:prepareEnvironment(listeners, applicationArguments)
准备环境(生成evironment环境变量),这个方法的作用总结来说就是生成environment实例,并填充各种属性,一般是基本属性,如jdk版本、路径,项目路径,文件编码,系统用户,所在机器的是什么系统,使用了什么版本的JVM等等这种基础属性。具体代码如下:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
//关键代码
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置环境变量,加了个解析器,可以解析@Value注解修饰的遍历了
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
如上,关键代码是getOrCreateEnvironment()方法,这个方法就是通过前面提到的WebApplicationType类型创建不同的环境,我们这里是SERVLET,创建的是一个StandardServletEnvironment()。调用的是父类的父类AbstractEnvironment的无参构造,在无参构造中,调用了StandardServletEnvironment中重写的customizePropertySources方法,对environment进行了初始化。
我们看一下在调用完构造函数后的environment实例中的属性:
可以看到systemProperties中加载了64个系统属性,systemEnvironment中加载了46个系统环境变量,具体看看有哪些变量,了解一下,可以让我们对environment理解更加透彻。
以上只是部分,屏幕太小,放不下了,系统变量的概念就是这种东西。
以下是系统环境变量systemEnvironment中加载的属性,如下:
看看,看看,熟悉不,所谓的系统环境变量,不就是我们在配置Java运行环境时所配置的那些吗,比如常见的JAVA_HOME,MAVEN_HOME等等,简直不要太熟悉,就是在environment变量中被加载了,所以现在对environment变量承当的作用就更加清晰了。
7、关键代码:configureIgnoreBeanInfo(environment)
private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
}
}
这代码的功能非常简单,就是获取上文中environment中的systemProperties中是否存在spring.beaninfo.ignore,存在则获取,不存在则添加到environment中的systemProperties中并设置为true。
根据Spring官网中对该属性的解释,
当值为true时,跳过对BeanInfo类的搜索(通常用于在应用程序中首先没有为bean定义此类类的场景)。建议为true,减少不必要的启动损耗。
8、关键代码:printBanner(environment)
这是进行控制台打印banner的方法(如果不知道banner是啥,百度),可以看到这块代码是在prepareEnvironment后面的,因为在application.properties文件中可以配置一些banner的配置,比如banner.txt文件的位置、文件名、打印开关等。所以需要先加载配置,再提供配置给后续代码调用。
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(getClassLoader());
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
if (this.bannerMode == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
如果在这里debug,注意从prepareEnviroment开始,就执行了一些监听器,又重新调用到了run方法,就导致了第一遍执行时,bannerMode是OFF的,跳过断点,第二次执行时,才是真正的调用,bannerMode变回了console,能够打印,我之前就是钻了牛角尖,无法理解为什么bannerMode是OFF,banner还是打印了。
这方面比较简单,就不赘述了,另外提供一个生成banner的网址:
banner在线生成器
9、关键代码:createApplicationContext()
/**
* 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() {
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);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
这方法非常简单,就是创建了一个空上下文,根据前文中的webApplicationType,创建对应类型的上下文,一般情况下,webApplicationType都是SERVLET,创建的是AnnotationConfigApplicationContext。提一嘴,AnnotationConfigApplicationContext的父类的父类实现了ConfigurableApplicationContext,所以可以使用ConfigurableApplicationContext进行统一接收这些子类。而父类可以统一接收不同的子类,这也是设计模式中,策略模式的理论基础。
10、关键代码:getSpringFactoriesInstances
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
熟悉的getSpringFactoriesInstances方法,见过好多次了。这个方法的作用就是从spring.factories文件中加载特定类型的实例,比如这里就是获取的SpringBootExceptionReporter类型。这里主要是加载异常分析器,用在try、catch中对异常进行处理。
11、prepareContext
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
//设置环境变量
context.setEnvironment(environment);
//上下文后缀处理
postProcessApplicationContext(context);
//执行Initializers
applyInitializers(context);
//发布contextPrepared事件
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
这里又有很多需要注意的了,比如context.setEnviroment(),不仅仅是为context所属的AnnotationConfigApplicationContext设置了环境变量,还为其父类AbstractApplicationContext设置了同一个环境变量。为后续进行refreshContext的时候,调用AbstractApplicationContext中的refresh方法时做准备。
并且通过debug可以发现,此时context其实已经存在了environment对象,那为什么还要setEnviroment,因为context中的是未解析profiles文件的对象,是不完整的;而即将要设置的environment对象则是解析profiles文件后的对象,是完整的;
public class AnnotationConfigServletWebServerApplicationContext
extends ServletWebServerApplicationContext implements AnnotationConfigRegistry {
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
//显式调用父类AbstractApplicationContext的setEnvironment方法
super.setEnvironment(environment);
//调用AnnotatedBeanDefinitionReader#setEnvironment()方法
this.reader.setEnvironment(environment);
//ClassPathBeanDefinitionScanner继承了ClassPathScanningCandidateComponentProvider,所以调用了父类setEnvironment方法
this.scanner.setEnvironment(environment);
}
}
12、关键代码:refreshContext(context)
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
//向 JVM 运行时注册一个关闭挂钩,JVM关闭时,关闭此上下文
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
就是调用的Spring的bean管理主要方法refresh,具体看我的文章:
Spring源码之AbstractApplicationContext解析(refresh)
13、关键代码:callRunners
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
//获取所有ApplicationRunner接口的实现类,并加入列表
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//获取所有ApplicationRunner接口的实现类,并加入列表
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//根据实现类上面的Order值进行排序,两种类型,如果order值相同,ApplicationRunner优先执行
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
//运行实现了ApplicationRunner的实现类的run方法
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
//运行实现了ApplicationRunner的实现类的run方法
callRunner((CommandLineRunner) runner, args);
}
}
}
这是一个项目运行成功后的回调通知方法,启动成功后,会调用所有实现了ApplicationRunner接口和CommandLineRunner接口的run方法。很巧,我目前主要负责的项目就有同事使用到了ApplicationRunner,每当项目启动完成,都会异步检查数据库中的省市区表中是否已有数据,没有就自动从高德地图同步并入库,有的话就不做操作。