Things you may not know about the jar startup principle in SpringBoot

Get into the habit of writing together! This is the sixth day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

foreword

This article needs to understand Classloader, URL and SpringBoot as a foundation.

I believe you have seen the jar format directory packaged by the SpringBoot plugin, but today we will talk about something else, that is, how SpringBoot loads the classes in the /BOOT-INF/lib/ directory.

In the standard jar format, jars cannot be nested and cannot be loaded. As we have said in the previous article, if your application needs to be packaged with other jars, there are two solutions. One is to decompress these jars together with you. The main jars are put together, but the directory structure is scattered. The second is to customize the Classloader. SpringBoot adopts the second method.

The next step is to study the Classloader used to load these classes in SpringBoot, but after entering the source code, you will indeed find that there is a LaunchedURLClassLoader. The first thing he loads is the startup class of our project, and all subsequent classes are also loaded by him.

But the problem is that the core source code is not in the LaunchedURLClassLoader, it is useless to look at it, it can actually be completed without the LaunchedURLClassLoader, and the core source code is in the URLClassLoader.

URLClassLoader knows by looking at the name that it loads the class according to the URL. Give him a URL address of the jar, and he can load the class in it without additional intervention, such as the following.

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"))
}
复制代码

So, how to represent the jar address in the jar? You might think like this.

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

This is not the correct approach. The above method cannot fulfill our expected requirements, but there is a jar protocol in java, which can represent a resource in the jar. The complete writing is like this.

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

This way of writing can directly read any path resource in the jar and convert it to InputStream, which is the way java itself provides us.

但问题就是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);
}
复制代码

The implementation class of URLStreamHandler is org.springframework.boot.loader.jar.Handlerthat he tries to return org.springframework.boot.loader.jar.JarURLConnection. Note that there is also a JarURLConnection in java. SpringBoot has the same name as him. This class is to read the class in the jar in the jar, and the rewritten getInputStream returns the input stream of this class.

At this point, URLClassPath can be returned. The following is the object it returns. Note that there is a getBytes method in Resource, which is used to return the byte array in this input stream.

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();
   }
};
复制代码

Then it will enter the following method of URLClassLoader. Note that this method is not unique to Classloader, but URLClassLoader's own. It is used to return Class from Resource, so this is connected with the above result.

private Class<?> defineClass(String name, Resource res)
复制代码

test code

Note that the org.springframework.boot.loader library is no longer in any dependencies in SpringBoot by default, and needs to be added by itself. The maven address is as follows

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"))
}
复制代码

Guess you like

Origin juejin.im/post/7084532763625259038