ByteBuddy(十二)—生成构造函数

本章介绍如何动态生成构造函数。

本章有两个功能代码,父类Producer.java及其派生类DataProducer.java

这是Producer.java代码:

public class Producer{
    private long producerId;
    private String record;
    public Producer(){}
    public Producer(long pid, String d){
        producerId = pid;
        record = d;
    }
    public long getProducerId(){
        return producerId;
    }
    public void setProducerId(long producerId){
        this.producerId = producerId;
    }
    public String getRecord(){
        return record;
    }
    public void setRecord(String data){
        this.record = data;
    }
}

这是DataProducer.java代码:

public class DataProducer extends Producer{
    private int dataProducerId;
    private String data;
    private BigInteger int01;
    public int getDataProducerId(){
        return dataProducerId;
    }
    public void setDataProducerId(int dataProducerId){
        this.dataProducerId = dataProducerId;
    }
    public String getData(){
        return data;
    }
    public void setData(String data){
        this.data = data;
    }
}

观察到DataProducer.java没有构造函数。
将向DataProducer.class添加七个构造函数。

使用define方法克隆构造函数

这是插件程序创建的第一个构造函数:

public DataProducer(int p1, String p2, String p3){}

这是创建构造函数的代码,它在InterceptorPlugin.javaapply方法中实现

Constructor c1 = ConstructorPrototype.class
                .getDeclaredConstructor(int.class, String.class, String.class);
        builder = builder.define(c1)
                .intercept(MethodCall
                        .invoke(Producer.class.getConstructor()));

ConstructorPrototype.java:

public class ConstructorPrototype{
    private int data1;
    private String data2;
    private String data3;
    public ConstructorPrototype(int a, String b, String c){
        data1 = a;
        data2 = b;
        data3 = c;
    }
}

define方法克隆ConstructorPrototype.java的构造函数,该构造函数包含三个参数:int.classString.classString.class
为了使define方法克隆构造函数,程序必须传递java.lang.reflect的实例。
构造函数设置为define方法的参数。
然而,define方法从不克隆方法体和在原型构造函数上声明的注解。

与Java方法类似,新声明的构造函数需要构造函数体。
intercept方法使用MethodCall生成构造函数体。

.intercept(MethodCall.invoke(Producer.class.getConstructor()));

在此构造函数中,MethodCall用于调用超类默认构造函数Producer(),该构造函数生成以下字节码:

public DataProducer(int p1, String p2, String p3){
    super();
}

使用defineConstructor方法生成构造函数

接下来,Plugin程序创建私有构造函数:

private DataProducer(long p1){}

这是用于生成构造函数的代码:

builder = builder.defineConstructor(Visibility.PRIVATE)
                .withParameter(long.class)
                .intercept(MethodCall
                        .invoke(Producer.class.getConstructor()));

代码使用define构造函数。
defineConstructor方法采用一个Visibility参数,该参数指定构造函数的修饰符。
在此示例中,参数指定私有可见性。
然后,该方法链接到withParameter方法,该方法声明long类型参数。
之后,intercept方法使用MethodCall创建构造函数体。

接下来,Plugin程序创建此构造函数:

public DataProducer(int p1, int p2){}

这是用于生成构造函数的代码:

builder = builder.defineConstructor(Opcodes.ACC_PUBLIC)
                .withParameters(int.class,int.class)
                .intercept(
                        MethodCall.invoke(
                                Producer.class.getConstructor()));

代码使用defineConstructor声明公共构造函数。
Opcodes是指定修改器的可见性的替代方法。
然后,该方法链接到带有两个参数的withParameters方法:int.classint.class,与withParameter方法不同,withParameters可以创建多个参数。
然后,intercept方法创建构造函数体。

apply方法进一步创建下一个构造函数:

public DataProducer(int var1, int var2, String var3, String var4) {
        super((long)var1, var3);
        this.dataProducerId = var2;
        this.data = var4;
}

