点击关注公众号,实用技术文章及时了解

来源:juejin.im/post/5d2d6812e51d45777b1a3e5a

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

涉及的知识点主要包括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.boot</groupId>  <artifactId>spring-boot-maven-plugin</artifactId>  <executions>  <execution>  <goals>  <goal>repackage</goal>  </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<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);  }  //将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-ClassStart-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.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
</ul>
**/

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

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {  JarFile.registerUrlProtocolHandler();
// 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理  LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(  new URL[] {  new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")  , new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")  },  Application.class.getClassLoader());
// 加载类
// 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)  classLoader.loadClass("org.springframework.boot.loader.JarLauncher");  classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出  classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");  //        SpringApplication.run(Application.class, args);  }
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader -->
<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-loader</artifactId>  <version>2.1.3.RELEASE</version>
</dependency>
<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-maven-plugin</artifactId>  <version>2.1.3.RELEASE</version>  </dependency>

总结

对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。

我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。

推荐

主流Java进阶技术(学习资料分享)

Java面试题宝典

加入Spring技术开发社区

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

彻底搞懂 SpringBoot jar 可执行原理相关推荐

  1. 彻底透析SpringBoot jar可执行原理

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 作者:plz叫我红领巾 juejin.im/post/5d2d6812e51d45777b1a ...

  2. springboot如何盈利_彻底透析SpringBoot jar可执行原理

    ​文章篇幅较长,但是包含了SpringBoot 可执行jar包从头到尾的原理,请读者耐心观看.同时文章是基于SpringBoot-2.1.3进行分析.涉及的知识点主要包括Maven的生命周期以及自定义 ...

  3. boot spring 怎么执行hql_彻底透析SpringBoot jar可执行原理

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 作者:plz叫我红领巾 juejin.im/post/5d2d6812e51d45777b1a ...

  4. 透析SpringBoot jar可执行原理

    spring-boot-maven-plugin SpringBoot 的可执行jar包又称fat jar ,是包含所有第三方依赖的 jar 包,jar 包中嵌入了除 java 虚拟机以外的所有依赖, ...

  5. SpringBoot中事务执行原理分析(一)

    关联博文: SpringBoot中事务执行原理分析(一) SpringBoot中事务执行原理分析(二) SpringBoot中事务执行原理分析(三) SpringBoot中事务执行原理分析(四) Sp ...

  6. 彻底搞懂java反射技术及其原理

    概述:反射是java中最强大的技术之一,很多高级框架都用到了反射技术,面试中也是经常问的点,所以搞懂反射非常重要! 文章目录 1.反射是什么? 2.反射的底层原理 3.三种方式获取Class对象 4. ...

  7. 一文搞懂Python Unittest测试方法执行顺序

    Unittest unittest大家应该都不陌生.它作为一款博主在5-6年前最常用的单元测试框架,现在正被pytest,nose慢慢蚕食. 渐渐地,看到大家更多的讨论的内容从unittest+HTM ...

  8. 这一次搞懂 Spring 的 Bean 实例化原理

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/l6108003/article/ details/106439525 前言 前两篇文章分析了Spring XML和注解的解析原 ...

  9. react map循环生成的button_【第1945期】彻底搞懂React源码调度原理(Concurrent模式)...

    前言 估计会懵逼.今日早读文章由成都@苏溪云投稿分享. 正文从这开始~~ 最早之前,React还没有用fiber重写,那个时候对React调度模块就有好奇.而现在的调度模块对于之前没研究过它的我来说更 ...

最新文章

  1. Java中的单例模式
  2. mysql忘记密码的处理方法
  3. echarts 仪表盘 文字位置_企业数据仪表盘设计,该怎样设计自己的BI产品?
  4. 设计模式学习之Factory Method模式和Abstract Factory模式
  5. 《零基础看得懂的C++入门教程 》——(3)表达式花样挺多鸭
  6. java中将字符串顺序反传转_如何在Java中将字符串序列化的Erlang术语反序列化为JInterface对象?...
  7. Broadcast Receiver注意事项
  8. 反编译获取任何微信小程序源码——看这篇就够了
  9. 面经——嵌入式常见面试题总结100题(下)
  10. 2018-2019年江苏省高等学校“阿里云大数据技术实战训练营”大学生万人计划学术冬令营开营... 1
  11. C语言ALG什么文件,alg.exe是什么进程文件?如何删除alg病毒?
  12. win10+VS2017+WDK环境下编译C++程序提示error LNK1104无法打开文件*.lib(mfc140ud.lib)的问题
  13. 深入理解自动装箱和自动拆箱
  14. 例说hg(一)————hg sum 与hg tip区别
  15. 【会声会影】视频导出、输出时,如何设置参数
  16. 一个机械专业小混混 gooogleman 学习嵌入式ARM的真实经历
  17. 天龙网游入师门拿福利 师门系统讲解
  18. 一致性服务实践,青岛高科有话要说
  19. W82 - 999、人工智能助理工程师认证
  20. 计算机辅助药物筛选教程,药物筛选之计算机辅助药物设计

热门文章

  1. 西瓜书课后题4.7(队列控制决策树深度)
  2. php ctr b,用PHP解密AES CTR Little Endian
  3. 闪光灯 flash 问题
  4. WARNING: Published ports are discarded when using host network mode 解决方法
  5. 卖座项目需要注意的点
  6. 利用Flashbug插件查看AMF数据
  7. 华中科技大学成立人工智能学院,两名长江学者坐镇
  8. 计算机属于电器还是学习用品,未来的学习用品作文
  9. 我们也看看Metaverse项目: Decentraland、Sandbox 、Axie Infinity、Cryptovoxels、Starlink 、Rfox Vault、Bit Country等
  10. 【web】movie review——静态页面训练、css训练