SpringBoot基于jar包启动核心原理及流程详解

简介: SpringBoot基于jar包启动核心原理及流程详解

得益于SpringBoot的封装,我们可以只通过jar -jar一行命令便启动一个web项目。再也不用操心搭建tomcat等相关web容器。那么,你是否探究过SpringBoot是如何达到这一操作的呢?

只有了解了底层实现原理,才能更好的掌握该项技术带来的好处以及性能调优。本篇文章带大家聊一探究竟。本文部分内容参考于网络文章,部分内容参考《SpringBoot技术内幕:架构设计与实现原理》。

Jar包的打包插件及核心方法

Spring Boot项目的pom.xml文件中默认使用如下插件进行打包:









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

执行maven clean package之后,会生成两个文件:



spring-learn-0.0.1-SNAPSHOT.jarspring-learn-0.0.1-SNAPSHOT.jar.original

spring-boot-maven-plugin项目存在于spring-boot-tools目录中。spring-boot-maven-plugin默认有5个goals:repackage、run、start、stop、build-info。在打包的时候默认使用的是repackage。

spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包为可执行的软件包,并将mvn package生成的软件包重命名为*.original。

spring-boot-maven-plugin的repackage在代码层面调用了RepackageMojo的execute方法,而在该方法中又调用了repackage方法。repackage方法代码及操作解析如下:

