这是实现构造函数的代码:

builder = builder.defineConstructor(Visibility.PUBLIC)
                .withParameters(int.class, int.class, String.class, String.class)
                .intercept(
                        MethodCall.invoke(Producer.class
                                .getDeclaredConstructor(long.class,String.class))
                                .withArgument(0,2)
                                .andThen(FieldAccessor.ofField("dataProducerId")
                                        .setsArgumentAt(1))
                                .andThen(FieldAccessor.ofField("data")
                                        .setsArgumentAt(3)));

构造函数及其参数的声明使用了前面的构造函数中已经解释过的类似方法。
然而,构造函数主体与之前的构造函数不同。
构造函数调用具有两个参数的父类构造函数:

super((long)var1, var3);

这行代码的字节码是通过以下建议代码创建的:

MethodCall.invoke(Producer.class
    .getDeclaredConstructor(long.class,String.class))
    .withArgument(0,2)

MethodCall调用另一个接受两个参数的超级构造函数:long.classString.class
然后使用WithArgument方法将var1var3参数传递给父类构造函数。
使用var1var3参数是因为withArgument方法指定了参数索引02
这意味着DataProducer构造函数的第一个和第三个参数。
类传递给其父类构造函数。
之后,构造函数主体将var2var4分配给dataProducerlddata实例变量:

dataProducerId = var2;
data = var4;

为了为这些代码行生成字节码,Advice代码将方法链接到andThen方法

andThen(FieldAccessor.
    ofField("dataProducerId")
    .setsArgumentAt(1))
net.bytebuddy.implementation.FieldAccessor    用于以编程方式访问实例变量。

这里FieldAccessor的用法是将var2参数设置为dataProducerld实例变量。
ofField方法配置dataProducerld实例变量,setsArgumentAt方法配置var2参数。
setsArgumentAt方法中的值指定构造函数的参数索引。
因此,选择var2参数。
类似地,为这行代码生成字节码:

data = var4;

FieldAccessor用于实现以下目的:

andThen(FieldAccessor.ofField("data")
    .setsArgumentAt(3))

接下来,Plugin程序创建此构造函数:

public DataProducer(long var1, String var3) throws ClassNotFoundException, SQLException {
        super(var1, var3);
}

这是生成构造函数的代码:

builder = builder.defineConstructor(Visibility.PUBLIC)
                .withParameters(long.class,String.class)
                .throwing(ClassNotFoundException.class, SQLException.class)
                .intercept(
                        MethodCall.invoke(
                                Producer.class.getDeclaredConstructor(long.class,String.class))
                                .withAllArguments());

与上一个构造函数类似,构造函数主体调用接受两个参数的父类构造函数。
但是,Advice代码使用withAllArguments而不是withArgument方法。
withAllArguments将所有参数值从构造函数传递给父类构造函数。

此构造函数声明throws子句。
throws子句引发ClassNotFoundExceptionSQLException
这些异常是通过throwing方法声明的。

接下来,Plugin程序插入默认构造函数。

这是生成的默认构造函数:

public DataProducer(){
    this.dataProducerId = 120;
    this.record = "<noData>";
}

这是插入默认构造函数的代码,注意到代码没有使用defineConstructor方法声明默认构造函数,而是使用构造函数方法来匹配DataProducer.class中的默认构造函数

builder = builder.constructor(ElementMatchers.isDefaultConstructor())
                .intercept(
                        MethodCall.invoke(
                                Producer.class.getDeclaredConstructor())
                                .andThen(
                                        FieldAccessor
                                                .ofField("dataProducerId").setsValue(120))
                                .andThen(
                                        FieldAccessor
                                                .ofField("data").setsValue("<noData>")));

