从原理和源码梳理SpringbootFatJar的机制
一、概述
SpringBoot FatJar 的设计,打破了标准 jar 的结构,在 jar 包内携带了其所依赖的 jar 包,通过 jar 中的 main 方法创建自己的类加载器,来识别加载运行其不规范的目录下的代码和依赖。二、标准的 jar 包结构
打开 Java 的 Jar 文件我们经常可以看到文件中包含着一个META-INF目录,这个目录下会有一些文件,其中必有一个MANIFEST.MF,这个文件描述了该 Jar 文件的很多信息 其中 Main-Class 定义 Jar 文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar xxx.jar 来运行该 jar 文件。
在生产环境是使用 java -jar xxx.jar 的方式来运行 SpringBoot 程序。 这种情况下,SpringBoot 应用真实的启动类并不是我们所定义的带有 main 方法的类,而是 JarLauncher 类。查看 SpringBoot 所打成的 FatJar,其 Main-Class 是org.springframework.boot.loader.JarLauncher,这便是微妙之处。Spring-Boot-Version: 2.1.3.RELEASE Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.rock.springbootlearn.SpringbootLearnApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk: 1.8.0_131 复制代码
JAR 包中的 MANIFEST.MF 文件详解以及编写规范三、探索JarLauncher
org.springframework.boot.loader.JarLauncher这个类是哪里来的呢?答案在 spring-boot-loader-***.jar 包中,可找到这个 JarLauncher 类的源码。在项目中加入 maven 依赖,以便查看源码和远程调试。 org.springframework.boot spring-boot-loader 复制代码
认真比较可以看出,这个 spring-boot-loader 包中的内容与 SpringBoot 的 FatJar 包中的一部分内容几乎一样。JarLauncher 在 jar 中的位置如下:
3.1 只能拷贝出来一份儿
重点重点重点:因 jar 规范要求 Main-Class 所指定的类必须位于 jar 包的顶层目录下,即 org.springframework.boot.loader.JarLauncher 这个 org 必须位于 jar 包中的第一级目录,不能放置在其他的目录下。所以所以所以只能将 spring-boot-loader 这个 jar 包的内容拷贝出来,而不是整个 jar 直接放置于执行 Jar 中。3.2 携带程序所依赖的jar而非仅class
上边 JarLauncher 的这个 org.springframework.xx 以及 META-INF 这两个目录是符合 jar 包规范的。但是 BOOT-INF 这个目录里边有点像我们开发中的一些用法:依赖 jar 包在 lib 目录下 但按照 jar 包规范 jar 中不能有 jar 包的情况下程序.class 文件在 classes 目录下 但xxx.class 文件应该按照 org.springframework.xx 这样放置在 jar 中的根目录中
所以classes 和 lib 你也能意识到,这个设计是独特的。早期 jar 包内携带依赖是采用如 maven-shade-plugin 的做法,把依赖的class文件拷贝到目标 jar 中,但也会造成重名(全限定名)的类会出现覆盖的情况。后来 SpringBoot 为了避免覆盖的情况,修改了打包机制,放弃了maven-shade-plugin那种拷贝class的方式,调整为依赖原始 jar 包;这同时意味着改变了 Jar 标准的运行机制,那么要想让classes和lib中代码能够正常运行,你试想一下如果没有自定义的 classLoader 来加载这些类文件,可以嘛?四、 自定义类加载器的运行机制
自定义类加载器的常规处理:指定资源指定委托关系指定线程上下文类加载器调用逻辑入口方法4.1 指定资源
构造方法中基于 jar 包的文件系统信息,构造 Archive 对象public ExecutableArchiveLauncher() { this.archive = createArchive(); } protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; String path = (location != null) ? location.getSchemeSpecificPart() : null; 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)); } 复制代码
采集 jar 包中的 classes 和 lib 目录下的归档文件。后边创建 ClassLoader 的时候作为参数传入@Override protected List getClassPathArchives() throws Exception { List archives = new ArrayList<>( this.archive.getNestedArchives(this::isNestedArchive)); postProcessClassPathArchives(archives); return archives; } protected boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } return entry.getName().startsWith(BOOT_INF_LIB); } 复制代码public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } 复制代码4.2 创建自定义 ClassLoaderprotected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); //创建类加载器, 并指定归档文件 ClassLoader classLoader = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), classLoader); } //创建类加载器, 将归档文件转换为URL protected ClassLoader createClassLoader(List archives) throws Exception { List urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL[0])); } //父加载器是AppClassLoader protected ClassLoader createClassLoader(URL[] urls) throws Exception { //getClass().getClassLoader() 是系统类加载器,因为默认情况下main方法所在类是由SystemClassLoader加载的,默认情况下是AppClassLoader. return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } 复制代码4.3 设置线程上下文类加载器,调用程序中的 main classpublic static void main(String[] args) throws Exception { new JarLauncher().launch(args); } protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { //设置线程上下文类加载器 Thread.currentThread().setContextClassLoader(classLoader); //调用MANIFEST.MF 中配置的Start-Class: xxx的main方法,还带入了参数 createMainMethodRunner(mainClass, args, classLoader).run(); } 复制代码
哈勃太空望远镜探测到矮星系的保护罩麦哲伦日冕研究人员证实了难以捉摸的麦哲伦日冕的存在,这是一个由热的电离气体组成的保护性光环,以前只在理论上认为有。几十亿年来,银河系最庞大的宇宙伙伴大麦哲伦云和小麦哲伦云一直在太空中进行着动
重阳节老年人饮食谨记四点农历九月初九是我国传统节日重阳节。俗话说家有一老,如有一宝,敬老孝老爱老是中国民族的传统美德。今年的重阳节正好处于国庆假期期间,针对老年人饮食,中国食品科学技术学会日前发布重阳节老
政策加力,为制造业高质量发展添动能新华社北京9月30日电题政策加力,为制造业高质量发展添动能新华社记者王雨萧吴雨张辛欣制造业是实体经济的根基。针对市场主体当前困难较大等情况,近日召开的国务院常务会议进一步加大纾困政
我国制造业区域高质量发展取得积极成效上半年25个集群完成产值6。2万亿元沿长江11省市已建设1134家绿色工厂,沿黄河9省区工业用水效率高于全国平均水平重点培育的25个先进制造业集群主导产业产值近10万亿元一组组数据折射出我国制造业区域协调发展的可喜变
普京的二难俄乌冲突的爆发与延宕,本源是在政治伦理上,国际社会陷入了民族国家与联邦国家的二难。早期斯拉夫人,对古希腊古罗马帝国而言,与盎格鲁萨克逊人日耳曼人一样,同为北方三大蛮族,是奴隶的来源
人品有问题的人,微信聊天就知道了01hr聊天是一件很奇妙的事情,黑的可以说成白的,白的可以说成黑的。从古至今,我们都期待一次真诚的聊天,要么给予安慰,要么启迪智慧。战国时的齐闵王,今年去攻打宋国,明年去攻打赵国和
一年半内融资三次,红杉腾讯均为股东,这家CRM公司怎么突破内卷?记者李京亚编辑互联网广告圈一位元老级人物杨炯纬进军企业服务赛道,被多方评价为ToB领域的风向标。他所创办的卫瓴科技在投资圈颇有热度,一年半内连续融资三次,红杉腾讯都有站台,最新一轮
从代工到合作,李一男的牛创新能源不造车去哪了李一男口中造车没偷过一天懒,也没一天缺过钱的牛创新能源,研发的首款车型自游家NV终于来了。9月29日,自游家品牌官宣,NV将于10月8日全面登场。同时,此前飘忽不定的车辆尾标也最终
买学习平板选护眼屏真有必要吗?究竟什么样的学习平板才值得入手学习没兴趣上课听不懂作业不会做家长教不了?等等这些学习问题都是目前家长们所面临的,加上双减政策的实施,没有了辅导班,孩子课堂听讲和课后自学就更加的重要。特别是对于中小学的孩子们,正
廿万公里10章新疆餐厅吃饭,有人给片哈密瓜,千万不要接,是笼子哈萨克骑手在北疆勒马驰骋第一节准备一个人到新疆,书记不干了。自从俺在紫鹊界小东江漓江徒步归来之后,觉得一个人独游那是太过瘾了。这不国庆节又要到了放假七天,俺又在发愁到哪里去混过这几
新疆怎一个美字了得草原牧区行新疆篇作者光明日报调研组开栏的话清朗的风,从广袤无垠的草原上轻轻拂过,在乳香茶香酒香间盘桓萦绕,又将泥土的芬芳骏马的嘶鸣牧人的歌声送往渺渺的远方草原牧区,是游人向往的大美