SpringBoot(一)— fatJar启动和LaunchedURLClassLoader加载class的原理

​ 基于 Spring Boot 2.7.x,从整体架构角度出发,梳理并串联了 fatjar 模式下的关键组件:ArchiveJarFileJarEntryHandlerJarURLConnection,重点分析它们在类加载过程中的职责与协作关系。全文不聚焦具体 jar 包结构解析细节,而是深入源码层面解析 fatjar 启动流程及 LaunchedURLClassLoader#loadClass 实现机制,并辅以实际测试用例进行debug和验证。

fatjar结构

​ SpringBoot 使用 spring-boot-maven-plugin 插件可以将项目打包成一个可执行的 fatjar,其内部结构不仅符合标准的 jar 规范,还增加了 SpringBoot 特有的扩展结构,以支持其定制的类加载机制

├── BOOT-INF
│   ├── classes/               # 你的 class 文件和 resource 资源
│   └── lib/                   # 所有依赖 jar 包
│       ├── spring-context-5.3.31.jar
│       ├── spring-web-5.3.31.jar
├── META-INF
│   └── MANIFEST.MF            # 包含 Main-Class 配置(默认指向 JarLauncher)
├── org
│   └── springframework
│       └── boot
│           └── loader         # SpringBoot 提供的加载器逻辑
│               ├── JarLauncher.class
│               ├── LaunchedURLClassLoader.class
│               ├── ExecutableArchiveLauncher.class
│               ├── Launcher.class
│               ├── MainMethodRunner.class

fatjar解压后如上,其中包含:

  • 当前项目编译后的 .class 文件和资源
  • 项目的所有依赖 jar 文件
  • SpringBoot 提供的启动器和类加载器(位于 org.springframework.boot.loader 包下)

标准jar部分

  1. /META-INF/MANIFEST.MF文件:jar的元信息文件(包含项目结构和启动类)。比如java -jar命令就是执行其中Main-Class的main方法
  2. SpringBoot Loader 相关类放在 jar 根路径:让LaunchedURLClassLoader相关依赖可以直接被AppClassLoader加载,无需特殊处理

特有的拓展

  1. 所有业务资源都被封装在 /BOOT-INF/ 下而不直接在跟路径:这部分资源不是标准的JVM classpath路径,不能被AppClassLoader加载,只能由SpringBoot提供的LaunchedURLClassLoader加载
  2. jar嵌套:标准jar不支持嵌套结构。SpringBoot拓展了一系列组件的功能来支持/BOOT-INF/lib/下嵌套jar的解析

核心组件

在正式进入源码前,先了解一些fatjar相关的核心组件和作用,对后续源码debug会有帮助。我这里主要关注fatjar启动方式,就不考虑exploded模式了

Archive

