本章介绍如何动态生成构造函数。
本章有两个功能代码,父类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.java的apply方法中实现
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.class、String.class,String.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.class和int.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.class和String.class。
然后使用WithArgument方法将var1和var3参数传递给父类构造函数。
使用var1和var3参数是因为withArgument方法指定了参数索引0和2。
这意味着DataProducer构造函数的第一个和第三个参数。
类传递给其父类构造函数。
之后,构造函数主体将var2和var4分配给dataProducerld和data实例变量:
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子句引发ClassNotFoundException和SQLException。
这些异常是通过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调用父类构造函数的代码。
然后,代码使用FieldAccessor将dataProducerld的实例变量的值设置为120,并将data实例变量设置为字符串值<noData>。
与第10章中介绍的值法相比,集合值法是正确的方法:
动态声明实例变量。为Java字节码编程时,程序必须使用构造函数来设置实例变量的初始值。
为什么DataProducer.java包含默认构造函数,即使插件程序没有声明它?当调用以下方法之一时,ByteBuddy将隐式创建默认构造函数:define,defineConstructor。
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方法接受两个参数:int和java.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.ofField的setsValue方法设置实例变量的初始值
bytebuddy书籍《Java Interceptor Development with ByteBuddy: Fundamental》
喜欢就点个👍吧