bytebuddy入门

简介

        Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口

核心api调用

创建类

@Test
    public void createTest() throws IOException {

        DynamicType.Unloaded<Object> upLoaded = new ByteBuddy()
                .subclass(Object.class)
                .make();
        upLoaded.saveIn(new File(path));
    }

以上即是一个bytebuddy的简单实例代码。创建了一个未被JVM加载的类的字节码,并且保存在了文件中。

除了将生成的字节码保存在文件中,还可以注入到jar包文件中

@Test
    public void createTest() throws IOException {

        DynamicType.Unloaded<Object> upLoaded = new ByteBuddy()
                .subclass(Object.class)
                .make();
        upLoaded.inject(new File("D:/test.jar"));

    }

也可以通过命名策略改变生成的类名称或者直接指定生成的类名称,以下是一段丰富的api调用示例:

@Test
    public 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方法指定类名,生成的类名如下:

加载新创建的类:

@Test
    public 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));
}

自定义类的加载路径

    @Test
    public 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(方法委托)完成方法拦截逻辑,实例代码如下:

    @Test
    public 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的参数,需要满足:

该类中有static方法的方法签名(返回值,参数列表)和被代理方法的方法签名(返回值、参数列表)相同

2 如果是传递实例对象作为MethodDelegation.to的参数,需要满足:

1 该类中有实例方法的方法签名(返回值,参数列表)和被代理方法的方法签名(返回值、参数列表)相同

所以其实在实际使用中,最常用的方式是使用注解,将相关的动作交由bytebuddy完成,示例代码如下:

@Test
    public 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 {

    @RuntimeType
    public 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 {

    @RuntimeType
    public Object MorphDelegate(@AllArguments Object[] args, @Morph MorphCall morphCall) {
        if (Objects.nonNull(args) && args.length > 0) {
            args[0] = "地球";
        }
        return morphCall.call(args);
    }
}

3 绑定自定义接口

    @Test
    public 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.OnMethodEnter
    public static void onMethodEnter(@Advice.Argument(value = 0,typing = DYNAMIC,readOnly = false) Object param){
        System.out.println("进入方法之前,参数是:" + param.toString());
    }

    @Advice.OnMethodExit
    public 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

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值