在运用Spring Boot 后,我们基本上摆脱之前项目每次上线的时候把项目打成war包。当然也不排除一些奇葩的规定,必须要用war包上线,不过很多时候,我们对一些东西只是处在使用的阶段,并不会去深入的研究使用的原理是什么,这貌似也是大多数人的固定思维。
或许正是如此,总会有些没有固定思维的人会去积极的探索原理,当然这话不是说我是积极的,我其实也是只原理的搬运工。今天和大家来简单的说下Spring Boot 的项目在运行Java -jar的原理。
在正式开始之前,我们先来看看把jar包进行解压。然后用tree /f命令查看目录结构(由于笔者写博文时用的是window,所以用的是tree /f命令),由于目录结构太长,这里做了相应省略,如下:
├─BOOT-INF
│ ├─classes
│ │ │ application.properties
│ │ │
│ │ └─com
│ │ └─spring
│ │ └─boot
│ │ └─test
│ │ SpringBootTestApplication.class
│ │
│ └─lib
│ classmate-1.5.1.jar
│ hibernate-validator-6.0.18.Final.jar
│ …………此处省略…………
│
├─META-INF
│ │ MANIFEST.MF
│ │
│ └─maven
│ └─com.spring.boot.test
│ └─spring-boot-test
│ pom.properties
│ pom.xml
│
└─org
└─springframework
└─boot
└─loader
│ ExecutableArchiveLauncher.class
│ JarLauncher.class
│ LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
│ LaunchedURLClassLoader.class
│ Launcher.class
│ MainMethodRunner.class
│ PropertiesLauncher$1.class
│ PropertiesLauncher$ArchiveEntryFilter.class
│ PropertiesLauncher$PrefixMatchingArchiveFilter.class
│ PropertiesLauncher.class
│ WarLauncher.class
│
├─archive
│ Archive$Entry.class
│ …………此处省略…………
│
├─data
│ RandomAccessData.class
│ …………此处省略…………
│
├─jar
│ AsciiBytes.class
│ Bytes.class
│ …………此处省略…………
│
└─util
SystemPropertyUtils.class
先简单说下上面目录结构,大体目录分三层:BOOT-INF、META-INF、org,BOOT-INF是存放对应的应用服务的.class文件和Maven依赖的jar包,包括启动类SpringBootTestApplication,META-INF下存放的是Maven相关的pom信息和MANIFEST.MF文件,org文件夹下存放的是Spring boot loader模块编译的.class文件,也就是jar启动的关键代码所在。
在执行java -jar命令的时候,它的启动类配置实在jar包目录下META-INF文件夹下的名MANIFEST.MF文件中,在这个文件中有一个名为Main-Class的属性,我们来看下这个文件的具体内容:
Manifest-Version: 1.0
Implementation-Title: spring-boot-test
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.spring.boot.test.SpringBootTestApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.3.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
从上面的配置文件中,可以看到Main-Class属性指向的Class为org.springframework.boot.loader.JarLauncher,而JarLauncher是JAR的启动器,这个类是在org/springframework/boot/loader/,然后可以看到项目所定义的启动类是指向Start-Class这个属性的。
在上面我们说了JarLauncher是JAR可执行的启动器,那么它和项目的启动类SpringBootTestApplication有什么关联呢?先给大家来个示例,先来到解压目录下执行命令:java org.springframework.boot.loader.JarLauncher ,然后便是如下界面:
C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT>java org.springframework.boot.loader.JarLauncher
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.3.RELEASE)
2020-01-18 14:28:19.866 INFO 3644 --- [ main] c.s.boot.test.SpringBootTestApplication : Starting SpringBootTestApplication on LAPTOP-R2NNI9CM with PID 3644 (C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT\BOOT-INF\classes started by elisha in C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT)
从上面的执行接口可以看到项目引导类SpringBootTestApplication会被JarLauncher类进行引导,但是如果我们到BOOT-INF/class目录下,然后也执行java com.spring.boot.test.SpringBootTestApplication,会报SpringApplication的ClassNotFoundException这个错误,由此可以得知这是因为java命令未指定Class Path。不过当前Spring Boot依赖的JAR文件都是存放在BOOT-INF/lib下,而org.springframework.boot.loader.JarLauncher会将JAR作为SpringBootTestApplication类库的依赖,这也就是为什么JarLauncher能引导SpringBootTestApplication,反之则是不可以的,那么对于SpringBootTestApplication是JarLauncher的子进程,还是处于同一层级呢?接下来我们来看看JarLauncher的原理。
因为org.springframework.boot.loader.JarLauncher的类是在spring-boot-loader中,但是若想在IDEA中来看源码,需要在pom文件中引入如下配置:
org.springframework.boot
spring-boot-loader
provided
在引入上面的配置文件后,便可以在IDEA中查看源码了,使用CTRL+N命令来搜索JarLauncher类,那就来看下源码,如下:
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
从上面的JarLauncher类中,可以看到两个常量:BOOT_INF_CLASSES、BOOT_INF_LIB,而它们又分别指向如下路径:BOOT-INF/classes/、BOOT-INF/lib/,并用isNestedArchive(Archive.Entry entry)方法进行判断(在Spring Boot中Archive,抽象出了Archive的概念,一个Archive可以是一个Jar(JarFileArchive)、也可以是一个目录(ExplodedArchive),在这里可以理解为Spring Boot抽象出来的同一访问资源层。),从isNestedArchive方法的参数Archive.Entry对象貌似为一个JAR文件中的资源,譬如application.properties,同时这个对象和JarEntry是类似的,其name属性(Archive.Entry#getName())便是Jar资源的相对路径。当application.properties资源在FAT JAR目录下时,其实Archive.Entry#getName()就是/BOOT-INF/classes/application.properties,此时便符合startsWith方法的判断,所以isNestedArchive(Archive.Entry entry)便返回为true。当返回为false时,便说明FAT JAR被解压到文件目录了,由此也说明了Spring Boot应用可以通过java org.springframework.boot.loader.JarLauncher 命令启动的原因了。
上面说了在Spring Boot中Archive,抽象出了Archive的概念,一个Archive可以是一个Jar(JarFileArchive)、也可以是一个目录(ExplodedArchive),这里所说的JarFileArchive、ExplodedArchive便是Archive的两种是想方式,对于这两个类的实现代码感兴趣额同学可以自己去看看。
不过由此也说明了JarLauncher 既支持JAR启动,又支持文件系统启动。同时JarLauncher 在作为引导类的时候,当执行java -jar 命令式,/META-INF/ 下MANIFEST.MF文件中的Main-Class属性将调用它的,main(String [])方法,其实它还是调用JarLauncher #launch(args)方法,这个方法是实现基类Launcher,这里简单看下这个方法的实现:
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
本篇文章简单的讲解了一下,java -jar命令的一个执行的原理,首先说了下jar包目录和jar命令启动入口,然后说了下JAR文件启动器——JarLauncher和JarLauncher实现引导原理,最后说了下Archive.Entry的实现,这个实现的原理也是比较复杂,后面如果有机会,会再写篇文章来进行说明。