Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Byte Buddy学习笔记

3
Jun
2019

Byte Buddy学习笔记

By Alex
/ in Java
1 Comment
简介

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

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

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

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

入门
创建新类型

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

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中实现方法拦截器:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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会自动注入:

Java
1
2
3
4
5
6
7
8
9
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 {

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

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

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

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

Java
1
2
3
4
5
6
7
8
9
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:利用反射机制注入动态类型

示例:

Java
1
2
3
4
5
Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();
修改类
redefine

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

rebase

类似于redefine,但是原有的方法不会消失,而是被重命名,添加后缀 $original,例如类:

Java
1
2
3
class Foo {
  String bar() { return "bar"; }
}

在rebase之后,会变成:

Java
1
2
3
4
class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}
重新加载类

得益于JVM的HostSwap特性,已加载的类可以被重新定义:

Java
1
2
3
4
5
6
7
8
9
10
// 安装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:

Java
1
2
3
4
5
6
7
8
9
10
// 获取默认类型池
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:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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类的方法名一致:

Java
1
2
3
4
5
6
7
8
9
10
11
12
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的实现可以如下: 

Java
1
2
3
4
5
class Target {
  public static String hello(String name) {
    return "Hello " + name + "!";
  }
} 

也可以如下:  

Java
1
2
3
4
5
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的方法中使用注解进行参数绑定:

Java
1
2
3
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,但是允许指定调用参数
添加字段
Java
1
2
3
Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE);

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

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

 

分享这篇文章到:
← 如何开发Java Agent
基于BCC进行性能追踪 →
1 Comment On This Topic
  1. 回复
    bytebuddy简单入门 – FIXBBS
    2021/01/17

    […] 不错的使用指南 […]

Leave a Reply Cancel reply

您可以使用以下网站的账户登录:

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Dom4j知识集锦
  • Java5新特性
  • ActiveMQ知识集锦
  • Aspject加载时织入示例
  • Groovy学习笔记

Recent Posts

  • Terraform快速参考
  • 草缸2021
  • 编写Kubernetes风格的APIServer
  • 记录一次KeyDB缓慢的定位过程
  • eBPF学习笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
  • Istio中的透明代理问题
    为何需要透明代理 Istio的Sidecar作为一个网络代理,它拦截入站、出站的网络流量。拦截入站流量后,会使 ...
  • Cilium学习笔记
    简介 Cilium Cilium是在Docker/K8S之类的容器管理平台下,透明的为应用程序服务提供安全网 ...
  • 彩彩 2020年6月黄崖关

  • 总部远眺 2020年5月深圳

  • 绚丽之花 寻味顺德

  • tuanbolake 团泊湖野餐

TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 88 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • 基于Calico的CNI 27 people like this
  • Ceph学习笔记 26 people like this
  • Three.js学习笔记 24 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
  • 绿色记忆:Nginx知识集锦 on Apache HTTP Server知识集锦
  • NotMeBug on AspectJ编程学习笔记
©2005-2023 Gmem.cc | Powered by WordPress