SpringBoot的main函數(shù)運(yùn)行之前都發(fā)生了什么

前言

SpringBoot項(xiàng)目通常都是由主類的main函數(shù)開始啟動(dòng)妨托,好奇心驅(qū)使我想搞明白通常項(xiàng)目所有的內(nèi)容都被打成了一個(gè)fat jar,按理說jar包中再包含的jar是沒有辦法被jdk加載的结缚,所以這個(gè)過程SpringBoot又是如何讓單個(gè)jar直接運(yùn)行起來的?

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

spring-boot-loader

通過解壓SpringBoot的maven插件二次打包的jar凡傅,可以看到目錄如下:

  • BOOT-INF/classes 下是spring-boot項(xiàng)目中編寫的java源碼編譯后的class
  • BOOT-INF/lib 下是spring-boot項(xiàng)目依賴的所有jar包
  • META-INF 是jar的信息粪糙,包含主類和sring-boot添加的額外的信息記錄
    -org.springgramework 下則是maven插件裝載進(jìn)去的class文件,也就是fat jar可以運(yùn)行起來的源碼
app
├── BOOT-INF
│   ├── classes
│   └── lib
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
└── org
    └── springframework

當(dāng)然鸯两,直接看org.springframework下反編譯的源碼有點(diǎn)晦澀闷旧,畢竟是反編譯而來的。查看spring boot的源碼可以在spring-boot-tools項(xiàng)目下看到spring-boot-loader子項(xiàng)目钧唐,這個(gè)其實(shí)就是maven插件裝載到fat jar中的class文件的源碼忙灼,所以閱讀這個(gè)子項(xiàng)目的源碼基本就可以搞清楚,spring boot的fat jar是如何把自己跑起來的。

SpringBoot項(xiàng)目的啟動(dòng)方式

1. idea中的啟動(dòng)

通常在IDEA中默認(rèn)的啟動(dòng)方式是直接通過主類啟動(dòng)该园,所有依賴的jar都通過jdk的參數(shù)添加進(jìn)來酸舍。很明顯,這種啟動(dòng)方式?jīng)]有借助于spring-boot-loader里初,是正常的java程序運(yùn)行方式啟動(dòng)父腕。

這種啟動(dòng)方式經(jīng)常用于開發(fā),畢竟直接啟動(dòng)更快一些青瀑。但是也有弊端,那就是通過command line的形式啟動(dòng)時(shí)萧诫,如果依賴的jar過多斥难,會(huì)導(dǎo)致拼接的命令過長而報(bào)錯(cuò),所以此中方式通常沒有辦法用于中大型項(xiàng)目

除了在idea中借助于開發(fā)工具拼接運(yùn)行命令之外帘饶,spring boot支持三種常見的啟動(dòng)方式哑诊。

  • jar
  • war
  • properties

2. jar

jar方式就是借助于spring boot的maven插件二次打包后的fat jar的形式啟動(dòng)。對應(yīng)spring-boot-loader項(xiàng)目中的JarLauncher類及刻,源碼如下(源碼中的注釋镀裤,部分翻譯,部分為我自己添加缴饭,幫助閱讀):

/**
 * 用于基于JAR形式的啟動(dòng)暑劝,該jar依賴的所有的其他jar包在/BOOT-INF/lib路徑下
 * 該jar對應(yīng)的spring boot的項(xiàng)目的java類全部位于/BOOT-INF/classes下
 */
public class JarLauncher extends ExecutableArchiveLauncher {
    // 依賴的class文件的路徑
    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    // 依賴的其他jar文件的路徑
    static final String BOOT_INF_LIB = "BOOT-INF/lib/";

    public JarLauncher() {
    }

    protected JarLauncher(Archive archive) {
        super(archive);
    }
    // 判斷entry是否為內(nèi)嵌依賴jar的,判斷的依據(jù)是名稱
    @Override
    protected boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(BOOT_INF_CLASSES);
        }
        return entry.getName().startsWith(BOOT_INF_LIB);
    }
        // jar形式的main-class
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

由于jar形式的啟動(dòng)是最常見的方式颗搂,所以本文會(huì)著重jar形式啟動(dòng)的分析担猛。

3. war包形式

在spring boot之前,大多數(shù)的spring mvc項(xiàng)目都是打成war包丢氢,置于tomcat的webapp目錄下來運(yùn)行傅联,所以springboot也是支持這種形式的啟動(dòng),只要在spring boot的maven插件中將打包的目前格式改為WAR即可疚察。在loader項(xiàng)目中對應(yīng)的啟動(dòng)類為:WarLauncher

