Java字节码ByteBuddy使用及原理解析上
作者:骑牛上青山 发布时间:2023-08-23 19:33:05
什么是ByteBuddy
ByteBuddy
是一个java的运行时代码生成库,他可以帮助你以字节码的方式动态修改java类的代码。
为什么需要ByteBuddy
Java是一个强类型语言,有着极为严格的类型系统。这个严格的类型系统可以帮助构建严谨,更不容易被腐化的代码,但是也在某些方面限制了java的应用。不过为了解决这个问题,java提供了一套反射的api来帮助使用者感知和修改类的内部。
不过反射也有他的缺点:
反射显而易见的缺点是慢。我们在使用反射之前都需要谨慎的考虑他对于当前性能的影响,唯有进过详细的评估,才能够放心的使用。
反射能够绕过类型安全检查。我们在使用反射的时候需要确保相应的接口不会暴露给外部用户,不然可能造成不小的安全隐患。
而ByteBuddy
就可以帮助我们做到反射能做的事情,而不必受困于他的这些缺点。
ByteBuddy使用
创建一个类
new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.saveIn(new File("result"));
上述代码创建了一个Object
的子类并且创建了toString
方法输出Hello World!
通过找到保存的输出类我们可以看到最后的类是这样的:
package net.bytebuddy.renamed.java.lang;
public class Object$ByteBuddy$tPSTnhZh {
public String toString() {
return "Hello World!";
}
public Object$ByteBuddy$tPSTnhZh() {
}
}
可以看到我们虽然创建了一个类,但是我们没有为这个类取名,通过结果得知最后的类名是net.bytebuddy.renamed.java.lang.Object$ByteBuddy$tPSTnhZh
,那么这个类名是怎么来的呢?
在ByteBuddy中如果没有指定类名,他会调用默认的NamingStrategy
策略来生成类名,一般情况下为
父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: org.example.MyTest$ByteBuddy$NsT9pB6w
如果父类是java.lang目录下的类,例如Object,那么会变成
net.bytebuddy.renamed. + 父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: net.bytebuddy.renamed.java.lang.Object$ByteBuddy$2VOeD4Lh
以此来规避java安全模型的限制。
类型重定义与变基
定义一个类
package org.example.bytebuddy.test;
public class MyClassTest {
public String test() {
return "my test";
}
}
用这个类来验证如下的能力
类型重定义(type redefinition)
ByteBuddy支持对于已存在的类进行重定义,即可以添加或者删除类的方法。只不过当类的方法被重定义之后,那么原先的方法中的信息就会丢失。
Class<?> dynamicType = new ByteBuddy()
.redefine(MyClassTest.class)
.method(ElementMatchers.named("test"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(String.class.getClassLoader()).getLoaded();
redefine结果是
类型变基(type rebasing)
rebase操作和redefinition操作最大的区别就是rebase操作不会丢失原先的类的方法信息。大致的实现原理是在变基操作的时候把所有的方法实现复制到重新命名的私有方法(具有和原先方法兼容的签名)中,这样原先的方法就不会丢失。
Class<?> dynamicType = new ByteBuddy()
.rebase(MyClassTest.class)
.method(ElementMatchers.named("test"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(String.class.getClassLoader()).getLoaded();
rebase之后结果
可以看到原先的方法被重命名后保留了下来,并且变成了私有方法。
注意redefinition和rebasing不能修改已经被jvm加载的类,不然会报错Class already loaded
类的加载
生成了之后为了在代码中使用,必须要经过load
流程。细心的读者可能已经发现了上文中已经使用到了load
相关的方法。
构建了具体的动态类之后,可以选择使用saveIn将其结构体存储下来,也可以选择将它装载到虚拟机中。在类加载器的选择中,ByteBuddy提供了几种选择放在ClassLoadingStrategy.Default
中:
WRAPPER
:这个策略会创建一个新的ByteArrayClassLoader
,并使用传入的类加载器为父类。WRAPPER_PERSISTENT
:该策略和WRAPPER
大致一致,只是会将所有的类文件持久化到类加载器中CHILD_FIRST
:这个策略是WRAPPER
的改版,其中动态类型的优先级会比父类加载器中的同名类高,即在此种情况下不再是类加载器通常的父类优先,而是“子类优先”CHILD_FIRST_PERSISTENT
:该策略和CHILD_FIRST
大致一致,只是会将所有的类文件持久化到类加载器中INJECTION
:这个策略最为特殊,他不会创建类加载器,而是通过反射的手段将类注入到指定的类加载器之中。这么做的好处是用这种方法注入的类对于类加载器中的其他类具有私有权限,而其他的策略不具备这种能力。
类的重载
前面提到过,rebase和redefine通常没办法重新加载已经存在的类,但是由于jvm的热替换(HotSwap)机制的存在,使得ByteBuddy
可以在加载后也能够重新定义类。
class Foo {
String m() { return "foo"; }
}
class Bar {
String m() { return "bar"; }
}
我们通过ByteBuddy的ClassRelodingsTrategy
即可完成热替换。
ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
.redefine(Bar.class)
.name(Foo.class.getName())
.make()
.load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
需要注意的是热替换机制必须依赖Java Agent才能使用。Java Agent是一种可以在java项目运行前或者运行时动态修改类的技术。通常可以使用-javaagent参数引入java agent。
处理尚未加载的类
ByteBuddy除了可以处理已经加载完的类,他也具备处理尚未被加载的类的能力。
ByteBuddy对java的反射api做了抽象,例如Class
实例就被表示成了TypeDescription
实例。事实上,ByteBuddy只知道如何通过实现TypeDescription
接口的适配器来处理提供的 Class
。这种抽象的一大优势是类信息不需要由类加载器提供,可以由任何其他来源提供。
ByteBuddy中可以通过TypePool
获取类的TypeDescription
,ByteBuddy提供了TypePool
的默认实现TypePool.Default
。这个类可以帮助我们把java字节码转换成TypeDescription
。
Java的类加载器只会在类第一次使用的时候加载一次,因此我们可以在java中以如下方式安全的创建一个类:
package foo;
class Bar { }
但是通过如下的方法,我们可以在Bar
这个类没有被加载前就提前生成我们自己的Bar
,因此后续jvm就只会使用到我们的Bar
参考文章
[1] https://bytebuddy.net/#/tutorial
来源:https://segmentfault.com/a/1190000043790997


猜你喜欢
- 一、Feign简介Feign是netflix开发的声明式、模板化的http客户端,在使用时就像调用本地(服务消费者自己)的方法一般,帮助我们
- Android studio开发工具中,如何如何删除Android项目,下面是在Android studio 1.5正式版删除Android
- 当我们需要在Unity客户端做一个限制功能,比如按钮 (最好是发送验证码按钮)要求每天只能点击三次,等到第二天又有三次机会,这个过程不涉及到
- Java7中文件IO发生了很大的变化,专门引入了很多新的类:import java.nio.file.DirectoryStream;imp
- 测试springboot项目出现Test Ignored今天在写springBoot项目运行测试类时出现了以下问题:Test ignored
- 本文实例为大家分享了Android文件下载功能的具体代码,供大家参考,具体内容如下1.普通单线程下载文件:直接使用URLConnection
- 开始接触分布式概念,学习之前要准备搭建Dubbo和Zookeeper环境的简单搭建。Window下安装Zookeeper和Dubbo-adm
- Kotlin基础教程之数据类型一切都是对象.在Kotlin中一切都是对象.Kotlin有一些基本类型Boolean,Byte,Shot,In
- 本文主要给大家介绍了关于RxJava的一些特殊用法,分享出来供大家参考学习,需要的朋友们下面来一起看看吧。一、按钮绑定通过 RxView 可
- Unity Shader学习:水墨效果偶然在网上看到9级铁甲蛹大神的水墨风格后处理觉得挺有意思,参照着实现一下,还是涉及到之前油画效果的算法
- 一般情况下在Word中输入的文字都是横向的,今天给大家分享两种方法来设置/更改一个section内的所有文本的方向及部分文本的方向,有兴趣的
- 今天就是国赛的第一天直接开摆打国赛不如玩羊了个羊玩羊了个羊不如玩MATLAB版写作不易留个赞叭(比赛之余放松一下也行,反正MATLAB版我设
- 详解Android使用@hide的API的方法今天早上想修改MediaPlaybackService.Java(/packages/apps
- springboot2启动时执行,初始化(或定时任务)servletContext需求:springboot 启动后自动执行,初始化数据,并
- 1、声明一个测试对象import java.time.LocalDate;import java.util.List;import lomb
- 在笔试编程过程中,关于数据的读取如果迷迷糊糊,那后来的编程即使想法很对,实现很好,也是徒劳,于是在这里认真总结了Java Scanner 类
- 1 低层级 asyncio 索引低层级 API 索引¶ 列出所有低层级的 asyncio API。1.1 获取事件循环获取
- 一、简介CyclicBarrier 字面意思回环栅栏(循环屏障),它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是
- 本文实例讲述了C#预处理器指令的用法。分享给大家供大家参考。具体用法分析如下:C#预处理器指令是在编译时调用的。预处理器指令(preproc
- 首先:看问题图,如下可以激活ide的网址很多,估计是个团队或者个人,直接买了全部产品的一年的有效期。而且还是会一直更新下去的。因为,后来我自