spring boot启动分析

spring boot确实对java开发者来说是一个很好的选择,自己也非常愿意使用spring boot开发(想想spring的配置文件就有点头疼),今天准备好好的学习一下spring boot启动到底是怎样一个流程!

一、SpringApplication初始化

spring boot启动是从SpringApplication.run();方法开始的,这是SpringApplication的静态方法,那么先进入SpringApplication.class来看一下这个方法:

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的对象,然后再调用run方法,先去看下SpringApplication类的构造方法:

public SpringApplication(Class... primarySources) {
    this((ResourceLoader)null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
},

可以看到SpringApplication的构造入参有两种类型:一个是ResourceLoader,一个是Class对象,其中Class可以是多个。
在SpringApplication的构造方法内主要完成了以下几个事情:

1、确定Web应用的类型。

我们看到WebApplicationType.deduceFromClasspath()方法是根据判断当前的类的类型来判断web应用的类型的:

 static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
        // webFlux
        return REACTIVE;
    } else {
        String[] var0 = SERVLET_INDICATOR_CLASSES;
        int var1 = var0.length;

        for(int var2 = 0; var2 < var1; ++var2) {
            String className = var0[var2];
            if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
                return NONE;
            }
        }
        //servlet
        return SERVLET;
    }
}

"org.springframework.web.reactive.DispatcherHandler"应该是WebFlux模块类似与MVC前端控制器这样的一个角色,关于WebFlux知识点因为自己也不了解这里就不介绍了。

2、initializers的初始化

通过set方法完成SpringApplication成员变量initializers,即一些初始化的相关操作。它是一个List<ApplicationContextInitializer>,这些初始化的initializer实例创建是通过反射创建的。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return this.getSpringFactoriesInstances(type, new Class[0]);
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}    
private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) {
    List<T> instances = new ArrayList(names.size());
    Iterator var7 = names.iterator();
    while(var7.hasNext()) {
        String name = (String)var7.next();
        try {
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            T instance = BeanUtils.instantiateClass(constructor, args);
            instances.add(instance);
        } catch (Throwable var12) {
            throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, var12);
        }
    }
    return instances;
}

在getSpringFactoriesInstances方法里面根据当前线程获取到其类的加载器,然后调用下面的方法

SpringFactoriesLoader.loadFactoryNames(type, classLoader)

这个方法会从META-INF/spring.factories配置文件中根"type"(就是ApplicationContextInitializer的全路径)作为key获取所有的值,通过DEBUG模式我们可以看到返回的names,见下图:


2018-11-17 20-48-55 的屏幕截图.png

关于各个类的作用以后再学习,今天主要是将项目启动的顺序理顺。
而在createSpringFactoriesInstances()方法中,通过反射的方式创建出各个ApplicationContextInitializer所有实现类的实例,然后添加到List中返回。setInitializers()方法最终完成SpringApplication类中initializers变量的注入。

public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList();
    this.initializers.addAll(initializers);
}

3、成员变量listeners的初始化

通过set方法完成SpringApplication成员变量listeners,即一些监听器的初始化操作。
这个和上面的方法基本相同,唯一不同的是监听器接口类型是ApplicationListener,下面是通过DEBUG模式获取到ApplicationListener所有实现类名称,即names变量的值:


2018-11-17 21-09-31 的屏幕截图.png

这里完成了一系列listener的初始化。

4、获取当前应用的启动类的类对象,顺便看下方法吧,比较简单:

private Class<?> deduceMainApplicationClass() {
    try {
        StackTraceElement[] stackTrace = (new RuntimeException()).getStackTrace();
        StackTraceElement[] var2 = stackTrace;
        int var3 = stackTrace.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            StackTraceElement stackTraceElement = var2[var4];
            if ("main".equals(stackTraceElement.getMethodName())) {
                return Class.forName(stackTraceElement.getClassName());
            }
        }
    } catch (ClassNotFoundException var6) {
        ;
    }

    return null;
}

先获取到所有的栈元素和正在执行的方法,如果是"main"方法则返回这个类的类对象(大概就是这样吧),这里倒没什么特别的,不过还是上个截图吧:


2018-11-17 21-41-12 的屏幕截图.png

但是这里面我有点不懂,就是SpringApplication.<init>这个方法,这个<init>方法代表什么意思?而且根据行数也看不出什么,有空要好好找下资料或者网上问问,但是根据方法名称的意思就是初始化,在考虑执行顺序在deduceMainApplicationClass()方法前,两个run()方法后。是不是代表着我正在执行的SpringApplication的两个构造方法??第二个构造方法还没有完成,自然第一个构造方法也还在执行中,所以就有两个<init>方法。有点迷惑,如果有了解的希望能够指点一下。