private void repackage() throws MojoExecutionException {   // maven生成的jar,最终的命名将加上.original后缀   Artifact source = getSourceArtifact();   // 最终为可执行jar,即fat jar   File target = getTargetFile();   // 获取重新打包器,将maven生成的jar重新打包成可执行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);   }   // 将maven生成的jar更新成.original文件   updateArtifact(source, target, repackager.getBackupFile());}

执行以上命令之后,便生成了打包结果对应的两个文件。下面针对文件的内容和结构进行一探究竟。

jar包目录结构

首先来看看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内容

在上述目录结构中,META-INF记录了相关jar包的基础信息,包括入口程序等。

Manifest-Version: 1.0
Implementation-Title: spring-learn
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.secbro2.learn.SpringLearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,这个是jar启动的Main函数。

还有一个Start-Class是com.secbro2.learn.SpringLearnApplication,这个是我们应用自己的Main函数。

Archive的概念

在继续了解底层概念和原理之前,我们先来了解一下Archive的概念:

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

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。关于Spring Boot中Archive的源码如下:









public interface Archive extends Iterable<Archive.Entry> {    // 获取该归档的url    URL getUrl() throws MalformedURLException;    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF    Manifest getManifest() throws IOException;    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;}

SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive。该接口有两个实现,分别是org.springframework.boot.loader.archive.ExplodedArchive和org.springframework.boot.loader.archive.JarFileArchive。前者用于在文件夹目录下寻找资源,后者用于在jar包环境下寻找资源。而在SpringBoot打包的fatJar中,则是使用后者。

JarLauncher

从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。JarLauncher类的继承结构是:



class JarLauncher extends ExecutableArchiveLauncherclass ExecutableArchiveLauncher extends Launcher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class,其实JarLauncher实现很简单:







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

其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序。在创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive。

JarLauncher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法最主要的功能就是构建了当前main方法所在的FatJar的JarFileArchive对象。下面来看launch方法。该方法主要是做了2个事情:

(1)以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。

(2)以第一步构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main。进而启动整个程序。
































public abstract class ExecutableArchiveLauncher extends Launcher {    private final Archive archive;    public ExecutableArchiveLauncher() {        try {            // 找到自己所在的jar,并创建Archive            this.archive = createArchive();        }        catch (Exception ex) {            throw new IllegalStateException(ex);        }    }}
public abstract class Launcher {    protected final Archive createArchive() throws Exception {        ProtectionDomain protectionDomain = getClass().getProtectionDomain();        CodeSource codeSource = protectionDomain.getCodeSource();        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());        String path = (location == null ? null : location.getSchemeSpecificPart());        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));    }}

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用。

至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。

URLStreamHandler

java中描述资源常使用URL。而URL有一个方法用于打开链接java.net.URL#openConnection()。由于URL用于表达各种各样的资源,打开资源的具体动作由java.net.URLStreamHandler这个类的子类来完成。根据不同的协议,会有不同的handler实现。而JDK内置了相当多的handler实现用于应对不同的协议。比如jar、file、http等等。URL内部有一个静态HashTable属性,用于保存已经被发现的协议和handler实例的映射。

获得URLStreamHandler有三种方法:

(1)实现URLStreamHandlerFactory接口,通过方法URL.setURLStreamHandlerFactory设置。该属性是一个静态属性,且只能被设置一次。

(2)直接提供URLStreamHandler的子类,作为URL的构造方法的入参之一。但是在JVM中有固定的规范要求:

子类的类名必须是Handler,同时最后一级的包名必须是协议的名称。比如自定义了Http的协议实现,则类名必然为xx.http.Handler;

JVM启动的时候,需要设置java.protocol.handler.pkgs系统属性,如果有多个实现类,那么中间用|隔开。因为JVM在尝试寻找Handler时,会从这个属性中获取包名前缀,最终使用包名前缀.协议名.Handler,使用Class.forName方法尝试初始化类,如果初始化成功,则会使用该类的实现作为协议实现。

SpringBoot自定义的classLoader能够识别FatJar中的资源,包括有:在指定目录下的项目编译class、在指令目录下的项目依赖jar。JDK默认用于加载应用的AppClassLoader只能从jar的根目录开始加载class文件,并且也不支持jar in jar这种格式。

为了实现这个目标,SpringBoot首先从支持jar in jar中内容读取做了定制,也就是支持多个!/分隔符的url路径。SpringBoot定制了以下两个方面:

(1)实现了一个java.net.URLStreamHandler的子类org.springframework.boot.loader.jar.Handler。该Handler支持识别多个!/分隔符,并且正确的打开URLConnection。打开的Connection是SpringBoot定制的org.springframework.boot.loader.jar.JarURLConnection实现。

(2)实现了一个java.net.JarURLConnection的子类org.springframework.boot.loader.jar.JarURLConnection。该链接支持多个!/分隔符,并且自己实现了在这种情况下获取InputStream的方法。而为了能够在org.springframework.boot.loader.jar.JarURLConnection正确获取输入流,SpringBoot自定义了一套读取ZipFile的工具类和方法。这部分和ZIP压缩算法规范紧密相连,就不拓展了。

WarLauncher

针对SpringBoot的项目,如果依旧想采用基于war包的形式进行部署,那么需要对打包形式进行修改,而构建war包也比较简单:

(1)修改packaging为war包。





<groupId>com.secbro2</groupId><artifactId>springboot-learn</artifactId><version>0.0.1-SNAPSHOT</version><packaging>war</packaging>

(2)排除掉默认的tomcat依赖。

















<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId>  <exclusions>    <exclusion>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-tomcat</artifactId>    </exclusion>  </exclusions></dependency>
<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-tomcat</artifactId>  <scope>provided</scope></dependency>

(3)启动类继承SpringBootServletInitializer,重写它的configure方法。

@SpringBootApplication
@RestController
public class LearnApp extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(LearnApp.class, args);
    }
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(LearnApp.class);
    }
}

构建出的war包,其目录结构为:

springboot-learn-0.0.1-SNAPSHOT
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   │   └── 应用程序
│   └── lib
│       └── 第三方依赖jar
│   └── lib-provided
│       └── 与内嵌容器相关的第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

MANIFEST.MF内容为:

Manifest-Version: 1.0
Start-Class: com.secbro2.LearnApp
Main-Class: org.springframework.boot.loader.WarLauncher

此时,启动类变为了org.springframework.boot.loader.WarLauncher,查看WarLauncher实现,其实与JarLauncher并无太大差别。

public class WarLauncher extends ExecutableArchiveLauncher {
    private static final String WEB_INF = "WEB-INF/";
    private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
    private static final String WEB_INF_LIB = WEB_INF + "lib/";
    private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
    public WarLauncher() {
    }
    @Override
    public boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(WEB_INF_CLASSES);
        }else {
            return entry.getName().startsWith(WEB_INF_LIB)
                    || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
        }
    }
    public static void main(String[] args) throws Exception {
        new WarLauncher().launch(args);
    }
}

