ByteBuddy(十一)—生成Java方法

本章介绍如何在函数代码中动态生成Java方法。

DataProducer.java是本章的功能代码:

public abstract class DataProducer{
    private String data;
}

在构建工程之后,DataProducer.class应包括以下Java方法:

public abstract class DataProducer{
    private String data;
    private int numberData;
    public void setNumberData(int var1){
      this.numberData = var1;
    }
    public int getNumberData(){
        return this.numberData;
    }
    protected String getData(){
        return this.data;
    }
    protected void setData(String var1){
        this.data = var1;
    }
    public static int calculate(int p1, int p2){
        return -1;
    }
    private long getInstantTime(){
        return -1L;
    }
    public abstract void process1();
    public static final void process2(final String strParam){
    }
    private static final boolean process3(){
        return false;
    }
    private synchronized String process4(){
        return null;
    }
    private strictfp String process5(){
        return null;
    }
    private String process6(int p1, String p2){
        return null;
    }
    private BigInteger process7(String var1) throws NoSuchMethodException, 
                                            SecurityException, 
                                            InstantiationException, 
                                            IllegalAccessException, 
                                            IllegalArgumentException, 
                                            InvocationTargetException {
        return BigIntegerProducer.create(var1);
    }
    private String process8(String var1, int...var2){
        //略
    }
    public int process9(long var1){
        //略
    }
    public int process10(long var1, Map var3){
        //略
    }
    public int[] process11(long p1, int[] p2){
        //略
    }
}

ByteBuddy提供了这些用于动态声明新方法的API,这些API在net.ByteBuddy.dynamic.DynamicType.Builder中提供

  • define
  • defineProperty
  • defineMethod

与变量不同,Java方法具有方法体。
因此,在声明方法之后,Plugin程序需要定义方法体。
本章使用了两个Plugin程序
InterceptorPlugin.java
InterfacePlugin.java

1、InterceptorPlugin

首先介绍InterceptorPlugin.java程序创建的第一个方法: calculate
这是用于此目的的Plugin程序代码:

Method m1 = MethodPrototype.class.getDeclaredMethod(
    "calculate",
    int.class,
    int.class);
builder = builder.define(m1).intercept(FixedValue.value(-1));
return builder;

这是生成的代码:

public static int calculate(int a, int b) {
    return -1;
}

ByteBuddy从MethodPrototype.class克隆calculate方法转换为DataProducer.class的方法。
这是MethodPrototype.javacalculate方法的代码:

public static int calculate(int a, int b){
  return a + b;
}

define

define方法克隆修饰符方法名返回数据类型参数,但该方法不克隆方法体。
apply方法使用net.bytebuddy.implementation.FixedValuecalculate方法创建方法体。

FixedValue

FixedValue.value(-1)创建一行返回值的字节码。
FixedValue是ByteBuddy提供的类。
FixedValue提供了许多方便的方法来为生成的Java方法创建单行方法体。
例如,FixedValue.nullValue()方法创建返回null值的方法体。

下面这个代码是从MethodPrototype.class中克隆getInstantTime方法转换为DataProducer.class的代码:

Method m2 = MethodPrototype.class.getDeclaredMethod("getInstantTime");
builder = builder.define(m2).intercept(FixedValue.value(-1L));
return builder;

生成此插入指令的代码:

private long getInstantTime(){
    return -1L;
}

原始的getInstantTime方法有一个@Deprecated注解,但是define方法不会克隆注解。

definePropertydefineMethod也具有define相同的功能。

defineProperty

defineProperty方法用于创建public 类型的getset方法。
例如,此代码:

builder.defineProperty("numberData", int.class);

DataProducer.class中生成以下代码行:

private int numberData;
public void setNumberData(int var1) {
  this.numberData = var1;
}
public int getNumberData() {
  return this.numberData;
}

defineMethod

然而,defineProperty只能声明publicgetset方法。
要声明除public之外具有其他可见性的getset方法,请使用 defineMethod