/**
 * 注釋翻譯:用于war包形式的啟動(dòng)蒸走,只支持標(biāo)準(zhǔn)的WAR歸檔文件。
 * 三方依賴的jar位于 WEB-INF/lib貌嫡, 也可以為WEB-INF/lib-provided比驻,
 * 項(xiàng)目的class文件位于WEB-INF/classes路徑下。
 */
public class WarLauncher extends ExecutableArchiveLauncher {

    private static final String WEB_INF = "WEB-INF/";

    private static final String WEB_INF_CLASSES = WEB_INF + "classes/";

    private static final String WEB_INF_LIB = WEB_INF + "lib/";

    private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";

    // 部分代碼刪除衅枫。嫁艇。。
    // 啟動(dòng)主類
    public static void main(String[] args) throws Exception {
        new WarLauncher().launch(args);
    }
}

4. 基于配置屬性的形式啟動(dòng)

基于自定義配置的啟動(dòng)方式弦撩,兼容Fat JAR步咪。這種啟動(dòng)方式就比較靈活,可以通過三方插件將項(xiàng)目打成多種格式益楼,或者不二次打包等等猾漫,最后通過配置解析來啟動(dòng)spring boot項(xiàng)目点晴。

比如,可以將依賴悯周,配置文件粒督,啟動(dòng)類打包到指定目錄,然后按照如下方式啟動(dòng):

java -Dloader.main=xxx.xxx.Application \ # 主類
-Dloader.path=lib,config,resource,xxx.jar \  依賴和配置資源
-Dspring.profiles.active=dev \  // profiles
org.springframework.boot.loader.PropertiesLauncher // 啟動(dòng)類

所以Spring Boot的loader項(xiàng)目禽翼,就是提供spring boot應(yīng)用可以在不同場景和需求下都可以正常啟動(dòng)的能力屠橄,完成了從打包和實(shí)際項(xiàng)目運(yùn)行的橋接過程。

接下來闰挡,我們以JAR啟動(dòng)的方式锐墙,來分析分析,Spring boot到底是如何完成啟動(dòng)過程的:

可執(zhí)行Jar啟動(dòng)

jar形式的啟動(dòng)长酗,主類為JarLauncher溪北,其繼承自ExecutableArchiveLauncher,最上層的父類為Launcher夺脾,同時(shí)也是所有其他啟動(dòng)形式的頂層父類之拨。

image.png

JarLauncher中代碼不多,直接調(diào)用了Launcher中的launch方法咧叭,所以我們的代碼分析也從這里開始蚀乔。

launch方法了主要干了三件事情,

  • 第一是注冊擴(kuò)展protocol handler
  • 第二是獲取fat jar中所有的歸檔(三方j(luò)ar菲茬,class乙墙,資源文件等等)來創(chuàng)建自定義的類加載器(ClassLoader)
  • 最后使用創(chuàng)建好的類加載器,攜帶啟動(dòng)參數(shù)生均,創(chuàng)建主類啟動(dòng)對象听想,啟動(dòng)主類(主類在loader中為Start Class,其實(shí)就是spring boot應(yīng)用的啟動(dòng)類马胧,在spring的maven插件中被打包定義為Start class)
protected void launch(String[] args) throws Exception {
    // 注冊URL protocol handler
    JarFile.registerUrlProtocolHandler();
    // 獲取fat jar中的archives(也就是三方j(luò)ar汉买,class,以及其他資源文件),創(chuàng)建類加載器
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 獲取sub class(也就是spring boot 應(yīng)用的主類)佩脊,使用啟動(dòng)參數(shù)和創(chuàng)建好的class loader啟動(dòng)
    launch(args, getMainClass(), classLoader);
}

接下來蛙粘,我們針對這三個(gè)步驟展開來講。

1注冊擴(kuò)展UrlProtocolHandler

其實(shí)這個(gè)方法相當(dāng)于在啟動(dòng)java應(yīng)用時(shí)添加參數(shù):-Djava.protocol.handler.pkgs=xxx.xxx.xxx威彰,其作用就是對Url類支持的協(xié)議進(jìn)行擴(kuò)展出牧。多個(gè)指定的包的地址使用|來連接。

/**
 * 翻譯:注冊一個(gè)handler歇盼,以便定位URLStreamHandler來處理jar urls
 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
 * {@link URLStreamHandler} will be located to deal with jar URLs.
 */
