简介
Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口
核心api调用
创建类
@Testpublic void createTest() throws IOException {DynamicType.Unloaded<Object> upLoaded = new ByteBuddy().subclass(Object.class).make();upLoaded.saveIn(new File(path));}
以上即是一个bytebuddy的简单实例代码。创建了一个未被JVM加载的类的字节码,并且保存在了文件中。
除了将生成的字节码保存在文件中,还可以注入到jar包文件中
@Testpublic void createTest() throws IOException {DynamicType.Unloaded<Object> upLoaded = new ByteBuddy().subclass(Object.class).make();upLoaded.inject(new File("D:/test.jar"));}
也可以通过命名策略改变生成的类名称或者直接指定生成的类名称,以下是一段丰富的api调用示例:
@Testpublic void createTest() throws IOException {DynamicType.Unloaded<Object> unloaded = new ByteBuddy().with(new NamingStrategy.Suffixing("lyc")).subclass(Object.class)
// .name("com.yc.learn.bytebuddy.learn.UserServiceImpl").make();unloaded.saveIn(new File(path));}
以上示例通过指定生成类的命名策略创建类,生成的类字节码反编译之后如下:
如果直接通过name方法指定类名,生成的类名如下:
加载新创建的类:
@Testpublic void loadCreateTest() throws InstantiationException, IllegalAccessException {DynamicType.Unloaded<Object> unloaded = new ByteBuddy().subclass(Object.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl").make();DynamicType.Loaded<Object> load = unloaded.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER);Class<?> loaded = load.getLoaded();Object objectInstance = loaded.newInstance();}
通过调用load方法,并且指定类加载器和加载策略(ClassLoadingStrategy.Default.WRAPPER)加载新创建的类
常用的类加载策略如下:
- WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
- CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
- INJECTION 策略:使用反射, 将动态生成的类型直接注入到当前 ClassLoader 中。
创建类的方式
bytebuddy提供了三种创建类的方式,分别是:
subClass :生成对应类的子类
rebase:变基,效果是保留袁方法并重命名为xx$$original$xxxx
redefine:重定义类,如果有冲突的方法、字段,原方法和字段不在保留
以下示例说明:
用到的基础类代码如下:
bytebuddy的api调用如下:
subClass生成的类如下
rebase生成的类如下(字节码信息)
redefine生成的类如下
生成新方法
@Test
public void createMethod() throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {DynamicType.Unloaded<UserService> unloaded = new ByteBuddy().subclass(UserService.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl_bytebuddy").defineMethod("init", String.class, Modifier.PUBLIC|Modifier.STATIC).withParameters(String.class).intercept(FixedValue.value("test defineMethod value")).make();DynamicType.Loaded<UserService> load = unloaded.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER);Class<? extends UserService> loaded = load.getLoaded();Method method = loaded.getMethod("init", String.class);Object value = method.invoke(null, "张三");System.out.println(value);}
生成新的字段
@Test
public void createNewField() throws IOException {DynamicType.Unloaded<Object> unloaded = new ByteBuddy().subclass(Object.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl_bytebuddy").defineField("age", int.class, Modifier.PRIVATE).implement(UserAgeInterface.class).intercept(FieldAccessor.ofField("age")).make();unloaded.saveIn(new File(path));
}
自定义类的加载路径
@Testpublic void jarLocatorTest() throws IOException {// 加载指定路径下的jar包ClassFileLocator jarLocator = ClassFileLocator.ForJarFile.of(new File("xxxx\\hutool-all-5.8.12.jar"));// 系统类加载,如果不加载会找不到jdk本身的类ClassFileLocator classFileLocator = ClassFileLocator.ForClassLoader.ofSystemLoader();ClassFileLocator.Compound compound = new ClassFileLocator.Compound(jarLocator, classFileLocator);TypePool pool = TypePool.Default.of(compound);// 此处调用不会触发类的加载TypeDescription typeDes = pool.describe("cn.hutool.core.util.StrUtil").resolve();DynamicType.Unloaded<Object> unloaded = new ByteBuddy().redefine(typeDes, compound).name("cn.hutool.core.util.YcUtil").method(named("isBlankIfStr")).intercept(FixedValue.value(false)).make();unloaded.saveIn(new File(path));}
方法拦截
MethodDelegation#to
在上面的代码中,已经有涉及到方法拦截的代码,其中使用了内置的方法拦截器FixedValue,在实际的使用中一般使用MethodDelegation(方法委托)完成方法拦截逻辑,实例代码如下:
@Testpublic void methodDelegation() throws InstantiationException, IllegalAccessException {DynamicType.Unloaded<UserService> dynamicType = new ByteBuddy().subclass(UserService.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl")// 拦截构造方法
// .constructor(any()).method(named("getUserName").and(returns(String.class).and(takesArguments(String.class)))).intercept(MethodDelegation.to(new CommonDelegation())).make();DynamicType.Loaded<UserService> load = dynamicType.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER);Class<? extends UserService> loaded = load.getLoaded();UserService userServiceLoaded = loaded.newInstance();String loadedValue = userServiceLoaded.getUserName("zhangsan");System.out.println(loadedValue);}
其中GetUserNameDelegation的代码如下:
在使用MethodDelegation.to作为方法委托时,可以传Clazz对象也可以传委托实例。
其实使用硬编码的方法委托也有诸多限制:
如果是传递Clazz对象做为MethodDelegation.to的参数,需要满足:
1 该类中有static方法的方法签名(返回值,参数列表)和被代理方法的方法签名(返回值、参数列表)相同
2 如果是传递实例对象作为MethodDelegation.to的参数,需要满足:
1 该类中有实例方法的方法签名(返回值,参数列表)和被代理方法的方法签名(返回值、参数列表)相同
所以其实在实际使用中,最常用的方式是使用注解,将相关的动作交由bytebuddy完成,示例代码如下:
@Testpublic void methodDelegation() throws InstantiationException, IllegalAccessException {DynamicType.Unloaded<UserService> dynamicType = new ByteBuddy().subclass(UserService.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl").method(named("getUserName").and(returns(String.class).and(takesArguments(String.class)))).intercept(MethodDelegation.to(new CommonDelegation())).make();DynamicType.Loaded<UserService> load = dynamicType.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER);Class<? extends UserService> loaded = load.getLoaded();UserService userServiceLoaded = loaded.newInstance();String loadedValue = userServiceLoaded.getUserName("zhangsan");System.out.println(loadedValue);}
package com.yc.learn.bytebuddy.learn;import cn.hutool.core.util.ArrayUtil;
import net.bytebuddy.implementation.bind.annotation.*;import java.lang.reflect.Method;
import java.util.concurrent.Callable;public class CommonDelegation {@RuntimeTypepublic Object intercept(@This Object target, @Origin Method targetMethod,@Super Object superObj, @AllArguments Object[] args,@SuperCall Callable<?> zuper) throws Exception {System.out.println("调用的对象类名称是:" + target.getClass().getName());System.out.println("调用的方法名称是:" + targetMethod.getName());System.out.println("调用对象的父类名称是:" + superObj.getClass().getName());System.out.println("调用的方法参数列表是:" + ArrayUtil.toString(args));return zuper.call();}
}
执行调用结果如下:
对于以上注解释义如下:
@RuntimeType
该注解表示bytebuddy框架对委托代理方法不做类型校验
@This
注解表示注入的是被委托方法的调用者,也就是调用被委托方法的实例对象(动态生成),注意:如果拦截的是静态方法,不能使用该注解,否则不能正常拦截
@Origin
表示注入被拦截的源方法,如果拦截的是字段,该注解应该标注到 Field 类型参数
@Super
注入当前被拦截的、动态生成的那个对象的父类对象,如果拦截的是静态方法,不能使用该注解,否则不能正常拦截
@AllArguments
被委托方法的参数列表,参数必须是数组类型,并分配一个包含所有源方法参数的数组
@Argument
绑定源方法的单个参数
@SuperCall
这个注解比较特殊,我们要在 intercept() 方法中用于调用目标对象方法(源方法),需要通过这种方式注入。
一般标注的对象是Callable对象,通过调用call方法返回源方法的返回值,也可以用于标注Runnable对象,此时,源方法的返回值将被丢弃
如果在创建类的时候使用了redefine创建新类,拦截不成功,因为redifine是丢弃了源方法
该种注入方式,不能通过修改被@AllArguments标注的参数数组的值修改源方法的入参。
@Morph
如果想要修改源方法的入参,使用@Morph注解,使用步骤如下:
1 自定义接口
2 在拦截器处通过@Morph标记自定义的接口
3 绑定自定义接口
示例如下:
1 自定义接口
package com.yc.learn.bytebuddy.learn;public interface MorphCall {Object call(Object[] args);
}
2 在拦截器处通过@Morph标记自定义的接口
package com.yc.learn.bytebuddy.learn;import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Morph;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;import java.util.Objects;public class MorphDelegation {@RuntimeTypepublic Object MorphDelegate(@AllArguments Object[] args, @Morph MorphCall morphCall) {if (Objects.nonNull(args) && args.length > 0) {args[0] = "地球";}return morphCall.call(args);}
}
3 绑定自定义接口
@Testpublic void methodDelegationParameterBinding() throws InstantiationException, IllegalAccessException {DynamicType.Unloaded<UserService> dynamicType = new ByteBuddy().subclass(UserService.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl").method(named("getUserName").and(returns(String.class).and(takesArguments(String.class)))).intercept(MethodDelegation.withDefaultConfiguration().withBinders(Morph.Binder.install(MorphCall.class)).to(new MorphDelegation())).make();DynamicType.Loaded<UserService> load = dynamicType.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER);Class<? extends UserService> loaded = load.getLoaded();UserService userServiceLoaded = loaded.newInstance();String loadedValue = userServiceLoaded.getUserName("zhangsan");System.out.println(loadedValue);}
执行以上配置之后打印结果如下:
动态修改入参生效
Advice
方法拦截除了使用如上的MethodDelegation@to实现方法拦截,还可以使用Advice#to完成,示例代码如下:
@Test
public void adviceTest() throws InstantiationException, IllegalAccessException {DynamicType.Unloaded<UserService> dynamicType = new ByteBuddy().subclass(UserService.class).name("com.yc.learn.bytebuddy.learn.UserServiceImpl").method(named("getUserName").and(returns(String.class).and(takesArguments(String.class)))).intercept(Advice.to(AdviceCommonDelegation.class)).make();DynamicType.Loaded<UserService> load = dynamicType.load(getClass().getClassLoader(),ClassLoadingStrategy.Default.WRAPPER);Class<? extends UserService> loaded = load.getLoaded();UserService userServiceLoaded = loaded.newInstance();String loadedValue = userServiceLoaded.getUserName("zhangsan");System.out.println(loadedValue);}
package com.yc.learn.bytebuddy.learn;import net.bytebuddy.asm.Advice;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;public class AdviceCommonDelegation {@Advice.OnMethodEnterpublic static void onMethodEnter(@Advice.Argument(value = 0,typing = DYNAMIC,readOnly = false) Object param){System.out.println("进入方法之前,参数是:" + param.toString());}@Advice.OnMethodExitpublic static void onMethodExit(@Advice.Argument(value = 0,typing = DYNAMIC,readOnly = false) Object param,@Advice.Return Object result){System.out.println("执行方法之后,参数是:" + param.toString() + " 返回数据是:" + result.toString());}
}
其中使用@Advice.OnMethodEnter和使用@Advice.OnMethodExit标注的方法必须是static修饰的,可以结合其他的一些注解完成方法拦截操作
@Advice.OnMethodEnter
标记被该注解标注的方法在源方法执行前执行,被该注解标记的方法必须是static修饰
@Advice.OnMethodExit
标记被该注解标注的方法在源方法执行后执行,被该注解标记的方法必须是static修饰
@Advice.Argument
用于绑定方法调用的某个参数(具体通过value绑定参数列表的参数下标位置),同时可以通过typeing属性和readonly属性设置改变方法入参的值
@Advice.AllArguments
用于绑定方法调用的参数数组,同时可以通过typeing属性和readonly属性设置改变方法入参的值
@Advice.Return
该注解只能用于被@Advice.onMethodExit标注的方法的参数中,表示方法调用的返回数据
更丰富的注解使用及释义,查看源码net.bytebuddy.asm.Advice和官方文档Byte Buddy - runtime code generation for the Java virtual machine