源码分析SpringBoot启动
遇到一个问题,需要从yml文件中读取数据初始化到static的类中。搜索需要实现ApplicationRunner,并在其实现类中把值读出来再set进去。于是乎就想探究一下SpringBoot启动中都干了什么。
引子
就像引用中说的,用到了ApplicationRunner类给静态class赋yml中的值。代码先量一下,是这样:
@Data
@Component
@EnableConfigurationProperties(MyApplicationRunner.class)
@ConfigurationProperties(prefix = "flow")
public class MyApplicationRunner implements ApplicationRunner {
private String name;
private int age;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner...start...");
MyProperties.setAge(age);
MyProperties.setName(name);
System.out.println("ApplicationRunner...end...");
}
}
public class MyProperties {
private static String name;
private static int age;
public static String getName() {
return name;
}
public static void setName(String name) {
MyProperties.name = name;
}
public static int getAge() {
return age;
}
public static void setAge(int age) {
MyProperties.age = age;
}
}
从SpringApplication开始
@SpringBootApplication
public class FlowApplication {
public static void main(String[] args) {
SpringApplication.run(FlowApplication.class, args);
}
}
这是一个SpringBoot启动入口,整个项目环境搭建和启动都是从这里开始的。我们就从SpringApplication.run()点进去看一下,Spring Boot启动的时候都做了什么。点进去run看一下。
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(primarySources).run(args),这句话做了两件事,首先初始化SpringApplication,然后进行开启run。首先看一下初始化做了什么。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
首先读取资源文件Resource,然后读取FlowApplication这个类信息(就是primarySources),然后从classPath中确定是什么类型的项目,看一眼WebApplicationType这里面有三种类型:
public enum WebApplicationType {
NONE, //不是web项目
SERVLET,//是web项目
REACTIVE;//2.0之后新加的,响应式项目
...
}
回到SpringApplication接着看,确定好项目类型之后,初始化一些信息setInitializers(),getSpringFactoriesInstances()看一下都进行了什么初始化:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
首先得到ClassLoader,这个里面记录了所有项目package的信息、所有calss的信息啊什么什么的,然后初始化各种instances,在排个序,ruturn之。
再回到SpringApplication,接着是设置监听器setListeners()。
然后设置main方法,mainApplicationClass(),点进deduceMainApplicationClass()看一看:
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
从方法栈stackTrace中,不断读取方法,通过名称,当读到“main”方法的时候,获得这个类实例,return出去。
到这里,所有初始化工作结束了,也找到了Main方法,ruturn给run()方法,进行后续项目的项目启动。
准备好,开始run吧
先上代码:
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);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
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;
}
首先开启一个计时器,记录下这次启动时间,咱们项目开启 XXXms statred 就是这么计算来的。
然后是一堆声明,知道listeners.starting(),这个starting(),我看了一下源码注释
Called immediately when the run method has first started. Can be used for very
early initialization.
早早初始化,是为了后面使用,看到后面还有一个方法listeners.started()
Called immediately before the run method finishes, when the application context has been refreshed and all {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner ApplicationRunners} have been called.
这会儿应该才是真正的开启完毕,值得一提的是,这里终于看到了引子中的ApplicationRunner这个类了,莫名的有点小激动呢。
我们继续进入try,接下来是读取一些参数applicationArguments,然后进行listener和environment的一些绑定。然后打印出Banner图,printBanner(),这个方法里面可以看到把environment,也存入Banner里面了,应该是为了方便打印,如果有日志模式,也打印到日志里面,所以,项目启动的打印日志里面记录了很多东西。
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);
}
接着 生成上下文环境 context = createApplicationContext();还记着webApplicationType三种类型吗,这边是根据webApplicationType类型生成不同的上下文环境类的。
接着开启 exceptionReporters,用来支持启动时的报错。
接着就要准备往上下文中set各种东西了,看prepareContext()方法:
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
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);
}
// 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);
}
首先把环境environment放进去,然后把resource信息也放进去,再让所有的listeners知道上下文环境。 这个时候,上下文已经读取yml文件了 所以这会儿引子中yml创建的参数,上下文读到了配置信息,又有点小激动了!接着看,向beanFactory注册单例bean:一个参数bean,一个Bannerbean。
prepareContext() 这个方法大概先这样,然后回到run方法中,看看项目启动还干了什么。
refreshContext(context) 接着要刷新上下问环境了,这个比较重要,也比较复杂,今天只看个大概,有机会另外写一篇博客,说说里面的东西,这里面主要是有个refresh()方法。看注释可知,这里面进行了Bean工厂的创建,激活各种BeanFactory处理器,注册BeanPostProcessor,初始化上下文环境,国际化处理,初始化上下文事件广播器,将所有bean的监听器注册到广播器(这样就可以做到Spring解耦后Bean的通讯了吧)
总之,Bean的初始化我们已经做好了,他们直接也可以很好的通讯。
接着回到run方法,
afterRefresh(context, applicationArguments); 这方法里面没有任何东西,网上查了一下,说这里是个拓展点,有机会研究下。
接着stopWatch.stop();启动就算完成了,因为这边启动时间结束了。
我正要失落的发现没找到我们引子中说到的ApplicationRunner这个类,就在下面看到了最后一个方法,必须贴出来源码:
callRunners(context, applicationArguments),当然这个方法前面还有listeners.started().
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
看到没,这会就执行了ApplicationRunner 方法(至于CommandLineRunner,和ApplicationRunner类似,只是参数类型不同,这边不做过多区分先)。所以可以说ApplicationRunner不是启动的一部分,不记录进入SpringBoot启动时间内,这也好理解啊,你自己初始化数据的时间凭什么算到我SpringBoot身上,你要初始化的时候做了个费时操作,回头又说我SpringBoot辣鸡,那我不是亏得慌...
最后run下这个,listeners.running(context);这会儿用户自定义的事情也会被调用了。
ok,结束了。
小结
今天只是大概看了下SpringBoot启动过程。有很多细节,比如refresh()都值得再仔细研究一下。SpringBoot之所以好用,就是帮助我们做了很多配置,省去很多细节(不得不说各种stater真实让我们傻瓜式使用了很多东西),但是同样定位bug或者通过项目声明周期搞点事情的时候会无从下手。所以,看看SpringBoot源码还是听有必要的。
源码分析SpringBoot启动的更多相关文章
- Appium Server 源码分析之启动运行Express http服务器
通过上一个系列Appium Android Bootstrap源码分析我们了解到了appium在安卓目标机器上是如何通过bootstrap这个服务来接收appium从pc端发送过来的命令,并最终使用u ...
- Appium Android Bootstrap源码分析之启动运行
通过前面的两篇文章<Appium Android Bootstrap源码分析之控件AndroidElement>和<Appium Android Bootstrap源码分析之命令解析 ...
- Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)
http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...
- Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7)【转】
原文地址:Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://bl ...
- Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7)【转】
原文地址:Linux内核源码分析--内核启动之(4)Image内核启动(setup_arch函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.c ...
- u-boot 源码分析(1) 启动过程分析
u-boot 源码分析(1) 启动过程分析 文章目录 u-boot 源码分析(1) 启动过程分析 前言 配置 源码结构 api arch board common cmd drivers fs Kbu ...
- v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码
本篇关键词:内核重定位.MMU.SVC栈.热启动.内核映射表 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 鸿蒙内核源码分析(汇编基础) | ...
- 安卓MonkeyRunner源码分析之启动
在工作中因为要追求完成目标的效率,所以更多是强调实战,注重招式,关注怎么去用各种框架来实现目的.但是如果一味只是注重招式,缺少对原理这个内功的了解,相信自己很难对各种框架有更深入的理解. 从几个月前开 ...
- Django源码分析之启动wsgi发生的事
前言 好多人对技术的理解都停留在懂得使用即可,因而只会用而不会灵活用,俗话说好奇害死猫,不然我也不会在凌晨1.48的时候决定写这篇博客,好吧不啰嗦了 继续上一篇文章,后我有个问题(上文:&qu ...
随机推荐
- Python调用OpenCV读显写
OpenCV提供了python的接口,而且很重要的一点是python下的很多接口名与C++的接口名是一样的,这一篇先记录python调用OpenCV去读取图像.显示图像和保存图像. 1.OpenCV读 ...
- sqli-labs注入lesson3-4闯关秘籍
·lesson 3 与第一二关不同的是,这一关是基于错误的get单引号变形字符型注入 要使用 ') 进行闭合 (ps:博主自己理解为字符型注入,是不过是需要加括号进行闭合,适用于博主自己的方便记忆的 ...
- JavaScript 之 "for"的衍生对象
JavaScript for/in 语句 作用:for/in 语句用于遍历循环对象属性. 循环中的代码每执行一次,就会对数组的元素或者对象的属性进行一次操作. 例子: 循环对象属性: var pers ...
- Digital Twin——IoT的下一个浪潮
选自Medium 作者:Tomasz Kielar 京东云开发者社区编译 当前的商业格局受到了众多新技术的影响,有时确实让人很难能跟上技术迭新的速度.虽然很多人都熟悉工业4.0和物联网(IoT)这两个 ...
- SQL 2005 带自增列 带外键约束 数据导入导出
1,生成建表脚本 选中要导的表,点右键-编写表脚本为-create到 ,生成建表脚本 2,建表(在新库),但不建外键关系 不要选中生成外键的那部分代码,只选择建表的代码 3,导数据,用SQL STU ...
- Tomcat server.xml常用配置 含有外带文件及默认host
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE server-xml [<!ENTITY ...
- java 环境变量配置搭建(1)
基础常识,classPath配置,朱姐,跨平台性,world组成部分
- goweb-动作
go-模板引擎 动作 Go 模板的动作就是一些嵌入到模板里面的命令,这些命令在模板中需要放到两个 大括号里{{ 动作 }},之前我们已经用过一个很重要的动作:点(.),它代表了传递给模 板的数据.下面 ...
- [HAOI2018]苹果树(组合数学)
首先有个很奇妙而且很有用的性质:每个二叉树对应唯一的中序遍历,然后每个二叉树出现概率相同.所以n个节点的二叉树形态是n!种(题目中说了*n!已经是提示了),对每种方案求和即可得到期望.令f[i]表示i ...
- JavaScript学习笔记 - 进阶篇(7)- 浏览器对象
window对象 window对象是BOM的核心,window对象指当前的浏览器窗口. window对象方法: 注意:在JavaScript基础篇中,已讲解了部分属性,window对象重点讲解计时器. ...