字节码加强技术-Byte Buddy

为何须要在运行时生成代码?

Java 是一个强类型语言系统,要求变量和对象都有一个肯定的类型,不兼容类型赋值都会形成转换异常,一般状况下这种错误都会被编译器检查出来,如此严格的类型在大多数状况下是比较使人满意的,这对构建具备很是强可读性和稳定性的应用有很大的帮助,这也是 Java 能在企业编程中的普及的一个缘由之一。然而,由于起强类型的检查,限制了其余领域语言应用范围。好比在编写一个框架是,一般咱们并不知道应用程序定义的类型,由于当这个库被编译时,咱们还不知道这些类型,为了能在这种状况下能调用或者访问应用程序的方法或者变量,Java 类库提供了一套反射 API。使用这套反射 API,咱们就能够检讨为知类型,进而调用方法或者访问属性。可是,Java 反射有以下缺点:java

  • 须要执行一个至关昂贵的方法查找来获取描述特定方法的对象,所以,相比硬编码的方法调用,使用 反射 API 很是慢。
  • 反射 API 能绕过类型安全检查,可能会由于使用不当照成意想不到的问题,这样就错失了 Java 编程语言的一大特性。

简介

正如官网说的:Byte Buddy 是一个代码生成和操做库,用于在Java应用程序运行时建立和修改Java类,而无需编译器的帮助。除了Java类库附带的代码生成实用程序外,Byte Buddy还容许建立任意类,而且不限于实现用于建立运行时代理的接口。此外,Byte Buddy提供了一种方便的API,可使用Java代理或在构建过程当中手动更改类。Byte Buddy 相比其余字节码操做库有以下优点:编程

  • 无需理解字节码格式,便可操做,简单易行的 API 能很容易操做字节码。
  • 支持 Java 任何版本,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它自己不须要任何其余依赖项。
  • 比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具备优点。

性能

在选择字节码操做库时,每每须要考虑库自己的性能。对于许多应用程序,生成代码的运行时特性更有可能肯定最佳选择。而在生成的代码自己的运行时间以外,用于建立动态类的运行时也是一个问题。官网对库进行了性能测试,给出如下结果图:file数组

图中的每一行分别为,类的建立、接口实现、方法调用、类型扩展、父类方法调用的性能结果。从性能报告中能够看出,Byte Buddy 的主要侧重点在于以最少的运行时生成代码,须要注意的是,咱们这些衡量 Java 代码性能的测试,都由 Java 虚拟机即时编译器优化过,若是你的代码只是偶尔运行,没有获得虚拟机的优化,可能性能会有所误差。因此咱们在使用 Byte Buddy 开发时,咱们但愿监控这些指标,以免在添加新功能时形成性能损失。安全

Hello world!

Class<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .method(ElementMatchers.named("toString"))
                .intercept(FixedValue.value("Hello World"))
                .make()
                .load(HelloWorldBuddy.class.getClassLoader())
                .getLoaded();

        Object instance = dynamicType.newInstance();
        String toString = instance.toString();
        System.out.println(toString);
        System.out.println(instance.getClass().getCanonicalName());复制代码

从例子中看到,操做建立一个类如此的简单。正如 ByteBuddy 说明的,ByteBuddy 提供了一个领域特定语言,这样就能够尽量地提升人类可读性简单易行的 API,可能能让你在初次使用的过程当中就能不须要查阅 API 的前提下完成编码。这也真是 ByteBuddy 能完爆其余同类型库的一个缘由。微信

上面的示例中使用的默认ByteBuddy配置会以最新版本的类文件格式建立Java类,该类文件格式能够被正在处理的Java虚拟机理解。subclass 指定了新建立的类的父类,同时 method 指定了 ObjecttoString 方法,intercept 拦截了 toString 方法并返回固定的 value ,最后 make 方法生产字节码,有类加载器加载到虚拟机中。app

此外,Byte Buddy不只限于建立子类和操做类,还能够转换现有代码。Byte Buddy 还提供了一个方便的 API,用于定义所谓的 Java 代理,该代理容许在任何 Java 应用程序的运行期间进行代码转换,代理会在下篇单独写一篇文章讲解。框架