代码调用构造函数方法并应用ElementMatchers.isDefaultConstructor方法以匹配默认构造函数。
选择默认构造函数后,构建器调用intercept方法使用MethodCall调用父类构造函数的代码。
然后,代码使用FieldAccessordataProducerld的实例变量的值设置为120,并将data实例变量设置为字符串值<noData>
与第10章中介绍的值法相比,集合值法是正确的方法:
动态声明实例变量。为Java字节码编程时,程序必须使用构造函数来设置实例变量的初始值。

为什么DataProducer.java包含默认构造函数,即使插件程序没有声明它?当调用以下方法之一时,ByteBuddy将隐式创建默认构造函数:definedefineConstructor
define方法也可以用于声明实例变量和Java方法。
如果使用define方法声明构造函数,则ByteBuddy将自动创建默认构造函数。
因此,在创建DataProducer(int, String, String)构造函数时,已经创建了默认构造函数。

使用visit方法生成构造函数

最后,Plugin程序声明了这个构造函数:

public DataProducer(int var1, String var2, String var3, String var4) {
        super((long)var1, var3);
        if (var1 % 2 == 0) {
            this.dataProducerId = var1 + 10000;
        } else {
            this.dataProducerId = var1 + 20000;
        }

        this.data = var2;
        this.int01 = new BigInteger(this.data);
}

这是生成构造函数的代码:

builder = builder.defineConstructor(Visibility.PUBLIC)
                .withParameters(int.class, String.class, String.class, String.class)
                .intercept(MethodCall
                        .invoke(Producer.class
                                .getDeclaredConstructor(long.class,String.class))
                        .withArgument(0,2));

builder = builder.visit(Advice
                .to(ValueSetter.class)
                .on(ElementMatchers.isConstructor()
                        .and(ElementMatchers.takesArgument(0,int.class))
                        .and(ElementMatchers.takesArgument(1,String.class))
                        .and(ElementMatchers.takesArgument(2,String.class))
                        .and(ElementMatchers.takesArgument(3,String.class))));

intercept方法只生成调用父类构造函数的字节码:

super((long)var1, var3);

剩余的代码由visit方法生成。
visit方法中,匹配构造函数ElementMatchers
是构造函数和ElementMatchers
使用takesArgument(0, int.class)方法。
isConstructor方法将匹配范围仅限于Constructor
takesArgument方法接受两个参数:intjava.lang.Class
第一个int参数指定参数的索引
第二个参数指定其数据类型
此方法仅匹配第一个参数

因此,为了匹配具有四个参数的构造函数,takesArgument被执行四次。
使用此配置,应选择构造函数public DataProducer(int, String, String, String)来应用Advice代码。

ValueSetter.java:

public class ValueSetter{
    @Advice.OnMethodExit
    public static void set(
            int param0, 
            String param1,
            @Advice.FieldValue(value="dataProducerId", readOnly=false)int var1,
            @Advice.FieldValue(value="data", readOnly=false) String var2,
            @Advice.FieldValue(value="int01", readOnly=false) BigInteger biginteger){
        if(param0 % 2 == 0)
            var1 = param0 + 10000;
        else
            var1 = param0 + 20000;
        var2 = param1;
        biginteger = new BigInteger(var2);
    }
}

之后,构造函数主体将包含ValueSetter.java中提供的Advice代码拷贝到DataProducer.class
Advice代码实例化了一个名为"int01"的实例变量,它是BigInteger的一个实例。
每当构造函数想要使用new运算符实例化对象时(例如,new BigInteger),都应该为此实现Advice代码。
然而,在撰写本文时,MethodCall的使用会在检测过程中抛出TllegalStateException

结论

本章说明:

  • 如何动态声明构造函数
  • 如何使用MethodCall声明构造函数体如何将参数传递给超级构造函数
  • 如何使用FieldAccessor.ofFieldsetsValue方法设置实例变量的初始值

bytebuddy书籍《Java Interceptor Development with ByteBuddy: Fundamental》

----END----

喜欢就点个👍吧

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容