欢迎访问我的GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

能独立运行的jar文件

在开发springboot应用时,通过java -jar命令启动应用是常用的方式,今天就来一起了解这个简单操作背后的技术;

开发demo

开发一个springboot应用作为本次研究的对象,对应的版本信息如下:

  • JDK:1.8.0_211
  • springboot:2.3.1.RELEASE
  • maven:3.6.0

接下来开发springboot应用,这个应用异常简单:

  1. springboot应用名为springbootstarterdemo,pom.xml文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bolingcavalry</groupId>
<artifactId>springbootstarterdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootstarterdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
  1. 只有一个java类,里面有个http接口:
package com.bolingcavalry.springbootstarterdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.Date; @SpringBootApplication
@RestController
public class SpringbootstarterdemoApplication { public static void main(String[] args) {
SpringApplication.run(SpringbootstarterdemoApplication.class, args);
}
@RequestMapping(value = "/hello")
public String hello(){
return "hello " + new Date();
}
}
  1. 编码完成,在pom.xml所在目录执行命令
mvn clean package -U -DskipTests
  1. 构建成功后,在target目录下得到文件springbootstarterdemo-0.0.1-SNAPSHOT.jar
  2. 就是这个springbootstarterdemo-0.0.1-SNAPSHOT.jar,此时执行java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar就能启动应用,如下图:

接下来就用这个springbootstarterdemo-0.0.1-SNAPSHOT.jar来分析jar文件能够独立启动的原因;

java -jar做了什么

  • 先要弄清楚java -jar命令做了什么,在oracle官网找到了该命令的描述:



    If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.

  • 再次秀出我蹩脚的英文翻译:

  1. 使用-jar参数时,后面的参数是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
  2. 该jar文件中包含的是class和资源文件;
  3. 在manifest文件中有Main-Class的定义;
  4. Main-Class的源码中指定了整个应用的启动类;(in its source code)
  • 小结一下:

    java -jar会去找jar中的manifest文件,在那里面找到真正的启动类;

探查springbootstarterdemo-0.0.1-SNAPSHOT.jar

  1. springbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的构建结果,是个压缩包,用常见的压缩工具就能解压,我这里的环境是MacBook Pro,用unzip即可解压;

  2. 解压后有很多内容,我们先关注manifest相关的,下图红框中就是manifest文件:

  3. 打开上图红框中的文件,内容如下:

Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springbootstarterdemo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
demoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
  1. 在上述内容可见Main-Class的值org.springframework.boot.loader.JarLauncher,这个和前面的java官方文档对应上了,正是这个JarLauncher类的代码中指定了真正的启动类;

疑惑出现

  1. 在MANIFEST.MF文件中有这么一行内容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
demoApplication
  1. 前面的java官方文档中,只提到过Main-Class ,并没有提到Start-Class;
  2. Start-Class的值是SpringbootstarterdemoApplication,这是我们的java代码中的唯一类,也只真正的应用启动类;
  3. 所以问题就来了:理论上看,执行java -jar命令时JarLauncher类会被执行,但实际上是SpringbootstarterdemoApplication被执行了,这其中发生了什么呢?

猜测

动手之前先猜一下,个人觉得原因应该如下:

  1. java -jar命令会启动JarLauncher;
  2. Start-Class是给JarLauncher用的;
  3. JarLauncher根据Start-Class找到了SpringbootstarterdemoApplication,然后执行它;

分析JarLauncher

  1. 先下载SpringBoot源码,我下载的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE

  2. JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的继承关系,如下图,可见JarLauncher继承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父类Launcher位于最顶层,是个抽象类:

  3. java -jar执行的是JarLauncher的main方法,如下,会实例化一个JarLauncher对象,然后执行其launch方法,并且将所有入参都带入:

public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
  1. 上面的launch方法在父类Launcher中:
