转载

比官方还详细的ByteBuddy入门教程

简介

ByteBuddy是一个代码生成和操作的库,类似于cglib、javassist,那么为什么选用该库呢?javassist更偏向底层,比较难于使用并且在动态组合字符串以实现更复杂的逻辑时很容易出错,而cglib现在维护的则相当慢了,基本处于无人维护的阶段了,而这些缺点ByteBuddy都没有,并且ByteBuddy性能相对来说在三者中是最优的,具体参照更详细的内容参照ByteBuddy官网 。

PS:当前Mockito、Hibernate、Jackson等系统都在使用ByteBuddy,具体参照 ByteBuddy的git统计 ,对于我来说可能更喜欢的是ByteBuddy的流式编程风格。

注意:本文仅是一个用户友好版的Get Start!!!下面开始教程。

首先是创建一个类

首先是创建一个类,继承Object并且重写toString方法,示例如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .method(named("toString"))
                .intercept(FixedValue.value(toString))
                .make();

        Class<? extends Object> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
复制代码

可以看到ByteBuddy的代码语义还是很清晰的,subclass方法声明了创建的类的父类,method声明了要拦截的方法(实际底层是一个方法过滤器),而intercept则对上一步过滤出来的方法进行了实际拦截处理。

同时可以注意到上边并没有为生成的Class指定名称,如果要为生成的Class指定名称可以使用 name() 方法,如下例子,将生成的Class名指定为 com.joe.ByteBuddyObject

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        String name = "com.joe.ByteBuddyObject";
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("toString"))
                .intercept(FixedValue.value(toString))
                .make();

        Class<? extends Object> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
        Assert.assertEquals(clazz.getName(), name);
    }
}
复制代码

代理方法到其他实现

前一个例子简单的重写了toString并返回了固定的值,但是实际使用中很少有返回固定值的,一般都是调用某个函数然后返回该函数计算结果,那么这该怎么实现呢?别接,看下面的例子。

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        DynamicType.Unloaded<People> unloaded = new ByteBuddy()
                .subclass(People.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("say"))
                .intercept(MethodDelegation.to(new JoeKerouac()))
                .make();

        Class<? extends People> clazz = unloaded
            .load(ByteBuddyTest.class.getClassLoader())
            .getLoaded();
        Assert.assertSame(clazz.getInterfaces()[0], People.class);
        Assert.assertEquals(clazz.newInstance().say(), "hello JoeKerouac");
    }

    public interface People{
        String say();
    }

    public class JoeKerouac {
        public String say() {
            return "hello JoeKerouac";
        }
    }
}
复制代码

需要注意的是:

  • People这个接口和JoeKerouac这个类必须是公共的可访问的
  • JoeKerouac的say方法除了名字其他定义必须与People相同,即方法必须是公共的、返回值必须是String类型,必须是无参数的,而名字则可以不叫say
  • 该写法仅支持同一个类中(本示例就是JoeKerouac中)有且只有一个相同签名的函数(符合上边三要素的),否则会报错,该问题后续会解决。错误示例:

    public class JoeKerouac {
     public String sayHello() {
        return "hello JoeKerouac";
     }
    
     public String sayHi() {
             return "hi JoeKerouac";
     }
    }
    复制代码

这样简单的几行就动态实现了一个People的子类。

代理方法到其他实现-进阶

上边的注意事项说明中有 该写法仅支持同一个类中(本示例就是JoeKerouac中)有且只有一个相同签名的函数(符合上边三要素的),否则会报错,该问题后续会解决。 ,那么是不是意味着ByteBuddy有很大局限性呢?并不是的,其实这个问题很好解决,报错的原因是如果存在多个签名相同的方法ByteBuddy不能决定到底用哪个方法。既然ByteBuddy不能决定,那么我们帮他决定不就好了?ByteBuddy的作者显然也意识到了该问题,并且提供了解决方案,示例如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String hiMsg = "hi JoeKerouac";
        String helloMsg = "hello JoeKerouac";

        People hi = build("sayHi");
        People hello = build("sayHello");

        Assert.assertEquals(hi.say(), hiMsg);
        Assert.assertEquals(hello.say(), helloMsg);
    }

    private People build(String method) throws Exception {
        DynamicType.Unloaded<People> unloaded = new ByteBuddy()
                .subclass(People.class)
                .name("com.joe.ByteBuddyObject")
                .method(named("say"))
                .intercept(MethodDelegation
                        .withDefaultConfiguration()
                        .filter(ElementMatchers.named(method))
                        .to(new JoeKerouac())
                )
                .make();

        Class<? extends People> clazz = unloaded
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        return clazz.newInstance();
    }

    public interface People{
        String say();
    }

    public class JoeKerouac {

        public String sayHello() {
            return "hello JoeKerouac";
        }

        public String sayHi() {
            return "hi JoeKerouac";
        }
    }
}
复制代码

这样,即使同一个类中有多个相同签名(此处的签名与java中的方法签名语义不一样,是符合上边三要素的签名,不要搞混)的方法也能区分开来,并且可以自主选择使用哪个方法,同时 ElementMatchers 也提供了很多其他开箱即用的选择器,可以自己看源代码来学习使用,并不算太难。

一个需要注意的点儿

ByteBuddy构建构成中生成的对象都是不可变对象,会出现下面的问题:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Builder<Object> builder = new ByteBuddy()
                .subclass(Object.class);
        builder.method(named("toString"))
                .intercept(FixedValue.value(toString));

        Class<? extends Object> clazz = builder.make()
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        // 会报错
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
复制代码

将第一个示例中的代码稍加改动,你会发现这个测试用例跑不通了,原因是因为在 builder.method 这一行开始一直到 intercept 方法结束后会生成一个新的builder,而不是更改原来的builder,因为ByteBuddy生成的中间对象都是不可变的,只能新建不能修改,所以需要将上述代码稍加修改就能通过测试了,修改如下:

package com.joe.utils;

import static net.bytebuddy.matcher.ElementMatchers.named;

import org.junit.Assert;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FixedValue;

/**
 * @author JoeKerouac
 * @version $Id: joe, v 0.1 2018年11月06日 22:15 JoeKerouac Exp $
 */
public class ByteBuddyTest {

    @org.junit.Test
    public void test() throws Exception {
        String toString = "hello ByteBuddy";
        DynamicType.Builder<Object> builder = new ByteBuddy()
                .subclass(Object.class);
        builder = builder.method(named("toString"))
                .intercept(FixedValue.value(toString));

        Class<? extends Object> clazz = builder.make()
                .load(ByteBuddyTest.class.getClassLoader())
                .getLoaded();
        Assert.assertEquals(clazz.newInstance().toString(), toString);
    }
}
复制代码

我们只需要将中间状态记录下来然后在后续使用中使用就行,这样这个测试用例就又能跑通了~

End

本文仅是一个Get Start教程,可以参照着ByteBuddy官网来看,同时里边将一些ByteBuddy没有的内容补充了一下,还有一些坑也做了一下说明,后续可能会写一个更详细的教程,在此之前如果想要深入了解一些其他用法只能自己通过看源码来学习了,这可能是ByteBuddy最不友好的一点儿了,不过一般使用ByteBuddy的场景都比较底层,而用的上ByteBuddy的人一般也有一定源码阅读能力,并且ByteBuddy源码也不算太难,所以这应该不是一个难事儿,本文仅用于结合一些实际场景快速入门。

比官方还详细的ByteBuddy入门教程

原文  https://juejin.im/post/5c9f292b51882525892e2431
正文到此结束
Loading...