Springboot启动流程,跟着源码看看启动的时候都做了什么
看了很多的Springboot的启动流程分析,自己跟着源码看了一遍还是决定自己再写个流程;
- 基于Springboot2.1.8版本
- 流程步骤可能跟其他人的分析有出入,但是以理解为主,理解了你觉得是几步那就是几步
- 本文大致分三段:详细步骤总结、简化步骤总结、源码分步骤跟进分析
先写详细总结(下有简化版),Springboot启动主要分两小段,一段为准备参数配置,一段是实际初始化
-
new SpringApplication(primarySources)为第一阶段
- 验证class参数非null,若null报错退出
- 将class参数放入LinkedHashSet
类型的primarySources变量中 - 推断应用应以什么类型启动(NONE/SERVLET/REACTIVE其一)
- 根据pom配置的各Starter包下的"META-INF/spring.factories"配置初始化ApplicationContextInitializer.class类型的上下文信息(contex应用上下文)
- 根据pom配置的各Starter包下的"META-INF/spring.factories"配置初始化ApplicationListener.class类型的上下文信息 (listener监听器相关)
- 推断main函数所在的应用执行主类
-
new SpringApplication(primarySources).run(args)后的run(args)为第二阶段
- 启动一个StopWatch()计时器,后面用于打印启动的耗时信息
- new一个异常相关集合exceptionReporters,后面会初始化
- 配置headless模式,即java.awt.headless,意为程序将在无鼠键支持的环境中运行
- 获取监听器listeners并开启监听
- 包装args参数为ApplicationArguments类
- prepareEnvironment开始准备将要配置环境的参数等信息
- 配置spring.beaninfo.ignore,默认为true,跳过搜索BeanInfo类
- 读取Banner并打印(打印logo)
- 获取配置应用环境(NONE/SERVLET/REACTIVE其一)
- 根据pom配置的各Starter包下的"META-INF/spring.factories"配置初始化SpringBootExceptionReporter.class异常相关的信息(Exception相关类)
- prepareContext(context, environment, listeners, applicationArguments, printedBanner) 开始上下文、环境、监听器、参数、Banner之间的关联配置
- refreshContext(context)开始IOC容器可用的最后配置,其中的refresh()方法是核心
- afterRefresh(context, applicationArguments)是个空方法,以后版本可能会加点东西做些什么吧
- 打印启动耗时日志
- 开启容器的所有监听器
- 执行allRunners(),一般为空,用户可实现相关接口用以执行
- 开始监听应用执行
- 返回上下文的引用,结束
简化版的步骤
- 一阶段
- 推断应用类型(NONE/SERVLET/REACTIVE其一)
- 读取加载所有"META-INF/spring.factories"配置
- 推断main方法所在主类
- 二阶段
- 启动StopWatch()计时器
- 启动监听器
- 推断应用类型并配置环境
- 容器上下文、环境、监听器、参数等之间的关联配置
- IOC容器最终实例化配置,核心refresh()
- 打印日志,应用使用中,开启所有监听器
进入源码分析
一个例子,下面是最简单的一个Springboot启动例子,除了必须的注解@SpringBootApplication外多配置了@RestControlle和一个注解了@RequestMapping("/hello")的简单方法
正常默认访问http://localhost:8080/hello就能看到"hello spring",例子很简单就不贴pom什么的源码了。
package com.example.hellospring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class HelloSpringApplication {
public static void main(String[] args) {
// 测试run的第一个参数为null的情况
// Class<String> test = null;
ConfigurableApplicationContext context = SpringApplication.run(HelloSpringApplication.class, args);
// context.refresh();
// context.close();
}
@RequestMapping("/hello")
public String hello() {
return "hello spring";
}
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
这里的new SpringApplication(primarySources).run(args)中的new和run就对应上面的两小段
先看一阶段new SpringApplication(primarySources)部分
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 类加载器
this.resourceLoader = resourceLoader;
// 传入的class参数判断,若null则打印异常并退出初始化
Assert.notNull(primarySources, "PrimarySources must not be null");
// 获取main方法中的args,通常是启动时配置的额外参数
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 推断应用应以和钟类型启动(NONE/SERVLET/REACTIVE其一)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 容器上下文相关的初始化
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 监听器相关的初始化
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 从当前的栈信息中寻找main所在主类
this.mainApplicationClass = deduceMainApplicationClass();
}
Assert.notNull就是判断传入的class参数是否为空,也就是例子中的HelloSpringApplication.class,一般没问题
this.primarySources是将calss参数放入了set,方便后面使用,其他的暂不知
this.webApplicationType,这里是推断应用类型的地方,进入deduceFromClasspath()所在类WebApplicationType源码细看下:
package org.springframework.boot;
import org.springframework.util.ClassUtils;
public enum WebApplicationType {
/**
* The application should not run as a web application and should not start an
* embedded web server.
*/
NONE,
/**
* The application should run as a servlet-based web application and should start an
* embedded servlet web server.
*/
SERVLET,
/**
* The application should run as a reactive web application and should start an
* embedded reactive web server.
*/
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework." + "web.servlet.DispatcherServlet";
private static final String WEBFLUX_INDICATOR_CLASS = "org." + "springframework.web.reactive.DispatcherHandler";
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
//这里是推断应用类型的方法
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
static WebApplicationType deduceFromApplicationContext(Class<?> applicationContextClass) {
if (isAssignable(SERVLET_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
return WebApplicationType.SERVLET;
}
if (isAssignable(REACTIVE_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
return WebApplicationType.REACTIVE;
}
return WebApplicationType.NONE;
}
private static boolean isAssignable(String target, Class<?> type) {
try {
return ClassUtils.resolveClassName(target, null).isAssignableFrom(type);
}
catch (Throwable ex) {
return false;
}
}
}
可以看到WebApplicationType是个枚举类,只有三个类型NONE/SERVLET/REACTIVE,也就是最开始说的那三个,除了三个枚举值还有一些类路径相关的静态变量和三个方法,
这个类还是比较简单的,deduceFromClasspath也就是依次循环遍历当前应用中是否存在相关的类来判断最终应用的启动类型;
- 若要启动为REACTIVE,则必须有WEBFLUX_INDICATOR_CLASS也即springframework.web.reactive.DispatcherHandler,且不能有WEBMVC_INDICATOR_CLASS和JERSEY_INDICATOR_CLASS
也就是org.springframework.web.servlet.DispatcherServlet和org.glassfish.jersey.servlet.ServletContainer,指的就是SpringMVC/Tomcat和jersey容器 - 若要启动为Servlet,则必须有SERVLET_INDICATOR_CLASSES中的javax.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext,缺一不可
- 以上都不是则为NONE应用
什么是NONE应用呢?我现在理解的就是指一个最简单的Springboot应用
然后到了这两行:
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
继续跟可以看到这里:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
// 加载各jar包中的"META-INF/spring.factories"配置候选配置
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 根据上一步获得的信息初始化为type相关实例
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
上面getSpringFactoriesInstances方法,后续初始化时会多次调用,唯一不同的就是传给它的type参数不同,初始化涉及的Type主要有:
- ApplicationContextInitializer.class 上下文相关
- ApplicationListener.class 监听器相关
- SpringApplicationRunListener.class 运行时监听器
- SpringBootExceptionReporter.class 异常类相关
源码中的SpringFactoriesLoader.loadFactoryNames(type, classLoader),loadFactoryNames方法跟进去看一下:
这是SpringFactoriesLoader配置类的一个方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
其中FACTORIES_RESOURCE_LOCATION的值:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
所以,到这里我就明白了Springboot自动配置的秘密所在,Springboot在启动时读取了所有starter jar包里的META-INF/spring.factories配置文件,
实现了所谓的自动化配置,当然,这里jar包里的都是默认配置,后续Springboot也会从xml、yaml文件中的用户配置去覆盖同名的配置。
另外,这里的缓存配置是保存在一个map类型的cache中,其中的key键对应上面提到的各种Type类型,value就是Type的各种初始jar包里的同类型Java类。
到了this.mainApplicationClass = deduceMainApplicationClass();
这里就是推断main方法所在的主类,源码比较简单:
private Class<?> deduceMainApplicationClass() {
try {
// 获取当前的栈信息
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
// 获取main方法所在的类class
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
可以看出,是获取当前的运行栈,然后遍历获取main方法所在的类,其实主类当然就是例子类 HelloSpringApplication.java,这里保存的是HelloSpringApplication.class
至此,一阶段就完成了。
再看new SpringApplication(primarySources).run(args)二阶段的run(args)部分
相关源码:
public ConfigurableApplicationContext run(String... args) {
// 启动计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
// new异常相关类集合,下面会初始化
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 配置headless模式
configureHeadlessProperty();
// 初始化运行时监听器并预启动
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 包装参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 配置环境及变量
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 配置忽略spring.beaninfo.ignore
configureIgnoreBeanInfo(environment);
// 读取Banner配置并打印
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
// 初始化异常相关类的集合
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 关联容器上下文、监听器、环境、参数等信息
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 配置并实例化IOC容器
refreshContext(context);
// 空方法,无内容
afterRefresh(context, applicationArguments);
stopWatch.stop();
// 打印启动耗时信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
// 执行CommandLineRunner和ApplicationRunner相关实现类
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// Springboot应用已在执行,开启监听
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
// 返回容器引用
return context;
}
首先就StopWatch搞起一个计时器;
在configureHeadlessProperty()这里进去可以看到:
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
其中的SYSTEM_PROPERTY_JAVA_AWT_HEADLESS:
private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
headless直译就是无头模式,......其实无头模式的意思就是明确Springboot要在无鼠键支持的环境中运行,一般程序也都跑在Linux之类的服务器上,无鼠键支持,这里默认值是true;
接下来的两行的listeners是初始化并预启动运行时监听器;
然后是try块,每行都有注释,挑几个分析下:
Banner printedBanner = printBanner(environment);这里是打印Banner,Banner就是启动时看到的那个logo:
贴一个生成banner的网站:https://www.bootschool.net/ascii
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.8.RELEASE)
可以在SpringBootBanner.java中找到打印来源:
class SpringBootBanner implements Banner {
private static final String[] BANNER = { "", " . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\", "( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )", " ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/" };
private static final String SPRING_BOOT = " :: Spring Boot :: ";
private static final int STRAP_LINE_SIZE = 42;
可以改为自己想要的logo,在src/main/resources下放入名字是banner的文件,后缀后可以是SpringApplicationBannerPrinter.java类里
的{ "gif", "jpg", "png" }或者是txt,是的图片也可以的,但是打印时会字符化,而不是打印图片本身,下面有演示:
class SpringApplicationBannerPrinter {
static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";
static final String BANNER_IMAGE_LOCATION_PROPERTY = "spring.banner.image.location";
static final String DEFAULT_BANNER_LOCATION = "banner.txt";
static final String[] IMAGE_EXTENSION = { "gif", "jpg", "png" };
private static final Banner DEFAULT_BANNER = new SpringBootBanner();
private final ResourceLoader resourceLoader;
private final Banner fallbackBanner;
banner演示:
在src/main/resources放入Banner.jpg和banner.txt
banner.jpg:
banner.txt:
___ ___ ___ ___
/\__\ /\ \ /\ \ /\ \
/::| | \:\ \ /::\ \ \:\ \
/:/:| | \:\ \ /:/\:\ \ \:\ \
/:/|:| |__ ___ /::\ \ /:/ \:\ \ ___ \:\ \
/:/ |:| /\__/\ /:/\:\__/:/__/ \:\__/\ \ \:\__\
\/__|:|/:/ \:\/:/ \/__\:\ \ /:/ \:\ \ /:/ /
|:/:/ / \::/__/ \:\ /:/ / \:\ /:/ /
|::/ / \:\ \ \:\/:/ / \:\/:/ /
|:/ / \:\__\ \::/ / \::/ /
|/__/ \/__/ \/__/ \/__/
打印效果:
@@@@@@@@@@@@@@#####@@@@#88&#@@#o:*o&oo88##88####888o8#########88@@@@@88#8&#@
@@@@@@@@@@@@@@#####8o#@@@#&:***::oooo&&8o8#####88&:@88#88888&8o####88&8&o&##
@@@@@@@@@@@@@@#####@@@8.:@8#@@#**:*::oo&&&8888888&@@@@@#8888&&o8&&&8@@@@@
@@@@@@#&#@@@@@######@@@@@@@@@#@@@@@@@@@#ooo&&&&###@@888@@#@8&*:&8@@@@@@@@
@@#@@@@@@#@##########@@@@#@@@@@8#@#@##@#@@#o##*:@&&:#@@@@@@@@@@@#@@@@@@@@@@@
@@@@@@@@@@@############@@@@#@@@@8###########o@:@@#&o##8#@@@@&@@@@@@@@@@@@
@@@@@@@@@@@@#8#######o###@@@8##@8##########@8@@*o#&@*&@#:&&&@@@@@@@@@@@@@
@@@@@@@@@@@@#############@#@####8######o@::@@@@@@o#&oo@@@o&ooo#@@@@@@@@@@@@@
@@@@@@@@@##@@@@@@#####@@########8#####*@@@@@@@##@@&8@@@@@8&8:@@##@#@@@#@@@@@
@@@@@##:#########@@@############8###88&#@#&:888@@@@@@@@@&#o@@#@#o##@&888#@@@
@@@@@@@8####@@@@##################888&o#@8:88#@#8@@@@@@8&@####8*#o#&&o#&o#@@
@@@@@@@@@@@@@@@@@@#####88#########8:o:&@##8:o##8@#@@@@#&&88oo. o@@##o&8:88#@
@@@#o:@@@@@@@@@@#####888#######8*ooo:&8*@@#&#oo:#@##&@@@####8@#@@@@@.*::o&88
@##&###@@@@################8:&8@&@@@@#@#@@@@@@#@#888@#&@#@@8#@@@@@@&88&8&8#@
@################::88&&:##@@@@@@#@@@@@#@@@@@@@@@@#@###&@&88o@#8##@@&8#8##&@@
@@@#####@@#@@@###@@@#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#@88#@@@@@@@88@@@@@@
@@@@@@@@@@@@@#####8:oo&@o#@@@@@@@@@@#88@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
o&############@@#:..*:&@@@@@@@@@@@@@##@#@@@@@@@@@@@@#8#@@@#@#@@@@@@@@@@@@#@@
@@@@@@##8o#@@@@@@@@@@@@@@@@@@@@@@#@#@##@@@@@@@@@@@@@@#@#######8&@8@@@@@@@8@@
@@@@@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@#&@@@@@@@@@@@@@@@@#@@@@####88888#8@@@@@@@
@@@@@@@@@@@@@@@@###@#@@@@@@########@@@@@@@###@@@@@@#@##@@@@@@@@##8@88@@@#@@@
@@@@@@@@@@@@@@@@@@#&o&&&8###@@@@#@@@@@@@@8@@@@@@@@@@@@@@@@@@@@@@@@@#@@#@@@@@
___ ___ ___ ___
/\__\ /\ \ /\ \ /\ \
/::| | \:\ \ /::\ \ \:\ \
/:/:| | \:\ \ /:/\:\ \ \:\ \
/:/|:| |__ ___ /::\ \ /:/ \:\ \ ___ \:\ \
/:/ |:| /\__/\ /:/\:\__/:/__/ \:\__/\ \ \:\__\
\/__|:|/:/ \:\/:/ \/__\:\ \ /:/ \:\ \ /:/ /
|:/:/ / \::/__/ \:\ /:/ / \:\ /:/ /
|::/ / \:\ \ \:\/:/ / \:\/:/ /
|:/ / \:\__\ \::/ / \::/ /
|/__/ \/__/ \/__/ \/__/
2020-03-17 18:46:03.334 INFO 6356 --- [ main]
所以,这里如果对图片转换为logo感兴趣的可以看看ImageBanner.java类的源码。
继续后面的prepareContext和refreshContext是操作比较多的地方,
这里看一下refreshContext内部的refresh方法:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
//记录启动时间、状态,web容器初始化其property,复制listener
prepareRefresh();
//这里返回的是context的BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//beanFactory注入一些标准组件,例如ApplicationContextAwareProcessor,ClassLoader等
prepareBeanFactory(beanFactory);
try {
//给实现类留的一个钩子,例如注入BeanPostProcessors,这里是个空方法
postProcessBeanFactory(beanFactory);
// 调用切面方法
invokeBeanFactoryPostProcessors(beanFactory);
// 注册切面bean
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// bean工厂注册一个key为applicationEventMulticaster的广播器
initApplicationEventMulticaster();
// 给实现类留的一钩子,可以执行其他refresh的工作,这里是个空方法
onRefresh();
// 将listener注册到广播器中
registerListeners();
// 实例化未实例化的bean
finishBeanFactoryInitialization(beanFactory);
// 清理缓存,注入DefaultLifecycleProcessor,发布ContextRefreshedEvent
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
再后面有个打印日志的地方:
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
这里跟进去可以看到:
private StringBuilder getStartedMessage(StopWatch stopWatch) {
StringBuilder message = new StringBuilder();
message.append("Started ");
message.append(getApplicationName());
message.append(" in ");
message.append(stopWatch.getTotalTimeSeconds());
try {
// ManagementFactory是一个JVM相关类,可以获得各种JVM的配置信息,运行状况等
double uptime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0;
message.append(" seconds (JVM running for " + uptime + ")");
}
catch (Throwable ex) {
// No JVM time available
}
return message;
}
这就是最后启动完毕的那行日志来源:
2020-03-17 18:46:06.173 INFO 6356 --- [ main] c.e.hellospring.HelloSpringApplication : Started HelloSpringApplication in 4.128 seconds (JVM running for 4.577)
这里ManagementFactory类不常用但是信息量很多,这是一个与jvm相关的类,从中可以获得大量的与jvm相关的信息;
日志里耗时不一致是因为前者是StopWatch的计时,只记录了run(args)的耗时,后者是从启动应用jvm就启动并到打印这里时的耗时,getUptime()是获取jvm的已启动时间;
至此,二阶段就分析完了;
技术有限,若有哪些地方分析的有误或遗漏,还请指正。