​ 是SpringBoot Loader 模块中的一个抽象接口。可以表示 fatjar 本身,也可以表示 fatjar 内部的某个嵌套 jar(BOOT-INF/lib/*.jar),或特定目录( BOOT-INF/classes/,此时其实现为org.springframework.boot.loader.archive.JarFileArchive。两个核心方法如下:

  1. Archive#getUrl:返回当前资源的URL,用于构造classpath。比如:

    jar:file:/Users/reef/IdeaProjects/soil/target/soil-1.0.0.jar!/BOOT-INF/classes!/

    jar:file:/Users/reef/IdeaProjects/soil/target/soil-1.0.0.jar!/BOOT-INF/lib/spring-core-5.3.31.jar!/

  2. Archive#getNestedArchives(EntryFilter filter):遍历当前 Archive 内部结构,筛选出所有符合条件的子 Archive

    SpringBoot中默认过滤出嵌套 jar(BOOT-INF/lib/*.jar文件)和指定目录(BOOT-INF/classes/)

JarFile

​ JDK 提供的java.util.jar.JarFile 类表示一个标准的 jar 文件,其本质是一个 classpath 元素,内部通常只包含 .class 文件和资源文件,不支持嵌套 jar结构

​ 为了支持 fatjar 的结构,SpringBoot 自定义了 org.springframework.boot.loader.jar.JarFile 类,它同样表示一个 jar 文件,但支持访问 fatjar 内部嵌套的 jar(例如 BOOT-INF/lib/*.jar),并提供对嵌套 jar 内容的解包与访问能力。核心方法如下:

  1. JarFile#getInputStream(java.util.zip.ZipEntry):获取指定 entry 的InputStream,可用于读取 .class 文件、配置文件、资源等内容
  2. JarFile#getJarEntry(java.lang.String):获取当前jar内部的指定资源对象(JarEntry)。支持查找 class 文件、配置文件、嵌套 jar 等
  3. JarFile#getNestedJarFile(JarEntry):将fatjar内部某个嵌套 jar转换为一个新的 JarFile 对象,实现对嵌套 jar 的透明访问

JarEntry

​ jdk 提供的 java.util.jar.JarEntry 表示 jar 文件中的一个具体条目,可以是 .class 文件、配置文件、目录等。并支持通过 JarFile#getInputStream(entry) 读取其内容

​ 但fatjar是一种嵌套的复杂结构,标准的 JarEntry无法支持对内部嵌套 jar 的进一步解析,所以SpringBoot对其拓展,定义了自己的org.springframework.boot.loader.jar.JarEntry 类,增加表示了fatjar 中嵌套的 jar 文件。其配合JarFile#getNestedJarFile(JarEntry entry) 方法,能够将某个嵌套 jar 条目转换为新的 JarFile 对象,实现对嵌套 jar 的透明访问

URLStreamHandler

​ 是 JDK 提供的抽象类,用于处理特定协议(如 httpfilejar)的 URL,并生成对应的 URLConnection 对象,以实现对资源的读取。

​ 对于 jar 协议,JDK 默认使用 sun.net.www.protocol.jar.Handler 创建标准的 JarURLConnection,用于读取普通 jar 文件中的资源。

SpringBoot 自定义了 org.springframework.boot.loader.jar.Handler,用于替代 JDK 默认的 Handler。该 Handler 会创建支持嵌套 jar 的 JarURLConnection,以满足 fatjar 场景下的资源访问需求

​ SpringBoot通过设置如下系统属性,确保了当构造jar协议的URL时,会使用org.springframework.boot.loader.jar.Handler作为当前URL的URLStreamHandler

// org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler方法里
System.setProperty("java.protocol.handler.pkgs", "org.springframework.boot.loader");

JarURLConnection

表示jar协议URL的连接对象,但标准的java.net.JarURLConnection不支持解析多层嵌套的资源,如

jar:file:/soil-1.0.0.jar!/BOOT-INF/lib/demo.jar!/site.shanzhao.Demo.class

为此,SpringBoot 提供了替代实现 org.springframework.boot.loader.jar.JarURLConnection,可直接在不解压 fatjar 的情况下访问嵌套 jar 内部的 class 和配置资源。核心方法如下

  1. JarURLConnection#getJarEntry:将当前URL资源解析为JarEntry
  2. JarURLConnection#getInputStream:通过JarFile获取该资源对应的InputStream,以直接读取 .class 文件或配置资源

fatjar启动流程

​ 入口为JarLauncher#main方法,JarLauncher继承了ExecutableArchiveLauncher

ExecutableArchiveLauncher实例化

主要做两件事:

  1. Launcher#createArchive:确定当前启动类是jar包还是目录启动,jar启动使用JarFileArchive,目录启动则使用ExplodedArchive
  2. 加载classpath.idx文件:只有目录启动模式(exploded)才会使用/BOOF-INF/classpath.idx文件来加速classpath URL的构建

Launcher#launch

启动器的核心入口,主要完成以下三项工作:

  1. fatjar启动才注册JarFile.registerUrlProtocolHandler(exploded为解压模式下启动,这种不存在嵌套jar)。让jar协议的URL使用SpringBoot提供的Handler,去创建支持解析嵌套jar内容的URLConnection
  2. 解析classpath URL,并创建相关的LaunchedURLClassLoader
  3. 绑定LaunchedURLClassLoader到当前线程上下文,并反射启动Start-Class
protected void launch(String[] args) throws Exception {
        if (!isExploded()) {
            // fatjar模式启动,注册一个 java.protocol.handler.pkgs=org.springframework.boot.loader 的系统属性
            // 以便让jar protocol的URL使用org.springframework.boot.loader.jar.Handler来创建URLConnection
            JarFile.registerUrlProtocolHandler();
        }
        // 遍历并筛选 fatjar 内部的 BOOT-INF/lib 和 BOOT-INF/classes,构建 URL 列表并创建LaunchedURLClassLoader
        ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        // 获取Start-Class,也就是我们主程序的启动类
        String launchClass = (jarMode != null && !jarMode.isEmpty()) ? fatjar_MODE_LAUNCHER : getMainClass();
        // 绑定LaunchedURLClassLoader到当前thread(即main线程)里,以便在后续加载class时使用到。并启动项目的主启动类
        launch(args, launchClass, classLoader);
    }

ExecutableArchiveLauncher#getClassPathArchivesIterator

是fatjar启动模式下构建classpath URL的关键代码,其定义的EntryFilter会构建出如下的Archive

  1. BOOT-INF/lib/ 下所有jar分别各自对应一个Archive
  2. BOOT-INF/classes 文件夹对应一个Archive
    @Override
    protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        // 以 BOOT-INF/ 开头
        Archive.EntryFilter searchFilter = this::isSearchCandidate;
        // 当存在 BOOT-INF/classpath.idx 文件时,跳过其中已列出的 jar 文件,不再走常规的扫描构建 Archive 路径。
        // 会在后续步骤里(ExecutableArchiveLauncher.createClassLoader方法)直接通过 BOOT-INF/classpath.idx 文件把这些jar转成URL加入classpath,从而提升启动效率。
        // 重点注意:只有在exploded模式下(解压了),才会使用BOOT-INF/classpath.idx。以java -jar启动使用的JarFileArchive是不会用到 BOOT-INF/classpath.idx 文件的
        Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
                (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
        if (isPostProcessingClassPathArchives()) {
            archives = applyClassPathArchivePostProcessing(archives);
        }
        return archives;
    }

总结

JarLauncher#main 启动到反射调用 Start-Classmain 方法,主要完成以下工作:

​ 解析 fatjar,将其中的依赖 jar 和项目 class 封装为 jar 协议的 URL,并用这些 URL 构造 LaunchedURLClassLoader;随后将该类加载器绑定到 main 线程的上下文,确保在应用启动过程中可以加载所有 class 和资源

测试用例

可以跟着debug看看fatjar到底解析出了那些Archive,和这些Archive的URL格式

public class FatJarResolveArchiveDemo {


    public static void main(String[] args) throws Exception {
        String fatJarPath = System.getProperty("user.home") + "/IdeaProjects/soil/target/soil-1.0.0.jar";
        JarFileArchive soilJar = new JarFileArchive(new File(fatJarPath));
        Archive.EntryFilter searchFilter = entry -> entry.getName().startsWith("BOOT-INF/");
        // fatjar启动下ExecutableArchiveLauncher.classPathIndex字段为null,ExecutableArchiveLauncher.isEntryIndexed就默认返回true了
        Iterator<Archive> archives = soilJar.getNestedArchives(searchFilter, NESTED_ARCHIVE_ENTRY_FILTER);
        while (archives.hasNext()) {
            System.out.println(archives.next());
        }

        soilJar.close();
    }


    private static final Archive.EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
        if (entry.isDirectory()) {
            return entry.getName().equals("BOOT-INF/classes/");
        }
        return entry.getName().startsWith("BOOT-INF/lib/");
    };
}

LaunchedURLClassLoader

这是SpringBoot项目的真正类加载器,主要关注其loadClass方法,顺着这个方法看看一个class文件是如何被找的

definePackage在当前类完成,随后真正loadClass还是调用父类URLClassLoader#loadClass方法

核心源码

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 先单独处理jarmode
    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) { // exploded解压模式,不是fatjar。可直接用传统的方式查找class
        return super.loadClass(name, resolve);
    }
    Handler.setUseFastConnectionExceptions(true);
    try {
        try {
            // 遍历所有可用的jar,并匹配到当前的name,最后定义为可用的Package
            definePackageIfNecessary(name);
        } catch (IllegalArgumentException ex) {
            if (getPackage(name) == null) {
                throw new AssertionError("Package " + name + " has already been defined but it could not be found");
            }
        }
        // 直接用父类URLClassLoader方法loadClass
        return super.loadClass(name, resolve);
    } finally {
        Handler.setUseFastConnectionExceptions(false);
    }
}

private void definePackageIfNecessary(String className) {
    int lastDot = className.lastIndexOf('.');
    if (lastDot >= 0) {
        // 截掉class的simpleName,只留下包名
        String packageName = className.substring(0, lastDot);
        if (getPackage(packageName) == null) { // 缓存中没查到,就define
            try {
                definePackage(className, packageName);
            } catch (IllegalArgumentException ex) {
                if (getPackage(packageName) == null) {
                    throw new AssertionError(
                            "Package " + packageName + " has already been defined but it could not be found");
                }
            }
        }
    }
}

private void definePackage(String className, String packageName) {
    try {
        AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
            // 例:site/shanzhao/common/bean/
            String packageEntryName = packageName.replace('.', '/') + "/";
            // 例:site/shanzhao/common/bean/Demo.class
            String classEntryName = className.replace('.', '/') + ".class";
            for (URL url : getURLs()) { // 遍历jar协议的URL(fatjar内的所有BOOT-INF/lib/*.jar和/BOOT-INF/classes/)
                try {
                    // 通过Handler创建JarURLConnection(返回的connection不会为空)
                    URLConnection connection = url.openConnection();
                    if (connection instanceof JarURLConnection) {
                        // 如果不存在,内部会抛FileNotFoundException异常
                        JarFile jarFile = ((JarURLConnection) connection).getJarFile();
                        if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
                                && jarFile.getManifest() != null) { // 找到了,定义这个Package并缓存
                            definePackage(packageName, jarFile.getManifest(), url);
                            return null;
                        }
                    }
                } catch (IOException ex) {
                    // 当前的classpath URL没找到className,忽略异常
                }
            }
            return null;
        }, AccessController.getContext());
    } catch (java.security.PrivilegedActionException ex) {
        // Ignore
    }
}

URLClassLoader

​ 其loadClass仍遵循双亲委派模型并优先查找缓存,这里聚焦其核心的 findClass 方法,分析如何通过 URL 加载资源并定义类。

findClass核心逻辑总结

  1. 通过内部的URLClassPath查找指定类名对应的资源,封装为 Resource
  2. 从Resource 中获InputStream,并转换为ByteBuffer 来读取 .class 字节,最终调用 defineClass 完成类的定义
protected Class<?> findClass(final String name)
        throws ClassNotFoundException {
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<>() {
                    public Class<?> run() throws ClassNotFoundException {
                        // 将类名转换为资源路径
                        String path = name.replace('.', '/').concat(".class");
                        // 使用内部的URLClassPath加载资源
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                // 定义class
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            } catch (ClassFormatError e2) {
                                if (res.getDataError() != null) {
                                    e2.addSuppressed(res.getDataError());
                                }
                                throw e2;
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

private Class<?> defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    int i = name.lastIndexOf('.');
    URL url = res.getCodeSourceURL();
    if (i != -1) {
        String pkgname = name.substring(0, i);
        Manifest man = res.getManifest();
        // 校验资源的Package
        if (getAndVerifyPackage(pkgname, man, url) == null) {
            try {
                if (man != null) {
                    definePackage(pkgname, man, url);
                } else {
                    definePackage(pkgname, null, null, null, null, null, null, null);
                }
            } catch (IllegalArgumentException iae) {
                if (getAndVerifyPackage(pkgname, man, url) == null) {
                    throw new AssertionError("Cannot find package " +
                            pkgname);
                }
            }
        }
    }
    // 内部使用Resource#getInputStream,获取ByteBuffer,开始真正的数据读取和class定义
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, bb, cs);
    } else {
        byte[] b = res.getBytes();
        CodeSigner[] signers = res.getCodeSigners();
        CodeSource cs = new CodeSource(url, signers);
        PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
        return defineClass(name, b, 0, b.length, cs);
    }
}

URLClassPath

​ 配合 URLClassLoader 使用,负责在所有 classpath URL 中懒加载并定位资源(如 .class 或配置文件)。其核心方法 getResource 的处理流程如下:

  1. 延迟创建并缓存每个 URL 对应的 Loader
  2. Loader构造具体目标资源的 URL,并通过其协议的 URLStreamHandler 创建URLConnection
  3. 若连接有效,返回封装好的 Resource 对象(包含 InputStream 等)
  4. 若未命中资源,继续遍历下一个 Loader

public class URLClassPath {
    // URLClassLoader里所有classpath下的URL
    private final ArrayList<URL> path;

    // // 尚未初始化的 URL 列表(懒加载机制)
    private final ArrayDeque<URL> unopenedUrls;

    // loader缓存,1个loader对应一个URL
    private final ArrayList<Loader> loaders = new ArrayList<>();

    // loader和URL的关系缓存
    // key比如
    // jar://file:/Users/reef/IdeaProjects/soil/target/soil-1.0.0.jar!/BOOT-INF/lib/spring-context-5.3.31.jar!/
    private final HashMap<String, Loader> lmap = new HashMap<>();

    public Resource getResource(String name, boolean check) {

        Loader loader;
        for (int i = 0; (loader = getLoader(i)) != null; i++) {
            Resource res = loader.getResource(name, check);
            if (res != null) {
                return res;
            }
        }
        return null;
    }

    private synchronized Loader getLoader(int index) {
        if (closed) {
            return null;
        }
        // 遍历完了loaders,尝试增加未open的URL
        while (loaders.size() < index + 1) {
            final URL url;
            synchronized (unopenedUrls) {
                url = unopenedUrls.pollFirst();
                // url为null则表示所有URL都以open了
                if (url == null)
                    return null;
            }
            String urlNoFragString = URLUtil.urlNoFragString(url);
            if (lmap.containsKey(urlNoFragString)) {
                continue;
            }
            Loader loader;
            try {
                // 创建URL的Loader
                loader = getLoader(url);

                URL[] urls = loader.getClassPath();
                if (urls != null) {
                    push(urls);
                }
            } catch (IOException e) {
                continue;
            } catch (SecurityException se) {
                if (DEBUG) {
                    System.err.println("Failed to access " + url + ", " + se);
                }
                continue;
            }
            // loader缓存
            loaders.add(loader);
            lmap.put(urlNoFragString, loader);
        }
        return loaders.get(index);
    }

    private Loader getLoader(final URL url) throws IOException {
        try {
            return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<>() {
                        public Loader run() throws IOException {
                            String protocol = url.getProtocol();
                            String file = url.getFile();
                            if (file != null && file.endsWith("/")) {
                                if ("file".equals(protocol)) {
                                    return new FileLoader(url);
                                } else if ("jar".equals(protocol) &&
                                        isDefaultJarHandler(url) &&
                                        file.endsWith("!/")) {
                                    // JarHandler为jdk默认的sun.net.www.protocol.jar.Handler才会进入
                                    URL nestedUrl = new URL(file.substring(0, file.length() - 2));
                                    return new JarLoader(nestedUrl, jarHandler, lmap, acc);
                                } else {
                                    // SpringBoot fatjar会创建这个Loader
                                    return new Loader(url);
                                }
                            } else {
                                return new JarLoader(url, jarHandler, lmap, acc);
                            }
                        }
                    }, acc);
        } catch (PrivilegedActionException pae) {
            throw (IOException) pae.getException();
        }
    }

    private static class Loader implements Closeable {
        Resource getResource(final String name, boolean check) {
            final URL url;
            try {
                // 构造具体资源的URL,举例
                // base为 jar:file:/Users/reef/IdeaProjects/soil/target/soil-1.0.0.jar!/BOOT-INF/lib/hutool-core-5.8.25.jar!/
                // spec为 cn/hutool/core/thread/AsyncUtil.class
                url = new URL(base, ParseUtil.encodePath(name, false));
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException("name");
            }
            final URLConnection uc;
            try {
                if (check) {
                    URLClassPath.check(url);
                }
                // 通过URL内部的Handler构造URLConnection
                uc = url.openConnection();
                // SpringBoot的JarURLConnection如果不存在指定的资源,这里面会抛异常
                InputStream in = uc.getInputStream();
                if (uc instanceof JarURLConnection) {
                    /*
                     * Need to remember the jar file so it can be closed
                     * in a hurry.
                     */
                    JarURLConnection juc = (JarURLConnection) uc;

                    boolean firstLoad = jarfile == null;

                    jarfile = JarLoader.checkJar(juc.getJarFile());

                    if (firstLoad && JarLoadEvent.isEnabled()) {
                        Tooling.notifyEvent(JarLoadEvent.jarLoadEvent(url, jarfile));
                    }
                }
            } catch (Exception e) {
                return null;
            }
            // 找到了资源,构造对应的Resource
            return new Resource() {
                public String getName() {
                    return name;
                }

                public URL getURL() {
                    return url;
                }

                public URL getCodeSourceURL() {
                    return base;
                }

                public InputStream getInputStream() throws IOException {
                    return uc.getInputStream();
                }

                public int getContentLength() throws IOException {
                    return uc.getContentLength();
                }
            };
        }
    }
}

