聊一聊Springboot的類加載機制

眾所周知拓售,Springboot的FAT JAR機制大大的簡化了應(yīng)用的打包和啟動剑令,并且統(tǒng)一了不同stack(command, web, batch)的打包和啟動方法侦厚,使得一個應(yīng)該的開發(fā)和部署都變得簡單了邑遏,本文想在這里解析一下FAT JAR的方式下穆端,Springboot的類加載機制忠售。

Springboot FAT JAR的結(jié)構(gòu)

我們知道Springboot提供了spring-boot-maven-plugin這個maven plugin在build時生成FAT jar传惠,如果我們build了一個springboot的應(yīng)用,去target folder下可以看見兩個名字非常類似的文件: abc-0.0.1-SNAPSHOT.jar 和abc-0.0.1-SNAPSHOT.jar.original,這兩個文件的大小相差非常明顯稻扬,我們解壓開.original文件可以發(fā)現(xiàn)如下結(jié)構(gòu)卦方,這個就是我們應(yīng)用中所有本地文件(代碼和資源文件),而不包含第三方的依賴等等泰佳。



如果我們解壓打開abc-0.0.1-SNAPSHOT.jar (FAT JAR)盼砍,目錄結(jié)構(gòu)如下, 本地的文件都在BOOT-INF/classes下,但是還多了:
BOOT-INF/lib - 存放所有dependences的JAR
org/Springframework - 存放springboot相關(guān)的class


image.png

目錄結(jié)構(gòu)有不少的變化逝她,可以認為在執(zhí)行spring-boot-maven-plugin后浇坐,abc-0.0.1-SNAPSHOT.jar.original被重新打包成了abc-0.0.1-SNAPSHOT.jar這個FAT JAR。
在BOOT-INF/lib下有一個dependency需要花功夫研究: spring-boot-loader黔宛,這個JAR保證了為什么通過java -jar命令能夠執(zhí)行FAT JAR從而啟動Springboot應(yīng)用近刘。

spring-boot-loader 做了什么

既然把所有的依賴,class和資源文件都包含在一個JAR內(nèi),那就必須要解決啟動時如何去load這些claas和資源文件觉渴,為了一探究竟介劫,我們需要把spring-boot-loader的source code拉下來,一個簡單的方法就是把spring-boot-loader作為依賴加到應(yīng)用的pom.xml中案淋,當然scope可以設(shè)置成provided座韵,因為FAT JAR中一定會包含這個依賴。

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-loader</artifactId>
      <scope>Provided</scope>
    </dependency>

這樣我們就能很方便的拿到spring-boot-loader的source code踢京。通過FAT JAR中的MANIFEST.MF誉碴,我們能快速找到真正的Springboot bootstrap方法應(yīng)該是org.springframework.boot.loader.JarLauncher#main,真正的code在基類Launcher#launch中漱挚。

public abstract class Launcher {

    private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";

    /**
     * Launch the application. This method is the initial entry point that should be
     * called by a subclass {@code public static void main(String[] args)} method.
     * @param args the incoming arguments
     * @throws Exception if the application fails to launch
     */
    protected void launch(String[] args) throws Exception {
        if (!isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }
        ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
        launch(args, launchClass, classLoader);
    }

可以發(fā)現(xiàn)這里創(chuàng)建了一個新的classloader - LaunchedURLClassLoader翔烁,具體是渺氧,

    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
    }

并把classload 加入thread 的context中旨涝。

protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        createMainMethodRunner(launchClass, args, classLoader).run();
    }

為什么需要LaunchedURLClassLoader呢?設(shè)想一下侣背,現(xiàn)在FAT JAR中依賴的各個jar文件其實并不在運行時應(yīng)用的classpath下白华,也就是根據(jù)類加載的雙親委派機制,這些依賴沒辦法被默認的任何一個classloader加載贩耐,Springboot為了解決這個問題弧腥,自定義了類加載機制。

P.S 不同的內(nèi)置classloader的scope 如下:
Bootstrap ClassLoader(加載JDK的/lib目錄下的類)
Extension ClassLoader(加載JDK的/lib/ext目錄下的類)
Application ClassLoader(程序自己classpath下的類)

LaunchedURLClassLoader做了什么

LaunchedURLClassLoader繼承了java.net.URLClassLoader潮太,自己實現(xiàn)了loadClass方法管搪。

@Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
            try {
                Class<?> result = loadClassInLaunchedClassLoader(name);
                if (resolve) {
                    resolveClass(result);
                }
                return result;
            }
            catch (ClassNotFoundException ex) {
            }
        }
        if (this.exploded) {
            return super.loadClass(name, resolve);
        }
        Handler.setUseFastConnectionExceptions(true);
        try {
            try {
                definePackageIfNecessary(name);
            }
            catch (IllegalArgumentException ex) {
                // Tolerate race condition due to being parallel capable
                if (getPackage(name) == null) {
                    // This should never happen as the IllegalArgumentException indicates
                    // that the package has already been defined and, therefore,
                    // getPackage(name) should not return null.
                    throw new AssertionError("Package " + name + " has already been defined but it could not be found");
                }
            }
            return super.loadClass(name, resolve);
        }
        finally {
            Handler.setUseFastConnectionExceptions(false);
        }
    }

