编译期动态替换三方包中的Class文件

背景

最近做业务时遇到一个问题,客户想在底层数据添加一个字段,只能乖乖的添加表字段、实体添加对应属性,一切都在预期中进行这,但是这个工程是经过二开的,展示层实体没法直接添加,于是想当然继承实体扩展字段,没想到顶层一堆Request、Response,如果一个一个进行扩展马也得累死,于是就思考有没有简便的方法仅对目标实体进行操作来完成字段添加的方法。

思考过程

在Java中要在类中添加字段属性,除了显示编码外,还有一种技术就是编译期间动态修改,比如Lombok、Mapstruct等都是在编译期动态生成代码,提高编码效率,所以我也考虑通过这种方式编译期添加目标字段属性,百度了一通没有合适的方式动态添加,但是手写通过字节码注入一定是可以实现的,想想成本还是有点高,赶紧转换思路,既然不能动态插入字段,那能不能直接替换目标类呢?一想到这就有戏,在Java中加载类是通过类加载器进行加载的,有了依据后赶紧接着百度,果不然让我发现一种方式,通过maven插件的方式实现,客官接着往下看。
一般情况下不建议用这么hack的方式哈,尽量保持三方包的新鲜度,避免未来升级导致的兼容性问题。

主角出场

主角:maven-dependency-plugin
这仅仅是处理的一种方式,大家如果有更好的处理方式,可以放到评论区,我们一起讨论,互相学习进步。

实现原理

通过配置maven-dependency-plugin,可以将我们指定的dependency解压到项目的class目录中,设置不覆盖本地项目相同class文件(类的全限定名相同),就实现了本地文件替换三方jar中类文件的目的了。
在Java应用中,如果存在多个同名类,最终只会加载一个目标类,到底会加载哪个同名类是又类加载器的双亲委派机制决定的,先请求父类加载器加载,父类无法加载回到应用程序加载器,应用程序无法加载就会到类路径下即class目录中加载,如果仍然不存在会到依赖中进行加载。
基于以上原理,实现类文件覆盖就有了依据,那么接下来具体实践演示下。

使用

这里我使用commons-lang3举例。

第一步:配置Maven插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<!-- unpack任务标识符,unpack是将依赖从仓库中解压到指定目录 -->
<id>unpack</id>
<!-- unpack任务默认执行阶段 -->
<phase>generate-sources</phase>
<goals>
<!-- 目标功能:unpack -->
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<!-- 设置为false,依赖解压到目录时不会进行覆盖,设置为true则会覆盖 -->
<overWrite>false</overWrite>
<!-- 目标class文件输出目录 -->
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>

第二步:编译工程

image.png
这一步还没有定义目标类,暂时仅是将目标三方包的类放到了class目录下,标红的位置是即将要进行修改的位置。

第三步:添加目标类

image.png

  • 包名和目标类所在包名完全一致;
  • 类名保持一致;

第四步:重新编译工程

image.png
至此已经能够看到效果了,整个方式过程也比较简单,去掉中间的编译过程,总共两步。

总结

这种Hack的方式在业务编码中建议少用,通过上边的方式虽然能解决问题,但同时也引入了一些副作用,一方面相当于依赖包引入两份,另一方面当依赖包升级时可能存在疏漏。看看学习学习,多一种解决方式多一条路,希望大家每天编码顺顺利,我就先溜了!

评论