例如:

builder = builder.defineMethod(
        "getData",
        String.class,
        Visibility.PROTECTED)
    .intercept(FieldAccessor.ofField("data"))
    .defineMethod(
        "setData",
        void.class,
        Visibility.PROTECTED)
    .withParameter(String.class)
    .intercept(FieldAccessor.ofField("data"));
return builder;

生成此插入指令的代码

private String data;  //这一行在java文件默认就有
protected String getData() {
  return this.data;
}
protected void setData(String var1) {
  this.data = var1;
}

defineMethod方法接受三个参数

第一个参数:方法名,
第二个参数:返回的数据类型,
第三个参数:该方法的可见性。

要创建get方法的方法体,使用FieldAccessor
因为getData希望返回变量值,所以FieldAccessor.ofField("data")为此生成返回值字节码。
set需要一个String参数。
要配置方法的参数,请使用withParameter方法。
withParameter适用于只有个参数的方法。
因为数据实例变量是String,所以指定String.class,以便该方法将创建字符串参数。
get方法类似,使用FieldAccessor.ofField("data")生成set方法体。

FieldAccessor.ofField方法是ByteBuddy提供的非常实用的类。
于生成setget方法相同,它可以生成其他必要的代码。

withoutCode

withoutCode方法用于生成抽象方法。

例如:

builder = builder.defineMethod(
    "process1",
    void.class,
    Visibility.PUBLIC)
.withoutCode();
return builder;

将生成这行字节码:

public abstract void process1();

接下来,这是用于声明public static final方法的代码:

builder = builder.defineMethod(
                "process2",
                void.class,
                Visibility.PUBLIC,
                MethodManifestation.FINAL,
                Ownership.STATIC)
                .withParameter(
                        String.class,
                        "strParam",
                        ParameterManifestation.FINAL).
                        intercept(StubMethod.INSTANCE);
return builder;

生成此插入指令的代码:

public static final void process2(final String strParam){
}

该代码使用了ByteBuddy中的几个新类来生成代码:

  • net.bytebyddy.description.modifier.MethodManifestation
  • net.bytebyddy.description.modifier.ParameterManifestation
  • net.bytebyddy.description.modifier.Ownership
  • net.bytebuddy.implementation.StubMethod

MethodManifestionOwnership用于指定Java元素的修饰符
StubMethod是一个特殊的Java类,用于返回方法的默认值。
process2方法返回void,因此使用StubMethod.INSTANCE会生成返回void代码。
要获取StubMethod的实例,请调用它的INSTANCE方法。
StubMethod是一个智能组件,因为它可以根据方法的返回数据类型生成返回语句。

process2有一个final参数。
要配置final参数,请使用withParameter方法。

withParameter可以接受三个参数。
第一个参数指定参数的数据类型。
第二个参数是参数的名称。
第三个参数指定其修饰符。

使用ParameterManifestion.FINAL指定参数

下一个示例生成一个返回boolean的方法,代码同样是StubMethod.INSTANCE,但它可以为方法生成"return false"

builder = builder.defineMethod(
        "process3",
        boolean.class,
        Opcodes.ACC_PUBLIC|Opcodes.ACC_FINAL|Opcodes.ACC_STATIC)
.intercept(StubMethod.INSTANCE);
return builder;

生成此插入指令的代码:

public static final boolean process3(){
    return false;
}

net.bytebuddyjar.asm.Opcodes可用于指定Java元素的修饰符。
上面的代码生成了一个public static final方法。
使用Opcodes的好处是它可以在一行中指定多个修饰符,每个修饰符用"|"字符分隔。

这行代码声明了一个同步方法:

builder = builder.defineMethod(
        "process4",
        String.class,
        Visibility.PRIVATE,
        SynchronizationState.SYNCHRONIZED)
.intercept(FixedValue.nullValue());
return builder;