差别仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar。

Spring Boot应用启动流程总结

总结一下Spring Boot应用的启动流程:

(1)Spring Boot应用打包之后,生成一个Fat jar,包含了应用依赖的jar包和Spring Boot loader相关的类。

(2)Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载/lib下面的jar,并以一个新线程启动应用的Main函数。

那么,ClassLoader是如何读取到Resource,它又需要哪些能力?查找资源和读取资源的能力。对应的API:



public URL findResource(String name)public InputStream getResourceAsStream(String name)

SpringBoot构造LaunchedURLClassLoader时,传递了一个URL[]数组。数组里是lib目录下面的jar的URL。

对于一个URL,JDK或者ClassLoader如何知道怎么读取到里面的内容的?流程如下:

  • LaunchedURLClassLoader.loadClass
  • URL.getContent()
  • URL.openConnection()
  • Handler.openConnection(URL)

最终调用的是JarURLConnection的getInputStream()函数。










//org.springframework.boot.loader.jar.JarURLConnection @Overridepublic InputStream getInputStream() throws IOException {   connect();   if (this.jarEntryName.isEmpty()) {     throw new IOException("no entry name specified");   }   return this.jarEntryData.getInputStream(); }

从一个URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:

  • Spring boot注册了一个Handler来处理”jar:”这种协议的URL。
  • Spring boot扩展了JarFile和JarURLConnection,内部处理jar in jar的情况。
  • 在处理多重jar in jar的URL时,Spring Boot会循环处理,并缓存已经加载到的JarFile。
  • 对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考JarFileArchive里的代码。
  • 在获取URL的InputStream时,最终获取到的是JarFile里的JarEntryData。

细节很多,上面只列出比较重要的步骤。最后,URLClassLoader是如何getResource的呢?URLClassLoader在构造时,有URL[]数组参数,它内部会用这个数组来构造一个URLClassPath:


URLClassPath ucp = new URLClassPath(urls);

在URLClassPath内部会为这些URLS都构造一个Loader,然后在getResource时,会从这些Loader里一个个去尝试获取。如果获取成功的话,就像下面那样包装为一个Resource。





































Resource getResource(final String name, boolean check) {    final URL url;    try {        url = new URL(base, ParseUtil.encodePath(name, false));    } catch (MalformedURLException e) {        throw new IllegalArgumentException("name");    }    final URLConnection uc;    try {        if (check) {            URLClassPath.check(url);        }        uc = url.openConnection();        InputStream in = uc.getInputStream();        if (uc instanceof JarURLConnection) {            /* Need to remember the jar file so it can be closed             * in a hurry.             */            JarURLConnection juc = (JarURLConnection)uc;            jarfile = JarLoader.checkJar(juc.getJarFile());        }    } catch (Exception e) {        return null;    }    return new Resource() {        public String getName() { return name; }        public URL getURL() { return url; }        public URL getCodeSourceURL() { return base; }        public InputStream getInputStream() throws IOException {            return uc.getInputStream();        }        public int getContentLength() throws IOException {            return uc.getContentLength();        }    };}

从代码里可以看到,实际上是调用了url.openConnection()。这样完整的链条就可以连接起来了。

在IDE/开放目录启动Spring boot应用

在上面只提到在一个fat jar里启动SpringBoot应用的过程,那么IDE里Spring boot是如何启动的呢?

在IDE里,直接运行的Main函数是应用的Main函数:

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

其实在IDE里启动SpringBoot应用是最简单的一种情况,因为依赖的Jar都让IDE放到classpath里了,所以Spring boot直接启动就完事了。

还有一种情况是在一个开放目录下启动SpringBoot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。

这时,Spring boot会判断当前是否在一个目录里,如果是的,则构造一个ExplodedArchive(前面在jar里时是JarFileArchive),后面的启动流程类似fat jar的。

总结

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

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

JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动。

WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动。


