SpringBootjava-jar命令行启动原理解析
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
SpringBootjava-jar命令⾏启动原理解析
在spring boot⾥,很吸引⼈的⼀个特性是可以直接把应⽤打包成为⼀个jar/war,然后这个jar/war是可以直接启动的,⽽不需要另外配置⼀个Web Server。
那么spring boot如何启动的呢?今天我们就来⼀起探究⼀下它的原理。
⾸先我们来创建⼀个基本的spring boot⼯程来帮助我们分析,本次spring boot版本为 2.2.5.RELEASE。
// SpringBootDemo.java
@SpringBootApplication
public class SpringBootDemo {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemo.class);
}
}
下⾯是pom依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<finalName>springboot-demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
创建完⼯程后,执⾏maven的打包命令,会⽣成两个jar⽂件:
springboot-demo.jar
springboot-demo.jar.original
其中springboot-demo.jar.original是默认的maven-jar-plugin⽣成的包。
springboot-demo.jar是spring boot maven插件⽣成的jar包,⾥⾯包含了应⽤的依赖,以及spring boot相关的类。
下⾯称之为executable jar或者fat jar。
后者仅包含应⽤编译后的本地资源,⽽前者引⼊了相关的第三⽅依赖,这点从⽂件⼤⼩也能看出。
图1
关于executable jar,中是这样解释的。
Executable jars (sometimes called “fat jars”) are archives containing your compiled classes along with all of the
jar dependencies that your code needs to run.
Executable jar(有时称为“fat jars”)是包含您的已编译类以及代码需要运⾏的所有jar依赖项的归档⽂件。
Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained
within a jar). This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking.
Java没有提供任何标准的⽅式来加载嵌套的jar⽂件(即,它们本⾝包含在jar中的jar⽂件)。
如果您需要分发⼀个⾃包含的应⽤程序,⽽该应⽤程序可以从命令⾏运⾏⽽⽆需解压缩,则可能会出现问题。
To solve this problem, many developers use “shaded” jars. A shaded jar packages all classes, from all jars, into a single “uber jar”. The problem with shaded jars is that it becomes hard to see which libraries are actually in your
application. It can also be problematic if the same filename is used (but with different content) in multiple jars.
为了解决这个问题,许多开发⼈员使⽤ shaded jars。
⼀个 shaded jar 将来⾃所有jar的所有类打包到⼀个 uber(超级)jar 中。
shaded jars的问题在于,很难查看应⽤程序中实际包含哪些库。
如果在多个jar中使⽤相同的⽂件名
(但具有不同的内容),也可能会产⽣问题。
Spring Boot takes a different approach and lets you actually nest jars directly.
Spring Boot采⽤了另⼀种⽅法,实际上允许您直接嵌套jar。
简单来说,Java标准中是没有来加载嵌套的jar⽂件,就是jar中的jar的⽅式的,为了解决这⼀问题,很多开发⼈员采⽤shaded jars,但是这种⽅式会有⼀些问题,⽽spring boot采⽤了不同于shaded jars的另⼀种⽅式。
Executable Jar ⽂件结构
那么spring boot具体是如何实现的呢?带着这个疑问,先来查看spring boot打好的包的⽬录结构(不重要的省略掉):
图6
可以发现,⽂件⽬录遵循了下⾯的规范:
Application classes should be placed in a nested BOOT-INF/classes directory. Dependencies should be placed in a
nested BOOT-INF/lib directory.
应⽤程序类应该放在嵌套的BOOT-INF/classes⽬录中。
依赖项应该放在嵌套的BOOT-INF/lib⽬录中。
我们通常在服务器中使⽤java -jar命令启动我们的应⽤程序,在Java官⽅⽂档是这样描述的:
Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void
main(String[] args) method that serves as your application's starting point.
执⾏封装在JAR⽂件中的程序。
filename参数是具有清单的JAR⽂件的名称,该清单包含Main-Class:classname 形式的⾏,该⾏使⽤公共静态void main(String [] args)⽅法定义该类,该⽅法充当应⽤程序的起点。
When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings
are ignored.
使⽤-jar选项时,指定的JAR⽂件是所有⽤户类的源,⽽其他类路径设置将被忽略。
简单说就是,java -jar 命令引导的具体启动类必须配置在清单⽂件 MANIFEST.MF 的 Main-Class 属性中,该命令⽤来引导标准可执⾏的jar⽂件,读取的是 MANIFEST.MF⽂件的Main-Class 属性值,Main-Class 也就是定义包含了main⽅法的类代表了应⽤程序执⾏⼊⼝类。
那么回过头再去看下之前打包好、解压之后的⽂件⽬录,找到 /META-INF/MANIFEST.MF ⽂件,看下元数据:
Manifest-Version: 1.0 Implementation-Title: spring-boot-demo Implementation-Version: 1.0-SNAPSHOT Start-
Class: com.example.spring.boot.demo.SpringBootDemo Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-
Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.5.RELEASE Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
可以看到Main-Class是org.springframework.boot.loader.JarLauncher,说明项⽬的启动⼊⼝并不是我们⾃⼰定义的启动类,⽽是JarLauncher。
⽽我们⾃⼰的项⽬引导类com.example.spring.boot.demo.SpringBootDemo,定义在了Start-Class属性中,这个属性并不是Java标准的MANIFEST.MF⽂件属性。
spring-boot-maven-plugin 打包过程
我们并没有添加org.springframework.boot.loader下的这些类的依赖,那么它们是如何被打包在 FatJar ⾥⾯的呢?这就必须要提到spring-boot-maven-plugin插件的⼯作机制了。
对于每个新建的 spring boot⼯程,可以在其 pom.xml ⽂件中看到如下插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这个是 SpringBoot 官⽅提供的⽤于打包 FatJar 的插件,org.springframework.boot.loader 下的类其实就是通过这个插件打进去的;
当我们执⾏package命令的时候会看到下⾯这样的⽇志:
[INFO] --- spring-boot-maven-plugin:2.2.5.RELEASE:repackage (repackage) @ spring-boot-demo ---
[INFO] Replacing main artifact with repackaged archive
repackage⽬标对应的将执⾏到org.springframework.boot.maven.RepackageMojo#execute,该⽅法的主要逻辑是调⽤了org.springframework.boot.maven.RepackageMojo#repackage
// RepackageMojo.java
private void repackage() throws MojoExecutionException {
// 获取使⽤maven-jar-plugin⽣成的jar,最终的命名将加上.orignal后缀
Artifact source = getSourceArtifact();
// 最终⽂件,即Fat jar
File target = getTargetFile();
// 获取重新打包器,将重新打包成可执⾏jar⽂件
Repackager repackager = getRepackager(source.getFile());
// 查找并过滤项⽬运⾏时依赖的jar
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
// 将artifacts转换成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
try {
// 提供Spring Boot启动脚本
LaunchScript launchScript = getLaunchScript();
// 执⾏重新打包逻辑,⽣成最后fat jar
repackager.repackage(target, libraries, launchScript);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
// 将source更新成 xxx.jar.orignal⽂件
updateArtifact(source, target, repackager.getBackupFile());
}
// 继续跟踪getRepackager这个⽅法,知道Repackager是如何⽣成的,也就⼤致能够推测出内在的打包逻辑。
private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, youtFactory);
repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
// 设置main class的名称,如果不指定的话则会查找第⼀个包含main⽅法的类,
// repacke最后将会设置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (yout != null) {
getLog().info("Layout: " + yout);
repackager.setLayout(yout());
}
return repackager;
}
repackager设置了 layout⽅法的返回对象,也就是youts.Jar
/**
* Executable JAR layout.
*/
public static class Jar implements RepackagingLayout {
@Override
public String getLauncherClassName() {
return "org.springframework.boot.loader.JarLauncher";
}
@Override
public String getLibraryDestination(String libraryName, LibraryScope scope) {
return "BOOT-INF/lib/";
}
@Override
public String getClassesLocation() {
return "";
}
@Override
public String getRepackagedClassesLocation() {
return "BOOT-INF/classes/";
}
@Override
public boolean isExecutable() {
return true;
}
}
layout我们可以将之翻译为⽂件布局,或者⽬录布局,代码⼀看清晰明了,同时我们⼜发现了定义在MANIFEST.MF ⽂件的Main-Class属性org.springframework.boot.loader.JarLauncher了,看来我们的下⾯的重点就是研究⼀下这个JarLauncher了。
JarLauncher构造过程
因为org.springframework.boot.loader.JarLauncher的类是在spring-boot-loader中的,关于spring-boot-loader,spring boot的github上是这样介绍的:
Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched
using java -jar. Generally you will not need to use spring-boot-loader directly, but instead work with the or plugin.
Spring Boot Loader提供了秘密⼯具,可让您构建可以使⽤java -jar启动的单个jar⽂件。
通常,您不需要直接使⽤spring-boot-loader,⽽可以使⽤Gradle或Maven插件。
但是若想在IDEA中来看源码,需要在pom⽂件中引⼊如下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
找到org.springframework.boot.loader.JarLauncher类
// JarLauncher.java
public class JarLauncher extends ExecutableArchiveLauncher {
// BOOT-INF/classes/
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
// BOOT-INF/lib/
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);
}
// main⽅法
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
可以发现,JarLauncher定义了BOOT_INF_CLASSES和BOOT_INF_LIB两个常量,正好就是前⾯我们解压之后的两个⽂件⽬录。
JarLauncher包含了⼀个main⽅法,作为应⽤的启动⼊⼝。
从 main 来看,只是构造了⼀个 JarLauncher对象,然后执⾏其 launch ⽅法。
再来看⼀下JarLauncher的继承结构:
图2
构造JarLauncherd对象时会调⽤⽗类ExecutableArchiveLauncher的构造⽅法:
// ExecutableArchiveLauncher.java
public ExecutableArchiveLauncher() {
try {
// 构造 archive 对象
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
// 构造 archive 对象
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
// 这⾥就是拿到当前的 classpath 的绝对路径
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);
}
// 将构造的archive 对象返回
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
Archive
这⾥⼜需要我们先来了解⼀下Archive相关的概念。
archive即归档⽂件,这个概念在linux下⽐较常见
通常就是⼀个tar/zip格式的压缩包
jar是zip格式
public abstract class Archive {
public abstract URL getUrl();
public String getMainClass();
public abstract Collection<Entry> getEntries();
public abstract List<Archive> getNestedArchives(EntryFilter filter);
}
Archive是在spring boot⾥抽象出来的⽤来统⼀访问资源的接⼝。
该接⼝有两个实现,分别是ExplodedArchive和JarFileArchive。
前者是⼀个⽂件⽬录,后者是⼀个jar,都是⽤来在⽂件⽬录和jar中寻找资源的,这⾥看到JarLauncher既⽀持jar启动,也⽀持⽂件系统启动,实际上我们在解压后的⽂件⽬录⾥执⾏ java org.springframework.boot.loader.JarLauncher 命令也是可以正常启动的。
图3
在FatJar中,使⽤的是后者。
Archive都有⼀个⾃⼰的URL,⽐如
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!
Archive类还有⼀个getNestedArchives⽅法,下⾯还会⽤到这个⽅法,这个⽅法实际返回的是springboot-demo.jar/lib下⾯的jar的Archive列表。
它们的URL是:
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-
INF/lib/spring-boot-starter-web-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-
INF/lib/spring-boot-starter-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-
INF/lib/spring-boot-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-
INF/lib/spring-boot-autoconfigure-2.2.5.RELEASE.jar!/
省略......
launch()执⾏流程
archive构造完成后就该执⾏JarLauncher的launch⽅法了,这个⽅法定义在了⽗类的Launcher⾥:
// Launcher.java
protected void launch(String[] args) throws Exception {
/*
* 利⽤ .URLStreamHandler 的扩展机制注册了SpringBoot的⾃定义的可以解析嵌套jar的协议。
* 因为SpringBoot FatJar除包含传统Java Jar中的资源外还包含依赖的第三⽅Jar⽂件
* 当SpringBoot FatJar被java -jar命令引导时,其内部的Jar⽂件是⽆法被JDK的默认实现
* .www.protocol.jar.Handler当做classpath的,这就是SpringBoot的⾃定义协议的原因。
*/
JarFile.registerUrlProtocolHandler();
// 通过 classpath 来构建⼀个 ClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 1
launch(args, getMainClass(), classLoader); // 2
}
重点关注下createClassLoader(getClassPathArchives()) 构建ClassLoader的逻辑,⾸先调⽤getClassPathArchives()⽅法返回
值作为参数,该⽅法为抽象⽅法,具体实现在⼦类ExecutableArchiveLauncher中:
// ExecutableArchiveLauncher.java
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
该⽅法会执⾏Archive接⼝定义的getNestedArchives⽅法返回的与指定过滤器匹配的条⽬的嵌套存档列表。
从上⽂可以发现,这⾥的archive其实就是JarFileArchive ,传⼊的过滤器是JarLauncher#isNestedArchive⽅法引⽤
// JarLauncher.java
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// entry是⽂件⽬录时,必须是我们⾃⼰的业务类所在的⽬录 BOOT-INF/classes/
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// entry是Jar⽂件时,需要在依赖的⽂件⽬录 BOOT-INF/lib/下⾯
return entry.getName().startsWith(BOOT_INF_LIB);
}
getClassPathArchives⽅法通过过滤器将BOOT-INF/classes/和BOOT-INF/lib/下的嵌套存档作为List<Archive>返回参数传⼊createClassLoader⽅法中。
// Launcher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
// 前⾯说到,archive有⼀个⾃⼰的URL的,获得archive的URL放到list中
urls.add(archive.getUrl());
}
// 调⽤下⾯的重载⽅法
return createClassLoader(urls.toArray(new URL[0]));
}
// Launcher.java
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
createClassLoader()⽅法⽬的是为得到的URL们创建⼀个类加载器 LaunchedURLClassLoader,构造时传⼊了当前Launcher 的类加载器作为其⽗加载器,通常是系统类加载器。
下⾯重点看⼀下LaunchedURLClassLoader的构造过程:
// LaunchedURLClassLoader.java
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
LaunchedURLClassLoader是spring boot⾃⼰定义的类加载器,继承了JDK的URLClassLoader并重写了loadClass⽅法,也就是说它修改了默认的类加载⽅式,定义了⾃⼰的类加载规则,可以从前⾯得到的 List<Archive>中加载依赖包的class⽂件了。
LaunchedURLClassLoader创建完成后,我们回到Launcher中,下⼀步就是执⾏launch的重载⽅法了。
// Launcher.java
launch(args, getMainClass(), classLoader);
在此之前,会调⽤getMainClass⽅法并将其返回值作为参数。
getMainClass的实现在Launcher的⼦类ExecutableArchiveLauncher中:
// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
// 从 archive 中拿到 Manifest⽂件
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 就是MANIFEST.MF ⽂件中定义的Start-Class属性,也就是我们⾃⼰写的com.example.spring.boot.demo.SpringBootDemo这个类
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
// 返回mainClass
return mainClass;
}
得到mainClass后,执⾏launch的重载⽅法:
// Launcher.java
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
// 将⾃定义的LaunchedURLClassLoader设置为当前线程上下⽂类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// 构建⼀个 MainMethodRunner 实例对象来启动应⽤
createMainMethodRunner(mainClass, args, classLoader).run();
}
// Launcher.java
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
MainMethodRunner对象构建完成后,调⽤它的run⽅法:
// MainMethodRunner.java
public void run() throws Exception {
// 使⽤当前线程上下⽂类加载器也就是⾃定义的LaunchedURLClassLoader来加载我们⾃⼰写的com.example.spring.boot.demo.SpringBootDemo这个类 Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
// 找到SpringBootDemo的main⽅法
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
// 最后,通过反射的⽅式调⽤main⽅法
mainMethod.invoke(null, new Object[] { this.args });
}
⾄此,我们⾃⼰的main⽅法开始被调⽤,所有我们⾃⼰的应⽤程序类⽂件均可通过/BOOT-INF/classes加载,所有依赖的第三⽅jar均可通过/BOOT-INF/lib加载,然后就开始了spring boot的启动流程了。
debug技巧
以上就是spring boot通过java -jar命令启动的原理了,了解了原理以后我们可不可以通过debug来进⼀步加深⼀下理解呢?通常我们在IDEA⾥启动时是直接运⾏main⽅法,因为依赖的Jar都让IDEA放到classpath⾥了,所以spring boot直接启动就完事了,并不会通过上⾯的⽅式来启动。
不过我们可以通过配置IDEA的 run/debug configurations 配置 JAR Application 来实现通过Jar⽅式启动。
图4
当我们做了以上设置后,就可以来⽅便的在IDEA⾥来dubug源码了。
图5⼩结
本⽂通过JarLauncher为切⼊点,介绍了spring boot的java -jar的启动⽅式,阐述了JarLauncher启动的基本⼯作原理,同时简单介绍了相关的spring-boot-maven-plugin插件和Archive、LaunchedURLClassLoader等相关概念,希望能够对⼤家的理解有所帮助。
到此这篇关于SpringBoot java-jar命令⾏启动原理解析的⽂章就介绍到这了,更多相关SpringBoot java-jar命令⾏启动内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!。