SpringBoot中jar启动原理你可能不知道的事情

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

本文需要了解Classloader、URL以及SpringBoot作为基础铺垫。

相信大家已经看过SpringBoot插件打包后的jar格式目录了,但今天我们说点别的,也就是SpringBoot是怎么加载/BOOT-INF/lib/目录下的class的。

标准的jar格式中是不能嵌套jar的,无法加载,在前面文章我们已经说过了,如果你的应用需要连其他jar一起打包,解决办法有两个,一是把这些jar解压后连同你的主jar放在一起,但这样目录结构就比较散落,二是自定义Classloader,SpringBoot就是采用第二种方式。

接下来就是研究SpringBoot中用来加载这些类的Classloader了,但是进入源码后,确实会发现有一个LaunchedURLClassLoader,他第一个加载的就是我们项目的启动类,后续所有的class也都由他加载。

但是问题就出在,核心源码不在LaunchedURLClassLoader中,看也白看,没有LaunchedURLClassLoader其实也能完成,核心源码在URLClassLoader中。

URLClassLoader看名称就知道他是根据URL加载class的,给他一个jar的URL地址,他就可以加载其中的类,不需要额外的干预,比如下面这样。

fun main() {
    var classloader = URLClassLoader(arrayOf(URL("file:/home/HouXinLin/project/java/XiaoAi/build/libs/XiaoAi-1.0-SNAPSHOT.jar")))
    println(classloader.loadClass("com.hxl.xiaoai.anno.Action"))
}

那么,如何表示jar中的jar地址呢?你可能会想到这样。

file:/home/HouXinLin/project/java/XiaoAi/build/libs/XiaoAi-1.0-SNAPSHOT.jar/demo.jar

这并不是正确的做法,上面这种方式是并不能完成我们期望的需求,但是java中内部有一个jar协议,可以表示jar中的一个资源,完整的写法是这样的。

jar:file:/home/HouXinLin/project/java/XiaoAi/build/libs/XiaoAi-1.0-SNAPSHOT.jar!/demo.jar

这样写法可以直接读取jar中的任何路径资源,转换为InputStream,这是java自身为我们提供的方式。

但问题就是URLClassLoader也不识别这样的路径,在说回来,SpringBoot给LaunchedURLClassLoader传递的就是这样的路径,比如你的项目main.jar依赖一个demo.jar,最终SpringBoot插件会把他放入/BOOT-INF/lib/下,在构建出下面这个URL地址,传递给LaunchedURLClassLoader,神奇的是你会发现这货居然能加载,不报错,而你试的时候永远看到的是ClassNotFoundException。

jar:file:/home/main.jar!/BOOT-INF/lib/demo.jar

解密

问题就出在URL上,看过他源码的人知道他构造方式有一个URLStreamHandler参数,这个参数用来自定义解析地址中的资源,在需要InputStream的时候根据你的需求自行转换。而SpringBoot中就是利用了这一特点。

但是首先得看URLClassLoader是怎么加载Class的,才能明白自定义URLStreamHandler的作用。

进入他重写的findClass,做的事情也不多,就是使用URLClassPath尝试获取要加载的class的Resource对象,URLClassPath是URLClassLoader构造方法中初始化的,参数也是URL集合,用来给定一个路径,返回一个Resource对象。

但是这个URLClassPath是不能读取jar中jar中的class的,他读取的方法是通过URL的openConnection方法尝试返回一个URLConnection,如果有,则不为空,而谁来返回URLConnection呢?就是SpringBoot中自定义的URLStreamHandler,看URL的openConnection源码,可以看到直接调用的是URLStreamHandler。

public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}

而URLStreamHandler的实现类是org.springframework.boot.loader.jar.Handler,他尝试返回org.springframework.boot.loader.jar.JarURLConnection,注意java中也有一个JarURLConnection,SpringBoot的和他同名,这个类就是读取jar中jar中的class,其中重写的getInputStream返回这个class的input流。

到这里URLClassPath就可以返回了,下面是他返回的对象,注意Resource中有一个getBytes方法,用来返回这个input流中的字节数组。

return new Resource() {
   public String getName() {
       return var1;
   }
   public URL getURL() {
       return var3;
   }
   public URL getCodeSourceURL() {
       return Loader.this.base;
   }
   public InputStream getInputStream() throws IOException {
       return var4.getInputStream();
   }
   public int getContentLength() throws IOException {
       return var4.getContentLength();
   }
};

然后会进入到URLClassLoader的下面这个方法,注意这个方法不是Classloader特有的,是URLClassLoader自己的,用来从Resource中返回Class,那么这就和上面的结果连通了。

private Class<?> defineClass(String name, Resource res)

测试代码

注意, org.springframework.boot.loader这个库默认不再SpringBoot中任何依赖中,需要自行添加,maven地址如下

implementation("org.springframework.boot:spring-boot-loader:2.6.1")
fun testSpring() {
    val springJarFile =
        org.springframework.boot.loader.jar.JarFile(File("/home/HouXinLin/project/java/blog/build/libs/OneBlog-0.0.1-SNAPSHOT.jar"))
        
    val url: URL = springJarFile.getNestedJarFile(springJarFile.getJarEntry("BOOT-INF/lib/freemarker-2.3.31.jar")).url
   
   println(URLClassPath(arrayOf(url)).getResource("freemarker/cache/AndMatcher.class"))
   
    var classloader 
= URLClassLoader(arrayOf(url))
    println(classloader.loadClass("freemarker.cache.AndMatcher"))
}

- END -


原文始发于微信公众号(十四个字节):SpringBoot中jar启动原理你可能不知道的事情