生成此插入指令的代码:

private synchronized String process4(){
    return null;
}

要在方法上指定synchronized关键字,使用SynchronizationState.SYNCHRONIZED
此方法使用FixedValue.nullValue方法生成返回null值的字节码。

这行代码声明了strictfp方法:

builder = builder.defineMethod(
          "process5",
          String.class,
          Visibility.PRIVATE,
          MethodStrictness.STRICT)
.intercept(FixedValue.nullValue());
return builder;

生成此插入指令的代码:

private strictfp String process5(){
    return null;
}

若要在方法上指定strictfp关键字,请使用MethodStrictness.STRICT

这行代码声明了一个具有多个参数的方法:

builder = builder.defineMethod(
            "process6",
            String.class,
            Visibility.PRIVATE)
            .withParameters(int.class, String.class)
            .intercept(FixedValue.nullValue());
return builder;

生成此插入指令的代码:

private String process6(int var1, String var2){
    return null;
}

要在一行中声明多个参数,请使用withParameters方法。
withParameters方法的每个参数都映射到生成方法的各个参数。

使用MethodDelegation生成方法

ByteBuddy可以使用net.bytebuddy.implementation.MethodDelegationnet.bytebuddy.asm.Advice
这行代码显示了MethodDelegation在应用里的作用。

builder = builder.defineMethod(
        "process7",
        BigInteger.class,
        Visibility.PRIVATE)
        .withParameter(String.class)
        .throwing(
            NoSuchMethodException.class,
            SecurityException.class,
            InstantiationException.class,
            IllegalAccessException.class,
            IllegalArgumentException.class,
            InvocationTargetException.class)
        .intercept(MethodDelegation.to(BigIntegerProducer.class));
return builder;

生成此插入指令的代码:

private BigInteger process7(String var1) throws 
                NoSuchMethodException, 
                SecurityException,
                InstantiationException,
                IllegalAccessException,
                IllegalArgumentException,
                InvocationTargetException {
    return BigIntegerProducer.create(var1);
}

plugin程序调用拦截方法,并使用MethodDelegation.to(BigIntegerProducer.class)作为其参数。

代码还可以通过使用throwing方法声明方法抛出的异常。
这是BigIntegerProducer.javaAdvice源代码:

public class BigIntegerProducer{

    @OnMethodExit
    public static BigInteger create(String param) throws 
                                NoSuchMethodException, 
                                SecurityException, 
                                InstantiationException,      
                                IllegalAccessException, 
                                IllegalArgumentException, 
                                InvocationTargetException {
        Constructor c = BigInteger.class.getDeclaredConstructor(String.class);
        return (BigInteger)c.newInstance(param);
    }
}

因此process7方法具有与Advice代码相同的throwing语句,并且throwing方法用于映射异常。
这里Advice代码特意使用Java反射技术来实例化BigInteger的实例,用来演示如何映射异常。
MethodDelegation没有将Advice代码拷贝到process7方法中。
相反,ByteBuddy生成的字节码中是process7的方法体中直接调用BigIntegerProducer.classcreate静态方法。

使用Advice生成方法

接下来演示如何使用Advice生成方法体。

  1. 首先使用defineMethod声明方法
  2. 使用FixedValue创建方法体
  3. 然后使用visit方法修改方法体
builder = builder.defineMethod(
        "process8",
        String.class,
        Opcodes.ACC_PRIVATE|Opcodes.ACC_VARARGS)
        .withParameter(String.class)
        .withParameter(int[].class)
        .intercept(FixedValue.nullValue());
builder = builder.visit(Advice.to(UuidGeneratorForInline.class)
        .on(ElementMatchers.named("process8")));
return builder;

这是UuidGeneratorForInline.java的代码

