专栏名称: 朱小厮的博客
著有畅销书:《深入理解Kafka》和《RabbitMQ实战指南》。公众号主要用来分享Java技术栈、Golang技术栈、消息中间件(如Kafka、RabbitMQ)、存储、大数据以及通用型技术架构等相关的技术。
目录
相关文章推荐
黑马营销  ·  深耕IP内核,泡泡玛特探索潮玩发展边界 ·  6 小时前  
安利云学堂  ·  直播预告 | 雅姿滤镜精华新品发布会 ·  14 小时前  
他化自在天  ·  「任侠」感谢2024年1月第2周10位光荣榜 ... ·  昨天  
赛博禅心  ·  超级硬广:输入 ikun,解锁 ... ·  3 天前  
赛博禅心  ·  超级硬广:输入 ikun,解锁 ... ·  3 天前  
51好读  ›  专栏  ›  朱小厮的博客

彻底透析SpringBoot jar可执行原理

朱小厮的博客  · 公众号  ·  · 2019-08-08 08:42

正文

点击上方“ 朱小厮的博客 ”,选择“ 设为星标

回复” 1024 “获取独家整理的学习资料




作者: plz叫我红领巾

来源:juejin.im/post/5d2d6812e51d45777b1a3e5a


文章篇幅较长,但是包含了SpringBoot 可执行jar包从头到尾的原理,请读者耐心观看。

同时文章是基于SpringBoot-2.1.3进行分析。涉及的知识点主要包括Maven的生命周期以及自定义插件,JDK提供关于jar包的工具类以及Springboot如何扩展,最后是自定义类加载器。

spring-boot-maven-plugin

SpringBoot 的可执行jar包又称fat jar ,是包含所有第三方依赖的 jar 包,jar 包中嵌入了除 java 虚拟机以外的所有依赖,是一个 all-in-one jar 包。

普通插件maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包之间的直接区别,是fat jar中主要增加了两部分,第一部分是lib目录,存放的是Maven依赖的jar包文件,第二部分是spring boot loader相关的类。

fat jar //目录结构
├─BOOT-INF
│  ├─classes
│  └─lib
├─META-INF
│  ├─maven
│  ├─app.properties
│  ├─MANIFEST.MF      
└─org
    └─springframework
        └─boot
            └─loader
                ├─archive
                ├─data
                ├─jar
                └─util

也就是说想要知道fat jar是如何生成的,就必须知道spring-boot-maven-plugin工作机制,而spring-boot-maven-plugin属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的

Maven的自定义插件

Maven 拥有三套相互独立的生命周期: clean、default 和 site, 而每个生命周期包含一些phase阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段。生命周期的阶段phase与插件的目标goal相互绑定,用以完成实际的构建任务。

<plugin>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-maven-pluginartifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackagegoal>
            goals>
        execution>
    executions>
plugin>

repackage目标对应的将执行到org.springframework.boot.maven.RepackageMojo#execute,该方法的主要逻辑是调用了org.springframework.boot.maven.RepackageMojo#repackage

private void repackage




    
() throws MojoExecutionException {
     //获取使用maven-jar-plugin生成的jar,最终的命名将加上.orignal后缀
   Artifact source = getSourceArtifact();
    //最终文件,即Fat jar
   File target = getTargetFile();
    //获取重新打包器,将重新打包成可执行jar文件
   Repackager repackager = getRepackager(source.getFile());
    //查找并过滤项目运行时依赖的jar
   Set 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);
   }
    //将source更新成 xxx.jar.orignal文件
   updateArtifact(source, target, repackager.getBackupFile());
}

我们关心一下org.springframework.boot.maven.RepackageMojo#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);
       //重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
      repackager.setLayout(this.layout.layout());
   }
   return repackager;
}


