SpringBoot应用jar包启动原理详解

目录
  • 1、maven打包
  • 2、Jar包目录结构
  • 3、可执行Jar(JarLauncher)
  • 4、WarLauncher
  • 5、总结

1、maven打包

Spring Boot项目的pom.xml文件中默认使用spring-boot-maven-plugin插件进行打包:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

在执行完maven clean package之后,会生成来个jar相关文件:

  • test-0.0.1-SNAPSHOT.jar
  • test-0.0.1-SNAPSHOT.jar.original

2、Jar包目录结构

以笔者的test-0.0.1-SNAPSHOT.jar为例,来看一下jar的目录结构,其中都包含哪些目录和文件?

可以概述为:

spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序
│ └── lib
│ └── 第三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动程序

其中主要包括三大目录:META-INF、BOOT-INF、org。

1)META-INF内容

META-INF记录了相关jar包的基础信息,包括:入口程序。具体内容如下:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: tms-start
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.saint.StartApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

  • Main-Classorg.springframework.boot.loader.JarLauncher,即jar启动的Main函数;
  • Start-Classcom.saint.StartApplication,即我们自己SpringBoot项目的启动类;也是下文提到的项目的引导类

2)BOOT-INF内容

  • BOOT-INF/classes目录:存放应用编译后的class文件源码;
  • BOOT-INF/lib目录:存放应用依赖的所有三方jar包文件;

3)org内容

org目录下存放着所有SpringBoot相关的class文件,比如:JarLauncher、LaunchedURLClassLoader。

3、可执行Jar(JarLauncher)

从jar包内META-INF/MANIFEST.MF文件中的Main-Class属性值为org.springframework.boot.loader.JarLauncher,可以看出main函数是JarLauncher,即:SpringBoot应用中的Main-class属性指向的class为org.springframework.boot.loader.JarLauncher

其实吧,主要是 Java官方文档规定:java -jar命令引导的具体启动类必须配置在MANIFEST.MF资源的Main-class属性中;又根据“JAR文件规范”,MANIFEST.MF资源必须存放在/META-INF/目录下。所以main函数才是JarLauncher

JarLauncher类继承图如下:

JarLauncher的类注释我们看出JarLauncher的作用:

  • 加载内部/BOOT-INF/lib下的所有三方依赖jar;
  • 加载内部/BOOT-INF/classes下的所有应用class;

1)JarLauncher的运行步骤?

  • 在解压jar包后的根目录下运行 java org.springframework.boot.loader.JarLauncher。项目引导类(META-INF/MANIFEST.MF文件中的Start-Class属性)被JarLauncher加载并执行。
  • 如果直接运行Start-Class(示例的StartApplication)类,会报错ClassNotFoundException。
  • Spring Boot依赖的JAR文件均存放在BOOT-INF/lib目录下。JarLauncher会将这些JAR文件作为Start-Class的类库依赖。

这也是为什么JarLauncher能够引导,而直接运行Start-Class却不行。

2)JarLauncher实现原理?

public class JarLauncher extends ExecutableArchiveLauncher {

	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

	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);
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}

}

JarLauncher#main()中新建了JarLauncher并调用父类Launcher中的launch()方法启动程序;

  • BOOT_INF_CLASSES、BOOT_INF_LIB变量对应BOOT-INF/classes和lib路径;
  • isNestedArchive(Archinve.Entry entry)方法用于判断FAT JAR资源的相对路径是否为nestedArchive嵌套文档。进而决定这些FAT JAR是否会被launch。 当方法返回false时,说明FAT JAR被解压至文件目录。

1> Archive的概念

archive即归档文件,这个概念在linux下比较常见;通常就是一个tar/zip格式的压缩包;而jar正是zip格式的。

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),也可以是文件目录(ExplodedArchive);这样也就统一了访问资源的逻辑层;

public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
    ....
}

Archive继承自Archive.Entry,Archive.Entry有两种实现:

JarFileArchive.JarFileEntry --> 基于java.util.jar.JarEntry实现,表示FAT JAR嵌入资源。

ExplodedArchive.FileEntry --> 基于文件系统实现;

两者的主要差别是ExplodedArchive相比于JarFileArchive多了一个获取文件的getFile()方法;

public File getFile() {
    return this.file;
}

也就是说一个在jar包环境下寻找资源,一个在文件夹目录下寻找资源;