public class UuidGeneratorForInline{
    // 注意包不要导错了,不然参数映射不对
    @OnMethodExit
    public static void generate(@Return(readOnly=false) String data, @Argument(0) Object param){
        String uuid = UUID.randomUUID().toString();
        if(param.equals("base64"))
            data = Base64.getEncoder().encodeToString(uuid.getBytes(Charset.forName("UTF-8")));
        else
            data = uuid;
    }
}

intercept方法生成返回空值的方法体。
visit方法通过使用Advice代码重写process8方法的方法体。
visit方法从UuidGeneratorForInline.class中复制OnMethodExit Advice方法的方法体,并将代码拷贝到DataProducer.classprocess8方法中。
这是最终生成的代码:

private String process8(String var1, int...paramInt){
    String var2 = null;
    String var3 = UUID.randomUUID().toString();
    if(var1.equals("base64")){
        var2 = Base64.getEncoder().encodeToString(var3.getBytes(Charset.forName("UTF-8")));
    } else {
        var2 = var3;
    }
    return var2;
}

观察到第二个参数是可变长度参数。
为了创建此类型参数,使用数组类型创建方法的最后一个参数,然后第三个参数处的Opcodes.ACC_VARARGS修饰符。

使用局部变量生成方法

下一个示例演示如何生成方法体并在方法体中声明局部变量。

builder = builder.defineMethod(
        "process9", 
        int.class, 
        Visibility.PUBLIC)
        .withParameter(long.class)
        .intercept(FixedValue.value(0));
builder = builder.visit(Advice.to(PriceProcessorAdvice.class)
        .on(ElementMatchers.named("process9")));
return builder;

Advice方法PriceProcessorAdvice.class

public class PriceProcessorAdvice{
    @Advice.OnMethodEnter
    public static void start(long id, @Advice.Local("total") int totalParam){
        int price = new PriceQuery().query(id);
        int discount = new DiscountQuery().query(id);
        totalParam = price - discount;
    }
    @Advice.OnMethodExit
    public static void end(@Advice.Local("total") int totalParam, @Advice.Return(readOnly=false) int returnTotal){
        returnTotal = totalParam;
    }
}

Advice代码同时声明方法enterexit Advice。
Advice代码使用一个名为@Local的新注解。
@Local注解包含在net.bytebuddy.asm.Advice包。
@Local注解用于在方法体中声明局部变量。
在Advice代码中,此变量是一个参数。
在编译过之后,参数将在编译代码的方法体中更改为局部变量。

必须在带有@OnMethodEnter注解的方法中声明@Local注解。
若要在exit Advice中使用局部变量,请使用@Local注解exit Advice中的一个参数,如果注解引用的是同一个局部变量,则注解必须与enter Advice中的@local注解具有相同的值。
在本例中,局部变量的名称为total
Advice代码执行计算。
将计算结果存储在totalParam中,该参数是用@Local注解的参数。
计算是在名为start方法的OnMethodEnter advice中执行的。
start方法使用PriceQuery.javaDiscountQuery.java来查询价格和折扣。
这两个Java类以int格式返回价格和折扣。

(这两个类代码就不展示了)
出于演示目的,pricediscount的值在PriceQuery.javaDiscountQuery.java中进行了硬编码。
因此,它们通过qurey方法分别返回28010的值。

名为"end"OnMethodExit Advice方法重用totalParam,并将totalParam的值传递给returnTotal
returnTotalend方法中声明的参数,它使用@return注解
因此,returnTotal将在插入指令的代码中可用,并且可以通过插入指令的方法的return语句返回。

这是生成的process9方法:

public int process9(long paramLong){
    int p = 0;
    int j = new PriceQuery().query(paramLong);
    int m = new DiscountQuery().query(paramLong);
    p = j - m;
    long var1 = paramLong;
    int k = 0;
    k = p;
    return k;
}

public int process9(long var1) {
    boolean var3 = false;
    int var4 = (new PriceQuery()).query(var1);
    int var5 = (new DiscountQuery()).query(var1);
    int var8 = var4 - var5;
    boolean var9 = false;
    return var8;
}

