springboot的jar能独立运行的原因解析

欢迎访问我的GitHub

https://github.com/zq2599/blog_demos

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

欢迎访问我的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应用,这个应用非常简单:

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>

只有一个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();
  }
}

编码完成,在pom.xml所在目录执行命令

mvn clean package -U -DskipTests

构建成功后,在target目录下得到文件springbootstarterdemo-0.0.1-SNAPSHOT.jar就是这个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.

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

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

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

探查springbootstarterdemo-0.0.1-SNAPSHOT.jarspringbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的构建结果,是个压缩包,用常见的压缩工具就能解压,我这里的环境是MacBook Pro,用unzip即可解压;解压后有很多内容,我们先关注manifest相关的,下图红框中就是manifest文件:

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

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

4.在上述内容可见Main-Class的值org.springframework.boot.loader.JarLauncher,这个和前面的java官方文档对应上了,正是这个JarLauncher类的代码中指定了真正的启动类;

疑惑出现

1.在MANIFEST.MF文件中有这么一行内容:

Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication

2.前面的java官方文档中,只提到过Main-Class ,并没有提到Start-Class;

3.Start-Class的值是SpringbootstarterdemoApplication,这是我们的java代码中的唯一类,也只真正的应用启动类;

4.所以问题就来了:理论上看,执行java -jar命令时JarLauncher类会被执行,但实际上是SpringbootstarterdemoApplication被执行了,这其中发生了什么呢?

猜测

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

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

分析JarLauncher先下载SpringBoot源码,我下载的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASEJarLauncher所在的工程是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);
}

4.上面的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);
}

5.可见要重点关注的是getMainClass()方法,在看这个方法之前,我们先去关注一个重要的成员变量archive,是JarLauncher的父类ExecutableArchiveLauncher的archive,如下可见,该变量又来自方法createArchive:

public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
			this.classPathIndex = getClassPathIndex(this.archive);
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}

6.方法来自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));
	}

7.现在回到getMainClass()方法,可见this.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;
	}

8.从上述分析可知: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);
	}

9.展开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,希望本文能给您一些参考;

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

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

