SpringBoot成长记2:从HelloWorld开始分析SpringBoot
上一节我们提到过,认识一个新技术的时候,通常是从一个入门的HelloWorld开始,之后阅读它的一些入门文档和书籍、视频,从而掌握它的基本使用。
这一节我就来带大家从HelloWorld开始,先摸清楚SpringBoot的核心脉络,之后再来逐步分析透彻SpringBoot,从而精通它。
从搭建HelloWorld入口开始分析SpringBoot
首先我们从官方的文档中搭建出一个2.2.2 版本的SpringBoot,增加了两个starter,mybatis-plus-boot-starter、spring-boot-starter-web,使用Maven进行项目和依赖管理,配置一个本地的mysql。相信这个对你们来说,都比较简单,我就不一一进行赘述了。
经过上面的基本搭建,你就会有类似一个下面的一个SpringBoot HelloWorld级别 的入口。
package org.mfm.learn.springboot;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("org.mfm.learn.springboot.mapper")
@SpringBootApplication
public class LearnSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(LearnSpringBootApplication.class, args);
}
}
通过上一节你知道SpringBoot定义了一个SpringApplication的web应用启动流程,入口通过一个java -jar的命令,执行main函数启动一个JVM进程,运行内部的tomcat监听一个默认8080的端口,提供web服务。
整个过程中第一个最关键的就是SpringBoot定义的SpringApplication,我们一起先来看下它是怎么创建new的。
SpringApplication的创建时核心组件图
SpringApplication的创建时的代码分析
在上面的示例代码中,main方法执行了 SpringApplication的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);
}
run方法核心入参就是main函数所在类+main函数args的参数,之后就直接创建了一个SpringApplication对象。让我们一起来看看这个SpringBoot定义的概念怎么创建的,创建时的核心组件又有哪些呢?
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
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();
}
SpringApplication的创建时核心脉络
SpringApplication的创建核心脉络比较简单:
1)ResourceLoader 指明资源加载器,这个暂时不清楚是啥东西,默认是个null。
2)webApplicationType 推断当前web应用类型,通过一个deduceFromClasspath方法推断出的。
3)之后设置了setInitializers、setListeners两个列表,分别是一堆Initializer和Listener,都是通过getSpringFactoriesInstances方法获取的。
4)通过primarySources、mainApplicationClass记录了启动主要资源类,也就是之前HelloWorld中的LearnSpringBootApplication.class。
上面是我第一次看这个类的一个脉络后,脑中得到的结果。
你第一次看这里,肯定什么都不清楚,不知道每个变量有什么用,是干嘛的,没关系的,第一次,你只要熟悉它的脉络就可以。知道这里设置了两个集合变量Initializer和Listener,可以设置esourceLoader ,标记了一些类型和类,这就够了。
之后你有时间,再挨个去了解每个变量或者组件的作用就可以了,这个不还是先脉络后细节的思想,是吧?
SpringApplication的创建时的细节分析
你可以慢慢拆解上面的每一步,单独看看每一个组件大体是作什么的,这个就是细节的研究,可以一步一步来。
你可以研究下ResourceLoader 是个啥? 你可以看它的类注解后可以发现,这个ResourceLoader 类负责使用ClassLoader加载ClassPath下的class和各种配置文件的。(如果你不知道JVM的ClassLoader机制,主要加载什么,可以自己去baidu、google了解下)。这里你可以进一步思考下,它设计成了一个接口,可以实现不同的类加载器来加载资源。
webApplicationType 如何被推断的?就是根据几个静态变量定义的类全限定名称,根据classPath下是否存在对应的类,来推断出类型,使用了web-starter。默认推断出为Servlet类型的应用。
至于primarySources、mainApplicationClass这个两个变量记录了LearnSpringBootApplication.class, 大体是为了之后扫描自动配置等考虑的,表示从什么包名的哪一个类下启动的。
最后两个集合变量Initializer和Listener如何设置的,这块比较值得研究下。
基本原理是通过ClassLoader扫描了classPath下所有META-INF/spring.factories这个目录中的文件,通过指定的factoryType,也就是接口名称,获取对应的所有实现类,并且实例化成对象,返回成一个list列表。
比如factoryType=ApplicationContextInitializer 就返回这个接口在META-INF/spring.factories定义的所有的实现类,并实例化为一个列表List ApplicationContextInitializer 。
ApplicationListener同理,获取到了List ApplicationListener一个集合。
这里面其实有很多细节,使用了类加载器、缓存机制,反射机制等,有兴趣的同学可以仔细研究下。
这里以我们抓大放小思想,概括成一句话:通过工具方法通过classLoader获取classPath指定位置某个接口所有实现类的实例对象列表。
这里获取的是ApplicationContextInitializer、ApplicationListener这两个接口的实例对象列表。
细节中可以学到知识,脉络中一样可以学到知识,这个思想你一定要慢慢有。抓大放小的意思,更多的是让你知道重点和关键点,而不是让你丢弃细节,这两者并不冲突,这个一定要注意。
最后这里细节分析,画一个简单组件图小结下:
SpringApplication Run方法的脉络分析
熟悉了SpringApplication 的创建,接着我们该分析它的run方法了。
其实之前一节,我们介绍过SpringApplication 的启动流程。就是高度概括了run方法的核心脉络,run方法的核心其实核心就是下图蓝色的部分:
run方法脉络可以主要概括为:
1)自动装配配置
2)Spring容器的创建
3)web容器启动(Tomcat的启动)
然而在run方法的执行过程,肯定不会这么简单,过程中还掺杂了很多杂七杂八的逻辑,其中有意思的扩展点,也有值得吐槽的坑。这是每个框架都会有的优势劣势吧。我们先大体摸一下run方法的脉络,给大家介绍几个术语,不然之后可能会看不懂代码细节。
SpringApplication Run方法的脉络进一步分析
要想进一步分析run方法的脉络,首先需要熟悉几个术语,就有点像DDD的通用语言似的,懂了这些语言,理解SpringBoot和Spring才会更得心应手。
术语普及Context/BeanFactory/Environment
ConfigurableApplicationContext,容器通常称为ApplicationContext或者BeanFactory,context也简称为容器。ApplicationContext包装了BeanFactory,封装更高级的API而已。
ConfigurableEnvironment ,是配置文件的抽象,有关什么properties或者yml等配置文件的key-value值,都会封装成这个类的某个实现类。
熟悉了这些术语后,我们看一起看下SpringApplication 的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;
}
上面的代码,主要就是执行了一堆方法,可以从方法名字看出,都是围绕Context、Environment这些术语。也就是围绕容器和配置文件组织的逻辑。
在整个Spring容器的创建,刷新,刷新之后穿插了很多逻辑。
另外,SpringBoot整个run方法中有几个很关键扩展点,设计SpringApplicationRunListeners、Runners等扩展入口。容器创建、刷新等也要各自的扩展点,对容器的增强扩展,如beanFactoryPostProcessor,对Bean的增加扩展,如beanPostProcessor。然而这些都是后话了。
我直接用一张图给大家概括了,上面run方法脉络:
(*黑色是直观的看出来的扩展逻辑,白色是run方法每个方法的字面理解,只是每一步有很多扩展点和做的事情比较多,让你感觉会有点云里雾里的。蓝色部分,概括了核心逻辑。也就是SpringBoot启动,说白了我们核心就是要找到这三点:自动装配配置、Spring容器的创建、web容器启动。)
这时候你一定要学会抓大放小的思想,之后带着这3个关键步骤,去理解SpringBoot,其他的实现可以单独来研究分析它的设计思路,比如各个扩展点的设计是如何考虑的,我们可以参考借鉴哪一些。这才是学习SpringBoot最最该学习的。
概括下就是,当一个技术看着比较复杂时,你应该顺着核心脉络理解原理,学习各个细节的亮点设计思想。不要陷入某一个细节,多思考才最重要。大家一定要记住这一点,在后续的成长记中,我会逐步带大家体验这一点的。
小结
好了,简单小结下。
主要思想学习了:
1)先脉络后细节的思想,抓大放小的思想,排除不重要的,分析最主要的。
2)细节中可以学到知识,脉络中一样可以学到知识,这个思想你一定要慢慢有。抓大放小的意思,更多的是让你知道重点和关键点,而不是让你丢弃细节,这两者并不冲突,这个一定要注意。
3)多思考才最重要。顺着核心脉络理解原理,学习各个细节的亮点设计思想,千万不能陷入知识本身。
主要知识学习了:
今天我们主要看了下SpringApplication的创建,它的核心组件有哪些,创建后执行的run方法,到底做了些什么,脉络是怎么样的。
熟悉了这些脉络,剩下的就简单了,逐步分析每个细节,看看每个细节有些值得我们学习的点,又有哪一些不太适合的点。
我们下期再见!
本文由博客群发一文多发等运营工具平台 OpenWrite 发布