我准备战斗到最后,不是因为我勇敢,是我想见证一切。 –双雪涛《猎人》
文章目录
Thinking
- 一个技术,为什么要用它,解决了那些问题?
- 如果不用会怎么样,有没有其它的解决方法?
- 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
- 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
- 这些问题你又如何去解决的呢?
声明:本文基于springboot 2.1.3.RELEASE
正文的第一个疑问就是:
- springboot 为什么可以直接使用jar 运行?
- 那么jar包的内部解构是怎么样的?
- springboot 的启动过程又是怎么样的?
这里首先声明在java的归档文件中(jar)的规定:
在一个jar文件当中,我们必须要将指定为
Main-Class
的那个类作为顶层的jar文件(就是该启动类的jar文件必须放置在整个解压目录或者jar文件内部的顶层目录下),意思就是,执行的
Main-Class
是不允许被嵌套的。 对于一个
Jar
文件来说,被指定为入口类
Main-Class
的一个具体类,那么这个类连同他的包结构,一定是位于这个
Jar
文件的顶层目录,而不能再位于其他的子目录中。
- 以上两种说法,强调了SpringBoot就存在了问题,在一个打包完jar文件后,只能允许一个
Main-Class
入口类,可是我们自己写的启动类是根本不具备启动整个项目的实力的,那么
SpringBoot
是怎么完美的避开这种协议,实现jar包中嵌套用户启动类,并且可以完美随处运行的呢?
1、SpringBoot jar包的内部结构
将打包好的Jar包,使用命令解压出来
- 存放的是 配置文件和编写的字节码文件
- 存放的是自动生成的一些配置信息(类似于:maven配置,jdk环境、os环境等)
you should not put anything into META-INF yourself. Instead
- 存放的是真正的springboot 的启动文件的字节码文件
![]()
- 大致可以看到在该启动类下的路径,尝试使用该路径引入jar依赖
<!--引入 springboot 启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
引入该jar后,可以简单的发现,该jar包的所有路径都是跟解压出来的jar的loader路径下的内容相同的。
不难发现了,speingboot正是使用的
spring-boot-loader
这个jar来完成springboot 的整个启动加载动作的。
在源码面前,所有的花哨都是扯淡的😄,来🐱一眼里面最显眼的
JarLauncher
类。
是不是一眼就感觉到亲切无比了哈?😄
这里声明一点,我们引入的
spring-boot-loader
是不会影响springboot 的启动的。但是会在加载的lib目录下生成jar文件,我们重新打包一次,🐱一眼。
其他目录并没有什么改变。
至此,大概的了解了,springboot 打包为jar后的整个目录样子。
下面,探讨一下这种路径的优点,或者说为什么是这样的结构
2、SpringBoot-启动原理
2.1、为什么SpringBoot打包的jar可以直接运行
当执行
java -jar spring_lecture-0.0.1.jar
命令时,整个jar的入口是什么?
在上面简单的查看了一下整个jar包的目录结构,在Java运行jar时,是根据
MATA-INF
中的
.MF
文件,寻找到
Main-Class
指定的全限定名进行加载的。
<jar ...>
<manifest>
<attribute name="Main-Class" value="MyApplication"/>
</manifest>
</jar>
所以,在jar文件中,
MATA-INF
中的
.MF
文件定义了启动类
Main-class
:即为整个jar文件的启动类(入口类)。
Start-Class
:就是我们自己定义的springboot启动类了。
可以看出来,整个文件都是以
key-value
形式的数据储存的。
所以在上面,引入了
org.springframework.boot.loader.JarLauncher
jar,就是为了可以看到该jar的源码的。🙂
2.2、JarLauncher 流程详解
org.springframework.boot.loader.JarLauncher
Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.
该launcher是基于jar协议的,
它假设依赖的jar文件是位于/BOOT-INF/lib下的
工程自己的依赖的jar是位于 /BOOT-INF/classes目录下的。
这样就可以看出来对应指定的两个目录
因此可以看出,这两个目录并不符合java对加载jar文件的标准,这是spring自己定义的两个文件目录
可以看出,在classes目录下是我们自己定义的文件,而lib目录是整个项目依赖的第三方jar包。
2.2.1、JarLauncher源码
可以看到在
ExecutableArchiveLauncher
下有两个实现,侧面说明了,springboot 也支持将工程打包成
war
包的。
-
现在我们就从
JarLauncher
类的
Main
方法入手,一步一步探索springboot 是如何巧妙的实现在规避java jar文件的规格下完成加载的。
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
该main方法对当前类进行初始化,在类加载中,会先初始化父类。先来🐱一眼在顶层类中的初始化情况。
Launcher
类初始化时,并没有做过多的初始化动作。
ExecutableArchiveLauncher
:一个可以执行的基类Launcher档案。该类在初始化时,会创建出指定的目录
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
// 定位当前被执行的那个jar文件的,jar文件的绝对路径
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain(); // 安全检查呗
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
String path = (location != null) ? location.getSchemeSpecificPart() : null;
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
![]()
在基类的初始化中,可以看到是为了将本地的jar文件读取到,并且根据文件类型创建
archive
![]()
- 发现一个好玩的东西:在
Debug
执行
org.springframework.boot.loader.JarLauncher#main
后,会报![]()
- 可以看到,在创建完
JarLauncher
后,该对象会去指定的
MATA-INF
中的
.MF
文件中寻找
Start-Class
指定的类
-
在父子 类都初始化完成后,调用
org.springframework.boot.loader.Launcher#launch(java.lang.String[])
方法。
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
在doc文档中也说的很清楚了,这是一个应用的Launch,该方法是一个实现的入口,应该由该类的子类使用Main函数直接调用。
/** * Register a {@literal 'java.protocol.handler.pkgs'} property so that a * {@link URLStreamHandler} will be located to deal with jar URLs. */ public static void registerUrlProtocolHandler() { String handlers = System.getProperty(PROTOCOL_HANDLER, ""); System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); resetCachedUrlHandlers(); }
![]()
ClassLoader classLoader = createClassLoader(getClassPathArchives());
创建类加载器
![]()
launch(args, getMainClass(), classLoader);
- 该方法中的
getMainClass()
会获取,启动清单,可以看出这里就是获取用户定义的入口类文件
Main-Class
类,见名知意!- 但是SpringBoot在其中耍了一个小聪明,将两个启动类嵌套,并且将启动本身
Jar
的类加载器指定去加载用户定义的
Main-class
并起名为
Start-Class
,这样一来,完美的实现了两个启动类嵌套并且绕开了java jar协议的情况下,实现了整个springboot 打包的jar的运行环境(
还有一个伏笔就是,为什么我们引入的lauch jar 是不会影响springboot 程序的运行的,并且官方也没有引入该依赖。
见下文)。![]()
![]()
- 到这一步,就是将所有的准备工作全部做好了,所有类路径都初始化完毕了。
2.2.2、Springboot jar 的思想
ClassLoader classLoader = createClassLoader(getClassPathArchives());
该行就是整个springBoot运行jar的核心思想。
org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchives
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive)); // 方法引用
postProcessClassPathArchives(archives);
return archives;
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
- 用于匹配所有的
BOOT-INF/classes/
的用户文件,
BOOT-INF/lib/
下所有的
jar
文件。- 但是从上面第一节就知道了,在SpringBoot 的整个jar文件中顶层是有三个文件夹的,那么为什么要这样设计呢?
- 原因就是在于Java对于jar文件的加载有协议,在前言就有说到, 对于一个
Jar
文件来说,被指定为入口类
Main-Class
的一个具体类,那么这个类连同他的包结构,一定是位于这个
Jar
文件的顶层目录,而不能再位于其他的子目录中。所以,Java在jar目录中是不允许jar文件嵌套的,那么拥有其他
Main-Class
的类是无法被启动的。- 所以SpringBoot就想到了一种解决方案,就是将SpringBoot的启动的Jar文件里面的内容单独复制出来一份,放在目录的根目录,这样一来,SpringBoot的入口类:
org.springframework.boot.loader.JarLauncher
是服务Java Jar规范的,是可以被Java的类加载器加载到的。- 那么就引出了一个问题,在Java jar规范中,
org.springframework.boot.loader.JarLauncher
被加载后,是不允许其他文件或者jar被加载的,所以,SpringBoot使用的了自己的
SpringBootClassLoader
–
LaunchedURLClassLoader
来加载用户的入口类,和程序所依赖的所有jar包。- 这样一来,即符合了Java jar 的规范,又在这种协议中开辟了一条蹊径,实现了一个单独的jar,不需要单独的容器就可以实现运行了。
-
所以基于上述的思想,必须将SpringBoot,所依赖的
Main-Class
的
org.springframework.boot.loader.JarLauncher
类中的所有文件单独辅助出来,才能实现。 - 如果不全部复制出来,将所有的jar 放在一个文件家中,就会出现jar文件嵌套的问题,这样一来,就完成不了多jar文件的运行。
这种多Jar运行的模式在SpringBoot中有一个专有名词就是
FatJar
意味:在SpringBoot打包的jar文件中,是一个包含多个jar包的嵌套jar,是一个大胖子 🐖。
惊叹SpringBoot 的这种设计思想。不仅实现了web程序的多样化,还简化了环境配置的麻烦还有报错的可能性。
下节将会详细介绍SpringBoot是如何使用自己的类加载器去加载这些第三方类的。
本文仅供笔者本人学习,有错误的地方还望指出,一起进步!望海涵!
转载请注明出处!
欢迎关注我的公共号,无广告,不打扰。不定时更新Java后端知识,我们一起超神。
![]()
——努力努力再努力xLg
加油!