美文网首页
ByteBuddy(十一)—生成Java方法

ByteBuddy(十一)—生成Java方法

作者: Wpixel | 来源:发表于2022-12-02 17:20 被阅读0次

    本章介绍如何在函数代码中动态生成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----

    喜欢就点个👍吧

    相关文章

      网友评论

          本文标题:ByteBuddy(十一)—生成Java方法

          本文链接:https://www.haomeiwen.com/subject/vfxpxdtx.html