//第一个构造方法
public SpringApplication(Class... primarySources) {
    this((ResourceLoader)null, primarySources);
}
// 第二个构造方法
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    this.sources = new LinkedHashSet();
    this.bannerMode = Mode.CONSOLE;
    this.logStartupInfo = true;
    this.addCommandLineProperties = true;
    this.headless = true;
    this.registerShutdownHook = true;
    this.additionalProfiles = new HashSet();
    this.isCustomEnvironment = false;
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    // 下面赋值操作未完成
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

上面的代码执行完成后创建出来一个SpringApplication类的实例。其实上面这些代码做的内容主要就2点:一是完成initializers的初始化,二是一些监听器的初始化,不过要深入下去的话内容还是很多的,今天就不继续追踪了。

二、SpringApplication实例run方法执行

这个方法应该可以代表整个应用的启动过程了,方法里面内容很多,只说一些我认为比较重要的。

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
    this.configureHeadlessProperty();
    SpringApplicationRunListeners listeners = this.getRunListeners(args);
    listeners.starting();

    Collection exceptionReporters;
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
        this.configureIgnoreBeanInfo(environment);
        Banner printedBanner = this.printBanner(environment);
        context = this.createApplicationContext();
        exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
        this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        this.refreshContext(context);
        this.afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
        }

        listeners.started(context);
        this.callRunners(context, applicationArguments);
    } catch (Throwable var10) {
        this.handleRunFailure(context, var10, exceptionReporters, listeners);
        throw new IllegalStateException(var10);
    }

    try {
        listeners.running(context);
        return context;
    } catch (Throwable var9) {
        this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
        throw new IllegalStateException(var9);
    }
}

上面方法执行完返回的是一个ConfigurableApplicationContext对象,也就是一个可配置应用的上下文对象,方法返回后我们的程序也就启动完成了。

1、应用启动监听器执行监听

我们先看这行代码:

SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting();

SpringApplicationRunListeners包含了一个SpringApplicationRunListener实现类的List集合。getRunListeners()方法中,会先通过new的方法创建SpringApplicationRunListeners实例,它需要两个参数,一个是Log,一个是SpringApplicationRunListener实现类对象的List(变量名也是listeners,注意和上面的listeners区分)。而SpringApplicationRunListener实现类的创建和上面的initializers和listeners初始化过程几乎完全相同,通过反射创建出其实现类的实例,但是通过DEBUG发现SpringApplicationRunListener只有一个实现类就是EventPubulishingRunListener,它实现了SpringApplicationRunListener和Ordered两个接口。因此执行listeners.starting()方法实际上调用的是EventPubulishingRunListener实例的starting()方法。另外在EventPubulishingRunListener实例化的过程中会注入前面的SpringApplication实例,和SpringApplication的成员变量listeners,其中listeners会被注入到EventPubulishingRunListener的成员变量initialMulticaster中。EventPubulishingRunListener的starting()方法:

public void starting() {
    this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

根据上面代码可见创建了一个ApplicationStartingEvent事件,然后由initialMulticaster将这个事件广播给所有监听ApplicationStartingEvent事件类型的监听器,最后监听器执行监听。代码量感觉量有点多,这里就不再继续跟踪了。

2、准备环境监听器执行(配置文件加载)

接下来看这行代码:

ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared((ConfigurableEnvironment)environment);
    this.bindToSpringApplication((ConfigurableEnvironment)environment);
    if (!this.isCustomEnvironment) {
        environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
    }

    ConfigurationPropertySources.attach((Environment)environment);
    return (ConfigurableEnvironment)environment;
}

根据名称我们应该就能猜测到这个应该是准备应用启动环境的。注意里面的listeners,可以直接认为就是一个EventPubulishingRunListener实例。prepareEnvironment()中会先根据应用的类型创建一个ConfigurableEnvironment实例,因为我们的应用类型是"SERVLET",因此创建出的实例实际上是StandardServletEnvironment。
然后执行configureEnvironment()方法,感觉这个是和配置相关的方法,看下面代码:

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    this.configurePropertySources(environment, args);
    this.configureProfiles(environment, args);
}

根据名称应该是和应用配置的一些操作,第一个方法configurePropertySources()主要是查看有没有指定新的配置源(通过命令行指定),因为我们并没有通过commandLine指定,这里就不往下继续看代码了。
第二个方法是configureProfiles(),看名字应该是和配置文件相关的,那么我们的application.properties是不是应该在这里面加载的呢??我们接着往下看configureProfiles()方法:

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
    environment.getActiveProfiles();
    Set<String> profiles = new LinkedHashSet(this.additionalProfiles);
    profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