建立一个类

任何一个由 ByteBuddy 建立的类型都是经过 ByteBuddy 类的实例来完成的。经过简单地调用 new ByteBuddy() 就能够建立一个新实例。dom

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();复制代码

上面的示例代码会建立一个继承至 Object 类型的类。这个动态建立的类型与直接扩展 Object 而且没有实现任何方法、属性和构造函数的类型是等价的。该列子没有命名动态生成的类型,可是在定义 Java 类时倒是必须的,因此很容易的你会想到,ByteBuddy 会有默认的策略给咱们生成。固然,你也能够很容易地明确地命名这个类型。编程语言

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();复制代码

那么默认的策略是如何作的呢?这个将与 ByteBuddy 与 约定大于配置息息相关,它提供了咱们认为比较全面的默认配置。至于类型命名,ByteBuddy 的默认配置提供了 NamingStrategy,它基于动态类型的超类名称来随机生成类名。此外,名称定义在与父类相同的包下,这样父类的包级访问权限的方法对动态类型也可见。若是你将示例子类命名为 example.Foo,那么生成的名称将会相似于 example.FooByteBuddy1376491271,这里的数字序列是随机的。ide

此外,在一些须要指定类型的场景中,能够经过重写 NamingStrategy 的方法来实现,或者使用 ByteBuddy 内置的NamingStrategy.SuffixingRandom 来实现。

同时须要注意的是,咱们编码时须要遵照所谓的领域特定语言和不变性原则,这是说明意思呢?就是说在 ByteBuddy 中,几乎全部的类都被构建成不可变的;极少数状况,咱们不可能把对象构建成不可变的。请看下面一个例子:

ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType1 = byteBuddy.subclass(Object.class).make();复制代码

上述例子你会发现类的命名策略仍是默认的,其根本缘由就是没有遵照上述原则致使的。因此在编码过程当中要基于此原则进行。

加载类

上节建立的 DynamicType.Unloaded,表明一个还没有加载的类,顾名思义,这些类型不会加载到 Java 虚拟机中,它仅仅表示建立好了类的字节码,经过 DynamicType.Unloaded 中的 getBytes 方法你能够获取到该字节码,在你的应用程序中,你可能须要将该字节码保存到文件,或者注入的如今的 jar 文件中,所以该类型还提供了一个 saveIn(File) 方法,能够将类存储在给定的文件夹中; inject(File) 方法将类注入到现有的 Jar 文件中,另外你只须要将该字节码直接加载到虚拟机使用,你能够经过 ClassLoadingStrategy 来加载。

若是不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中

  • WRAPPER:建立一个新的Wrapping类加载器
  • CHILD_FIRST:相似上面,可是子加载器优先负责加载目标类
  • INJECTION:利用反射机制注入动态类型

示例

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();复制代码

这样咱们建立并加载了一个类。咱们使用 WRAPPER 策略来加载适合大多数状况的类。getLoaded 方法返回一个 Java Class 的实例,它就表示如今加载的动态类。

从新加载类

得益于JVM的HostSwap特性,已加载的类能够被从新定义:

// 安装Byte Buddy的Agent,除了经过-javaagent静态安装,还能够:
ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
assertThat(foo.m(), is("bar"));    复制代码

能够看到,即便时已经存在的对象,也会受到类Reloading的影响。可是须要注意的是HostSwap具备限制:

  • 类再从新载入先后,必须具备相同的Schema,也就是方法、字段不能减小(能够增长)
  • 不支持具备静态初始化块的类

修改类

redefine

重定义一个类时,Byte Buddy 能够对一个已有的类添加属性和方法,或者删除已经存在的方法实现。新添加的方法,若是签名和原有方法一致,则原有方法会消失。

rebase

相似于redefine,可是原有的方法不会消失,而是被重命名,添加后缀 $original,这样,就没有实现会被丢失。重定义的方法能够继续经过它们重命名过的名称调用原来的方法,例如类:

class Foo {
  String bar() { return "bar"; }
}复制代码

rebase 以后:

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}复制代码

方法拦截

