本章介绍如何在函数代码中动态生成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.java
中calculate
方法的代码:
public static int calculate(int a, int b){
return a + b;
}
define
define
方法克隆修饰符
、方法名
、返回数据类型
和参数
,但该方法不克隆方法体。
apply
方法使用net.bytebuddy.implementation.FixedValue
为calculate
方法创建方法体。
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
方法不会克隆注解。
defineProperty
和defineMethod
也具有define
相同的功能。
defineProperty
defineProperty
方法用于创建public
类型的get
和set
方法。
例如,此代码:
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
只能声明public
的get
和set
方法。
要声明除public
之外具有其他可见性的get
和set
方法,请使用 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提供的非常实用的类。
于生成set
和get
方法相同,它可以生成其他必要的代码。
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
MethodManifestion
和Ownership
用于指定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.MethodDelegation
和net.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.java
的Advice
源代码:
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.class
的create
静态方法。
使用Advice生成方法
接下来演示如何使用Advice生成方法体。
- 首先使用
defineMethod
声明方法 - 使用
FixedValue
创建方法体 - 然后使用
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.class
的process8
方法中。
这是最终生成的代码:
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代码同时声明方法enter
和exit
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.java
和DiscountQuery.java
来查询价格和折扣。
这两个Java类以int
格式返回价格和折扣。
(这两个类代码就不展示了)
出于演示目的,price
和discount
的值在PriceQuery.java
和DiscountQuery.java
中进行了硬编码。
因此,它们通过qurey
方法分别返回280
和10
的值。
名为"end"
的OnMethodExit
Advice方法重用totalParam
,并将totalParam
的值传递给returnTotal
。
returnTotal
是end
方法中声明的参数,它使用@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.java
和DiscountQuery.java
封装在PriceQueryAdvice.java
和DiscountQueryAdvice.java
中。
而process9
方法封装了PriceQuery.java
和DiscountQuery.java
。
因此,@Local
注解不适用,PriceQueryAdvice.java
和DiscountQueryAdvice.java
希望访问相同的总变量来执行价格计算。
这里使用方法的参数来创建一个对PriceQueryAdvice.java
和DiscountQueryAdvice.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
实现了方法enter
和exit
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
属性为1
,readOnly
属性为false,然后是PriceQueryAdvice.java
和DiscountQueryAdvice.java
利用HashMap存储计算结果。
计算结果以关键字total
存储在HashMap中。
因此,HashMap的total
元素包含价格计算的结果。
为了确保插入指令的代码获得正确的总值,实现OnMethodExit
Advice的LocalVariableAdvice.java
的end
方法负责从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数组
的公共方法。
该方法有两个参数:long
和int数组
。
builder.defineMethod("process11", int[].class, Visibility.PUBLIC)
.withParameters(long.class, int[].class)
long
参数是PriceQuery
的query
方法将使用的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.java
的execute
方法。
LocalVariableAdvice.java
的execute
方法将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.java
的execute
方法。
第二个MethodCall
调用withArgument(0, 1)
,它将process11
方法的第一个
和第二个
参数传递给PriceUtil.java
的execute
方法。
这是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
调用DiscountUtil
的execute
方法。
第三个MethodCall
调用withArgument(0, 1)
,它将process11
方法的第一个
和第二个
参数传递给DiscountUtil.java
的execute
方法。
这是DiscountUtil.java
中execute
方法的实现:
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
方法在方法enter
和exit
Advice中使用@Local
注解来实现。
与使用局部变量相比,价格计算可以使用实例变量
来存储计算结果。
当程序想要使用实例变量来存储计算结果时,那么应该使用带有@FieldValue
注解的Advice代码来实现。
Advice代码可以使用onMethodEnter
Advice、onMethodExist
Advice,或者两者都使用。
InterfacePlugin
InterfacePlugin.java是一个插件程序,它为com.wpixel
中的所有java接口提供功能。
匹配逻辑在InterfacePlugin.java
的matches
方法中指定
@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.java
和InterfacePlugin.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》
喜欢就点个👍吧