public static void registerUrlProtocolHandler() {
    // 獲取當(dāng)前jvm中的handler參數(shù)
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    // 如果已有參數(shù)為空舔痕,則直接指定springboot的handler,否則|拼接進(jìn)行擴(kuò)展
    System.setProperty(PROTOCOL_HANDLER,
            ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
    // 最后重置緩存的handler
    resetCachedUrlHandlers();
}

那Spring boot擴(kuò)展這個(gè)來干嘛呢,方法注釋上說時(shí)為了定位URLStreamHandler來處理jar文件伯复,后續(xù)我們分析的過程中再繼續(xù)看慨代。

  1. 獲取ClassPath下的文件資源

createClassLoader(getClassPathArchives());雖然第二個(gè)步驟只有一句話,但這其實(shí)就是SpringBoot可以直接啟動(dòng)jar文件的核心邏輯啸如,所以展開來講侍匙,首先是獲取jar中的資源文件。

getClassPathArchives在Launcher中是abstract的叮雳,其具體實(shí)現(xiàn)在ExecutableArchiveLauncher中想暗。getClassPathArchives的實(shí)現(xiàn)其實(shí)代碼不多,核心方法是getNestedArchives(獲取嵌套的jar等文件)帘不〗酰看到這里其實(shí)我們就能稍微理解為什么Spring Boot能夠直接啟動(dòng)并直接嵌套自身jar中的其他jar了,其邏輯就是通過某種方式解析并獲取jar(猜測是作為普通資源文件獲取厌均,然后讀內(nèi)存或者寫到其他目錄,再加載進(jìn)來告唆,不過因?yàn)槲乙呀?jīng)讀過了棺弊,所以猜測其實(shí)是對的,哈哈哈)然后傳遞給自定義的classLoader加載擒悬,從而完成了依賴的jar的加載模她。

/**
 * 獲取class path下的文件,jar啟動(dòng)方式其實(shí)主要是獲取嵌套在fat jar中的其他三方j(luò)ar
 */
@Override
protected List<Archive> getClassPathArchives() throws Exception {
    // 獲取嵌套的文檔文件懂牧,
    List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
    // 后置處理歸檔文件
    postProcessClassPathArchives(archives);
    // 返回結(jié)果
    return archives;
}

看著這里其實(shí)有點(diǎn)疑惑侈净,this.archive是啥,之前沒有提到過僧凤。JarLauncher剛剛是在main方法中無參new的畜侦,所以就是隱含的執(zhí)行ExecutableArchiveLauncher的無參的構(gòu)造方法,這個(gè)archive就是在那個(gè)時(shí)候?qū)嵗摹?/p>

public ExecutableArchiveLauncher() {
    try {
        this.archive = createArchive();
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
}

ok躯保,所以這個(gè)時(shí)候需要擱置剛才的邏輯旋膳,先看看這個(gè)archive是什么東西,才能接著看它是如何獲取jar中的archives的途事。

createArchive方法主要有兩個(gè)邏輯验懊,首先是獲取當(dāng)前類對應(yīng)的絕對路徑。接著判斷尸变,如果絕對路徑對應(yīng)的是目錄义图,則archive就是ExplodedArchive,當(dāng)前我們假設(shè)是用jar啟動(dòng)的召烂,那絕對路徑對應(yīng)的就是jar文件本省碱工,此時(shí)this.archive就會(huì)被實(shí)例化成JarFileArchive

看到這里就清楚了,this.archiveJarFileArchive的實(shí)例痛垛。所以草慧,獲取jar中的archive邏輯就是在這個(gè)類中實(shí)現(xiàn)的。

ExplodedArchive的實(shí)現(xiàn)會(huì)用于warproperties的啟動(dòng)形式的archives的獲取匙头。

/**
 * 創(chuàng)建Archive
 */
protected final Archive createArchive() throws Exception {
    // 反射獲取當(dāng)前jar的 protectionDomain,可以理解為一個(gè)jar會(huì)對應(yīng)一個(gè)ProtectionDomain漫谷,主要是jar中資源的權(quán)限檢查和控制
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    // 然后獲取當(dāng)前類的codeSource
    CodeSource codeSource = protectionDomain.getCodeSource();
    // 最后獲取當(dāng)前類的路徑URL,再拿到path
    URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    String path = (location != null) ? location.getSchemeSpecificPart() : null;
    if (path == null) {
        throw new IllegalStateException("Unable to determine code source archive");
    }
    // 有了path蹂析,就可以將其包裝為java的抽象文件類
    File root = new File(path);
    if (!root.exists()) {
        throw new IllegalStateException("Unable to determine code source archive from " + root);
    }
    // 最后如果是目錄舔示,archive就會(huì)被實(shí)例化成ExplodedArchive, 如果是Jar形式啟動(dòng),那就是非目錄电抚,所以實(shí)例化成JarFileArchive
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}

到這里惕稻,就到最重要的邏輯:解析fat jar中的資源文件,包括三方j(luò)ar蝙叛,class文件俺祠,資源文件等等。

首先看入口方法借帘,方法的邏輯很簡單蜘渣。首先是迭代自身,獲取entry肺然,第二是包裝entry為Archive蔫缸,最后返回。所以對應(yīng)的搞清楚這兩個(gè)邏輯际起,就能理解jar中的資源文件是如何解析的拾碌。

/**
 * 獲取嵌套的archives
 * @param filter the filter used to limit entries
 */
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
    List<Archive> nestedArchives = new ArrayList<>();
    // 迭代自己本身,通過外部傳遞的isNestedArchive街望,JarLauncher中實(shí)現(xiàn)了校翔,通過class的前綴判斷
    for (Entry entry : this) {
        if (filter.matches(entry)) {
            // 通過entry,獲取并包裝為Archive
            nestedArchives.add(getNestedArchive(entry));
        }
    }
    // 包裝為不可變集合返回
    return Collections.unmodifiableList(nestedArchives);
}

