源碼分析SpringBoot啟動

林允兒1.jpg

遇到一個問題,需要從yml文件中讀取數(shù)據(jù)初始化到static的類中功咒。搜索需要實現(xiàn)ApplicationRunner,并在其實現(xiàn)類中把值讀出來再set進(jìn)去。于是乎就想探究一下SpringBoot啟動中都干了什么比默。

引子

就像引用中說的,用到了ApplicationRunner類給靜態(tài)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啟動入口,整個項目環(huán)境搭建和啟動都是從這里開始的谐岁。我們就從SpringApplication.run()點進(jìn)去看一下侈百,Spring Boot啟動的時候都做了什么。點進(jìn)去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);
}

首先經(jīng)過了兩個方法钝域,馬上就要進(jìn)入關(guān)鍵了。SpringApplication(primarySources).run(args)锭魔,這句話做了兩件事例证,首先初始化SpringApplication,然后進(jìn)行開啟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之后新加的笙蒙,響應(yīng)式項目
    ...
}

回到SpringApplication接著看抵屿,確定好項目類型之后,初始化一些信息setInitializers()捅位,getSpringFactoriesInstances()看一下都進(jìn)行了什么初始化:

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衷笋,接著是設(shè)置監(jiān)聽器setListeners()。

然后設(shè)置main方法矩屁,mainApplicationClass()辟宗,點進(jìn)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中,不斷讀取方法吝秕,通過名稱泊脐,當(dāng)讀到“main”方法的時候,獲得這個類實例郭膛,return出去晨抡。

到這里,所有初始化工作結(jié)束了则剃,也找到了Main方法耘柱,ruturn給run()方法,進(jìn)行后續(xù)項目的項目啟動棍现。

準(zhǔn)備好调煎,開始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.

這會兒應(yīng)該才是真正的開啟完畢赤拒,值得一提的是,這里終于看到了引子中的ApplicationRunner這個類了,莫名的有點小激動呢挎挖。

我們繼續(xù)進(jìn)入try这敬,接下來是讀取一些參數(shù)applicationArguments,然后進(jìn)行l(wèi)istener和environment的一些綁定蕉朵。然后打印出Banner圖崔涂,printBanner(),這個方法里面可以看到把environment,也存入Banner里面了始衅,應(yīng)該是為了方便打印冷蚂,如果有日志模式,也打印到日志里面觅闽,所以帝雇,項目啟動的打印日志里面記錄了很多東西涮俄。

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);
}

接著 生成上下文環(huán)境 context = createApplicationContext();還記著webApplicationType三種類型嗎蛉拙,這邊是根據(jù)webApplicationType類型生成不同的上下文環(huán)境類的。

接著開啟 exceptionReporters彻亲,用來支持啟動時的報錯孕锄。

接著就要準(zhǔn)備往上下文中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);
}

首先把環(huán)境environment放進(jìn)去苞尝,然后把resource信息也放進(jìn)去畸肆,再讓所有的listeners知道上下文環(huán)境。 這個時候宙址,上下文已經(jīng)讀取yml文件了 所以這會兒引子中yml創(chuàng)建的參數(shù)轴脐,上下文讀到了配置信息,又有點小激動了抡砂!接著看大咱,向beanFactory注冊單例bean:一個參數(shù)bean,一個Bannerbean注益。

prepareContext() 這個方法大概先這樣碴巾,然后回到run方法中,看看項目啟動還干了什么丑搔。

refreshContext(context) 接著要刷新上下問環(huán)境了厦瓢,這個比較重要,也比較復(fù)雜啤月,今天只看個大概煮仇,有機(jī)會另外寫一篇博客,說說里面的東西谎仲,這里面主要是有個refresh()方法浙垫。看注釋可知强重,這里面進(jìn)行了Bean工廠的創(chuàng)建绞呈,激活各種BeanFactory處理器贸人,注冊BeanPostProcessor,初始化上下文環(huán)境佃声,國際化處理艺智,初始化上下文事件廣播器,將所有bean的監(jiān)聽器注冊到廣播器(這樣就可以做到Spring解耦后Bean的通訊了吧)

