Java 是一个强类型语言系统,要求变量和对象都有一个肯定的类型,不兼容类型赋值都会形成转换异常,一般状况下这种错误都会被编译器检查出来,如此严格的类型在大多数状况下是比较使人满意的,这对构建具备很是强可读性和稳定性的应用有很大的帮助,这也是 Java 能在企业编程中的普及的一个缘由之一。然而,由于起强类型的检查,限制了其余领域语言应用范围。好比在编写一个框架是,一般咱们并不知道应用程序定义的类型,由于当这个库被编译时,咱们还不知道这些类型,为了能在这种状况下能调用或者访问应用程序的方法或者变量,Java 类库提供了一套反射 API。使用这套反射 API,咱们就能够检讨为知类型,进而调用方法或者访问属性。可是,Java 反射有以下缺点:java
正如官网说的:Byte Buddy 是一个代码生成和操做库,用于在Java应用程序运行时建立和修改Java类,而无需编译器的帮助。除了Java类库附带的代码生成实用程序外,Byte Buddy还容许建立任意类,而且不限于实现用于建立运行时代理的接口。此外,Byte Buddy提供了一种方便的API,可使用Java代理或在构建过程当中手动更改类。Byte Buddy 相比其余字节码操做库有以下优点:编程
在选择字节码操做库时,每每须要考虑库自己的性能。对于许多应用程序,生成代码的运行时特性更有可能肯定最佳选择。而在生成的代码自己的运行时间以外,用于建立动态类的运行时也是一个问题。官网对库进行了性能测试,给出如下结果图:数组
图中的每一行分别为,类的建立、接口实现、方法调用、类型扩展、父类方法调用的性能结果。从性能报告中能够看出,Byte Buddy 的主要侧重点在于以最少的运行时生成代码,须要注意的是,咱们这些衡量 Java 代码性能的测试,都由 Java 虚拟机即时编译器优化过,若是你的代码只是偶尔运行,没有获得虚拟机的优化,可能性能会有所误差。因此咱们在使用 Byte Buddy 开发时,咱们但愿监控这些指标,以免在添加新功能时形成性能损失。安全
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World"))
.make()
.load(HelloWorldBuddy.class.getClassLoader())
.getLoaded();
Object instance = dynamicType.newInstance();
String toString = instance.toString();
System.out.println(toString);
System.out.println(instance.getClass().getCanonicalName());复制代码
从例子中看到,操做建立一个类如此的简单。正如 ByteBuddy
说明的,ByteBuddy
提供了一个领域特定语言,这样就能够尽量地提升人类可读性简单易行的 API,可能能让你在初次使用的过程当中就能不须要查阅 API 的前提下完成编码。这也真是 ByteBuddy
能完爆其余同类型库的一个缘由。微信
上面的示例中使用的默认ByteBuddy配置会以最新版本的类文件格式建立Java类,该类文件格式能够被正在处理的Java虚拟机理解。subclass
指定了新建立的类的父类,同时 method
指定了 Object
的 toString
方法,intercept
拦截了 toString
方法并返回固定的 value ,最后 make
方法生产字节码,有类加载器加载到虚拟机中。app
此外,Byte Buddy不只限于建立子类和操做类,还能够转换现有代码。Byte Buddy 还提供了一个方便的 API,用于定义所谓的 Java 代理,该代理容许在任何 Java 应用程序的运行期间进行代码转换,代理会在下篇单独写一篇文章讲解。框架
任何一个由 ByteBuddy
建立的类型都是经过 ByteBuddy
类的实例来完成的。经过简单地调用 new ByteBuddy()
就能够建立一个新实例。dom
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.make();复制代码
上面的示例代码会建立一个继承至 Object 类型的类。这个动态建立的类型与直接扩展 Object 而且没有实现任何方法、属性和构造函数的类型是等价的。该列子没有命名动态生成的类型,可是在定义 Java 类时倒是必须的,因此很容易的你会想到,ByteBuddy
会有默认的策略给咱们生成。固然,你也能够很容易地明确地命名这个类型。编程语言
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("example.Type")
.make();复制代码
那么默认的策略是如何作的呢?这个将与 ByteBuddy
与 约定大于配置息息相关,它提供了咱们认为比较全面的默认配置。至于类型命名,ByteBuddy
的默认配置提供了 NamingStrategy,它基于动态类型的超类名称来随机生成类名。此外,名称定义在与父类相同的包下,这样父类的包级访问权限的方法对动态类型也可见。若是你将示例子类命名为 example.Foo,那么生成的名称将会相似于 example.FooByteBuddy1376491271,这里的数字序列是随机的。ide
此外,在一些须要指定类型的场景中,能够经过重写 NamingStrategy
的方法来实现,或者使用 ByteBuddy
内置的NamingStrategy.SuffixingRandom 来实现。
同时须要注意的是,咱们编码时须要遵照所谓的领域特定语言和不变性
原则,这是说明意思呢?就是说在 ByteBuddy
中,几乎全部的类都被构建成不可变的;极少数状况,咱们不可能把对象构建成不可变的。请看下面一个例子:
ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.with(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType1 = byteBuddy.subclass(Object.class).make();复制代码
上述例子你会发现类的命名策略仍是默认的,其根本缘由就是没有遵照上述原则致使的。因此在编码过程当中要基于此原则进行。
上节建立的 DynamicType.Unloaded
,表明一个还没有加载的类,顾名思义,这些类型不会加载到 Java 虚拟机中,它仅仅表示建立好了类的字节码,经过 DynamicType.Unloaded
中的 getBytes
方法你能够获取到该字节码,在你的应用程序中,你可能须要将该字节码保存到文件,或者注入的如今的 jar 文件中,所以该类型还提供了一个 saveIn(File)
方法,能够将类存储在给定的文件夹中; inject(File)
方法将类注入到现有的 Jar 文件中,另外你只须要将该字节码直接加载到虚拟机使用,你能够经过 ClassLoadingStrategy
来加载。
若是不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中
示例
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();复制代码
这样咱们建立并加载了一个类。咱们使用 WRAPPER 策略来加载适合大多数状况的类。getLoaded
方法返回一个 Java Class 的实例,它就表示如今加载的动态类。
得益于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具备限制:
重定义一个类时,Byte Buddy 能够对一个已有的类添加属性和方法,或者删除已经存在的方法实现。新添加的方法,若是签名和原有方法一致,则原有方法会消失。
相似于redefine,可是原有的方法不会消失,而是被重命名,添加后缀 $original,这样,就没有实现会被丢失。重定义的方法能够继续经过它们重命名过的名称调用原来的方法,例如类:
class Foo {
String bar() { return "bar"; }
}复制代码
rebase 以后:
class Foo {
String bar() { return "foo" + bar$original(); }
private String bar$original() { return "bar"; }
}复制代码
ByteBuddy
提供了不少用于匹配方法的 DSL,以下例子:
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();复制代码
ByteBuddy
经过 net.bytebuddy.matcher.ElementMatcher
来定义配置策略,能够经过此接口实现本身定义的匹配策略。库自己提供的 Matcher 很是多。
使用MethodDelegation能够将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致
class Source {
public String hello(String name) { return null; }
}
class Target {
public static String hello(String name) {
return "Hello " + name + "!";
}
}
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 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遵循一个最接近原则:
同时须要注意的是被拦截的方法须要声明为 public,不然无法进行拦截加强。除此以外,还可使用 @RuntimeType
注解来标注方法
@RuntimeType
public static Object intercept(@RuntimeType Object value) {
System.out.println("Invoked method with: " + value);
return value;
}复制代码
能够在拦截器(Target)的拦截方法 intercept
中使用注解注入参数,ByteBuddy
会根据注解给咱们注入对于的参数值。好比:
void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)复制代码
经常使用的注解以下表:
注解 | 描述 |
---|---|
@Argument |
绑定单个参数 |
@AllArguments |
绑定全部参数的数组 |
@This |
当前被拦截的、动态生成的那个对象 |
@DefaultCall |
调用默认方法而非super的方法 |
@SuperCall |
用于调用父类版本的方法 |
@RuntimeType |
能够用在返回值、参数上,提示ByteBuddy禁用严格的类型检查 |
@Super |
当前被拦截的、动态生成的那个对象的父类对象 |
@FieldValue |
注入被拦截对象的一个字段的值 |
public class UserType {
public String doSomething() { return null; }
}
public interface Interceptor {
String doSomethingElse();
}
public interface InterceptionAccessor {
Interceptor getInterceptor();
void setInterceptor(Interceptor interceptor);
}
public interface InstanceCreator {
Object makeInstance();
}
public class HelloWorldInterceptor implements Interceptor {
@Override
public String doSomethingElse() {
return "Hello World!";
}
}
Class<? extends UserType> dynamicUserType = new ByteBuddy()
.subclass(UserType.class)
.method(not(isDeclaredBy(Object.class))) // 非父类 Object 声明的方法
.intercept(MethodDelegation.toField("interceptor")) // 拦截委托给属性字段 interceptor
.defineField("interceptor", Interceptor.class, Visibility.PRIVATE) // 定义一个属性字段
.implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty()) // 实现 InterceptionAccessor 接口
.make()
.load(getClass().getClassLoader())
.getLoaded();
InstanceCreator factory = new ByteBuddy()
.subclass(InstanceCreator.class)
.method(not(isDeclaredBy(Object.class))) // 非父类 Object 声明的方法
.intercept(MethodDelegation.toConstructor(dynamicUserType)) // 委托拦截的方法来调用提供的类型的构造函数
.make()
.load(dynamicUserType.getClassLoader())
.getLoaded().newInstance();
UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
String s = userType.doSomething();
System.out.println(s); // Hello World!复制代码
上述例子将 UserType
类实现了 InterceptionAccessor
接口,同时使用 MethodDelegation.toField
可使拦截的方法能够委托给新增的字段。
本文是本身学习 ByteBuddy
后本身稍加整理的基础教程。最后感谢你阅读!!!
微信公众号关注:ByteZ,获取更多学习资料