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
iginal
其中iginal是默认的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打好的包的⽬录结构(不重要的省略掉):
spring怎么读取jar文件图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: ample.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。⽽我们⾃⼰的项⽬引导类ample.spring.boot.demo.SpringBootDemo,定义在了Start-Class属性中,这个属性并不是Java标准的MANIFEST.MF⽂件属性。
spring-boot-maven-plugin 打包过程
我们并没有添加org.springframework.boot.loader下的这些类的依赖,那么它们是如何被打包在 FatJar ⾥⾯的呢?这就必须要提到spring-boot-maven-plugin插件的⼯作机制了。对于每个新建的 spring boot⼯程,可以在其 l ⽂件中看到如下插件:
<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 = File());
// 查并过滤项⽬运⾏时依赖的jar
Set<Artifact> artifacts = filterDependencies(Artifacts(), getFilters(getAdditionalFilters()));
// 将artifacts转换成libraries
Libraries libraries = new ArtifactsLibraries(artifacts, quiresUnpack, getLog());
try {
// 提供Spring Boot启动脚本
LaunchScript launchScript = getLaunchScript();
// 执⾏重新打包逻辑,⽣成最后fat jar
}
catch (IOException ex) {
throw new Message(), ex);
}
// 将source更新成 ignal⽂件
updateArtifact(source, target, BackupFile());
}
// 继续跟踪getRepackager这个⽅法,知道Repackager是如何⽣成的,也就⼤致能够推测出内在的打包逻辑。
private Repackager getRepackager(File source) {
Repackager repackager = new Repackager(source, this.layoutFactory);
repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
// 设置main class的名称,如果不指定的话则会查第⼀个包含main⽅法的类,
// repacke最后将会设置org.springframework.boot.loader.JarLauncher
repackager.setMainClass(this.mainClass);
if (this.layout != null) {
getLog().info("Layout: " + this.layout);
repackager.setLayout(this.layout.layout());
}
return repackager;
}
repackager设置了 layout⽅法的返回对象,也就是org.springframework.ls.Layouts.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-lo
ader,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()) {
Name().equals(BOOT_INF_CLASSES);
}
Name().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 = CodeSource();
URI location = (codeSource != null) ? Location().toURI() : null;
// 这⾥就是拿到当前的 classpath 的绝对路径
String path = (location != null) ? SchemeSpecificPart() : null;
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!ists()) {
throw new IllegalStateException("Unable to determine code source archive from " + root);
}
// 将构造的archive 对象返回
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论