​文章篇幅较长,但是包含了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相互绑定,用以完成实际的构建任务。

org.springframework.boot

spring-boot-maven-plugin

repackage

​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

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

}

总结

​对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。

​作者:plz叫我红领巾

本博客欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。码字不易,您的点赞是我写作的最大动力。

springboot如何盈利_彻底透析SpringBoot jar可执行原理相关推荐

  1. 透析ICMP协议(一): 协议原理

    透析ICMP协议(一): 协议原理 =============================== 这篇文章原创自bugfree/CSDN 平台: VC6 Windows XP ICMP简介: --- ...

  2. 深入透析springboot加载之spring-boot-maven-plugin,应该是目前最详细的讲解了

    如下图,从springboot官网下载一个demo,执行mvn install 命令 即可生成一个可以执行的springboot的jar包. 上面怎么有两个文件?首先我们查看springboot项目的 ...

  3. java序列化算法透析_Java序列化机制与原理的深入分析

    Java序列化算法透析 Serialization(序列化)是一种将对象以一连串的字节描述的过程:反序列化deserialization是一种将这些字节重建成一个对象的过程.Java序列化API提供一 ...

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

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

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

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

  6. 透析SpringBoot jar可执行原理

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

  7. springboot如何盈利_给大家推荐8个SpringBoot精选项目

    前言 2017年,曾在自己的博客中写下这样一段话:有一种力量无人能抵挡,它永不言败生来倔强.有一种理想照亮了迷茫,在那写满荣耀的地方. 如今2018年已过大半,虽然没有大理想抱负,但是却有自己的小计划 ...

  8. springboot 得到端口_带你入门SpringBoot

    SpringBoot是一个非常强大的框架,该框架是由 Pivotal 团队提供的一个全新框架,目前已经成为当今最流行的微服务开发框架.SpringBoot设计的目的就是用来简化新 Spring 应用的 ...

  9. springboot md5加密_实在!基于Springboot和WebScoket,写了一个在线聊天小程序

    基于Springboot和WebScoket写的一个在线聊天小程序 (好几天没有写东西了,也没有去练手了,就看了看这个...) 项目说明 此项目为一个聊天的小demo,采用springboot+web ...

最新文章

  1. index.php user r,安装cms :在浏览器中打开http://你的网址/install/index.php 出现白屏怎样处理?...
  2. VM虚拟机中的centos6.3命令行模式添加GCC
  3. [转]C++函数模板与模板函数
  4. 网络营销外包专员浅析网站网络营销外包如何快速获取关键词排名
  5. 零基础学Python-爬虫-5、下载网络视频
  6. javabean 连接mysql_连接mysql的javabean实例+简单分页
  7. 【bzoj1738】[Usaco2005 mar]Ombrophobic Bovines 发抖的牛 Floyd+二分+网络流最大流
  8. mysql 主键索引如何创建_SQL创建索引、主键
  9. laravel5.4+vue+vux+element的环境搭配
  10. 内网穿透反向代理应用 xfrp
  11. 常用的流式布局FlowLayout
  12. Audio之音频帧周期(四十三)
  13. 采购订单模板_金蝶KIS商贸版(采购模块)常见问题汇总
  14. matlab2016对硬件要求,matlab2016b配置libsvm的各中坑及解决办法
  15. 斗鱼 虎牙24 小时直播电影教程
  16. 论MongoDB索引选择的重要性
  17. centos启动停留在started GNOME display manager
  18. SaaS是什么?企业采购SaaS有什么好处?
  19. c 全国计算机二级考试真题及答案,全国计算机二级考试《C语言》真题练习及答案...
  20. jdk8移除了PermGen,取而代之的是MetaSpace

热门文章

  1. Freeze 风暴导致的IOPS飙升 - 追溯与解法
  2. 《Java从入门到放弃》框架入门篇:hibernate基本配置
  3. ArcGIS JS API加载GeoServer发布的WFS服务
  4. IDEA创建方法时快速添加注释
  5. ubunut 下关闭apache服务自动启动
  6. 关于数据中台的深度思考与总结,20000 字不到一丢丢。。。
  7. 支撑日活百万用户的高并发系统,应该如何设计其数据库架构?
  8. Java堆内存是线程共享的!面试官:你确定吗?
  9. 用Telnet发送HTTP请求
  10. 炸!亿级数据DB秒级平滑扩容!!!