背景:
反射调用比较慢
反射调用API可以绕过安全检查。 什么是安全检查: 比如Java严格的类型系统检查等特性,丢失后,只能在运行时才会执行的对应的检查
byte buddy 创建自己的类: 最终是make() 完成一个unload 的对象,还是比较符合语义的
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
// 允许自定义创建自己的类的名称
.with(new NamingStrategy.AbstractBase() {
@Override
protected String name(TypeDescription typeDescription) {
return "Hello."+ typeDescription.getSimpleName();
}
})
.name("example.type")
.make();
byte buddy 围绕一个不可变的对象构建自己的实例, 即可以表现为链式写法,如果希望对最后实现的 make() 方法单独返回对象,是会被直接丢弃的
byte buddy 中存在两类逻辑完成类的重写: redefine 和 rebase ; redefine 直接覆盖原来的实现 , rebase 会将原来的实现修改方法名称,类似 old.name$oringal
byte buddy 默认生成的对象类型是 DynamicType.Unloaded ,即 仅仅在内存中生成了这个二进制流,不会load到虚拟机中,所以我们可以选择将二进制流的基础操作,如写到文件或者写到jar里面。
仅仅是将class的二进制流保存在内存中是没有任何作用的,如果需要加载到虚拟机内部,还需要指定对应的classloader完成加载。
如果新建一个 classloader 来加载这个内存流, jvm 不会认为其和其他class相同,因为归属于不同的classloader ,所以即使是同名的class都是认为其是不同的类型。所以自然无法访问任何jvm中的其他类型;
同理,在加载类的时候,jvm会使用同一个classloader完成关联的class加载,如果关联的class是其他的classloader创建的,那么加载这个关联关系就会失败。
—>
那么如何解决这个问题? JVM第一次运行的时候其实是懒加载解析引用的类。
byte buddy 最终还是选择自己创建一个类加载器,比如创建一个子类加载优先的加载,比如 child_first 策略;
warpper策略即基于目前的classloader ,封装一个新的;
inject 即反射注入动态类型
下面对 unload 的类型指定加载器和模式,开始load 完成到 class 对象的转换
Class<?> clazz = new ByteBuddy()
// 允许自定义创建自己的类的名称
.with(new NamingStrategy.AbstractBase() {
@Override
protected String name(TypeDescription typeDescription) {
return "Hello."+ typeDescription.getSimpleName();
}
})
.subclass(Object.class)
.make()
.load(ClassNotFoundException.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
上述均为如何重新创建一个新的 class 和加载到 虚拟机中 , 重新使用一个新的 classloader 来加载一个新的 class , 我理解理论是不存在任何的问题的。
对于已经加载进去的class , 可以使用 redefine 完成其功能的覆盖
static class Foo {
String m() {
return "foo";
}
}
static class Bar {
String m(){
return "bar";
}
}
public static void main(String[] args) {
ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
// 新的方法实现
.redefine(Bar.class)
// 重新加载的目标位置
.name(Foo.class.getName())
.make()
.load(Foo.class.getClassLoader(),ClassReloadingStrategy.fromInstalledAgent());
System.out.println(foo.m()); // bar
}
上述完成将已经加载的 foo 对象重新使用 Bar 的类型去重新写
Java 的热加载需要使用 agent完成,上述可以看到已经使用 byte buddy 提供的内容完成了一次加载
同时,Java的热加载需要保证新的 Bar 里面的方法等完全和对应的 Foo 保持一致
下面这个例子使用 load 的 class 对象 使用反射的方法完成到一个对象实例的创建,然后调用 object 默认的 tostring 逻辑
String s = new ByteBuddy()
.subclass(Object.class)
.name("example.type")
.make()
.load(DebugPlugin.class.getClassLoader())
.getLoaded()
.newInstance()
.toString();
System.out.println(s); // example.type@25bbe1b6
下面这个例子将父类的方法重新拦截和对返回值做了一个重新定义
String s = new ByteBuddy()
.subclass(Object.class)
.name("example.type")
.method(ElementMatchers.named("toString")).intercept(FixedValue.value("Hello"))
.make()
.load(DebugPlugin.class.getClassLoader())
.getLoaded()
.newInstance()
.toString();
System.out.println(s); // hello
如果存在多个同名的逻辑,可以添加 过滤完成匹配
String s = new ByteBuddy()
.subclass(Object.class)
.name("example.type")
.method(ElementMatchers.named("toString")
.and(ElementMatchers.returns(String.class))
.and(ElementMatchers.takesArguments(0))).
intercept(FixedValue.value("Hello"))
.make()
.load(DebugPlugin.class.getClassLoader())
.getLoaded()
.newInstance()
.toString();
System.out.println(s); // hello
上述是基于自己构建的class对象来修改父类的逻辑,同时使用反射完成到具体的实例的逻辑。
如果我们不希望是继承来修改对应的类,即不想自己修改自己,想直接使用另一个class完成对当前的class的一个方法的逻辑的重写(这个场景还是比较常见的)
static class Foo {
public String m() {
return "foo";
}
}
static class Bar {
public String m() {
return "bar";
}
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
ByteBuddyAgent.install();
String s = new ByteBuddy()
.subclass(Foo.class)
.method(ElementMatchers.named("m")
.and(ElementMatchers.returns(String.class))
.and(ElementMatchers.takesArguments(0))).intercept(MethodDelegation.to(Bar.class))
.make()
.load(DebugPlugin.class.getClassLoader())
.getLoaded()
.newInstance()
.m();
System.out.println(s); // hello
}