目录
相关文章
|
6月前
|
安全 Java 持续交付
如何实现上传jar直接部署成功,这篇文章直接带你上手springboot实现jar包热更新!
本文详细讲解了在Spring Boot应用中实现Jar包热更新的实践方法。通过自定义类加载器(`HotClassLoader`),动态加载和卸载指定目录下的Jar包,结合Spring Bean动态注册机制,使新加载的类能够被Spring容器管理。同时,提供了文件上传接口,方便用户手动触发Jar包更新。文章还强调了安全性、依赖管理和线程安全等注意事项,并给出了测试步骤和总结,帮助开发者高效实现热更新功能,减少服务中断和提升开发效率。
|
12月前
|
前端开发 Java 数据安全/隐私保护
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
文章通过一个简单的SpringBoot项目,详细介绍了前后端如何实现用户登录功能,包括前端登录页面的创建、后端登录逻辑的处理、使用session验证用户身份以及获取已登录用户信息的方法。
1551 2
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
|
9月前
|
XML Java 应用服务中间件
SpringBoot项目打war包流程
本文介绍了将Spring Boot项目改造为WAR包并部署到外部Tomcat服务器的步骤。主要内容包括:1) 修改pom.xml中的打包方式为WAR;2) 排除Spring Boot内置的Tomcat依赖;3) 添加Servlet API依赖;4) 改造启动类以支持WAR部署;5) 打包和部署。通过这些步骤,可以轻松地将Spring Boot应用转换为适合外部Tomcat服务器的WAR包。
476 64
SpringBoot项目打war包流程
|
7月前
|
Java
SpringBoot启动流程
springboot项目在启动的时候, 首先会执行启动引导类里面的SpringApplication.run(AdminApplication.class, args)方法 这个run方法主要做的事情可以分为三个部分 : 第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器 第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块 第三部分是自动化配置模块,该模块作为springboot自动配置核心,在后面的分析中会详细讨论
|
Java 应用服务中间件 Spring
为什么SpringBoot的 jar 可以直接运行?
SpringBoot的 jar 可以直接运行的原因
753 2
|
8月前
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
830 12
|
8月前
|
Java Maven 开发者
编写SpringBoot的自定义starter包
通过本文的介绍,我们详细讲解了如何创建一个Spring Boot自定义Starter包,包括自动配置类、配置属性类、`spring.factories`文件的创建和配置。通过自定义Starter,可以有效地复用公共配置和组件,提高开发效率。希望本文能帮助您更好地理解和应用Spring Boot自定义Starter,在实际项目中灵活使用这一强大的功能。
549 17
|
8月前
|
Java 应用服务中间件 Maven
SpringBoot项目打包成war包
通过上述步骤,我们成功地将一个Spring Boot应用打包成WAR文件,并部署到外部的Tomcat服务器中。这种方式适用于需要与传统Servlet容器集成的场景。
562 8
|
9月前
|
存储 Java 数据安全/隐私保护
SpringBoot整合Flowable【03】- 通过Flowable-UI体验一个简单流程
本文介绍了如何使用Flowable 7.0以下版本的flowable-ui进行流程建模、发布和执行。首先,通过解压并启动flowable-ui war包,访问http://localhost:8080/flowable-ui/idm/#/login登录系统。接着,创建并绘制一个简单的绩效流程模型,包含开始节点、任务节点(自评、上级评、隔级评)和结束节点,并为各节点分配处理人。然后,创建应用并发布绩效流程。最后,通过创建a、b、c三个用户分别完成各节点任务,演示了整个流程的执行过程。本文旨在帮助读者理解Flowable的基本操作和流程元素,后续将介绍通过Java代码控制流程的方法。
1491 1
SpringBoot整合Flowable【03】- 通过Flowable-UI体验一个简单流程
|
8月前
|
IDE Java 应用服务中间件
spring boot 启动流程
Spring Boot 启动流程简介: 在使用 Spring Boot 之前,启动 Java Web 应用需要配置 Web 容器(如 Tomcat),并将应用打包放入容器目录。而使用 Spring Boot,只需运行 main() 方法即可启动 Web 应用。Spring Boot 的核心启动方法是 SpringApplication.run(),它负责初始化和启动应用上下文。 主要步骤包括: 1. **应用启动计时**:使用 StopWatch 记录启动时间。 2. **打印 Banner**:显示 Spring Boot 的 LOGO。 3. **创建上下文实例**:通过反射创建
429 5

热门文章

最新文章