源碼分析SpringBoot啓動

圖片描述

遇到一個問題,須要從yml文件中讀取數據初始化到static的類中。搜索須要實現ApplicationRunner,並在其實現類中把值讀出來再set進去。因而乎就想探究一下SpringBoot啓動中都幹了什麼。web

引子

就像引用中說的,用到了ApplicationRunner類給靜態class賦yml中的值。代碼先量一下,是這樣:spring

@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看一下。app

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。首先看一下初始化作了什麼。less

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這裏面有三種類型:ide

public enum WebApplicationType {
    NONE, //不是web項目
    SERVLET,//是web項目
    REACTIVE;//2.0以後新加的,響應式項目
    ...
}

回到SpringApplication接着看,肯定好項目類型以後,初始化一些信息setInitializers(),getSpringFactoriesInstances()看一下都進行了什麼初始化:post

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之。this

再回到SpringApplication,接着是設置監聽器setListeners()。spa

而後設置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出去。code

到這裏,全部初始化工做結束了,也找到了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源碼仍是聽有必要的。

相關文章
相關標籤/搜索