什么是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
TypePool typePool = TypePool.Default.ofSystemLoader(); Class bar = new ByteBuddy() .redefine(typePool.describe("foo.Bar").resolve(), ClassFileLocator.ForClassLoader.ofSystemLoader()) .defineField("qux", String.class) .make() .load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION) .getLoaded();
参考文章
[1] https://bytebuddy.net/#/tutorial