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

简介

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

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

注意:本文仅是一个用户友好版的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则对上一步过滤出来的方法进行了实际拦截处理。app

同时能够注意到上边并无为生成的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源码也不算太难,因此这应该不是一个难事儿,本文仅用于结合一些实际场景快速入门。



没有关注的能够扫下方二维码关注我,若是在阅读过程当中有任何问题还能够加我QQ1213812243询问~



         

长按二维码关注我吧

不要错过



本文分享自微信公众号 - java初学者(JoeKerouac_public)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索