聊一聊Springboot的類加載機(jī)制

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

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)有不少的變化,可以認(rèn)為在執(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中,當(dāng)然scope可以設(shè)置成provided,因?yàn)镕AT 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文件其實(shí)并不在運(yùn)行時應(yīng)用的classpath下,也就是根據(jù)類加載的雙親委派機(jī)制,這些依賴沒辦法被默認(rèn)的任何一個classloader加載,Springboot為了解決這個問題,自定義了類加載機(jī)制。

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

LaunchedURLClassLoader做了什么

LaunchedURLClassLoader繼承了java.net.URLClassLoader,自己實(shí)現(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,這其實(shí)又回到了雙親委派機(jī)制,最后讓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的。這種差別會導(dǎo)致調(diào)用classloader.getResourceAsStream()得到不一樣的結(jié)果,這是因?yàn)镕AT 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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容