/**
 * 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我们可以将之翻译为文件布局,或者目录布局,代码一看清晰明了,同时我们需要关注,也是下一个重点关注对象org.springframework.boot.loader.JarLauncher,从名字推断,这很可能是返回可执行jar文件的启动类。

MANIFEST.MF文件内容

Manifest-Version: 1.0
Implementation-Title: oneday-auth-server
Implementation-Version: 1.0.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: oneday
Implementation-Vendor-Id: com.oneday
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.oneday.auth.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_171

repackager生成的MANIFEST.MF文件为以上信息,可以看到两个关键信息Main-Class和Start-Class。我们可以进一步,程序的启动入口并不是我们SpringBoot中定义的main,而是JarLauncher#main,而再在其中利用反射调用定义好的Start-Class的main方法

JarLauncher

重点类介绍

  • java.util.jar.JarFile JDK工具类提供的读取jar文件

  • org.springframework.boot.loader.jar.JarFileSpringboot-loader 继承JDK提供JarFile类

  • java.util.jar.JarEntryDK工具类提供的``jar```文件条目

  • org.springframework.boot.loader.jar.JarEntry Springboot-loader 继承JDK提供JarEntry类

  • org.springframework.boot.loader.archive.Archive Springboot抽象出来的统一访问资源的层
    JarFileArchivejar包文件的抽象
    ExplodedArchive文件目录

这里重点描述一下JarFile的作用,每个JarFileArchive都会对应一个JarFile。在构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹类。我们可以看一下该类的注释。

/* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.

  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
    * on any directory entry.

  • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
    * embedded JAR files (as long as their entry is not compressed).


**/

jar里的资源分隔符是!/,在JDK提供的JarFile URL只支持一个’!/‘,而Spring boot扩展了这个协议,让它支持多个’!/‘,就可以表示jar in jar、jar in directory、fat jar的资源了。

自定义类加载机制

  • 最基础:Bootstrap ClassLoader(加载JDK的/lib目录下的类)

  • 次基础:Extension ClassLoader(加载JDK的/lib/ext目录下的类)

  • 普通:Application ClassLoader(程序自己classpath下的类)

首先需要关注双亲委派机制很重要的一点是,如果一个类可以被委派最基础的ClassLoader加载,就不能让高层的ClassLoader加载,这样是为了范围错误的引入了非JDK下但是类名一样的类。

其二,如果在这个机制下,由于fat jar中依赖的各个第三方jar文件,并不在程序自己classpath下,也就是说,如果我们采用双亲委派机制的话,根本获取不到我们所依赖的jar包,因此我们需要修改双亲委派机制的查找class的方法,自定义类加载机制。

先简单的介绍Springboot2中LaunchedURLClassLoader,该类继承了java.net.URLClassLoader,重写了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我们再探讨他是如何修改双亲委派机制。

在上面我们讲到Spring boot支持多个’!/‘以表示多个jar,而我们的问题在于,如何解决查找到这多个jar包。我们看一下LaunchedURLClassLoader的构造方法。

public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
   super(urls, parent);
}

urls注释解释道the URLs from which to load classes and resources,即fat jar包依赖的所有类和资源,将该urls参数传递给父类java.net.URLClassLoader,由父类的java.net.URLClassLoader#findClass执行查找类方法,该类的查找来源即构造方法传递进来的urls参数。

//LaunchedURLClassLoader的实现
protected Class> loadClass(String name, boolean resolve)
      throws ClassNotFoundException {
   Handler.setUseFastConnectionExceptions(true);
   try {
      try {
          //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联               //的package关联起来
         definePackageIfNecessary(name);
      }
      catch (IllegalArgumentException ex) {
         // Tolerate race condition due to being parallel capable
         if (getPackage(name) == null) {
            // This should never happen as the IllegalArgumentException indicates
            // that the package has already been defined and, therefore,
            // getPackage(name) should not return null.

            //这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
            throw new AssertionError("Package " + name + " has already been "
                  + "defined but it could not be found");
         }
      }
      return super.loadClass(name, resolve);
   }
   finally {
      Handler.setUseFastConnectionExceptions(false);
   }
}

方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass

如何真正的启动

Springboot2和Springboot1的最大区别在于,Springboo1会新起一个线程,来执行相应的反射调用逻辑,而SpringBoot2则去掉了构建新的线程这一步。

方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射调用逻辑比较简单,这里就不再分析,比较关键的一点是,在调用main方法之前,将当前线程的上下文类加载器设置成LaunchedURLClassLoader

protected void launch(String[] args, String mainClass, ClassLoader classLoader)
      throws Exception 
{
   Thread.currentThread().setContextClassLoader(classLoader);
   createMainMethodRunner(mainClass, args, classLoader).run();
}

Demo







请到「今天看啥」查看全文