这里通过AbstractEnvironment的方法获取所有的"ActiveProfiles",然后添加的应用环境的"activeProfiles"变量中去。而AbstractEnvironment获取"ActiveProfiles"就是获取配置文件的"spring.profiles.active"属性来获取相应的文件名称的,可是遗憾的是这里并没有能获取到任何的"ActiveProfiles",我把这个段代码跟踪完并没用发现任何的配置文件。
那只能继续往下执行下面的方法:

listeners.environmentPrepared((ConfigurableEnvironment)environment);

前文说到了,这个"listeners"其实就是EventPubulishingRunListener,是一个发布事件的监听器。这里它会创建一个ApplicationEnvironmentPreparedEvent实例,然后是通过SimpleApplicationEventMulticaster这一事件广播器拿到对应的监听器,并执行。这里最终拿到的监听器是ConfigFileApplicationListener,它会根据配置文扩展名以及默认配置文件名"application",在"classpath:/,classpath:/config/,file:./,file:./config/"下面寻找配置文件,这里面的方法感觉比较复杂,很多次的遍历(当时看的很晕),有兴趣可以去仔细看一下这部分代码。
到这里就完成我们应用配置文件的加载。
下面是在ConfigFileApplicationListener中DEBUG时的几个截图:


图-001.png

图-001显示的是配置文件扩展名的一个set集合,其实我一直以为spring boot配置文件只支持properties和yml,看来是错的,实际上应该是支持图中这4种的,properties和yml都是使用过的,就不说了。xml是传统的spring开发的配置文件,这个支持也是情理之中。但是yaml语言文件的后缀名是yml,所以yaml出现在这里是什么情况??(难道我理解有误?)


图-002.png

图-002就是在“classpath:/”下寻找默认名称为application,扩展名为properties的文件,根据图中的变量名称和值还是很好理解的。
图-003.png

图-003是找到"ActiveProfiles"后加载配置文件的截图,之所以名称是"test",是我在application.properties配置了spring.profiles.active=test。
图-004.png

图-004是所有的配置源列表,我自己创建的application.properties和application-test.properties都在,其他的应该是一些默认配置源。

3、创建应用上下文
context = this.createApplicationContext();

这个方法会根据我们应用的类型,完成应用ConfigurableApplicationContext的创建,上面也有提到,我们的应用是一个"servlet",这个方法里也是根据反射获取到对应"context"的类对象,然后进行实例化。最终创建出的是AnnotationConfigServletWebServerApplicationContext实例,根据名称就可以知道这是一个注解可配置的web应用的上下文对象。

4、创建各种bean对象
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);

这三个方法放到一起说,根据方法名称可以看出这里也是做一些"context"容器的一些准备工作,然后刷新容器。自己跟踪了一下代码,但是感觉自己能力有限,有些内容并不太清楚,所以值简单的介绍一下。
首先会将在"SpringApplication"中的initializers的几个对象作为注入到"context"容器中,这个过程中也会向"context"容器中注入beanFactoryProcessor,此外还会广播ApplicationPreparedEvent事件给监听器,启动相关的监听器,此外我们自定义的bean,比如controller、service等组件也会在refreshContext()方法执行后注入到"context"容器中,我觉得这几个方法应该是spring boot的核心,但是自己能力有限,跟踪代码把自己都跟丢了....有些方法又是云里雾里,看来还是自己对spring不熟悉的原因,等自己有时间再一点点的看吧。

5、应用启动完成
listeners.started(context);

这个方法最终执行的也是EventPublishingRunListener的started(),即有"context"容器发布应用启动的事件,即:ApplicationStartedEvent,这里整个应用就已经启动成功了。

6、runner执行
this.callRunners(context, applicationArguments);

之所以说这个方法,是因为上次做spring boot项目的时候用到过,我觉得有必要说一下。spring boot提供了两个接口CommandLineRunner和ApplicationRunner,如果你想在项目启动后做一些操作的话,实现这两个接口重写run方法就可以了,我感觉也是非常的方便,比如添加一些数据到缓存中。这两个接口的实现类也会做为组件在refreshContext方法之后注入到"context"容器中。
以上就是spring boot的启动,自己也是第一次专门的看框架源码,不知从何处着手,所以就只能大概的分析下启动的过程,而且有很多地方自己看的还不够深入,还有各个监听器的作用也需要自己去了解,等自己有时间再来学习一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351