本文转载自Byte Buddy学习笔记

简介

Byte Buddy是一个JVM的运行时代码生成器,你可以利用它创建任何类,且不像JDK动态代理那样强制实现一个接口。Byte Buddy还提供了简单的API,便于手工、通过Java Agent,或者在构建期间修改字节码。

Java反射API可以做很多和字节码生成器类似的工作,但是它具有以下缺点:

  1. 相比硬编码的方法调用,使用 反射 API 非常慢
  2. 反射 API 能绕过类型安全检查

    比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有优势。

入门

创建新类型

下面是一个最简单的例子:

Class<?> dynamicType = new ByteBuddy()
// 指定父类
.subclass(Object.class)
// 根据名称来匹配需要拦截的方法
.method(ElementMatchers.named("toString"))
// 拦截方法调用,返回固定值
.intercept(FixedValue.value("Hello World!"))
// 产生字节码
.make()
// 加载类
.load(getClass().getClassLoader())
// 获得Class对象
.getLoaded(); assertThat(dynamicType.newInstance().toString(), is("Hello World!"));

ByteBuddy利用Implementation接口来表示一个动态定义的方法,FixedValue.value就是该接口的实例。

完全实现Implementation比较繁琐,因此实际情况下会使用MethodDelegation代替。使用MethodDelegation,你可以在一个POJO中实现方法拦截器:

public class GreetingInterceptor {
// 方法签名随意
public Object greet(Object argument) {
return "Hello from " + argument;
}
} Class<? extends java.util.function.Function> dynamicType = new ByteBuddy()
// 实现一个Function子类
.subclass(java.util.function.Function.class)
.method(ElementMatchers.named("apply"))
// 拦截Function.apply调用,委托给GreetingInterceptor处理
.intercept(MethodDelegation.to(new GreetingInterceptor()))
.make()
.load(getClass().getClassLoader())
.getLoaded(); assertThat((String) dynamicType.newInstance().apply("Byte Buddy"), is("Hello from Byte Buddy"));

编写拦截器时,你可以指定一些注解,ByteBuddy会自动注入:

public class GeneralInterceptor {
// 提示ByteBuddy根据被拦截方法的实际类型,对此拦截器的返回值进行Cast
@RuntimeType
// 所有入参的数组
public Object intercept(@AllArguments Object[] allArguments,
// 被拦截的原始方法
@Origin Method method) {
}
}

修改已有类型

上面的两个例子中,我们利用ByteBuddy创建了指定接口的新子类型,ByteBuddy也可以用来修改已存在的。

ByteBuddy提供了便捷的创建Java Agent的API,本节的例子就是通过Java Agent方式来修改已存在的Java类型:public class TimerAgent {

public static void premain(String arguments,
Instrumentation instrumentation) {
new AgentBuilder.Default()
// 匹配被拦截方法
.type(ElementMatchers.nameEndsWith("Timed"))
.transform(
(builder, type, classLoader, module) ->
builder.method(ElementMatchers.any()) .intercept(MethodDelegation.to(TimingInterceptor.class))
).installOn(instrumentation);
}
} public class TimingInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
// 调用该注解后的Runnable/Callable,会导致调用被代理的非抽象父方法
@SuperCall Callable<?> callable) {
long start = System.currentTimeMillis();
try {
return callable.call();
} finally {
System.out.println(method + " took " + (System.currentTimeMillis() - start));
}
}
}

API

创建类

subclass

调用此方法可以创建一个目标类的子类:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("example.Type") // 子类的名称
.make();

如果不指定子类名称,Byte Buddy会有一套自动的策略来生成。你还可以指定子类命名策略:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.with(new NamingStrategy.AbstractBase() {
@Override
public String subclass(TypeDescription superClass) {
return "i.love.ByteBuddy." + superClass.getSimpleName();
}
})
.subclass(Object.class)
.make();

加载类

上节创建的DynamicType.Unloaded,代表一个尚未加载的类,你可以通过ClassLoadingStrategy来加载这种类。

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

  1. WRAPPER:创建一个新的Wrapping类加载器
  2. CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
  3. INJECTION:利用反射机制注入动态类型

示例:

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

修改类

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"; }
}

