使用 Javassist 运行时生成泛型子类

越是复杂的项目希望使用者能愉快的编码的话,可能就要使用到字节码增强工具来暗地里做些手脚。这方面的工具有 JDK 的 Instrumentation, ASM, BCEL, CGLib, Javassist, 还有 Byte Buddy. Javassist 和 Byte Buddy 更贴近我们编码中的概念,使用起来也简单,而其他几个工具需要我们更多的了解字节码指令,以及常量池等概念。所以我着重去了解怎么运用 Javassist 和 Byte Buddy 来动态修改来生成类文件。

所以本文是系列中的第一篇,旨在以一个 Javassist 的例子来了解它的基本使用方法。本例中在运行时动态生成一个类的子类,并且是泛型的,实现了一个方法,给类加上了一个注解,最终生成一个类文件。总之尽可能的让这个例子具有代表性,同时又需控制它的复杂性。最后通过加载类文件的方式来验证前面生成的类是否是正确的,也可以直接反编译生成的类文件来查看源代码,不过实际操作中我们可能会被反编译出来的源代码欺骗。

本例所使用的 Javassist 的版本是 3.21.0-GA, 是在一个 Maven 项目中测试的,所以 Maven 的依赖是

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.21.0-GA</version>
</dependency>

接着创建好基类 Repository 和注解 Scope, 它们的内容分别如下

泛型的 Repository 类

注解 Scope

下面是动态生成子类以及测试的代码

从控制台的输出可以说明动态生成的类是我们期望的结果。详情请参考源代码中的注释。

代码中我们想要生成的类原型是 class UserRepository extends Repository<String>{}, 那么应该实现的就是

public String findOne() { ... }

但要是把生成方法的那行代码改成如下

CtNewMethod.make("public String findOne(){return \"Yanbin\";}, subClass);

这时候你要是查看生成类文件经反编译的源代码也很漂亮,显示为返回类型是 String, 可是一加载调用 fineOne() 方法时就悲剧了,控制台的输出就成了

Request
Exception in thread "main" java.lang.AbstractMethodError: cc.unmi.Repository.findOne()Ljava/lang/Object;
    at cc.unmi.Main.main(Main.java:45)

对于如何为类指定泛型,可参考 CtClass.setGenericSignature API

最后我们可以看一下生成的 target/classes/cc/unmi/UserRepository.class 文件在 IntelliJ IDEA 中反编译后的样子

应用延伸:

  1. 复杂的泛型,如 <List<User>>, class UserRepository<T extend User> extends Repository<T> 等,或是方法中的泛型参数
  2. 定义新的方法,较少情况,因为基本上我们用以生成字节码的话是基于接口来编程
  3. 实现接口,或生成子接口, 相应的就是 makeInterface(...)
  4. 是否能更多使用 Java 代码的方式来生成类各种部件
  5. 除了生成 Class 文件,我们也可以得到所生成类的引用; 或字节码的内容,可用自定义的类加载器进行加载
  6. 是否能同时生成相应的源代码到 Maven 项目的 generated-sources 目录中?未曾试过,找到两个相关的库 RoasterSrcGen4Javassist.

相关链接: Javassist 官方指南, 进去有几页内容。

本文链接 https://yanbin.blog/leverage-javassist-generate-generic-subclass/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments