SpringBoot成长记2:从HelloWorld开始分析SpringBoot

SpringBoot成长记2:从HelloWorld开始分析SpringBoot_第1张图片

上一节我们提到过,认识一个新技术的时候,通常是从一个入门的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成长记2:从HelloWorld开始分析SpringBoot_第2张图片

整个过程中第一个最关键的就是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。

上面是我第一次看这个类的一个脉络后,脑中得到的结果。

SpringBoot成长记2:从HelloWorld开始分析SpringBoot_第3张图片

你第一次看这里,肯定什么都不清楚,不知道每个变量有什么用,是干嘛的,没关系的,第一次,你只要熟悉它的脉络就可以。知道这里设置了两个集合变量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这两个接口的实例对象列表。

细节中可以学到知识,脉络中一样可以学到知识,这个思想你一定要慢慢有。抓大放小的意思,更多的是让你知道重点和关键点,而不是让你丢弃细节,这两者并不冲突,这个一定要注意。

最后这里细节分析,画一个简单组件图小结下:

SpringBoot成长记2:从HelloWorld开始分析SpringBoot_第4张图片

SpringApplication Run方法的脉络分析

熟悉了SpringApplication 的创建,接着我们该分析它的run方法了。

其实之前一节,我们介绍过SpringApplication 的启动流程。就是高度概括了run方法的核心脉络,run方法的核心其实核心就是下图蓝色的部分:

SpringBoot成长记2:从HelloWorld开始分析SpringBoot_第5张图片

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 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容器启动。)

SpringBoot成长记2:从HelloWorld开始分析SpringBoot_第6张图片

这时候你一定要学会抓大放小的思想,之后带着这3个关键步骤,去理解SpringBoot,其他的实现可以单独来研究分析它的设计思路,比如各个扩展点的设计是如何考虑的,我们可以参考借鉴哪一些。这才是学习SpringBoot最最该学习的。

概括下就是,当一个技术看着比较复杂时,你应该顺着核心脉络理解原理,学习各个细节的亮点设计思想。不要陷入某一个细节,多思考才最重要。大家一定要记住这一点,在后续的成长记中,我会逐步带大家体验这一点的。

小结

好了,简单小结下。

主要思想学习了:

1)先脉络后细节的思想,抓大放小的思想,排除不重要的,分析最主要的。

2)细节中可以学到知识,脉络中一样可以学到知识,这个思想你一定要慢慢有。抓大放小的意思,更多的是让你知道重点和关键点,而不是让你丢弃细节,这两者并不冲突,这个一定要注意。

3)多思考才最重要。顺着核心脉络理解原理,学习各个细节的亮点设计思想,千万不能陷入知识本身。

主要知识学习了:

今天我们主要看了下SpringApplication的创建,它的核心组件有哪些,创建后执行的run方法,到底做了些什么,脉络是怎么样的。

熟悉了这些脉络,剩下的就简单了,逐步分析每个细节,看看每个细节有些值得我们学习的点,又有哪一些不太适合的点。

我们下期再见!

本文由博客一文多发平台 OpenWrite 发布!

你可能感兴趣的