Spring Boot -- 如何获取已加载的JAR文件流
最近遇到一个需求,在程序运行期间,拿到已加载类对应的jar包,然后上传到另一个地方,本以为利用ClassLoader直接定位到jar的InputStream
流直接读取就ok,事实却没有这么简单,我把问题总结为以下几个小点,逐一解决。
如何根据已加载的类定位到jar?
对于已加载的类,可以通过其对应的Class类的getProtectionDomain()
方法获取到对应的文件信息,以获取commons-lang3
jar包为例,如清单1所示。
清单1: 根据加载类定位到文件
1 | Class<StringUtils> clazz = StringUtils.class; |
该代码在不同环境下运行返回又是什么情况呢?
本地IDE运行
在IDEA中直接运行返回如下所示,很明显IDEA在运行时会把maven仓库中对应的jar路径放入classpath下,运行起来后类加载器自动寻找对应的jar,所以定位到了具体的maven目录。
清单2: IDE直接运行输出
1 | file:/Users/quding/.m2/repository/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar |
打包成jar运行
单纯的打包为一个jar,Java会把其中的依赖第三方jar解压后一起放入到jar中,如下图所示,因此定位到的是我最终打包为的jar文件,而并非第三方jar文件。因此如果是在这种环境下推荐使用指定classpath形式。
清单3: 打包成jar输出
1 | file:/Users/quding/workspace/git/jar-mvn1/target/jar-mvn1-1.0-SNAPSHOT.jar |
打包成war运行
写了个接口,返回值是一个具体的文件路径,原因也很简单,因为Tomcat在启动一个webapp时会将对应的war解压,然后针对解压后的路径使用一个单独的类加载器进行加载。
清单4: 打包成war包输出
1 | { |
打包成fat jar
fat jar是Spring Boot引入的一种新格式,其打包后的结构与war包比较类似,但是可以直接执行并不需要先解压再加载,打包后类似目录如下:
- BOOT-INF/classes – 用户代码
- BOOT-INF/lib –依赖第三方架包
- org/springframewora/boot/loader – Fat jar启动核心,后续会分析。
此时获取对应的jar,输出如清单5所示,可以看到与前面几种不同,此时路径为jar嵌套形式,暂且定义为jar in jar
。
清单5: 打包成Fat jar输出
1 | { |
其路径可以分为两个部分看,第一部分jar:file:/Users/quding/workspace/git/read-jar-demo/target/read-jar-demo-0.0.1-SNAPSHOT.jar!
,表示当前根架包位置,第二部分/BOOT-INF/lib/commons-lang3-3.7.jar!/
所需要的jar在根架包中的位置路径。
如何读取jar?
对于非jar in jar
形式,其获取到的目录是一个真是的物理文件路径,因此可以直接使用File
读取,从而拿到文件流,这里不重点关注。对于jar in jar
因为并不是规范的文件路径,因此无法使用File
直接读取,那么该怎么读呢?要解决这个问题需要先了解Spring Boot是怎么做的.
Spring Boot启动原理
打开Spring Boot最终产出的jar包,其MANIFEST.MF
文件表明项目的启动入口为org.springframework.boot.loader.JarLauncher
,该类在spring-boot-loader
模块下,运行时由Spring Boot所提供,因此可以通过maven引入provided类型的依赖从而查看到源码。
清单6: Spirng Boot启动模块
1 | <dependency> |
org.springframework.boot.loader.JarLauncher
做的第一步是找到自己所在jar的位置,使用方法与上文介绍的一致。
清单7: Spring Boot定位启动jar包
1 | protected final Archive createArchive() throws Exception { |
之后通过java.protocol.handler.pkgs
参数注册对应的URL协议扩展,该参数格式为[package_path].[protocol].Handler
,因此Spring Boot注册的为org.springframework.boot.loader.jar.Handler
这个jar协议扩展处理器,其在读取资源时会调用openConnection
方法,如清单8所示:
清单8: Spring Boot URL处理器
1 | protected URLConnection openConnection(URL url) throws IOException { |
由上述逻辑可以发现,当URL为jar:file:/Users/quding/workspace/git/read-jar-demo/target/read-jar-demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/commons-lang3-3.7.jar!/
形式,最终会调用JarURLConnection.get(url, this.jarFile)
方法来获取真正的jar文件,该JarURLConnection
并非JDK自带的类,其为class JarURLConnection extends java.net.JarURLConnection
,因此在运行时可以安全的向上转型为java.net.JarURLConnection
,在其get方法中会对URL进行循环处理,对结果进行嵌套包装,近而解决jar in jar类型的读取问题。
清单9: jar in jar循环读取
1 | while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { |
最后会根据得到的URL路径创建对应的类加载器org.springframework.boot.loader.LaunchedURLClassLoader
,使用该类加载器进行加载。
利用Handler读取jar
Spring Boot启动原理的关键点是实现了jar in jar
协议的处理器org.springframework.boot.loader.jar.Handler
,读取的主要功能为Handler中实现的openConnection
方法,因此当在项目代码中想要读取jar in jar格式的架包,则可以用该Handler进行资源读取。
1 | URL url = new URL(jarFile, "", new org.springframework.boot.loader.jar.Handler()); |
参考
- 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
- 文章标题: Spring Boot -- 如何获取已加载的JAR文件流
- 文章链接: https://mrdear.cn/posts/framework-spring-jar-in-jar.html