所以从实现层面证明了JarLauncher支持JAR和文件系统两种启动方式

当执行java -jar命令时,将调用/META-INF /MANIFEST.MF文件的Main-Class属性的main()方法,实际上调用的是JarLauncher#launch(args)方法;

3) Launcher#launch(args)方法

protected void launch(String[] args) throws Exception {
    if (!isExploded()) {
        // phase1:注册jar URL处理器
        JarFile.registerUrlProtocolHandler();
    }
    // phase2:创建ClassLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
    String jarMode = System.getProperty("jarmode");
    String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
    // phase3:调用实际的引导类launch
    launch(args, launchClass, classLoader);
}

launch()方法分三步:

  • 注册jar URL处理器;
  • 为所有的Archive创建可以加载jar in jar目录的ClassLoader;
  • 调用实际的引导类(Start-Class);

1> phase1 注册jar URL处理器

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

	private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";

public static void registerUrlProtocolHandler() {
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
                                          : handlers + "|" + HANDLERS_PACKAGE));
    // 重置缓存的UrlHandlers;
    resetCachedUrlHandlers();
}

private static void resetCachedUrlHandlers() {
    try {
        // 由URL类实现:通过URL.setURLStreamHandlerFactory()获得URLStreamHandler。
        URL.setURLStreamHandlerFactory(null);
    }
    catch (Error ex) {
        // Ignore
    }
}

JarFile#resetCachedUrlHandlers()方法利用java.net.URLStreamHandler扩展机制,实现由URL#getURLStreamHandler(String)提供。

URL#getURLStreamHandler(String protocol)方法:

首先,URL的关联协议(Protocol)对应一种URLStreamHandler实现类。

JDK内建了一些协议的实现,这些实现均存放在sun.net.www.protocol包下,并且类名必须为Handler,其类全名模式为sun.net.www.protocol.${protocol}.Handler(包名前缀.协议名.Handler),其中${protocol}表示协议名

如果需要扩展,则必须继承URLStreamHandler类,通过配置Java系统属性java.protocol.handler.pkgs,追加URLStreamHandler实现类的package,多个package以“|”分割。

所以对于SpringBoot的JarFile,registerURLProtocolHandler()方法将package org.springframework.boot.loader追加到java系统属性java.protocol.handler.pkgs中。

也就是说,org.springframework.boot.loader包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler;并且按照类名模式,其实现协议为JAR。

另外:在URL#getURLStreamHandler()方法中,处理器先读取Java系统属性java.protocol.handler.pkgs无论其是否存在,继续读取sun.net.www.protocol包;所以JDK内建URLStreamHandler实现是兜底的

为什么SpringBoot要选择覆盖URLStreamHandler?

  • Spring BOOT FAT JAR除包含传统Java Jar资源之外,还包含依赖的JAR文件;即存在jar in jar的情况;
  • 默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件,对于jar in jar的包无法加载;
  • 当SpringBoot FAT JAR被java -jar命令引导时,其内部的JAR文件无法被内嵌实现sun.net.www.protocol.jar.Handler当做class Path,故需要定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar in jar包的class类文件;

2> phase2 创建可以加载jar in jar目录的ClassLoader

获取所有的Archive,然后针对每个Archive分别创建ClassLoader;

ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());

/**
 * 获取所有的Archive(包含jar in jar的情况)
 */
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
    return getClassPathArchives().iterator();
}

/**
 * 针对每个Archive分别创建ClassLoader
 */
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}

3> phase3 调用实际的引导类(Start-Class)

// case1: 通过ExecutableArchiveLauncher#getMainClass()获取MainClass
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
// 2、运行实际的引导类
launch(args, launchClass, classLoader);

对于phase3,大致可以分为两步:

  • 首先通过ExecutableArchiveLauncher#getMainClass()获取mainClass(即:/META-INF/MANIFEST.MF资源中的Start-Class属性);
  • 利用反射获取mainClass类中的main(Stirng[])方法并调用;

<1> 获取mainClass:

Start-Class属性来自/META_INF/MANIFEST.MF资源中。Launcher的子类JarLauncherWarLauncher没有实现getMainClass()方法。所以无论是Jar还是War,读取的SpringBoot启动类均来自此属性。

<2> 执行mainClass的main()方法:

获取mainClass之后,MainMethodRunner#run()方法利用反射获取mainClass类中的main(Stirng[])方法并调用。

运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。