protected void launch(String[] args) throws Exception {
// 将jar解压后运行的方式叫做exploded mode
// 如果是exploded mode,就不能支持通过URL加载jar
// 如果不是exploded mode,就可以通过URL加载jar
if (!isExploded()) {
// 如果允许通过URL加载jar,就在此注册对应的处理类
JarFile.registerUrlProtocolHandler();
}
// 创建classLoader
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
// jarmode是创建docker镜像时用到的参数,使用该参数是为了生成带有多个layer信息的镜像
// 这里暂时不关注jarmode
String jarMode = System.getProperty("jarmode");
//如果没有jarmode参数,launchClass的值就来自getMainClass()返回
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
  1. 可见要重点关注的是getMainClass()方法,在看这个方法之前,我们先去关注一个重要的成员变量archive,是JarLauncher的父类ExecutableArchiveLauncher的archive,如下可见,该变量又来自方法createArchive:
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
this.classPathIndex = getClassPathIndex(this.archive);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
  1. 方法来自Launcher.createArchive,如下所示,可见成员变量archive实际上是个JarFileArchive对象:
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));
}
  1. 现在回到getMainClass()方法,可见his.archive.getManifest方法返回的是META-INF/MANIFEST.MF文件的内容,然后getValue(START_CLASS_ATTRIBUTE)方法实际上就是从META-INF/MANIFEST.MF中取得了Start-Class的属性:
@Override
protected String getMainClass() throws Exception {
// 对应的是JarFileArchive.getManifest方法,
// 进去后发现对应的就是JarFile.getManifest方法,
// JarFile.getManifest对应的就是META-INF/MANIFEST.MF文件的内容
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 对应的是META-INF/MANIFEST.MF文件中的Start-Class的属性
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
  1. 从上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了Start-Class的属性com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication,再次回到launch方法中,可见最终运行的代码是launch(args, launchClass, classLoader),它的launchClass参数就是com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication:
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
// 这里的launchClass等于"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
// 这里就是启动SpringbootstarterdemoApplication的地方
launch(args, launchClass, classLoader);
}
  1. 展开launch(args, launchClass, classLoader),最终查到了MainMethodRunner类:
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
// mainClassName被赋值为"com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
} public void run() throws Exception {
// 得到SpringbootstarterdemoApplication的Class对象
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
// 得到SpringbootstarterdemoApplication的main方法对象
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
// 通过反射执行main方法
mainMethod.invoke(null, new Object[] { this.args });
}
}

终于,真相大白了;

小结

最后尽可能简短做个小结,先看jar是如何产生的,如下图,maven插件生成的jar文件中,有常见的class、jar,也有符合java规范的MANIFEST.MF文件,并且,还在MANIFEST.MF文件中额外生成了名为Start-Class的配置,这里面是我们编写的应用启动类SpringbootstarterdemoApplication:

启动类是JarLauncher,它是如何与MANIFEST.MF文件关联的呢?从下图可以看出,最终是通过JarFile类的成员变量manifestSupplier关联上的:

再来看看关键代码的执行情况,如下图:

至此,SpringBoot的jar独立运行的基本原理已经清楚,探究的过程中,除了熟悉关键代码流程,还对jar中的文件有了更多了解,如果您正在学习SpringBoot,希望本文能给您一些参考;

官方文档

  1. 最后附上SpringBoot官方文档,可以看到Start-Class描述信息:

  2. 上述文档明确提到:Start-Class定义的是实际的启动类,此时的您应该对一切都了然于胸,产生本该如此的感慨;