EntryIterator

首先是自身的迭代器灾前,通過內(nèi)部類EntryIterator來實(shí)現(xiàn)展融,這里的邏輯很簡單不贅述。核心就一句話豫柬,entries都是通過this.jarFile獲取的告希。所以核心邏輯就在JarFile類中 。

JarFile

基礎(chǔ)jdk的JarFile進(jìn)行擴(kuò)展的子類烧给,類注釋上解釋說擴(kuò)展的功能有兩點(diǎn)燕偶。

  • 獲取嵌套的jar中的任一目錄下的文件
  • 獲取嵌套的jar中的jar文件

finally,到了解析自身jar最核心的邏輯了础嫡≈该矗看完JarFile類就能明白~酝惧!

JarFileEntry

JarFile中有一個(gè)很重要的類: JarFileEntry,其類圖如下 :

image.png

首先是其實(shí)現(xiàn)了迭代接口伯诬,用于jar中entry的迭代遍歷晚唇。第二個(gè)比較重要的就是實(shí)現(xiàn)了中央目錄的Visitor,這個(gè)是核心盗似。借助于CentralDirectoryParser類哩陕,在RandomAccessDataloader.dat下的類,輔助數(shù)據(jù)讀群帐妗)的幫助下悍及,解析并遍歷了整個(gè)JarFile中的文件,然后JarFileEntry作為visitor被setCentralDirectoryParser中接癌,也完成了整個(gè)JarFile中的文件的遍歷心赶,并將其緩存在entriesCache中。entriesCache是一個(gè)被同步的synchronizedMap包裹的LinkedHashMap缺猛。上文提到的EntryIterator迭代數(shù)據(jù)其實(shí)就來自于這里的map緩存的數(shù)據(jù)缨叫。

所以,loader的是如何解析jar中jar呢荔燎,還得繼續(xù)往前看耻姥,搞明白RandomAccessDataCentralDirectoryParser后,也許這次就真的弄明白了jar中jar的解析的代碼湖雹。

事實(shí)上這里其實(shí)才是整個(gè)loader項(xiàng)目中代碼量最大的地方。因?yàn)镴arFile牽扯到整個(gè)jar路徑和data路徑的所有類曙搬。其互相配合摔吏,相互調(diào)用,雖然看起來清晰纵装,但是要說明白還是要花點(diǎn)時(shí)間征讲,這周先到這里,下周繼續(xù)填坑橡娄。

未完待續(xù)诗箍。。挽唉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滤祖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瓶籽,更是在濱河造成了極大的恐慌匠童,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件塑顺,死亡現(xiàn)場離奇詭異汤求,居然都是意外死亡俏险,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門扬绪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來竖独,“玉大人,你說我怎么就攤上這事挤牛∮。” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵赊颠,是天一觀的道長格二。 經(jīng)常有香客問我,道長竣蹦,這世上最難降的妖魔是什么顶猜? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮痘括,結(jié)果婚禮上长窄,老公的妹妹穿的比我還像新娘。我一直安慰自己纲菌,他們只是感情好挠日,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翰舌,像睡著了一般嚣潜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椅贱,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天懂算,我揣著相機(jī)與錄音,去河邊找鬼庇麦。 笑死计技,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的山橄。 我是一名探鬼主播垮媒,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼航棱!你這毒婦竟也來了睡雇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饮醇,失蹤者是張志新(化名)和其女友劉穎入桂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體驳阎,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抗愁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年馁蒂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜘腌。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沫屡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出撮珠,到底是詐尸還是另有隱情沮脖,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布芯急,位于F島的核電站勺届,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏娶耍。R本人自食惡果不足惜免姿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望榕酒。 院中可真熱鬧胚膊,春花似錦、人聲如沸想鹰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辑舷。三九已至喻犁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間何缓,已是汗流浹背肢础。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留歌殃,地道東北人乔妈。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓蝙云,卻偏偏與公主長得像氓皱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子勃刨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

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