4、WarLauncher

WarLauncher是可执行WAR的启动器。

WarLauncher与JarLauncher的差异很小,主要区别在于项目文件和JAR Class Path路径的不同。

  • 相比于FAT Jar的目录,WAR增加了WEB-INF/lib-provided,并且该目录仅存放<scope>provided</scope>的JAR文件。
  • 传统的Servlet应用的Class Path路径仅关注WEB-INF/classes/和WEB-INF/lib/目录,因此WEB-INF/lib-provided/中的JAR将被Servlet忽略

好处:打包后的WAR文件能够在Servlet容器中兼容运行

所以JarLauncher和WarLauncher并无本质区别。

5、总结

Spring Boot应用Jar/War的启动流程:

Spring Boot应用打包之后,生成一个Fat jar,包含了应用依赖的所有三方jar包和SpringBoot Loader相关的类。

Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载BOOT-INF/classes目录以及/BOOT-INF/lib下面的jar,并利用反射获取mainClass类中的main(Stirng[])方法并调用。即:运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。

其他点:

SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。

SpringBoot通过扩展URLClassLoader --> LauncherURLClassLoader,实现了jar in jar中class文件的加载。

WarLauncher相比JarLauncher只是多加载WEB-INF/lib-provided目录下的jar文件。

到此这篇关于SpringBoot应用jar包启动原理详解的文章就介绍到这了,更多相关SpringBoot jar包启动内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Linux编辑启动、停止与重启springboot jar包脚本实例

    前言 springboot的配置文件中,配置文件的名字都有各自的意义跟用途 dev 开发环境 prod 生产环境(默认) test 测试环境 加载指定配置文件 --spring.profiles.active=prod springboot加载jar包的方式有 // 直接在控制台进行启动,缺点就是控制台关闭项目也就关闭了. java -jar bootdo.jar // 这种方式可以运行在后台,但是如果推出了shell的话,那也会挂 java -jar /bootdo-2.0.0.jar > b

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

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

  • SpringBoot为何可以使用Jar包启动详解

    目录 引言 Spring Boot 打包插件 SpringBoot FatJar 的组织结构 MAINFEST.MF 元信息 启动原理 源码分析 JarLauncher Launcher PropertiesLauncher MainMethodRunner 总结 引言 很多初学者会比较困惑,Spring Boot 是如何做到将应用代码和所有的依赖打包成一个独立的 Jar 包,因为传统的 Java 项目打包成 Jar 包之后,需要通过 -classpath 属性来指定依赖,才能够运行.我们今天就

  • SpringBoot的jar包如何启动的实现

    目录 一.简介 二.jar包的内部结构 三.加载过程 1.使用到的一些类 2.过程分析 四.总结 一.简介 ​ 使用过SprongBoot打过jar包的都应该知道,目标文件一般都会生成两个文件,一个是以.jar的包,一个是.jar.original文件.那么使用SpringBoot会打出两个包,而.jar.original的作用是什么呢?还有就是java -jar是如何将一个SpringBoot项目启动,之间都进行了那些的操作? ​ 其实.jar.original是maven在SpringBoo

  • SpringBoot工程搭建打包、启动jar包和war包的教程图文详解

    工程搭建 1.File->new->project: 2.选择"Spring Initializr",点击next:(jdk1.8默认即可) 3.完善项目信息,组名可不做修改,项目名可做修改:最终建的项目名为:test,src->main->java下包名会是:com->example->test:点击next: 4.Web下勾选Spring Web Start,(网上创建springboot项目多是勾选Web选项,而较高版本的Springboot没

  • SpringBoot应用jar包启动原理详解

    目录 1.maven打包 2.Jar包目录结构 3.可执行Jar(JarLauncher) 4.WarLauncher 5.总结 1.maven打包 Spring Boot项目的pom.xml文件中默认使用spring-boot-maven-plugin插件进行打包: <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>s

  • SpringBoot内置tomcat启动原理详解

    前言 不得不说SpringBoot的开发者是在为大众程序猿谋福利,把大家都惯成了懒汉,xml不配置了,连tomcat也懒的配置了,典型的一键启动系统,那么tomcat在springboot是怎么启动的呢? 内置tomcat 开发阶段对我们来说使用内置的tomcat是非常够用了,当然也可以使用jetty. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-bo

  • Spring Boot 的java -jar命令启动原理详解

    导语 在运用Spring Boot 后,我们基本上摆脱之前项目每次上线的时候把项目打成war包.当然也不排除一些奇葩的规定,必须要用war包上线,不过很多时候,我们对一些东西只是处在使用的阶段,并不会去深入的研究使用的原理是什么,这貌似也是大多数人的固定思维. 或许正是如此,总会有些没有固定思维的人会去积极的探索原理,当然这话不是说我是积极的,我其实也是只原理的搬运工.今天和大家来简单的说下Spring Boot 的项目在运行Java -jar的原理. jar包目录和jar命令启动入口 在正式开

  • springboot的war和jar包的使用详解

    本篇和大家分享的是通过maven对springboot中打war包和jar包:war通常来说生成后直接放到tomcat的webapps下面就行,tomcat配置自动解压war,而jar一般通过命令行部署和启动: 首先,来实战怎么生成war包,主要来说可以分为3个步骤: •程序入口改造 •排除springboot内置tomcat •spring-boot-maven-plugin插件中配置程序入口 程序入口改造,我们需要把springboot的main入口屏蔽掉,然后继承SpringBootSer

  • java Spring的启动原理详解

    目录 引入 Spring启动过程 总结: 总结 引入 为什么突然说一下Spring启动原理呢,因为之前面试的时候,回答的那可谓是坑坑洼洼,前前后后,补补贴贴... 总而言之就是不行,再次看一下源码发掘一下... 在Spring Boot还没有广泛到家家在用的时候,我们都还在书写繁琐的配置,什么web.xml.spring.xml.bean.xml等等.虽然现在很少,可以说几乎没有企业在去使用Spring的老一套,而会去使用Spring Boot约定大于配置来进行快速开发,但是,Spring的也要

  • Android Studio 生成自定义jar包的步骤详解

    想要将一个项目导出为jar包,供其它项目使用,在eclipse中可以直接导出该项目为jar包,而 在AS中可以通过修改gradle才处理. 接下来就介绍下具体的步骤: 1.新建一个项目,项目名随意,eg:MakeJarApplication,在项目中新建一个module类型为android-library ,命名为testLibrary.如图: 项目结构图 2.让app依赖这个库,在app下的build.gradle文件中添加compile project(':testlibrary') dep

  • Java关于jar包的知识详解

    在学习jar包之前,要先弄懂Java包,以及关于Java包的相关概念. 一.包 为了更好地组织类,Java提供了包机制.包是类的容器,用于分隔类名空间.如果没有指定包名,所有的示例都属于一个默认的无名包. 格式为: package pkg1[.pkg2[.pkg3-]]: 代码实例: package cn.com.zhouzhou;//包名一定要由小写字母组成 public class Lession1 { public static void main(String[] args) { Sys

  • intellij idea 将模块打jar包的步骤详解

    今天要想在本机开多个java压测进程测试目标程序.所以需要在本机开多个终端执行jar程序.步骤如下: 1.点开Project Structure,点击左侧Artifacts菜单 选择下图中1.后点击2处 +号,选中3 jar的子菜单 点击4处"from modules with dependencise". 2. 进入下图:Module:选择你要打jar包的模块,MainClass:选择你main 函数所在的类 Jar files from libraries: 选中第一个打完包后是一

  • Android Studio3.2中导出jar包的过程详解

    1.)说明. 本项目是来自github上的一个项目roottools (https://github.com/Stericson/RootTools),这里只是想本地编译后输出下jar包供自己进行使用. 2.)操作步骤. 步骤1)按之前你熟悉的方式进行开发待输出为jar的项目. 步骤2) 一般的gradle设置,比如gradle版本,android sdk的编译,目标,最小要求版本..还有compileOptions的jdk版本设置等. 步骤3)gradle中的apply plugin设置: a

  • Springboot jar包远程调试详解

    对于运行在服务器上面,隐藏较深,通过日志也排查不出的问题,有的时候需要通过本地调试来一步步排错,教过好些同事远程调试所需配置步骤,特此用笔记录下来,避免重复劳动. 一.idea打包 二.install 后jar包位置 三.运行程序(远程调试启动) 找到jar所在路径,进入cmd窗口,命令如下,监听启动5005端口(和程序的端口8089没关系)-远程调试启动语句: // 找到对应的jar包目录,cmd窗口中执行下面语句即可java -Xdebug -Xrunjdwp:transport=dt_so

随机推荐