观察到,生成的字节码使用了不同的变量名称,即使@Local注解声明了名称"total",还生成了一些意外的变量,例如var数字
请注意,每当重新执行编译之后,ByteBuddy都可以生成不同的代码。
(bytebuddy版本的不同,也会生成不同的代码,可自行验证)

使用嵌套访问生成方法

接下来,Plugin程序声明process10方法:

builder = builder.defineMethod(
        "process10", 
        int.class, 
        Visibility.PUBLIC)
        .withParameter(long.class)
        .withParameter(Map.class)
        .intercept(FixedValue.value(0));
builder = builder.visit(Advice.to(LocalVariableAdvice.class)
        .on(ElementMatchers.named("process10")))
        .visit(Advice.to(PriceQueryAdvice.class)
        .on(ElementMatchers.named("process10")))
        .visit(Advice.to(DiscountQueryAdvice.class)
        .on(ElementMatchers.named("process10")));  

Plugin程序使用defineMethod来声明process10方法。
process10方法是返回int类型的public方法。
withParameter用于声明两个参数:一个long参数和一个Map参数。
然后使用intercept方法生成返回一个0值的方法体。

Plugin程序然后通过使用多个visit方法修改process10方法。
使用了三个Advice:

  • LocalVariableAdvice.class
  • PriceQueryAdvice.class
  • DiscountQueryAdvice.class

请注意,Advice代码的顺序很重要

process10方法的目的是执行价格计算,与process9方法类似:
总计 = 价格 - 折扣
然而,process10方法与process9方法不同,因为PriceQuery.javaDiscountQuery.java封装在PriceQueryAdvice.javaDiscountQueryAdvice.java中。
process9方法封装了PriceQuery.javaDiscountQuery.java
因此,@Local注解不适用,PriceQueryAdvice.javaDiscountQueryAdvice.java希望访问相同的总变量来执行价格计算。
这里使用方法的参数来创建一个对PriceQueryAdvice.javaDiscountQueryAdvice.java都可见的局部变量,可以使用process10方法的第二个参数,该参数的数据类型为java.util.Map
因为所有Advice代码都使用相同的方法:process10方法,所以所有Advice代码都能够访问存储在process10方法的第二个参数中的数据。

插入的指令希望阻止调用process10方法的程序将数据传递到第二个参数中。
因此,LocalVariableAdvice.java用于此目的。
这是LocalVariableAdvice.java的代码:

public class LocalVariableAdvice {
    @Advice.OnMethodEnter
    public static void enter(@Advice.Argument(value=1, readOnly=false) Map<String, Object> data){
        data = new HashMap<>();
    }
    @Advice.OnMethodExit
    public static void end(@Advice.Argument(value=1,readOnly=false) Map<String,Object> data,
                           @Advice.Return(readOnly=false) int total){
        total = (Integer)data.get("total");
    }
}

LocalVariableAdvice.java实现了方法enterexit Advice。
观察到LocalVariableAdvice.class在第一次访问方法中使用。
因此ByteBuddy将按以下顺序为插入指令的代码生成嵌套结构:

(1) LocalVariableAdvice.enter
(2) PriceQueryAdvice
(3) DiscountQueryAdvice
(4) LocalVariableAdvice.end

为了防止第二个参数包含从process10方法外部传递的恶意数据,enter方法在每次调用process10方法时重置第二个值:

data = new HashMap<>();

enter方法实例化一个新的HashMap。
新的HashMap传递给第二个参数,该参数由数据变量表示。

data变量是带有@Argument注解的enter方法的参数,它被映射到process10方法的第二个参数,因为它的value属性为1readOnly属性为false,然后是PriceQueryAdvice.javaDiscountQueryAdvice.java利用HashMap存储计算结果。
计算结果以关键字total存储在HashMap中。
因此,HashMap的total元素包含价格计算的结果。
为了确保插入指令的代码获得正确的总值,实现OnMethodExit Advice的LocalVariableAdvice.javaend方法负责从HashMap中检索total元素的值,然后将其传递给returnTotal参数:

