前面我们学过Spring定制了自己的可执行jar,将真正执行时需要的类和依赖放到BOOT-INF/classes、BOOT-INF/lib来,为了能够识别这些为止的源文件,Spring定制了自己类加载器,本节我们来讲解这个类加载器。本节涉及的内容主要包括:
- SpringBoot启动流程
- SpringBoot类加载器
1. SpringBoot启动流程
在SpringBoot的可执行jar中,配置了MANIFEST.MF的Main-Class设置为JarLauncher,JarLauncher的加载过程如下图
- 调用Launcher.launch方法,开始引导程序
- 通过Handlers.reigster()注册URLStreamHandler的jar协议处理类,支持getResource读取BOOT-INF/下的内容
- 通过archive.getClassPathUrls()筛选可执行jar的所有元素,通过JarLauncher.isIncludedOnClassPath过滤,筛选出所有ClassPath元素
- 用这些筛选的ClassPath创建LaunchedClassLoader
- 使用LaunchedClassLoader加载MANIFEST.MF的Start-Class
- 反射调用main方法
2. SpringBoot类加载器
使用URLClassLoader加载SpringBoot Jar时不会加载BOOT-INF下的classes、lib的类和依赖包,SpringBoot通过自定义类加载器LaunchedClassLoader解决这个问题。它继承自URLClassLoader。参照SpringBoot的启动流程,我们讲手动使用LaunchedClassLoader加载可执行jar,并尝试引导SpringBoot应用。
1. 创建Archive
从启动流程可以看到,核心流程是先通过Archive.getClassPathUrls()获取所有的ClassPath,包括jar文件中内嵌的。JarFileArchive是包可见的,用Archive.create创建实例
URLClassLoader ucl = new URLClassLoader("MyURLClassLoader", new URL[]{url}, null);
Class<?> clazz = ucl.loadClass("org.springframework.boot.loader.JarLauncher");
Archive archive = Archive.create(clazz);
2. 枚举内嵌Jar
Archive提供了getClassPathUrls筛选所有的Archive.Entry,Archive和Archive.Entry可以按JarFile、JarEntry来理解,正是它们有不同的抽象,不仅仅支持jar文件。
Set<URL> su = archive.getClassPathUrls((Archive.Entry e) -> true);
su.stream().forEach(x -> System.out.println(x));
这个方法的输出是这样的,它会是一些jar:nested:协议的URL集合。我们知道URLClassLoader接收一组根路径实现类加载,把这个集合都传递给URLClassLoader是不是就可以了呢?
3. 使用内嵌Jar
讲上一步获取的Set作为根路劲提供给URLClassLoader
URLClassLoader lcl = new URLClassLoader("MyNested", su.toArray(URL[]::new),null);
clazz = lcl.loadClass("com.keyniu.yangsi.YangsiApplication");
System.out.println(clazz);
并不能正常工作,因为jar:nested不是Java支持的内嵌协议
4. 注册协议处理器
在SpringBoot的启动流程里我们专门有提到过Handlers,它就是用来注册协议处理器的在使用3.3.3的代码之前,执行如下代码,就能执行成功了。
Handlers.register();
Handlers.register()实际是通过包名: org.springframework.boot.loader.net.protocol,注册了jar、nested(包名)这两个协议的处理器Handler(对于包下固定类名)
5. 启动SpringBoot
现在我们已经能完整的加载整个SpringBoot可以执行jar了,我们是不是能像JarLauncher一样调用Start-Class的main方法来启动应用呢?答案是肯定的。
private static void testLaunched(URL url) throws Exception {
URLClassLoader ucl = new URLClassLoader("MyURLClassLoader", new URL[]{url}, null);
Class<?> clazz = ucl.loadClass("org.springframework.boot.loader.JarLauncher");
Archive archive = Archive.create(clazz);
Set<URL> su = archive.getClassPathUrls((Archive.Entry e) -> true);
Handlers.register();
su.add(url);
LaunchedClassLoader lcl = new LaunchedClassLoader(false, su.toArray(URL[]::new), null);
clazz = Class.forName("com.keyniu.yangsi.YangsiApplication", false, lcl); // 启动类
Method mainMethod = clazz.getDeclaredMethod("main", String[].class);
// main方法
mainMethod.setAccessible(true);
mainMethod.invoke(null, (Object) new String[0]); // 调用main方法
}
5. 参考资料
- JVM Specification: Loading、Linking And Initializing,Chapter 5. Loading, Linking, and Initializing
- SClass Loaders in Java | Baeldung