经过匹配模式拦截

ByteBuddy 提供了不少用于匹配方法的 DSL,以下例子:

Foo dynamicFoo = new ByteBuddy()
  .subclass(Foo.class)
  // 匹配由Foo.class声明的方法
  .method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
  // 匹配名为foo的方法
  .method(named("foo")).intercept(FixedValue.value("Two!"))
  // 匹配名为foo,入参数量为1的方法
  .method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance();复制代码

ByteBuddy 经过 net.bytebuddy.matcher.ElementMatcher 来定义配置策略,能够经过此接口实现本身定义的匹配策略。库自己提供的 Matcher 很是多。Uploading file...

方法委托

使用MethodDelegation能够将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致

class Source {
  public String hello(String name) { return null; }
}

class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
}

String helloWorld = new ByteBuddy()
  .subclass(Source.class)
  .method(named("hello")).intercept(MethodDelegation.to(Target.class))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded()
  .newInstance()
  .hello("World");复制代码

其中 Target 还能够以下实现:

class Target {
  public static String intercept(String name) { return "Hello " + name + "!"; }
  public static String intercept(int i) { return Integer.toString(i); }
  public static String intercept(Object o) { return o.toString(); }
}复制代码

前一个实现由于只有一个方法,并且类型也匹配,很好理解,那么后一个呢,Byte Buddy到底会委托给哪一个方法?Byte Buddy遵循一个最接近原则:

  • intercept(int)由于参数类型不匹配,直接Pass
  • 另外两个方法参数都匹配,可是 intercept(String)类型更加接近,所以会委托给它

同时须要注意的是被拦截的方法须要声明为 public,不然无法进行拦截加强。除此以外,还可使用 @RuntimeType 注解来标注方法

@RuntimeType
public static Object intercept(@RuntimeType Object value) {
        System.out.println("Invoked method with: " + value);
        return value;
}复制代码

参数绑定

能够在拦截器(Target)的拦截方法 intercept 中使用注解注入参数,ByteBuddy 会根据注解给咱们注入对于的参数值。好比:

void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)复制代码

经常使用的注解以下表:

注解 描述
@Argument 绑定单个参数
@AllArguments 绑定全部参数的数组
@This 当前被拦截的、动态生成的那个对象
@DefaultCall 调用默认方法而非super的方法
@SuperCall 用于调用父类版本的方法
@RuntimeType 能够用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
@Super 当前被拦截的、动态生成的那个对象的父类对象
@FieldValue 注入被拦截对象的一个字段的值

字段属性

public class UserType {
  public String doSomething() { return null; }
}

public interface Interceptor {
  String doSomethingElse();
}

public interface InterceptionAccessor {
  Interceptor getInterceptor();
  void setInterceptor(Interceptor interceptor);
}

public interface InstanceCreator {
  Object makeInstance();
}

public class HelloWorldInterceptor implements Interceptor {
  @Override
  public String doSomethingElse() {
    return "Hello World!";
  }
}

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class))) // 非父类 Object 声明的方法
    .intercept(MethodDelegation.toField("interceptor")) // 拦截委托给属性字段 interceptor
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE) // 定义一个属性字段
  .implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) // 实现 InterceptionAccessor 接口
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();
    
InstanceCreator factory = new ByteBuddy()
  .subclass(InstanceCreator.class)
    .method(not(isDeclaredBy(Object.class))) // 非父类 Object 声明的方法
    .intercept(MethodDelegation.toConstructor(dynamicUserType)) // 委托拦截的方法来调用提供的类型的构造函数
  .make()
  .load(dynamicUserType.getClassLoader())
  .getLoaded().newInstance();

UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
String s = userType.doSomething();
System.out.println(s); // Hello World!复制代码

上述例子将 UserType 类实现了 InterceptionAccessor 接口,同时使用 MethodDelegation.toField 可使拦截的方法能够委托给新增的字段。

End

本文是本身学习 ByteBuddy 后本身稍加整理的基础教程。最后感谢你阅读!!!

微信公众号关注:ByteZ,获取更多学习资料

file

相关文章
相关标签/搜索