除了OnMethodEnter
和OnMethodExit
Advice之外,ByteBuddy还支持Around Advice
。
Around Advice
通过方法委托将Advice代码添加到函数方法的开始和结束。
本章使用两个Advice代码:
PerfObjectReturnInterceptor.java
PerfVoidReturnInterceptor.java
这是功能代码
public class DataProducer{
public void create(){
logger.info("create data");
}
public String createData(){
String uniqueId = UUID.randomUUID().toString();
return new String(
Base64.getEncoder().encode(uniqueId.getBytes()),
Charset.forName("UTF-8"));
}
}
create方法在屏幕上打印状态字符串"create data"。
createData方法返回base64编码的UUID。
这是InterceptorPlugin.java
方法的实现(3个方法不在解释,不懂看之前的内容)
public class InterceptorPlugin implements Plugin {
@Override
public DynamicType.Builder> apply(DynamicType.Builder> builder,
TypeDescription typeDescription,
ClassFileLocator classFileLocator) {
//
}
@Override
public void close() throws IOException {
//
}
@Override
public boolean matches(TypeDescription typeDefinitions) {
//
}
}
这是apply
方法的实现
public DynamicType.Builder> apply(Builder> builder,
TypeDescription typeDescription,
ClassFileLocator classFileLocator){
return builder
.method(ElementMatchers.named("create"))
.intercept(MethodDelegation
.to(PerfVoidReturnInterceptor.class))
.method(ElementMatchers.named("createData"))
.intercept(MethodDelegation
.to(PerfObjectReturnInterceptor.class));
}
Around Advice create方法
在apply
方法中,调用builder.method
的方法。
method
方法使用ElementMatchers.named
Advice匹配逻辑。
named
方法使用"create"
作为其参数值:
builder.method(ElementMatchers.named("create"))
因此,DataProducer.java
的create
方法被选择用于instrumentation
。
之后,调用builder
的intercept
方法。
intercept
方法的参数指定MethodDelegation.to(PerfVoidReturnInterceptor.class)
这些方法与onMethodEnter
和onMethodExit
Advice完全不同。
首先,builder
调用intercept
方法来执行检测,但不调用visit
方法。
然后,检测使用MethodDelegation
,而不是Advice.to
方法。
匹配逻辑是通过method
方法实现的,但不是Advice方法链中的on
方法。
PerfVoidReturnInterceptor.java
public class PerfVoidReturnInterceptor{
public static Logger logger = Logger.getLogger(PerfVoidReturnInterceptor.class.getName());
public static void aroundVoid(@SuperCall Runnable targetCode, @Origin Method method){
long startTime = System.currentTimeMillis();
logger.info("Method start");
targetCode.run();
logger.info("Methodend:" + method.getName() + "executiontime:" + (System.currentTimeMillis() - startTime));
}
}
以下是实施的Around Advice的要求
:
- Advice方法是一种
静态方法
。 - Advice方法返回
void
。 - Advice方法有一个
java.lang.Runnable
参数,并用@SuperCall
注解进行注解。
在本例中,aroundVoid
方法是Advice方法。
ByteBuddy将基于aroundVoid
方法的内容生成插入指令的代码。
由于MethodDelegation
和@SuperCall
注解,ByteBuddy创建了一个代理类来实现插入指令的代码。
这个代码的目的是测试业务代码的运行性能。
targetCode
参数表示DataProducer.java
的函数代码的create
方法。
插入指令的代码将执行以下指令:
1.将方法开始时间存储到startTime变量
2.记录指示方法执行开始的消息
3.执行功能代码,在屏幕上打印状态"create data"。
4.记录一条消息,指示方法执行结束,以及方法执行时间(毫秒)。
启动maven build并执行Main1.java
这是插入指令的代码在屏幕上产生的结果:
11月 10, 2022 2:52:01 下午 com.wpixel.bytebuddy.chapter1.PerfVoidReturnInterceptor aroundVoid
信息: Method start
11月 10, 2022 2:52:01 下午 com.wpixel.bytebuddy.chapter1.DataProducer create$original$nNDKo4Mw
信息: create data
11月 10, 2022 2:52:01 下午 com.wpixel.bytebuddy.chapter1.PerfVoidReturnInterceptor aroundVoid
信息: Method end: create execution time: 140
方法委托不同于访问方法的字节码。
为了更好地理解这一差异,让我们看看生成的字节码的Java源代:
public class DataProducer {
public static Logger logger = Logger.getLogger(DataProducer.class.getName());
public DataProducer() {
}
public void create() {
PerfVoidReturnInterceptor.aroundVoid(new DataProducer$auxiliary$oGTr3aHb(this), cachedValue$4Pj3oKix$k5ssu03);
}
public String createData() {
return (String)PerfObjectReturnInterceptor.aroundReturn(new DataProducer$auxiliary$wfWjMGB6(this), cachedValue$DokzLtVw$daoi2o0);
}
}
class DataProducer$auxiliary$oGTr3aHb implements Runnable, Callable {
private DataProducer argument0;
public Object call() throws Exception {
this.argument0.create$original$nNDKo4Mw$accessor$4Pj3oKix();
return null;
}
public void run() {
this.argument0.create$original$nNDKo4Mw$accessor$4Pj3oKix();
}
DataProducer$auxiliary$oGTr3aHb(DataProducer var1) {
this.argument0 = var1;
}
}
在生成的字节码中,ByteBuddy创建一个名为create$original
的新私有方法,并将原始create
方法的方法内容复制到该方法。
然后,ByteBuddy修改创建方法。
ByteBuddy声明了一个名为auxiliary
的内部类。
这个内部类是代理类。
代理类包含对函数代码的引用:DataProducer.java
代理类使用参数来存储该引用,该引用通过代理类的构造函数传递。
代理类实现了两个Java接口:Java.lang.Runnable
和Java.util.concurrent.Callable
。
他们的派生方法调用并运行通过argument0
实例变量调用create$original
方法。
然后,ByteBuddy在Advice方法的末尾使用PerfVoidReturnInterceptor.java
调用aroundVoid
方法,该方法调用targetCode.run
方法。
其中run
方法是java.lang.Runnable
的实现。
可在auxiliary.oGTr3aHb
中运行。
尽管PerfVoidReturnInterceptor.java
只为SuperCall
参数指定了Runnable
,但是ByteBuddy在其生成的字节码中隐式地实现了Runnaable
和Callable
接口。然而,如果函数方法返回void,则@SuperCall
注解参数应在Advice代码中使用java.lang.Runnable
。
关于createData方法的Advice
类似地,要拦截DataProducer.java的createData方法,这是InterceptorPlugin的代码段。
.method(ElementMatchers.named("createData"))
.intercept(MethodDelegation
.to(PerfObjectReturnInterceptor.class));
createData方法是返回java.lang.String对象的方法。PerfObjectReturnInterceptor.java是createData方法的Advice代码:
public class PerfObjectReturnInterceptor{
public static Logger logger = Logger.getLogger(PerfObjectReturnInterceptor.class.getName());
@RuntimeType
public static Object aroundReturn(
@SuperCall Callable
以下是实现支持aroundReturn
函数方法的Advice的要求:
- Advice方法是一种静态方法。
- advice方法返回
Object
实例,或函数方法返回类型之后的类型。 - advice法有一个
java.util.concurrent
。可调用参数,并用@SuperCall
注解进行注解。 - Advice方法使用
@RuntimeType
注解进行注解。
即使Advice代码使用Callable
接口而不是Runnable,ByteBuddy也会为create
方法生成的类似字节码,唯一的区别是DataProducer的createData
方法。
java将调用PerfObjectReturnInterceptor.java
的aroundReturn
方法(在createData方法里),其第一个参数使用auxiliary
的代理类实例:DataProducer$auxiliary$wfWjMGB6
:
class DataProducer$auxiliary$wfWjMGB6 implements Runnable, Callable {
private DataProducer argument0;
public Object call() throws Exception {
return this.argument0.createData$original$qFMFHOxS$accessor$DokzLtVw();
}
public void run() {
this.argument0.createData$original$qFMFHOxS$accessor$DokzLtVw();
}
DataProducer$auxiliary$wfWjMGB6(DataProducer var1) {
this.argument0 = var1;
}
}
aroundReturn
方法可以更改函数代码的返回值。
如在aroundReturn
方法返回之前添加这行代码:
result = "test";
然后,返回值将更改为test
,它将替换base64编码的UUID。
Advice方法要返回值,则需要@RuntimeType
注解。
执行项目
为了查看检测过程的结果,请执行maven build ,然后执行Main1.java,将在屏幕上生成以下结果:
11月 10, 2022 3:26:56 下午 com.wpixel.bytebuddy.chapter1.PerfVoidReturnInterceptor aroundVoid
信息: Method start
11月 10, 2022 3:26:56 下午 com.wpixel.bytebuddy.chapter1.DataProducer create$original$qFMFHOxS
信息: create data
11月 10, 2022 3:26:56 下午 com.wpixel.bytebuddy.chapter1.PerfVoidReturnInterceptor aroundVoid
信息: Method end: create execution time: 109
11月 10, 2022 3:26:57 下午 com.wpixel.bytebuddy.chapter1.PerfObjectReturnInterceptor aroundReturn
信息: Method start
11月 10, 2022 3:26:57 下午 com.wpixel.bytebuddy.chapter1.PerfObjectReturnInterceptor aroundReturn
信息: Method end: createData execution time:15
结果表明,create
方法的执行时间为109毫秒
,createData
方法为15毫秒
。
@Origin注解
注意到Advice方法有第二个参数,它用@Origin注解。
请注意,此注解是net.bytebuddy.implementation.bind.annotation.Origin
,
而不是net.bytebuddy.asm.Advice
。
用于OnMethodEnter
或OnMethodExit
Advice。
劝告Origin返回包含函数方法名称的String值。
而Around Advice的@Origin 返回java.lang.reflect
的实例。
表示函数方法的方法。
例如:
public static void aroundVoid(@SuperCall Runnable targetCode,
@Origin Method method){
System.out.println(method.getName());
}
getName方法返回"create",这是函数方法的名称。
MethodDelegation 和 Advice
MethodDelegation和Advice之间的区别:
-
DynamicType.Builder
的intercept
方法使用MethodDelegation
。 - Advice由
DynamicType.Builder
的访问方法使用。 - 当使用
visit
方法时,ByteBuddy生成将Advice代码内联到函数代码中的代码。 -
MethodDelegation
和Advice
都可以应用于OnMethodEnter或Exit通知,但Advice不能应用Around Advice。
结论本章解释了:
- 如何为返回void的函数方法创建Around Advice
- 如何为返回对象引用的函数方法创建Around Advice
bytebuddy书籍《Java Interceptor Development with ByteBuddy: Fundamental》
喜欢就点个吧