字节码不止有 Java Proxy、 Cglib 和 Javassist 还有 ByteBuddy
提到字节码增强技术,相信用过 Spring
的小伙伴都会知道 Java Proxy
和 Cglib
。
毕竟面试准备的八股文中说过,Spring
的动态代理有两种实现方式,在有接口存在的时候使用 Java Proxy
,当没有接口的时候使用的是 Cglib
。
这两种方式的区别不在本文的讨论范围之内,今天想给大家介绍了是另一个字节码增强技术 Byte Buddy
。
Byte Buddy
根据 Byte Buddy
官网所说,Byte Buddy
是一个代码生成和操作库,用于在 Java
应用程序运行时创建和修改 Java
类,而无需编译器的帮助。
Byte Buddy
提供一套简单易用的 API
,可以很方便的使用 Java
流式编程的形式来动态创建类或者创建接口的实现类,这一点跟 Java Proxy
和 Cglib
不一样。
使用 Byte Buddy
的方式也非常简单,只要直接引入 Maven
依赖即可,没有其他繁琐的依赖。总的来说,使用 Byte Buddy
有下面的优势:
-
无需理解字节码格式,简单易用的 API
能很容易操作字节码; -
支持 Java
任何版本,库轻量,仅取决于Java
字节代码解析器库ASM
的访问者API
,它本身不需要任何其他依赖项。 -
比起 JDK
动态代理、cglib
、Javassist
,Byte Buddy
在性能上具有优势。
这一份测试报告是官网提供的,表中的每一行分别为,类的创建、接口实现、方法调用、类型扩展、父类方法调用的性能结果。
从性能报告中可以看出,Byte Buddy
在一些场景是有优势的,但是在有些场景也不见得特别有优势,不过整体来看还是不错的。
测试
说了那么多,下面给大家演示一下,如果使用 Byte Buddy
,首先我们需要引入 Maven
依赖,我这里用的版本是 1.14.6,也可以使用其他版本。
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.6</version>
</dependency>
创建一个类,并覆盖 toString
public static void test1() {
try {
Class<?> dynamicType = new ByteBuddy().
subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(ByteBuddyDemo.class.getClassLoader())
.getLoaded();
System.out.println(dynamicType.newInstance().toString());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
public static void test2() {
try {
DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make();
DynamicType.Loaded<Object> load = unloaded.load(ByteBuddyDemo.class.getClassLoader());
System.out.println(load.getLoaded().newInstance().toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
整个代码的思路是通过 Byte Buddy
,构造出一个 Class
对象,然后调用 Class
对象的 newInstance()
方法,再执行 toString()
方法。上面两个方式的功能是一样的,写出来更方便大家理解。
其中各个方法的含义如下:
subClass
:表示构造的类是 Object
的子类;
method
:表示要构造的具体方法,类似于过滤的功能;
intercept
:表示对过滤后的方法进行拦截;
FixedValue.value("Hello World!")
:表示构造返回一个”Hello World!“ 字符串;
make
:创建 DynamicType.Unloaded
对象,此时这个对象被构造出来,但是还没有被 JVM
加载,还不能使用;
load
,getLoaded
:加载当前类的构造器,并进行加载;
等到加载到 JVM
过后,就可以使用 newInstance().toString()
进行调用了。
代理方法
上面的例子是创建一个简单的类和方法,下面我们介绍一个代理方法的使用,这里我们有一个目标类 Target 和一个方法 saySomething()
方法,有一个代理类 Agent
,里面有一个代理方法 agentSaySomething()
,如下所示:
public class Target {
public String saySomething() {
return "Hello target";
}
}
public class Agent {
public static String agentSaySomething() {
System.out.println("agentSaySomething");
return "hello agent";
}
}
public static void test4() {
try {
DynamicType.Unloaded<Target> agent = new ByteBuddy()
.subclass(Target.class)
.method(named("saySomething")
.and(isDeclaredBy(Target.class)
.and(returns(String.class))))
.intercept(MethodDelegation.to(Agent.class))
.make();
// 将 agent 字节码写入文件中
outputClazz(agent.getBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static void outputClazz(byte[] bytes) {
FileOutputStream out = null;
try {
String pathName = ByteBuddyDemo.class.getResource("/").getPath() + "AgentTarget.class";
out = new FileOutputStream(new File(pathName));
System.out.println("类输出路径:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
test4();
}
运行过后我们可以看到生成了一个 class
文件,通过查看代码如下,可以看到是创建了一个 Target
的子类,并且调用了 Agent
的 agentSaySomething
方法。
总结
Byte Buddy
的 API
很丰富,这里只是很简单的给大家使用了几个 API
,还有包括方法,字段的设定等等,感兴趣的小伙伴可以继续去学习学习。
发表评论