到此这篇关于springboot的jar为何能独立运行的文章就介绍到这了,更多相关springboot的jar为何能独立运行内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 解决SpringBoot打成jar运行后无法读取resources里的文件问题

    开发一个word替换功能时,因替换其中的内容功能需要 word 模版,就把 word_replace_tpl.docx 模版文件放到 resources 下 在开发环境中通过下面方法能读取word_replace_tpl.docx文件,但是打成jar包在 linux下运行后无法找到文件了 File file = ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "static/office_template/xxx.docx&q

  • SpringBoot项目运行jar包启动的步骤流程解析

    SpringBoot项目在开发中,方便快捷,有一点原因就是SpringBoot项目可以打jar包运行:把jar包直接扔服务器上,然后运行jar包就能访问项目接口了.下面介绍SpringBoot项目打jar包运行的步骤流程: 一.我们所熟悉的是在开发环境下,直接用开发工具来运行那个启动类,然后就能启动这个项目: 开发环境下启动项目 二. SpringBoot项目打jar包方法: [1]在cmd界面中,进入项目的本地存储地址 cmd命令下进入项目地址 [2]运行maven的打包命令,mvn clea

  • 简单了解springboot的jar包部署步骤

    由于springboot常用war包部署,改为cloud开发模式多端口情况下,部署反而不习惯 毕竟,war包要不要项目名访问都必须放在tomcat的root目录下 而此目录限制只能放置一个项目,并且登录端口限制为tomcat的接口了 因此,jar包部署就成了必然的方式了 1.添加pom设置 静态文件需要访问,所以静态文件webapp下的文件需要重新指定位置,具体配置如下 <resource> <directory>src/main/webapp</directory>

  • springboot web项目打jar或者war包并运行的实现

    (一)springboot web项目打jar包 1.打包 两种打包方式 maven命令打包 切换目录到工程根下,pom.xml所在位置,运行maven的打包命令 mvn clean package -Dmaven.test.skip=true IDEA 工具执行maven任务打包 会在xxx项目模块下/target目录下生成xxx-0.0.1-SNAPSHOT.jar 2.运行jar包 启动运行(切换目录到target下,执行命令) F:\Java\idea-workspace\shixun0

  • 解决idea中Springboot找不到BASE64Encoder或Decoder的jar包

    问题描述: Springboot项目.明明都把包下载放进去了,就是报错找不到.JDK11 解决方法之一: 换为JDK8. 然后重启就可以了. 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们

  • springboot项目打成jar包后无法获取static下的静态资源文件的问题分析

    springboot 后端项目 做某个功能时 需要读取根目录下的.doc文件,具体项目中路径如下: 开始是通过绝对路径读取文档,在本地没有任何问题. 但是 讲项目打成jar包 部署到测试环境发现无论怎样都读取不到,然后在本地运行jar包出现同样的情况. 捕获异常:java.io.FileNotFoundException [org.apache.ibatis.session.defaults.DefaultSqlSession@55b40849] java.io.FileNotFoundExce

  • SpringBoot项目没有把依赖的jar包一起打包的问题解决

    这篇文章主要介绍了SpringBoot项目没有把依赖的jar包一起打包的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一般未一起打包是因为pom不是继承自spring-boot-starter-parent导致的需要在pom.xml文件写入以下配置 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId>

  • springboot的jar能独立运行的原因解析

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS等: 欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS等: 能独立运行的jar文件 在开发springboot应用时,通过java -jar命令启动

  • 为什么SpringBoot的jar可以直接运行

    SpringBoot提供了一个插件spring-boot-maven-plugin用于把程序打包成一个可执行的jar包.在pom文件里加入这个插件即可: <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>

  • SpringBoot程序的打包与运行的实现

    目录 程序打包 程序运行 SpringBoot程序打包失败处理 命令行启动常见问题及解决方案 SpringBoot项目快速启动(Linux版) 刚开始做开发学习的小伙伴可能在有一个知识上面有错误的认知,我们天天写程序是在Idea下写的,运行也是在Idea下运行的. 但是实际开发完成后,我们的项目是不可能运行在自己的电脑上的. 我们以后制作的程序是运行在专用的服务器上的,简单说就是将你做的程序放在一台独立运行的电脑上,这台电脑要比你开发使用的计算机更专业,并且安全等级各个方面要远超过你现在的电脑.

  • 解决springboot项目打成jar包后运行时碰到的小坑

    目录 项目打成jar包后运行时的坑 问题 解决办法 打jar包后运行提示找不到界面 项目打成jar包后运行时的坑 问题 我的springboot项目中有用的一段代码是往static目录下添加文件获取路径的代码如下 String path=ResourceUtils.getURL("classpath:static/task").getPath().replace("%20"," "); 部署阿里云的服务器上,通过运行jar包的形式运行的时候,发现

  • 使用Springboot打成jar包thymeleaf的问题

    目录 Springboot打成jar包thymeleaf 1.使用springboot打成jar包 2. controller的书写 springboot + thymeleaf jar包运行就报错 你应该知道这样排错 1.静态文件错误 2.controller层返回页面错误 3.小结一下 Springboot打成jar包thymeleaf 1.使用springboot打成jar包 需要在maven中添加插件 <build> <plugins> <plugin> <

  • SpringBoot统计、监控SQL运行情况的方法详解

    目录 1 基本概念 2 添加依赖 3 配置相关属性 3.1 如何配置 Filter 4 监控页面 5 sql监控 6 慢sql记录 7 spring 监控 8 去 Ad(广告) (1) 直接手动注释这段代码 (2) 使用过滤器过滤 9 获取 Druid 的监控数据 总结 1 基本概念 Druid 是Java语言中最好的数据库连接池. 虽然 HikariCP 的速度稍快,但是,Druid能够提供强大的监控和扩展功能,也是阿里巴巴的开源项目. Druid是阿里巴巴开发的号称为监控而生的数据库连接池,

  • 使用Springboot 打jar包实现分离依赖lib和配置

    目录 打jar包实现分离依赖lib和配置 操作步骤 Springboot中jar包分离(分离lib包) jar包分离介绍 jar包分离实施步骤 小结一下 打jar包实现分离依赖lib和配置 为了业务需要配置文件和jar分离,便于使用者修改配置信息,在网上找了很久,最终找到一个简单有效的方法. 操作步骤 打开project工程的pom.xml文件,将下列配置粘贴到pom文件中: <properties> <java.version>1.8</java.version> &

  • springboot工程jar包部署到云服务器的方法

    首先,springboot在这里不多做介绍了,关于怎么打成jar包,我相信凡是用过的同学都知道吧! 然后就是将jar包上传到云服务器了,这个有好些工具的,博主在这里也不多做说明了! 咱们还是直奔主题吧.... 运行命令:java -jar xxx.jar,然后springboot工程的内置tomcat就开启了,项目已经启动,接下来你就可以访问了. 感觉一切ok,其实你已经掉到坑里了... 怎么回事呢? 当你关闭当前的xshell 命令界面时,再次访问就失效了,why? why? why? 这是因

  • SpringBoot项目Jar包如何瘦身部署的实现

    一.背景 SpringBoot 为我们快速开发提供了很好的架子,使得我们只需要少量配置就能开始我们的开发工作,但是当我们需要打包上传部署时,却是很神伤的一个问题,因为打出来的 Jar 包少则十几兆,多则一百来兆,我们需要上传至公网服务器时,是非常慢的,这就引出了今天的主题,SpringBoot项目Jar包如何瘦身部署 1.思路分析 jar,我们可以看出,jar 包里面分为以下三个模块 分为 BOOT-INF,META-INF,org 三个部分,打开 BOOT-INF 可以看到有 classes,

随机推荐