@Advice.OnMethodExit
public static void end(@Advice.Argument(value=1,readOnly=false) Map<String,Object> data,
                           @Advice.Return(readOnly=false) int total){
    total = (Integer)data.get("total");
}

returnTotal是带有@Return注解的参数,它表示process10方法的返回数据。
因此,process10方法应该能够接收价格计算的结果,并在返回语句中使用它。

使用LocalVariableAdvice.java的好处是,Advice可以不断确保HashMap在价格计算开始之前重置为新的HashMap。
然后,exit Advice始终向插入指令的代码返回正确的总数。

即使使用了HashMap,ByteBuddy也会生成不同的HashMap副本来存储数据。

这是PriceQueryAdvice.java的代码

public class PriceQueryAdvice {
    @Advice.OnMethodExit
    public static void end(@Advice.Argument(value = 0, readOnly = false) long paramLong,
                           @Advice.Argument(value = 1, readOnly = false) Map<String,Object> data,
                           @Advice.Return(readOnly=false) int total){
        int discount = (int)data.get("total");
        int price = discount - new PriceQuery().query(paramLong);
        data.put("total", price);
    }
}

这是PriceQueryAdvice.java的代码

public class PriceQueryAdvice {
    @OnMethodExit
    public static void end(@Argument(value = 0,readOnly = false) long paramLong, @Argument(value = 1,readOnly = false) Map<String, Object> data, @Return(readOnly = false) int total) {
        int discount = (Integer)data.get("total");
        int price = discount - (new PriceQuery()).query(paramLong);
        data.put("total", price);
    }
}

这是DiscountQueryAdvice.java代码

public class DiscountQueryAdvice {
    @Advice.OnMethodExit
    public static void end(@Advice.Argument(value = 0, readOnly = false) long paramLong,
            @Advice.Argument(value = 1, readOnly = false) Map<String,Object> data,
            @Advice.Return(readOnly=false) int total){

        int discount = new DiscountQuery().query(paramLong);
        data.put("total", discount);
    }
}

这是生成的process10方法:

public int process10(long var1, Map var3) {
        HashMap var20 = new HashMap();
        boolean var12 = false;
        int var13 = (new DiscountQuery()).query(var1);
        var20.put("total", var13);
        int var9 = (Integer)var20.get("total");
        int var10 = var9 - (new PriceQuery()).query(var1);
        var20.put("total", var10);
        int var4 = (Integer)var20.get("total");
        return var4;
}

在生成的字节码中,ByteBuddy创建HashMap(var20)的副本,而不是直接使用使用第二个参数的Map。
然而,该方法确实正确地执行了计算。
生成的字节码也不使用第二个参数中的数据,该参数可能由其他使用该方法的程序设置。
因此,制定的Advice代码符合其目标,最终计算出的结果也是正确的。

使用数组而不是HashMap

process10方法中的HashMap可以替换为int数组。
在数据查询和存储方面,使用int数组更有效,因为数组可以使用数组索引来存储和查询数据。
使用数组索引还可以确保数据的一致性。
HashMap有其自身的优点,因为它可以存储不同类型的数据,并且存储大小是灵活的。

使用MethodCall链生成方法

Plugin程序将生成process11方法。
与过程process10类似,此方法将计算总价(总价=价格-折扣)。
区别在于:

  • process11使用多个MethodCall并将总价存储到int数组中。

net.bytebuddy.implementation.MethodCall是ByteBuddy组件,它可以生成字节码来调用Java构造函数或方法。
这是生成process11方法的代码:

