Byte Buddy学习笔记
本文转载自Byte Buddy学习笔记
简介
Byte Buddy是一个JVM的运行时代码生成器,你可以利用它创建任何类,且不像JDK动态代理那样强制实现一个接口。Byte Buddy还提供了简单的API,便于手工、通过Java Agent,或者在构建期间修改字节码。
Java反射API可以做很多和字节码生成器类似的工作,但是它具有以下缺点:
- 相比硬编码的方法调用,使用 反射 API 非常慢
- 反射 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中:
- WRAPPER:创建一个新的Wrapping类加载器
- CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
- 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具有限制:
- 类再重新载入前后,必须具有相同的Schema,也就是方法、字段不能减少(可以增加)
- 不支持具有静态初始化块的类
操控未加载类
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遵循一个最接近原则:
- intercept(int)因为参数类型不匹配,直接Pass
- 另外两个方法参数都匹配,但是 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学习笔记的更多相关文章
- Golang 语法学习笔记
Golang 语法学习笔记 包.变量和函数. 包 每个 Go 程序都是由包组成的. 程序运行的入口是包 main. 包名与导入路径的最后一个目录一致."math/rand" 包由 ...
- linux驱动开发之块设备学习笔记
我的博客主要用来存放我的学习笔记,如有侵权,请与我练习,我会立刻删除.学习参考:http://www.cnblogs.com/yuanfang/archive/2010/12/24/1916231.h ...
- 0032 Java学习笔记-类加载机制-初步
JVM虚拟机 Java虚拟机有自己完善的硬件架构(处理器.堆栈.寄存器等)和指令系统 Java虚拟机是一种能运行Java bytecode的虚拟机 JVM并非专属于Java语言,只要生成的编译文件能匹 ...
- 《Java学习笔记(第8版)》学习指导
<Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...
- 软件测试第六周学习笔记之“Win8 APP应用程序的白盒测试”
这周的学习笔记我想写点自己关于实验中碰到的问题和感想. 因为这次做的是白盒测试,所以我决定去测试一下上回测试的app的功能函数. 这次我用的是单元测试项目来做的白盒测试: 创建单元测试的步骤: 1.点 ...
- Rest API 开发 学习笔记(转)
Rest API 开发 学习笔记 概述 REST 从资源的角度来观察整个网络,分布在各处的资源由URI确定,而客户端的应用通过URI来获取资源的表示方式.获得这些表徵致使这些应用程序转变了其状态.随着 ...
- Extjs 学习笔记1
学习笔记 目 录 1 ExtJs 4 1.1 常见错误处理 4 1.1.1 多个js文件中有相同的控件,切换时无法正常显示 4 1.1.2 Store的使用方法 4 1.1.3 gridPanel ...
- Android学习笔记(二十二)——短信接收与发送
//此系列博文是<第一行Android代码>的学习笔记,如有错漏,欢迎指正! 当手机接收到一条短信的时候, 系统会发出一条值为 android.provider.Telephony.SMS ...
- 20145213《Java程序设计学习笔记》第六周学习总结
20145213<Java程序设计学习笔记>第六周学习总结 说在前面的话 上篇博客中娄老师指出我因为数据结构基础薄弱,才导致对第九章内容浅尝遏止地认知.在这里我还要自我批评一下,其实我事后 ...
随机推荐
- Java获取类路径的方式
Java环境中,如何获取当前类的路径.如何获取项目根路径等: @Test public void showURL() throws IOException { // 第一种:获取类加载的根路径 Fil ...
- CF-1440C2 Binary Table (Hard Version) (构造,模拟)
Binary Table (Hard Version) 题意 \(n*m(2\le n,m\le 100)\) 的01矩阵,每次可以选择一个宽度为2的子矩阵,将四个位置中的任意3个进行翻转,即0变1, ...
- java——API
API定义: 可以网上下载一个jdk_api文档用来查找一些函数. 匿名对象的创建 匿名对象做为返回值和参数实例: Random的使用:
- Codeforces Round #575 (Div. 3) B. Odd Sum Segments 、C Robot Breakout
传送门 B题题意: 给你n个数,让你把这n个数分成k个段(不能随意调动元素位置).你需要保证这k个段里面所有元素加起来的和是一个奇数.问可不可以这样划分成功.如果可以打印YES,之后打印出来是从哪里开 ...
- Codeforces Round #481 (Div. 3) F. Mentors (模拟,排序)
题意:有一个长度为\(n\)的序列\(a\),求这个序列中有多少比\(a_{i}\)小的数,如果某两个位置上的数有矛盾,则不能算小. 题解:用\(pair\)来记录序列中元素的位置和大小,将他们升序排 ...
- 2019 ICPC Asia Taipei-Hsinchu Regional Problem K Length of Bundle Rope (贪心,优先队列)
题意:有\(n\)堆物品,每次可以将两堆捆成一堆,新堆长度等于两个之和,每次消耗两个堆长度之和的长度,求最小消耗使所有物品捆成一堆. 题解:贪心的话,每次选两个长度最小的来捆,这样的消耗一定是最小的, ...
- 【转】Docker 核心技术与实现原理
转自:https://draveness.me/docker 提到虚拟化技术,我们首先想到的一定是 Docker,经过四年的快速发展 Docker 已经成为了很多公司的标配,也不再是一个只能在开发阶段 ...
- MHA 高可用介绍
目录 MHA 介绍 MHA 简介(Master High Availability) MHA 工作原理(转载) MHA 架构 MHA 工具 Manager 节点 Node 节点 MHA 优点 MHA ...
- 用阿里云ecs部署kubernetes/K8S的坑(VIP、slb、flannel、gw模式)
1 阿里云ecs不支持keepalived vip 1.1 场景描述 本来计划用keepalived配合nginx做VIP漂移,用以反代多台master的apiserver的6443端口,结果部署了v ...
- 左神算法第一节课:复杂度、排序(冒泡、选择、插入、归并)、小和问题和逆序对问题、对数器和递归(Master公式)
第一节课 复杂度 排序(冒泡.选择.插入.归并) 小和问题和逆序对问题 对数器 递归 1. 复杂度 认识时间复杂度常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数 ...