使用 Byte Buddy 运行时生成泛型子类

在上一篇中尝试了 使用 Javassist 运行时生成泛型子类,这里要用另一个更方便的字节码增加组件 Byte Buddy 来实现类似的功能, 但代码上要直白一些。就是运用 Byte Buddy 在运行时生成一个类的子类,带泛型的,给类加上一个注解,可生成类文件或 Class 实例,不过这里更进一步,实现的方法是带参数的。

用 Byte Buddy 操作起来更简单,根本不需要接触任何字节码相关的,诸如常量池等概念。与 Javassist 相比,Byte Buddy 更为先进的是能生成的类文件都是可加载运行的,不像 Javassist 生成的类文件反编译出来是看起来是正常的,但一加载执行却不那回事。

本例所使用的 Byte Buddy 的版本是当前最新的 1.6.7,在 Maven 项目中用下面的方式引入依赖
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.6.7</version>
</dependency>
下面是几个需要在本例中用到的类定义

泛型的 Repository 类
1package cc.unmi;
2
3public abstract class Repository<T> {
4    abstract T findOne(int id);
5}

与前面有所不同的是上面的抽象方法带有一个参数

注解 Scope
1package cc.unmi;
2
3import java.lang.annotation.Retention;
4import static java.lang.annotation.RetentionPolicy.RUNTIME;
5
6@Retention(RUNTIME)
7public @interface Scope {
8    String value();
9}

测试代码如下
 1package cc.unmi;
 2
 3import net.bytebuddy.ByteBuddy;
 4import net.bytebuddy.description.annotation.AnnotationDescription;
 5import net.bytebuddy.description.type.TypeDescription;
 6import net.bytebuddy.dynamic.DynamicType;
 7import net.bytebuddy.implementation.MethodDelegation;
 8import net.bytebuddy.implementation.bind.annotation.AllArguments;
 9import net.bytebuddy.implementation.bind.annotation.Origin;
10import net.bytebuddy.matcher.ElementMatchers;
11
12import java.io.File;
13import java.lang.reflect.Method;
14
15public class Main {
16
17    @SuppressWarnings("unchecked")
18    public static void main(String[] args) throws Exception {
19        //泛型类型需要这么声明参数类型
20        TypeDescription.Generic genericSuperClass =
21            TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build();
22
23        //new ByteBuddy().subclass(Repository.class) //简单非泛型类可以这么做
24        DynamicType.Unloaded<?> unloadedType = new ByteBuddy().subclass(genericSuperClass)
25            .name(Repository.class.getPackage().getName().concat(".").concat("UserRepository"))
26            .method(ElementMatchers.named("findOne"))  //ElementMatchers 提供了多种方式找到方法
27          //.intercept(FixedValue.value("Yanbin"))   //最简单的方式就是返回一个固定值
28            .intercept(MethodDelegation.to(FindOneInterceptor.class)) //使用 FindOneInterCeptor 中的实现,定义在下方
29            .annotateType(AnnotationDescription.Builder.ofType(Scope.class).define("value", "Session").build())
30            .make();
31        
32        // 在 Maven 项目中,写类文件在 target/classes/cc/unmi/UserRepository.class 中
33        unloadedType.saveIn(new File("target/classes"));
34
35        //可以这样生成字节码得到 Class 实例来加载使用
36        //Class<?> subClass = unloadedType.load(Main.class.getClassLoader(),
37        //    ClassLoadingStrategy.Default.WRAPPER).getLoaded();
38
39        Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("cc.unmi.UserRepository");
40        System.out.println(repositoryClass.getAnnotation(Scope.class).value()); //输出 Session
41
42        Repository<String> repository = repositoryClass.newInstance();
43        System.out.println(repository.findOne(7792));  //输出 http://unmi.cc/?p=7792
44    }
45
46    private static class FindOneInterceptor {
47        //通过 method 和 arguments 可获得原方法引用与实际传入参数
48        //如果不关心它们或其中某个,可在声明 intercept() 方法时省略。也能加更多参数,如 @SuperCall Callable<?> call
49        static String intercept(@Origin Method method, @AllArguments Object[] arguments) {
50            return "http://unmi.cc/?p=" + arguments[0];
51        }
52    }
53}

请从运行输出结果与上面的注释中去理解操作过程。

上面的实现与 使用 Javassist 运行时生成泛型子类 相比要简洁了许多:

  1. 其中子类中默认构造调用父类默认构造的情况不需要特别说明
  2. 给泛型指定参数类型也不容易产生问题
  3. 给类加个注解也无需涉到了常量池的概念
  4. 并且泛型方法无须关心类型擦除的机制, 这一点可对比两篇中生成的字节码反编译出的 findOne() 方法返回类型, Object v.s. String

控制台的输出为
Feb 01, 2017 10:32:30 PM net.bytebuddy.dynamic.DynamicType$Default saveIn
INFO: Writing file to existing folder structure: target/classes/cc/unmi
Session
http://unmi.cc/?p=7792
不妨看看生成的 target/classes/cc/unmi/UserRepository.class 文件在 IntelliJ IDEA 中反编译后的样子
 1//
 2// Source code recreated from a .class file by IntelliJ IDEA
 3// (powered by Fernflower decompiler)
 4//
 5
 6package cc.unmi;
 7
 8import cc.unmi.Repository;
 9import cc.unmi.Scope;
10import cc.unmi.Main.FindOneInterceptor;
11
12@Scope("Session")
13public class UserRepository extends Repository<String> {
14    String findOne(int var1) {
15        return FindOneInterceptor.intercept(cachedValue$kkUnaNUd$e1iccd2, new Object[]{Integer.valueOf(var1)});
16    }
17
18    public UserRepository() {
19    }
20
21    static {
22        cachedValue$kkUnaNUd$e1iccd2 = Repository.class.getDeclaredMethod("findOne", new Class[]{Integer.TYPE});
23    }
24}

应用拓展可参考前一篇 使用 Javassist 运行时生成泛型子类 的末尾部分

在搜索关于 Byte Buddy 使用疑问时常常可以看到作者 Rafael Winterhalter 本人热心解答。

相关链接:

  1. Byte Buddy 官方指南
  2. Easily Create Java Agents with Byte Buddy
永久链接 https://yanbin.blog/leverage-bytebuddy-generate-generic-subclass/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。