测试用例

可以跟着这个用例debug一下,看看整体的LaunchedURLClassLoader的loadClass流程

public class LaunchedURLClassLoaderDemo {

    public static void main(String[] args) throws Exception {
        String fatJarPath = System.getProperty("user.home") + "/IdeaProjects/soil/target/soil-1.0.0.jar";
        File fatJar = new File( fatJarPath);
        Assert.isTrue(fatJar.exists(), "Fat jar not found");
        JarFile jarFile = new JarFile(fatJar);
        JarFile nestedJar = jarFile.getNestedJarFile(jarFile.getJarEntry("BOOT-INF/lib/hutool-json-5.8.25.jar"));
        // 内部会直接创建org.springframework.boot.loader.jar.handler,所以在这里可以先不注册java.protocol.handler.pkgs
        URL jsonUrl = nestedJar.getUrl();
        // ======== 此时必须注册jar包处理器org.springframework.boot.loader.jar.handler,才能读取fatjar =========
        JarFile.registerUrlProtocolHandler();
        // eg: jar:file:/Users/reef/IdeaProjects/soil/target/soil-1.0.0.jar!/BOOT-INF/lib/hutool-core-5.8.25.jar!/
        URL coreUrl = new URL("jar:file:" + fatJarPath + "!/BOOT-INF/lib/hutool-core-5.8.25.jar!/");
        // 创建 LaunchedURLClassLoader
        ClassLoader appClassLoader = LaunchedURLClassLoaderDemo.class.getClassLoader();
        try (LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(new URL[]{jsonUrl, coreUrl}, appClassLoader)) {
            String coreJarClass = "cn.hutool.core.thread.AsyncUtil";
            String jsonJarClass = "cn.hutool.json.JSONUtil";
            // 确保当前运行环境的classpath没有指定的class
            Assertions.assertThrows(ClassNotFoundException.class, () -> appClassLoader.loadClass(coreJarClass));
            Assertions.assertThrows(ClassNotFoundException.class, () -> appClassLoader.loadClass(jsonJarClass));
            // 使用自定义的LaunchedURLClassLoader加载class
            loadAndAssert(classLoader, coreJarClass);
            loadAndAssert(classLoader, jsonJarClass);
        }
        jarFile.close();
    }


    private static void loadAndAssert(ClassLoader loader, String className) throws Exception {
        Class<?> clazz = loader.loadClass(className);
        Assertions.assertNotNull(clazz);
        Assertions.assertSame(loader, clazz.getClassLoader(), "Class not loaded from expected classloader");
        System.out.printf("Loaded class: %-40s → %s%n", className, clazz.getProtectionDomain().getCodeSource().getLocation());
    }

}

总结

通过上诉启动流程和LaunchedURLClassLoader#loadClass源码的分析,现在可以对前面的核心组件进行串联一下

  1. Archive组件仅在fatjar启动阶段使用,负责将fatjar内所有资源解析为jar协议的classpath URL,并用其构造LaunchedURLClassLoader,用于后续类加载的使用

  2. JarFile / JarEntry / URLStreamHandler / JarURLConnection:用于类加载阶段,流程如下

    ​ LaunchedURLClassLoader#loadClass → URLClassPath#getResource → URL#openConnection → URLStreamHandler 创建 JarURLConnection解析和获取 JarFile 和 JarEntryJarURLConnection#getInputStream -> InputStream加载 .class 字节 → defineClass