面试官:SpringBoot jar 可执行原理,知道吗?
文章篇幅较长,但是包含了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.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 jarFile target = getTargetFile();//获取重新打包器,将重新打包成可执行jar文件Repackager repackager = getRepackager(source.getFile());//查找并过滤项目运行时依赖的jarSet < Artifact > artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));//将artifacts转换成librariesLibraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());try {//提供Spring Boot启动脚本LaunchScript launchScript = getLaunchScript();//执行重新打包逻辑,生成最后fat jarrepackager.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.JarLauncherrepackager.setMainClass(this.mainClass);if (this.layout != null) {getLog().info("Layout: " + this.layout);//重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jarrepackager.setLayout(this.layout.layout());}return repackager;}
/*** Executable JAR layout.*/public static class Jar implements RepackagingLayout {@Overridepublic String getLauncherClassName() {return "org.springframework.boot.loader.JarLauncher";}@Overridepublic String getLibraryDestination(String libraryName, LibraryScope scope) {return "BOOT-INF/lib/";}@Overridepublic String getClassesLocation() {return "";}@Overridepublic String getRepackagedClassesLocation() {return "BOOT-INF/classes/";}@Overridepublic boolean isExecutable() {return true;}}
layout我们可以将之翻译为文件布局,或者目录布局,代码一看清晰明了,同时我们需要关注,也是下一个重点关注对象 org.springframework.boot.loader.JarLauncher,从名字推断,这很可能是返回可执行 jar文件的启动类。
MANIFEST.MF文件内容
Manifest-Version: 1.0Implementation-Title: oneday-auth-serverImplementation-Version: 1.0.0-SNAPSHOTArchiver-Version: Plexus ArchiverBuilt-By: onedayImplementation-Vendor-Id: com.onedaySpring-Boot-Version: 2.1.3.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: com.oneday.auth.ApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.3.9Build-Jdk: 1.8.0_171
repackager生成的MANIFEST.MF文件为以上信息,可以看到两个关键信息 Main-Class和 Start-Class。我们可以进一步,程序的启动入口并不是我们SpringBoot中定义的 main,而是 JarLauncher#main,而再在其中利用反射调用定义好的 Start-Class的 main方法
JarLauncher
重点类介绍
1、 java.util.jar.JarFile JDK工具类提供的读取 jar文件
2、 org.springframework.boot.loader.jar.JarFileSpringboot-loader 继承JDK提供 JarFile类
3、 java.util.jar.JarEntryDK工具类提供的``jar```文件条目
4、 org.springframework.boot.loader.jar.JarEntry Springboot-loader 继承JDK提供 JarEntry类
5、 org.springframework.boot.loader.archive.Archive Springboot抽象出来的统一访问资源的层
6、 JarFileArchivejar包文件的抽象
7、 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注释解释道 theURLsfromwhich to load classesandresources,即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 capableif (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),遵循双亲委派机制进行查找类,而 BootstrapClassLoader和 ExtensionClassLoader将会查找不到fat jar依赖的类,最终会来到 ApplicationClassLoader,调用 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
<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的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。
敬请关注「搜云库技术团队」微信公众号,获取最新文章
面试官:SpringBoot jar 可执行原理,知道吗?的更多相关文章
- 彻底透析SpringBoot jar可执行原理
文章篇幅较长,但是包含了SpringBoot 可执行jar包从头到尾的原理,请读者耐心观看.同时文章是基于SpringBoot-2.1.3进行分析.涉及的知识点主要包括Maven的生命周期以及自定 ...
- 如何完美回答面试官问的Mybatis初始化原理!!!
前言 对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章将通过以下几点详细介绍MyBatis的初始化过程. MyBatis的初始化做了什么 MyBatis基于XML配置文件 ...
- 看完这一篇,再也不怕面试官问到IntentService的原理
IntentService是什么 在内部封装了 Handler.消息队列的一个Service子类,适合在后台执行一系列串行依次执行的耗时异步任务,方便了我们的日常coding(普通的Service则是 ...
- 面试官:Redis集群有哪些方式,Leader选举又是什么原理呢?
哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 作为一名Java程序员,Redi ...
- 面试官:我们来聊一聊Redis吧,你了解多少就答多少
哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新,建议收藏关注 一.前言 作为一名Java程 ...
- 面试官:Redis如何实现持久化的、主从哨兵又是什么?
哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 作为一名Java程序员,Redi ...
- 面试官:RocketMQ是什么,它有什么特性与使用场景?
哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 作为一名Java程序员,Roc ...
- 面试官又整新活,居然问我for循环用i++和++i哪个效率高?
原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 前几天,一个小伙伴告诉我,他在面试的时候被面试官问了这么一个问题: 在for循环中,到底应该用 i++ 还是 ++i ? 听到这,我感觉这面试官 ...
- 面试官:Zookeeper是什么,它有什么特性与使用场景?
哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 作为一名Java程序员,Zook ...
随机推荐
- Git-Runoob:Git 安装配置
ylbtech-Git-Runoob:Git 安装配置 1.返回顶部 1. Git 安装配置 在使用Git前我们需要先安装 Git.Git 目前支持 Linux/Unix.Solaris.Mac和 W ...
- 【flask-Email】邮件发送
使用依赖: flask_mail 安装方式: pip3 install flask-mail 代码示例: from flask import Flask from flask_mail import ...
- wpf进程间通讯
wpf进程间通讯 在联想智能识别项目中,需要用到进程间通讯,并且是低权限向高权限发送消息.首先声明一下,此项目是wpf的. 首先先简要说一下什么时候会用到进程间通讯,如:在Windows程序中,各个进 ...
- 2018.03.27 pandas concat 和 combin_first使用
# 连接和修补concat.combine_first 沿轴的堆叠连接 # 连接concatimport pandas as pdimport numpy as np s1 = pd.Series([ ...
- Application.CreateForm()和TForm.Create()创建的窗体有什么区别么?二者在使用上各有什么技巧?(50分)
https://wedelphi.com/t/135849/ 请详细些,并给出例子.谢谢. Application.CreateForm()创建的第一个可显示的窗体是自动成为主窗体,并且自动显示,并且 ...
- MariaDB增删改
1.MariaDB 数据类型 MariaDB数据类型可以分为数字,日期和时间以及字符串值. 使用数据类型的原则:够用就行, 尽量使用范围小的,而不用大的 常用的数据类型: 1.整数:int, bit( ...
- linux使用ltrace和strace跟踪程序执行过程
yum install strace yum install ltrace 1.strace ping -c 1 www.baidu.com 2.ltrace ping -c 1 www.baid ...
- 【Qt开发】V4L2 API详解 Buffer的准备和数据读取
前面主要介绍的是:V4L2 的一些设置接口,如亮度,饱和度,曝光时间,帧数,增益,白平衡等.今天看看V4L2 得到数据的几个关键ioctl,Buffer的申请和数据的抓取. 1. 初始化 Memory ...
- 简述在Vue脚手架中,组件以及父子组件(非父子组件)之间的传值
1.组件的定义 组成: template:包裹HTML模板片段(反映了数据与最终呈现给用户视图之间的映射关系) 只支持单个template标签: 支持lang配置多种模板语法: script:配置Vu ...
- Redis的 SLAVEOF 命令
SLAVEOF host port SLAVEOF 命令用于在 Redis 运行时动态地修改复制(replication)功能的行为. 通过执行 SLAVEOF host port 命令,可以将当前服 ...