maven 高阶玩法:如何优雅地通过 Class Shadowing "劫持" 第三方库源码
副标题:我的代码被“吃”了?排查 Maven 多模块下 Class Overlapping 导致的诡异失效
接上篇:Maven 骚操作:使用 Unpack + Exclude 给第三方库打补丁
Class Shadowing
前一篇文章所利用的机制,这种技术通常被称为 Class Shadowing(类遮盖),通过在本地源码中建立同名包和同名类来“劫持”库的实现。
问题在于 Maven 的构建机制(特别是 Shade 插件)在合并 JAR 包时,如果处理不当,确实可能出现“本来想覆盖,结果被库里的原版反覆盖”或者“两者共存导致类加载器混乱”的情况。
场景
先前的文章中,使用了maven-dependency-plugin 进行了unpack,然后再把魔改的类进行打包,属于物理级覆盖。
然而今年发现,在 SpringBoot项目中, 通常使用 spring-boot-maven-plugin,它本身就会使得你魔改的类优先级最高。
<build>
...
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.4.5</version>
</build>
在使用了一段时间后,本身是生效的,结果有一天发现“劫持”不生效了。
AI 分析
在 Spring Boot (spring-boot-maven-plugin) 的体系下,逻辑与传统的 maven-shade-plugin(Fat Jar)完全不同。
在 Spring Boot 中,不需要做任何特殊配置,你的本地代码(src/main/java)天然拥有最高优先级,会自动覆盖掉 lib 文件夹中 JAR 包里的同名类。
如果你的“劫持”没有生效,90% 的情况是因为该第三方库启用了 Package Sealing(包密封) 安全机制。
为什么 Spring Boot 默认应该生效?
Spring Boot 生成的 Jar 结构如下:
BOOT-INF/classes/ —— 你的代码
BOOT-INF/lib/ —— 第三方依赖
Spring Boot 的类加载器(LaunchedURLClassLoader)有着严格的加载顺序:
优先加载 BOOT-INF/classes/ 中的资源。
其次加载 BOOT-INF/lib/*.jar 中的资源。
因此,只要你的 package 和 ClassName 完全一致,JVM 会先读到你的类,直接无视掉 Lib 里的那个。
我的分析
由于 Package Sealing 是AI给出的原因,它不太常见。经过分析是,AI 没有指出的另外10%是因为某个模块使用了maven-shade-plugin将你要“劫持”的字节码又一次复原了回去。
在 IntelliJ IDEA当中,双击 Shift , 输入你所要覆盖的类全称,会显示 Shade到某个jar内了。
事故现场还原
子模块(Sub-module) 配置了 maven-shade-plugin 且没有限制 artifactSet。
我“劫持”的是 httpclient当中的某个类型
问题出现在这份 maven-shade-plugin的配置内:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>com.google.gson</pattern>
<shadedPattern>com.suse.salt.shaded.com.google.gson</shadedPattern>
</relocation>
</relocations>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
看起来似乎完全和httpclient无关,然而就是这份配置,因为没有制定 artifactSet 限定范围,导致把这个模块内所有的 maven依赖(包括引入的 httpclient )shade到本模块的jar内。
过程分析
构建时:Maven 会把该子模块所有的运行时依赖(包括那个你想覆盖的 library)全部解压,并打入这个子模块生成的 JAR 包(sub-module.jar)中。
最终打包(Spring Boot App):
Spring Boot 插件收集依赖。
它拿到了 sub-module.jar(里面已经藏了一份 library 的类)。
它可能还通过传递依赖拿到了原始的 library.jar。
它加上了本地复写的 classes/。
结果:我的 Classpath 里现在可能有 3 份 同样的类!
BOOT-INF/classes/... (本项目的源码,优先级最高)
BOOT-INF/lib/httpclient-4.5.3.jar (原始库)
BOOT-INF/lib/sub-module.jar (被 Shade 进去的原始类副本)
为什么会失效? 虽然 classes 优先级最高,但如果我的子模块代码(在 sub-module.jar 里)调用该类时,由于它和那个被 Shade 进去的类在同一个 JAR 包内,类加载器极有可能直接就近使用了 JAR 包内部的那份副本,完全绕过了Springboot Uber jar内的 classes 目录。
解决方案
在上述 xml 配置当中,一定要限定shade处理的的范围,否则整个模块的 runtime以外scope级别的传递依赖都会被“打进去”。
<artifactSet>
<includes>
<include>com.google.code.gson:gson</include>
</includes>
</artifactSet>

浙公网安备 33010602011771号