重新加载类

得益于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具有限制:

  1. 类再重新载入前后,必须具有相同的Schema,也就是方法、字段不能减少(可以增加)
  2. 不支持具有静态初始化块的类

操控未加载类

Byte Buddy提供了类似于Javassist的、操控未加载类的API。它在TypePool中维护类型的元数据TypeDescription:

// 获取默认类型池
TypePool typePool = TypePool.Default.ofClassPath();
new ByteBuddy()
.redefine(typePool.describe("foo.Bar").resolve(), // 根据名称进行解析类
// ClassFileLocator用于定位到被修改类的.class文件
ClassFileLocator.ForClassLoader.ofClassPath())
.defineField("qux", String.class) // 定义一个新的字段
.make()
.load(ClassLoader.getSystemClassLoader());
assertThat(Bar.class.getDeclaredField("qux"), notNullValue());

拦截方法

匹配方法

Byte Buddy提供了很多用于匹配方法的DSL:

class Foo {
public String bar() { return null; }
public String foo() { return null; }
public String foo(Object o) { return null; }
} 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();

委托方法

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

class Source {
public String hello(String name) { return null; }
} 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 hello(String name) {
return "Hello " + name + "!";
}
}

也可以如下:

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遵循一个最接近原则:

  1. intercept(int)因为参数类型不匹配,直接Pass
  2. 另外两个方法参数都匹配,但是 intercept(String)类型更加接近,因此会委托给它

参数绑定

你可以在Target的方法中使用注解进行参数绑定:

void foo(Object o1, Object o2)
// 等价于
void foo(@Argument(0) Object o1, @Argument(1) Object o2)

全部注解如下表

注解 说明
@Argument 绑定单个参数
@AllArguments 绑定所有参数的数组
@This 当前被拦截的、动态生成的那个对象
@Super 当前被拦截的、动态生成的那个对象的父类对象
@Origin 可以绑定到以下类型的参数:Method 被调用的原始方法 Constructor 被调用的原始构造器 Class 当前动态创建的类 MethodHandle MethodType String 动态类的toString()的返回值 int 动态方法的修饰符
@DefaultCall 调用默认方法而非super的方法
@SuperCall 用于调用父类版本的方法
@Super 注入父类型对象,可以是接口,从而调用它的任何方法
@RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
@Empty 注入参数的类型的默认值
@StubValue 注入一个存根值。对于返回引用、void的方法,注入null;对于返回原始类型的方法,注入0
@FieldValue 注入被拦截对象的一个字段的值
@Morph 类似于@SuperCall,但是允许指定调用参数

添加字段

Class<? extends UserType> dynamicUserType = new ByteBuddy()
.subclass(UserType.class)
.defineField("interceptor", Interceptor.class, Visibility.PRIVATE);

方法调用也可以委托给字段(而非外部对象):

Class<? extends UserType> dynamicUserType = new ByteBuddy()
.subclass(UserType.class)
.method(not(isDeclaredBy(Object.class)))
.intercept(MethodDelegation.toField("interceptor"));

