进入spring boot 时代,java开发者们都熟知使用spring-boot-maven-plugin,可以将web项目打成jar包,然后java -jar 就可以执行,各位是否考虑过底层是怎么做的?类是如何加载的?以前为什么不是这么做的?
在了解具体细节之前,我们需要先了解一下不使用任何打包工具时,java程序时如何完成引用的,以及如何使用jdk默认的jar命令进行打包。
java程序加载jar包
在不使用任何工具的情况下,java程序加载jar包可以通过指定classpath的方式。
java -classpath <目录和 zip/jar 文件的类搜索路径>
用 : 分隔的目录, JAR 档案
和 ZIP 档案列表, 用于搜索类文件。
大家面试的时候一定遇到过类加载、双亲委派等问题。
类加载器从顶向下依次是:
BootStrapClassLoader->ExtClassLoader->AppClassLoader。
其中AppClassLoader作为默认类加载器,读取的就是classpath路径下的类文件。
我们不指定classpath时,默认会将 当前目录.
加入到classpath中。
使用 jar命令打jar包
首先我简单创建这样一个结构,其中App类内有main方法。
将项目编译一下,编译主类,依赖的类都会被编译。
javac com/yuvenhol/App.java
使用jar命令打jar包,注意这里需要使用-e命令,然后指定jar包入口。
jar -cvfe mytest.jar com.yuvenhol.App .
我们可以解开jar包,看看里面都有什么。
可以看到除了源码、字节码和原数据(META-INF)。MANIFEST.MF内容如下。
Manifest-Version: 1.0
Created-By: 1.8.0_312 (Azul Systems, Inc.)
Main-Class: com.yuvenhol.App
这里Main-Class和打jar包时指定的入口一致。当我们使用java -jar 运行jar包时,读取MANIFEST.MF内容,读到正确的class。
spring boot程序的jar包的不同
以sentinel-dashboard.jar 为例,我们先看一个可运行的jar包里面都有什么。
首先看最重要的MANIFEST.MF 文件里面有什么内容。
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Built-By: sczyh30
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.alibaba.csp.sentinel.dashboard.DashboardApplication //应用程序的main方法
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.5.12
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_152
Main-Class: org.springframework.boot.loader.JarLauncher //jar包启动的入口
需要注意的是,springboot 可运行jar包并非标准jar包,实际上是一个springboot特制的jar包。
我们可以看到里面描述了,jar包运行的入口(Main-Class)、springboot版本、lib所在的位置和lib目录等等信息,以及docker build所需的layers描述(这是一个boot2.3推出的功能,旨在减少docker iamge体积,具体细节可以看SpringBoot-2.3镜像方案为什么要做多个layer)。
为什么要搞一个JarLauncher 来作为启动类,而不直接使用我们定的main函数呢?
因为java规范默认是不支持嵌套jar包,在过去要实现类似的功能通常将多个jar包内的类提出来,再打到一个jar包内,这会带来很多冲突问题,并不好维护。而spring boot使用一个巧妙的方法绕过了这个限定,其中的奥秘就在arLauncher内。这是spring-boot-loader模块的内容,你可以直接下载源码了解,当然你只是想粗看一下也可以在github上看一下源码 link。
代码写的比较复杂,抽象程度很高。我给大家简述一下,实际上就是在运行JarLauncher之后,根据BOOT-INF/classpath.idx内存储的jar包文件索引,加上我们的源码地址,组合成成文件资源列表被称为urls,使用urls作为参数创建了一个自定义的类加载器LaunchedURLClassLoader (和默认类加载器AppClassLoader一样extend URLClassLoader)。然后使用这个类加载器再去加载我们自己的main函数,需要类加载时会遍历urls去找是否有所需的class文件。