builder = builder.defineMethod("process11", int[].class, Visibility.PUBLIC)
        .withParameters(long.class, int[].class)
        .intercept(
            MethodCall.invoke(LocalVariableAdvice.class.getDeclaredMethod("execute", int[].class))
        .withArgument(1).andThen(
            MethodCall.invoke(PriceUtil.class.getDeclaredMethod("execute", long.class, int[].class))
                .withArgument(0,1)
        ).andThen(
            MethodCall.invoke(DiscountUtil.class.getDeclaredMethod("execute", long.class, int[].class))
                .withArgument(0,1)
        ).andThen(FixedValue.argument(1)));

process11方法是一个返回int数组的公共方法。

该方法有两个参数:longint数组

builder.defineMethod("process11", int[].class, Visibility.PUBLIC)
    .withParameters(long.class, int[].class)

long参数是PriceQueryquery方法将使用的id值。
int数组类似于process10方法中使用的HashMap:它们用于存储总价的结果。

process11方法也将使用intercept方法生成方法体。
intercept方法使用MethodCall,并且此MethodCall通过andThen方法链接到多个MethodCall
这是链式方法的第一个MethodCall

MethodCall.invoke(
    LocalVariableAdvice.class
        .getDeclaredMethod("execute", int[].class))
.withArgument(1)

withArgument(1)方法将process11方法的第二个参数传递给LocalVariableAdvice.javaexecute方法。
LocalVariableAdvice.javaexecute方法将int数组及其第一个元素重置为零。
execute方法是静态方法:

public static void execute(int[] total){
    total = new int[1];
}

然后,MethodCall通过andThen方法链接到第二个MethodCall:

.andThen(MethodCall.invoke(PriceUtil.class
    .getDeclaredMethod("execute", long.class, int[].class))
    .withArgument(0,1))

第二个MethodCall调用PriceUtil.javaexecute方法。

第二个MethodCall调用withArgument(0, 1),它将process11方法的第一个第二个参数传递给PriceUtil.javaexecute方法。
这是PriceUtil.java的execute方法的实现:

public static void execute(long id, int[] total){
    total[0] += new PriceQuery().query(id);
}

execute方法使用id执行价格查询,添加价格并将结果存储到total数组的第一个元素中。

之后,第三个MethodCall通过另一个andThen方法链接:

.andThen( MethodCall.invoke(DiscountUtil.class
          .getDeclaredMethod("execute", long.class, int[].class))
      .withArgument(0, 1))

第三个MethodCall调用DiscountUtilexecute方法。
第三个MethodCall调用withArgument(0, 1),它将process11方法的第一个第二个参数传递给DiscountUtil.javaexecute方法。
这是DiscountUtil.javaexecute方法的实现:

public static void execute(long id, int[] total){
    total[0] -= new DiscountQuery().query(id);
}

execute方法使用id执行折扣查询,然后减去存储在total数组第一个元素中的总价,并将最新结果存储到total数组的第一个元素。

之后,计算完成,total数组的第一个元素包含最新的总价。
最后一个andThen方法被调用并链接到FixedValue.argument(1)

FixedValue.argument(1)生成返回processs11方法的第二个参数的代码。
由于所有MethodCall都使用第二个参数来存储总价,并且它是一个int数组,因此int数组应该包含最新的总价。
所以process11方法可以使用第二个参数来创建return语句。

这是生成的process11方法代码:

public int[] process11(long var1, int[] var3) {
        LocalVariableAdvice.execute(var3);
        PriceUtil.execute(var1, var3);
        DiscountUtil.execute(var1, var3);
        return var3;
}

ByteBuddy在代码生成方面有一些限制。
某些代码无法直接生成。
例如,可以使用以下代码实现价格计算代码:

public int calculatePrice(long id){
    int total = new PriceQuery().query(id);
    total -= new DiscountQuery().query(id);
    return total;
}

没有直接的API(例如MethodCall)来生成以下代码行:

new PriceQuery().query (id);
访问全局变量并在DiscountQuery中使用它。
访问total局部变量并在return语句中使用它。

