ByteBuddy(八)—Around Advice

除了OnMethodEnterOnMethodExit Advice之外,ByteBuddy还支持Around Advice
Around Advice通过方法委托将Advice代码添加到函数方法的开始和结束。

image.png

本章使用两个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.javacreate方法被选择用于instrumentation
之后,调用builderintercept方法。
intercept方法的参数指定MethodDelegation.to(PerfVoidReturnInterceptor.class)

这些方法与onMethodEnteronMethodExit 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.RunnableJava.util.concurrent.Callable

他们的派生方法调用并运行通过argument0实例变量调用create$original方法。
然后,ByteBuddy在Advice方法的末尾使用PerfVoidReturnInterceptor.java调用aroundVoid方法,该方法调用targetCode.run方法。
其中run方法是java.lang.Runnable的实现。
可在auxiliary.oGTr3aHb中运行。

尽管PerfVoidReturnInterceptor.java只为SuperCall参数指定了Runnable,但是ByteBuddy在其生成的字节码中隐式地实现了RunnaableCallable接口。然而,如果函数方法返回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 targetCode,
                    @Origin Method method) throws Throwable{
        long startTime = System.currentTimeMillis();
        logger.info("Method start");
        Object result = targetCode.call();
        logger.info("Method end: " + method.getName() + " execution time: " + (System.currentTimeMillis() - startTime));
        return result;
    }
}

以下是实现支持aroundReturn函数方法的Advice的要求:

  • Advice方法是一种静态方法。
  • advice方法返回Object实例,或函数方法返回类型之后的类型。
  • advice法有一个java.util.concurrent。可调用参数,并用@SuperCall注解进行注解。
  • Advice方法使用@RuntimeType注解进行注解。

即使Advice代码使用Callable接口而不是Runnable,ByteBuddy也会为create方法生成的类似字节码,唯一的区别是DataProducer的createData方法。
java将调用PerfObjectReturnInterceptor.javaaroundReturn方法(在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
用于OnMethodEnterOnMethodExit 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.Builderintercept方法使用MethodDelegation
  • Advice由DynamicType.Builder的访问方法使用。
  • 当使用visit方法时,ByteBuddy生成将Advice代码内联到函数代码中的代码。
  • MethodDelegationAdvice都可以应用于OnMethodEnter或Exit通知,但Advice不能应用Around Advice。

结论本章解释了:

  • 如何为返回void的函数方法创建Around Advice
  • 如何为返回对象引用的函数方法创建Around Advice

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

喜欢就点个吧

你可能感兴趣的:(ByteBuddy(八)—Around Advice)