需求:针对某些DB不同版本的冲突问题,采用自定义类加载器的方式自行加载 Driver,驱动包放至 jdbc module 下,而 web module 在使用 Spring Boot 打成 jar 包后,需要获取到内部 jdbc module 中的驱动包。Spring Boot jar 包启动时,自定义类加载需要能够读到驱动包资源。
曾几何时,我以为所谓的Spring Boot 启动原理
这些知识点最多就是面试时候可能问到的东西,后来在工作时还真用到了。
前期尝试
这种需求,本以为并不难做,毕竟针对 jar 包内部的 jar,Java URL 协议中本身就有jar:file:
前缀提供支持。使用PathMatchingResourcePatternResolver
获取到 URL 后,直接赋给 URLClassLoader 应该是可行的。这种方式在 IDE 中运行时没有问题的,urls 中的 URL 都是以file:
为前缀,运行正常;而打成 jar 包运行时,URL的格式变为jar:file:/...web.jar!/BOOT-INF/lib/...jdbc.jar!/.../hive/2.1.1/hive-common-2.1.1.jar
,运行时出现了java.lang.ClassNotFoundException
。
1 | String location = "classpath:drivers/hive/2.1.1/*.jar"; |
重新调试
我选择在一个单独的调试项目下对刚才生成的jar包进行调试。第一直觉是 URLClassloader
是不是不支持这种 jar:file
前缀 URL,不过并没有找到相关描述信息,不过倒是发现刚才的jar:file
的URL中没有加上!/
后缀(这表示在 jar 包内部就行查找)。实际上,对于原始的JarFile URL,只支持一个!/
。那 Spring Boot 是怎么做到的呢,他是怎么做到能够读取到 jar in jar 呢,原来 Spring Boot 扩展了协议,使其能够支持读取嵌套 jar。于是在调试项目下,我引入了spring-boot-loader
的依赖进行试验。
1 | <dependency> |
由于一般 Spring Boot 打成的 jar 包中的META-INF/MANIFEST.MF
文件中会包含Main-Class: org.springframework.boot.loader.JarLauncher
。即指定了 Main Class 为 JarLauncher
。在其 launch 方法中发现,Spring Boot 就是在 JarFile.registerUrlProtocolHandler()
注册了URLStreamHandler
的实现:org.springframework.boot.loader.jar.Handler
。
1 | // in Launcher |
在测试项目下,由于是 IDE 运行,我先在 main 方法中加入了JarFile.registerUrlProtocolHandler()
。在Handle的openConnection方法中调用了JarURLConnection
的get(URL url, JarFile jarFile)
方法,真正报错的地方在于内部的jarFile.getNestedJarFile(jarEntry)
,这个信息在java.lang.ClassNotFoundException
的堆栈中是没有显示的:
1 | java.lang.IllegalStateException: Unable to open nested entry '....jar'. It has been compressed and nested jar files must be stored without compression. Please check the mechanism used to create your executable jar file |
显示是 Jar 包压缩的问题?于是我在 jdbc module 的 pom 文件中加了这么一段,compress 置为了false,结果发现终于成功了。
1 | <build> |
最终实现
到最后发现实际上改的地方不多,但是如果自己不去调试 Spring Boot 的启动包,也发现不了真正的原因。
改动1就是 jdbc module 加入了maven-jar-plugin
插件且compress
置为 false。 改动2是在一开始实现的 for 循环中,对于jar:file
开头的URL加了!/
后缀。
1 | for (int i = 0; i < resources.length; i++) { |
实际上代码中不需要加JarFile.registerUrlProtocolHandler()
注册协议,由于刚才的调试项目的main方法运行的,所以才加上。而在正式项目代码处,若打包运行的话,jar 包启动时本身就执行了注册协议;若在 IDE 里运行,file:
前缀则不需要走此协议。当然如果不是加上这句话进行调试,也发现不了问题。