springboot的jar为何能独立运行的更多相关文章

  1. springBoot 项目 jar/war打包 并运行

    一:idea  打jar  包 简述:springboor  项目最好的打包方式就是打成jar  ,下边就是简单的过程 1.打开idea工具 ,选着要打开的项目, 然后打开view--> too ...

  2. windos上安装jenkins部署springboot的jar包(未运行,只是在打包并上传linux成功了)

    流程: 从linux上的svn拉取代码,到本地(windos)jenkins的工作区间的workspace,然后通过构建,打包,部署到linux上 环境: windos上安装:maven jdk je ...

  3. hadoop:将WordCount打包成独立运行的jar包

    hadoop示例中的WordCount程序,很多教程上都是推荐以下二种运行方式: 1.将生成的jar包,复制到hadoop集群中的节点,然后运行 $HADOOP_HOME/bin/hadoop xxx ...

  4. springboot 以jar方式在linux后台运行

    linux命令如下: nohup java -jar 自己的springboot项目.jar >日志文件名.log 2>&1 & 命令解释: nohup:不挂断地运行命令, ...

  5. springboot以jar运行时参数传递

    springboot以jar运行时参数传递 spring boot项目我们都习惯以内嵌tomcat方式.直接打包成jar,运行时使用: java -jar XXX.jar  --spring.prof ...

  6. springboot项目jar包运行

    springboot项目jar包运行 参考 Linux后台运行java的jar包 步骤 进入maven项目中,打包项目. mvn package -Dmaven.test.skip=true 运行ja ...

  7. 将Java项目打包成可以独立运行的Jar包

    因为一直也没怎么用过Java的原因,所以以前本科上课的时候,在控制台下运行Java程序都是仅仅运行单个的没有第三方库依赖的情况下运行的 那种情况特别简单,只要输入Javac 文件名.java等到编译完 ...

  8. springboot打包jar包后运行

    我们知道,spring boot内嵌tomcat,打包成jar包以后,直接就可以运行. 我们也可以使用启动项里面的mian入口来运行程序. 运行jar包时,我们一般是java -jar xxx.jar ...

  9. springboot打jar包,调用webservice出错

    错误提示 Caused by: java.lang.ClassNotFoundException: com/sun/tools/internal/xjc/api/XJC 在idea中没有问题,但是打成 ...

随机推荐

  1. JS遍历对象修改属性名

    根据接口返回数据中number属性值,对数据进行截取,并改变属性名.直接上码: 下面是需要处理的数据 let data={"minValue":7400, "maxVal ...

  2. Java实现 LeetCode 738 单调递增的数字(暴力)

    738. 单调递增的数字 给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增. (当且仅当每个相邻位数上的数字 x 和 y 满足 x <= ...

  3. Java实现 蓝桥杯VIP 算法训练 完数

    问题描述 一个数如果恰好等于它的因子之和,这个数就称为"完数".例如,6的因子为1.2.3,而6=1+2+3,因此6就是"完数".又如,28的因子为1.2.4. ...

  4. Java实现 LeetCode 53 最大子序和

    53. 最大子序和 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和. 示例: 输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 ...

  5. java实现漏掉的账目明细

    某财务部门结账时发现总金额不对头.很可能是从明细上漏掉了某1笔或几笔.如果已知明细账目清单,能通过编程找到漏掉的是哪1笔或几笔吗? 如果有多种可能,则输出所有可能的情况. 我们规定:用户输入的第一行是 ...

  6. java实现猜算式

    题目:猜算式 你一定还记得小学学习过的乘法计算过程,比如: x 15 ------ 273 ------ 请你观察如下的乘法算式 *** x *** -------- *** *** *** ---- ...

  7. PAT 有几个PAT

    字符串 APPAPT 中包含了两个单词 PAT,其中第一个 PAT 是第 2 位(P),第 4 位(A),第 6 位(T):第二个 PAT 是第 3 位(P),第 4 位(A),第 6 位(T). 现 ...

  8. 【asp.net core 系列】6 实战之 一个项目的完整结构

    0. 前言 在<asp.net core 系列>之前的几篇文章中,我们简单了解了路由.控制器以及视图的关系以及静态资源的引入,让我们对于asp.net core mvc项目有了基本的认识. ...

  9. Cypress系列(14)- 环境变量详解

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 前言 环境变量,其实就是根据环境的变化, ...

  10. Matplotlib 的正确使用方法,画一张好看的图

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:Python中文社区 本文用的数据如图所示,Dataframe中显示 ...