net.bytebuddy.implementation.bytecode.StacManipulation好像可以解决这个问题,这里不做介绍,可以自行研究。
StackManipulation的使用需要Java字节码编程的知识,它可能会实现更详细的代码以实现相同的结果代码。
process9方法具有与上述代码类似的最接近的代码。
process9方法在方法enterexit Advice中使用@Local注解来实现。

与使用局部变量相比,价格计算可以使用实例变量来存储计算结果。
当程序想要使用实例变量来存储计算结果时,那么应该使用带有@FieldValue注解的Advice代码来实现。
Advice代码可以使用onMethodEnter Advice、onMethodExist Advice,或者两者都使用。

InterfacePlugin

InterfacePlugin.java是一个插件程序,它为com.wpixel中的所有java接口提供功能。
匹配逻辑在InterfacePlugin.javamatches方法中指定

@Override
public boolean matches(TypeDescription target){
        if(target.getName().startsWith("com.wpixel.bytebuddy.chapter1") && target.isInterface())
            return true;
        else
            return false;
}

匹配逻辑使用一个新方法:isInterface来检查目标是否是Java接口。

Producer.java是本次中唯一的一个java接口。
apply方法为匹配的Java接口声明了三个方法:

builder.defineMethod("getUniqueId", String.class, Visibility.PUBLIC)
    .intercept(FixedValue.value(UUID.randomUUID().toString()))
    .defineMethod("createData", String.class, Opcodes.ACC_PUBLIC|Opcodes.ACC_STATIC)
    .intercept(StubMethod.INSTANCE)
    .defineMethod("verify", void.class, Visibility.PUBLIC)
    .withoutCode();

getUniqueld是默认方法。
要声明default方法,只需为该方法创建方法体,ByteBuddy将智能地生成该方法。
这是生成的getUniqueId方法:

public default String getUniqueId(){
    return "d33c6ab0-f12a-4d3e-8fdd-1de7aac47b90";
}

当ByteBuddy检测到方法体具有实现代码、方法不是静态的并且在Java接口中声明时,ByteBuddy会自动创建default方法。

代码使用FixedValue.value(UUID.randomUUID().toString())创建随机ID。
FixedValue.value方法是另一种方便的方法,可用于在构建时或运行时生成常量值。

Java接口可以有静态方法。
这是生成的createData静态方法:

public static String createData(){
    return null;
}

要在Java接口中创建抽象方法,请使用withoutCode方法。
这是生成的方法:

void verify();

在maven pom.xml中配置两个插件程序

本章使用两个插件程序:Interceptorplugin.javaInterfacePlugin.java
本节介绍了如何在pom.xml中添加两个插件程序:

<plugin>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-maven-plugin</artifactId>
    <version>${bytebuddy.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>transform</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <transformations>
            <transformation>
                <plugin>com.wpixel.bytebuddy.chapter1.InterceptorPlugin</plugin>
            </transformation>
            <transformation>
                <plugin>com.wpixel.bytebuddy.chapter1.InterfacePlugin</plugin>
            </transformation>
        </transformations>
    </configuration>
</plugin>

pom.xml为第二个插件程序添加第二个转换标记。
maven构建过程使用两个循环:

第一个循环迭代Java类文件,Java类文件是在maven构建过程中编译的Java类。
第二个循环迭代插件程序,调用matches方法,并在matches返回true时调用apply方法。

伪代码解释了该过程:

for each Java class file{
    for each plugin program{
        invoke matches method
        if matches method return ture, then invokes
            apply method
    }
}

结论

本章解释

  • 如何生成Java方法
  • 如何生成setter和getter方法
  • 如何使用固定值生成方法体
  • 如何使用StubMethod生成方法体
  • 如何使用MethodDelegation生成方法体
  • 如何使用Advice生成方法体
  • 如何使用MethodCall生成方法体
  • 如何为maven构建过程启用两个插件程序

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

推荐阅读更多精彩内容