SpringBoot(一)— fatJar启动和LaunchedURLClassLoader加载class的原理
基于 Spring Boot 2.7.x,从整体架构角度出发,梳理并串联了 fatjar 模式下的关键组件:Archive、JarFile、JarEntry、Handler、JarURLConnection,重点分析它们在类加载过程中的职责与协作关系。全文不聚焦具体 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部分
- /META-INF/MANIFEST.MF文件:jar的元信息文件(包含项目结构和启动类)。比如java -jar命令就是执行其中Main-Class的main方法
- SpringBoot Loader 相关类放在 jar 根路径:让LaunchedURLClassLoader相关依赖可以直接被AppClassLoader加载,无需特殊处理
特有的拓展
- 所有业务资源都被封装在
/BOOT-INF/下而不直接在跟路径:这部分资源不是标准的JVM classpath路径,不能被AppClassLoader加载,只能由SpringBoot提供的LaunchedURLClassLoader加载 - 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。两个核心方法如下:
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!/
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 内容的解包与访问能力。核心方法如下:
- JarFile#getInputStream(java.util.zip.ZipEntry):获取指定 entry 的InputStream,可用于读取
.class文件、配置文件、资源等内容 - JarFile#getJarEntry(java.lang.String):获取当前jar内部的指定资源对象(JarEntry)。支持查找 class 文件、配置文件、嵌套 jar 等
- 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 提供的抽象类,用于处理特定协议(如 http、file、jar)的 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 和配置资源。核心方法如下
- JarURLConnection#getJarEntry:将当前URL资源解析为JarEntry
- JarURLConnection#getInputStream:通过JarFile获取该资源对应的InputStream,以直接读取
.class文件或配置资源
fatjar启动流程
入口为JarLauncher#main方法,JarLauncher继承了ExecutableArchiveLauncher
ExecutableArchiveLauncher实例化
主要做两件事:
- Launcher#createArchive:确定当前启动类是jar包还是目录启动,jar启动使用JarFileArchive,目录启动则使用ExplodedArchive
- 加载classpath.idx文件:只有目录启动模式(exploded)才会使用/BOOF-INF/classpath.idx文件来加速classpath URL的构建
Launcher#launch
启动器的核心入口,主要完成以下三项工作:
- fatjar启动才注册JarFile.registerUrlProtocolHandler(exploded为解压模式下启动,这种不存在嵌套jar)。让jar协议的URL使用SpringBoot提供的Handler,去创建支持解析嵌套jar内容的URLConnection
- 解析classpath URL,并创建相关的LaunchedURLClassLoader
- 绑定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
- BOOT-INF/lib/ 下所有jar分别各自对应一个Archive
- 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-Class 的 main 方法,主要完成以下工作:
解析 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核心逻辑总结:
- 通过内部的URLClassPath查找指定类名对应的资源,封装为
Resource - 从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 的处理流程如下:
- 延迟创建并缓存每个 URL 对应的
Loader - Loader构造具体目标资源的 URL,并通过其协议的
URLStreamHandler创建URLConnection - 若连接有效,返回封装好的
Resource对象(包含 InputStream 等) - 若未命中资源,继续遍历下一个
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源码的分析,现在可以对前面的核心组件进行串联一下
Archive组件仅在fatjar启动阶段使用,负责将fatjar内所有资源解析为jar协议的classpath URL,并用其构造LaunchedURLClassLoader,用于后续类加载的使用
JarFile / JarEntry / URLStreamHandler / JarURLConnection:用于类加载阶段,流程如下
LaunchedURLClassLoader#loadClass → URLClassPath#getResource → URL#openConnection → URLStreamHandler 创建 JarURLConnection → 解析和获取 JarFile 和 JarEntry → JarURLConnection#getInputStream -> InputStream加载 .class 字节 → defineClass