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>
posted @ 2025-12-04 11:49  一杯半盏  阅读(6)  评论(0)    收藏  举报