definePackageIfNecessary 確保了Jar in Jar里的class manifest能夠和package關(guān)聯(lián)起來。

private void definePackage(String className, String packageName) {
        try {
            AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                String packageEntryName = packageName.replace('.', '/') + "/";
                String classEntryName = className.replace('.', '/') + ".class";
                for (URL url : getURLs()) {
                    try {
                        URLConnection connection = url.openConnection();
                        if (connection instanceof JarURLConnection) {
                            JarFile jarFile = ((JarURLConnection) connection).getJarFile();
                            if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
                                    && jarFile.getManifest() != null) {
                                definePackage(packageName, jarFile.getManifest(), url);
                                return null;
                            }
                        }
                    }
                    catch (IOException ex) {
                        // Ignore
                    }
                }
                return null;
            }, AccessController.getContext());
        }
        catch (java.security.PrivilegedActionException ex) {
            // Ignore
        }
    }

可以看到最終load class還是調(diào)了super.loadClass铡买,也就是java.lang.ClassLoader#loadClass更鲁,這其實又回到了雙親委派機制,最后讓Application Classloader來load奇钞。

LaunchedURLClassLoader的作用是在FAT JAR(Jar in Jar)這樣的目錄結(jié)構(gòu)中澡为,能夠找到要load的class(依賴中的類或者應(yīng)用自己的類),并且load他們景埃。

我們看看這個class load是怎么load 我們在springboot應(yīng)用中定義的main class媒至,也就是應(yīng)用的入口程序的。
在org.springframework.boot.loader.MainMethodRunner中谷徙,通過LaunchedURLClassLoader load并且通過反射調(diào)用了main 方法拒啰。

public void run() throws Exception {
        Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.setAccessible(true);
        mainMethod.invoke(null, new Object[] { this.args });
    }

Java -Jar 和IDE里啟動Sprintboot 有什么區(qū)別

Java -Jar是以FAT JAR的方式用LaunchedURLClassLoader來load class。而在IDE中則是直接以ApplicationClassLoader來load的完慧。這種差別會導致調(diào)用classloader.getResourceAsStream()得到不一樣的結(jié)果谋旦,這是因為FAT JAR啟動時,LaunchedURLClassLoader的load的urls并沒有FAT JAR本身,如abc-0.0.1-SNAPSHOT.jar, 但是應(yīng)用中的src/main/resources/META-INF/resources目錄被打包到了FAT JAR里蛤织,也就是abc-0.0.1-SNAPSHOT.jar!/META-INF/resources赴叹,這樣這些resource也就不會被訪問到了

這也就是為什么有時候在IDE里能讀到的resource在Run FAT JAR的情況下讀不到了,Springboot也給了多種方式來正確的load resource: https://www.baeldung.com/spring-load-resource-as-string

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末指蚜,一起剝皮案震驚了整個濱河市乞巧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摊鸡,老刑警劉巖绽媒,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異免猾,居然都是意外死亡是辕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進店門猎提,熙熙樓的掌柜王于貴愁眉苦臉地迎上來获三,“玉大人,你說我怎么就攤上這事锨苏「斫蹋” “怎么了?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵伞租,是天一觀的道長贞谓。 經(jīng)常有香客問我,道長葵诈,這世上最難降的妖魔是什么裸弦? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮作喘,結(jié)果婚禮上理疙,老公的妹妹穿的比我還像新娘。我一直安慰自己徊都,他們只是感情好沪斟,可當我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著暇矫,像睡著了一般主之。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上李根,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天槽奕,我揣著相機與錄音,去河邊找鬼房轿。 笑死粤攒,一個胖子當著我的面吹牛所森,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夯接,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼焕济,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盔几?” 一聲冷哼從身側(cè)響起晴弃,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逊拍,沒想到半個月后上鞠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡芯丧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年芍阎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缨恒。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡谴咸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肿轨,到底是詐尸還是另有隱情寿冕,我是刑警寧澤蕊程,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布椒袍,位于F島的核電站,受9級特大地震影響藻茂,放射性物質(zhì)發(fā)生泄漏驹暑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一辨赐、第九天 我趴在偏房一處隱蔽的房頂上張望优俘。 院中可真熱鬧,春花似錦掀序、人聲如沸帆焕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽叶雹。三九已至,卻和暖如春换吧,著一層夾襖步出監(jiān)牢的瞬間折晦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工沾瓦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留满着,地道東北人谦炒。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像风喇,于是被迫代替她去往敵國和親宁改。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,687評論 2 351

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