Byte Buddy学习笔记的更多相关文章

  1. Golang 语法学习笔记

    Golang 语法学习笔记 包.变量和函数. 包 每个 Go 程序都是由包组成的. 程序运行的入口是包 main. 包名与导入路径的最后一个目录一致."math/rand" 包由 ...

  2. linux驱动开发之块设备学习笔记

    我的博客主要用来存放我的学习笔记,如有侵权,请与我练习,我会立刻删除.学习参考:http://www.cnblogs.com/yuanfang/archive/2010/12/24/1916231.h ...

  3. 0032 Java学习笔记-类加载机制-初步

    JVM虚拟机 Java虚拟机有自己完善的硬件架构(处理器.堆栈.寄存器等)和指令系统 Java虚拟机是一种能运行Java bytecode的虚拟机 JVM并非专属于Java语言,只要生成的编译文件能匹 ...

  4. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  5. 软件测试第六周学习笔记之“Win8 APP应用程序的白盒测试”

    这周的学习笔记我想写点自己关于实验中碰到的问题和感想. 因为这次做的是白盒测试,所以我决定去测试一下上回测试的app的功能函数. 这次我用的是单元测试项目来做的白盒测试: 创建单元测试的步骤: 1.点 ...

  6. Rest API 开发 学习笔记(转)

    Rest API 开发 学习笔记 概述 REST 从资源的角度来观察整个网络,分布在各处的资源由URI确定,而客户端的应用通过URI来获取资源的表示方式.获得这些表徵致使这些应用程序转变了其状态.随着 ...

  7. Extjs 学习笔记1

    学习笔记 目   录 1 ExtJs 4 1.1 常见错误处理 4 1.1.1 多个js文件中有相同的控件,切换时无法正常显示 4 1.1.2 Store的使用方法 4 1.1.3 gridPanel ...

  8. Android学习笔记(二十二)——短信接收与发送

    //此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 当手机接收到一条短信的时候, 系统会发出一条值为 android.provider.Telephony.SMS ...

  9. 20145213《Java程序设计学习笔记》第六周学习总结

    20145213<Java程序设计学习笔记>第六周学习总结 说在前面的话 上篇博客中娄老师指出我因为数据结构基础薄弱,才导致对第九章内容浅尝遏止地认知.在这里我还要自我批评一下,其实我事后 ...

随机推荐

  1. Java获取类路径的方式

    Java环境中,如何获取当前类的路径.如何获取项目根路径等: @Test public void showURL() throws IOException { // 第一种:获取类加载的根路径 Fil ...

  2. CF-1440C2 Binary Table (Hard Version) (构造,模拟)

    Binary Table (Hard Version) 题意 \(n*m(2\le n,m\le 100)\) 的01矩阵,每次可以选择一个宽度为2的子矩阵,将四个位置中的任意3个进行翻转,即0变1, ...

  3. java——API

    API定义: 可以网上下载一个jdk_api文档用来查找一些函数. 匿名对象的创建  匿名对象做为返回值和参数实例: Random的使用:

  4. Codeforces Round #575 (Div. 3) B. Odd Sum Segments 、C Robot Breakout

    传送门 B题题意: 给你n个数,让你把这n个数分成k个段(不能随意调动元素位置).你需要保证这k个段里面所有元素加起来的和是一个奇数.问可不可以这样划分成功.如果可以打印YES,之后打印出来是从哪里开 ...

  5. Codeforces Round #481 (Div. 3) F. Mentors (模拟,排序)

    题意:有一个长度为\(n\)的序列\(a\),求这个序列中有多少比\(a_{i}\)小的数,如果某两个位置上的数有矛盾,则不能算小. 题解:用\(pair\)来记录序列中元素的位置和大小,将他们升序排 ...

  6. 2019 ICPC Asia Taipei-Hsinchu Regional Problem K Length of Bundle Rope (贪心,优先队列)

    题意:有\(n\)堆物品,每次可以将两堆捆成一堆,新堆长度等于两个之和,每次消耗两个堆长度之和的长度,求最小消耗使所有物品捆成一堆. 题解:贪心的话,每次选两个长度最小的来捆,这样的消耗一定是最小的, ...

  7. 【转】Docker 核心技术与实现原理

    转自:https://draveness.me/docker 提到虚拟化技术,我们首先想到的一定是 Docker,经过四年的快速发展 Docker 已经成为了很多公司的标配,也不再是一个只能在开发阶段 ...

  8. MHA 高可用介绍

    目录 MHA 介绍 MHA 简介(Master High Availability) MHA 工作原理(转载) MHA 架构 MHA 工具 Manager 节点 Node 节点 MHA 优点 MHA ...

  9. 用阿里云ecs部署kubernetes/K8S的坑(VIP、slb、flannel、gw模式)

    1 阿里云ecs不支持keepalived vip 1.1 场景描述 本来计划用keepalived配合nginx做VIP漂移,用以反代多台master的apiserver的6443端口,结果部署了v ...

  10. 左神算法第一节课:复杂度、排序(冒泡、选择、插入、归并)、小和问题和逆序对问题、对数器和递归(Master公式)

    第一节课 复杂度 排序(冒泡.选择.插入.归并) 小和问题和逆序对问题 对数器 递归 1.  复杂度 认识时间复杂度常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数 ...