本章介绍如何动态生成构造函数。
本章有两个功能代码,父类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》
喜欢就点个👍吧