總之圾亏,Bean的初始化我們已經(jīng)做好了十拣,他們直接也可以很好的通訊。

接著回到run方法志鹃,
afterRefresh(context, applicationArguments); 這方法里面沒有任何東西夭问,網(wǎng)上查了一下,說這里是個拓展點曹铃,有機(jī)會研究下缰趋。

接著stopWatch.stop();啟動就算完成了,因為這邊啟動時間結(jié)束了陕见。
我正要失落的發(fā)現(xiàn)沒找到我們引子中說到的ApplicationRunner這個類,就在下面看到了最后一個方法秘血,必須貼出來源碼:
callRunners(context, applicationArguments),當(dāng)然這個方法前面還有l(wèi)isteners.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);
        }
    }
}

看到?jīng)]评甜,這會就執(zhí)行了ApplicationRunner 方法(至于CommandLineRunner灰粮,和ApplicationRunner類似,只是參數(shù)類型不同忍坷,這邊不做過多區(qū)分先)粘舟。所以可以說ApplicationRunner不是啟動的一部分,不記錄進(jìn)入SpringBoot啟動時間內(nèi)佩研,這也好理解啊柑肴,你自己初始化數(shù)據(jù)的時間憑什么算到我SpringBoot身上,你要初始化的時候做了個費時操作韧骗,回頭又說我SpringBoot辣雞嘉抒,那我不是虧得慌...

最后run下這個,listeners.running(context);這會兒用戶自定義的事情也會被調(diào)用了袍暴。

ok些侍,結(jié)束了。

小結(jié)

今天只是大概看了下SpringBoot啟動過程政模。有很多細(xì)節(jié)岗宣,比如refresh()都值得再仔細(xì)研究一下。SpringBoot之所以好用淋样,就是幫助我們做了很多配置耗式,省去很多細(xì)節(jié)(不得不說各種stater真實讓我們傻瓜式使用了很多東西),但是同樣定位bug或者通過項目聲明周期搞點事情的時候會無從下手。所以刊咳,看看SpringBoot源碼還是聽有必要的彪见。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市娱挨,隨后出現(xiàn)的幾起案子余指,更是在濱河造成了極大的恐慌,老刑警劉巖跷坝,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酵镜,死亡現(xiàn)場離奇詭異,居然都是意外死亡柴钻,警方通過查閱死者的電腦和手機(jī)淮韭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贴届,“玉大人靠粪,你說我怎么就攤上這事×荒澹” “怎么了庇配?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵斩跌,是天一觀的道長绍些。 經(jīng)常有香客問我,道長耀鸦,這世上最難降的妖魔是什么柬批? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮袖订,結(jié)果婚禮上氮帐,老公的妹妹穿的比我還像新娘。我一直安慰自己洛姑,他們只是感情好上沐,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著楞艾,像睡著了一般参咙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上硫眯,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天蕴侧,我揣著相機(jī)與錄音,去河邊找鬼两入。 笑死净宵,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播择葡,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼紧武,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了敏储?” 一聲冷哼從身側(cè)響起脏里,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虹曙,沒想到半個月后迫横,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡酝碳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年矾踱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疏哗。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡呛讲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出返奉,到底是詐尸還是另有隱情贝搁,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布芽偏,位于F島的核電站雷逆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏污尉。R本人自食惡果不足惜膀哲,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望被碗。 院中可真熱鬧某宪,春花似錦、人聲如沸锐朴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焚志。三九已至衣迷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娩嚼,已是汗流浹背蘑险。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留岳悟,地道東北人佃迄。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓泼差,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呵俏。 傳聞我的和親對象是個殘疾皇子堆缘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容