ByteBuddy 简介
字节码增强技术-ASM
字节码增强技术-Javassist
前面我们了解了字节码增强技术的ASM
与Javassist
,今天我们看下另一个高效类库ByteBuddy
。
ByteBuddy
是一个开源的 Java 字节码操作库,由 Rafael Winterhalter
创建并维护,它提供了一个简洁且强大的 API,使开发人员能够在不修改源代码的情况下,实现对类的定制和增强。无论是创建动态代理、生成新的类、修改方法体,还是在方法调用前后插入自定义逻辑,ByteBuddy
都能胜任。
Rafael Winterhalter是一位软件咨询师,在挪威的奥斯陆工作。他是静态类型的支持者,对 JVM 有极大的热情,尤其关注于代码 instrumentation、并发和函数式编程。Rafael 日常会撰写关于软件开发的博客,经常出席相关的会议,并被认定为 JavaOne Rock Star。在工作以外的编码过程中,他为多个开源项目做出过贡献,经常会花精力在 Byte Buddy 上,这是一个为 Java 虚拟机简化运行时代码生成的库。因为他的贡献,Rafael 得到过 Duke’s Choice 奖项。
2015年10月,ByteBuddy
被 Oracle 授予了 Duke’s Choice大奖。该奖项对ByteBuddy
的“ Java技术方面的巨大创新 ”表示赞赏。
ByteBuddy
仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
下面是ByteBuddy
与其他字节码增强工具的对比:
baseline | Byte Buddy | cglib | Javassist | Java proxy | |
---|---|---|---|---|---|
trivial class creation | 0.003 (0.001) | 142.772 (1.390) | 515.174 (26.753) | 193.733 (4.430) | 70.712 (0.645) |
interface implementation | 0.004 (0.001) | 1'126.364 (10.328) | 960.527 (11.788) | 1'070.766 (59.865) | 1'060.766 (12.231) |
stub method invocation | 0.002 (0.001) | 0.002 (0.001) | 0.003 (0.001) | 0.011 (0.001) | 0.008 (0.001) |
class extension | 0.004 (0.001) | 885.983 5'408.329** (7.901) (52.437) | 1'632.730 (52.737) | 683.478 (6.735) | - |
super method invocation | 0.004 (0.001) | 0.004 0.004 (0.001) (0.001) | 0.021 (0.001) | 0.025 (0.001) | - |
核心功能与特点
动态代理与AOP: ByteBuddy
为创建动态代理类提供了丰富的功能,我们能够灵活地为接口或已有类生成代理实现。这对于实现面向切面编程(AOP)和其他代理模式至关重要。使用 ByteBuddy,我们可以在方法调用前后注入自定义的逻辑,从而实现日志记录、性能监控、事务管理等功能。
代码生成: ByteBuddy
允许在运行时生成新的类、接口或枚举,以满足动态创建类的需求。您可以通过 ByteBuddy 在代码中编写类的结构,添加字段、方法、构造函数等。这为编写模板化的代码或动态生成特定类提供了便捷的方式。
字节码修改: 通过直接操作类的字节码,您可以在方法内部添加新的指令、修改现有指令,从而实现方法的增强或调整。这种能力使得您可以在运行时对现有类进行修改,无需修改源代码。例如,您可以通过字节码修改来实现方法级别的安全增强、性能优化或错误检测等功能。
自定义类加载器: ByteBuddy
不仅可以生成和修改类的字节码,还可以用于创建自定义类加载器。通过自定义类加载器,您可以实现类加载环境的隔离,加载来自不同源的类,从而增强应用程序的灵活性和安全性。
注解处理: ByteBuddy
可以作为注解处理的强大工具,用于生成或修改类的字节码。您可以根据注解中的元数据信息,在编译时或运行时生成相应的字节码,从而实现注解驱动的开发模式。
性能优化: 与其他字节码操作库相比,ByteBuddy
在性能方面表现出色。它采用了一些高效的技术,如使用 ASM
作为字节码引擎,以提高生成的代码的执行速度。这使得 ByteBuddy
在需要高性能的场景中非常有用,如在性能敏感的代码中进行方法增强。
使用示例
首先引入jar
net.bytebuddy
byte-buddy
1.14.6
生成一个Java类
这个类是 Object 的子类,类名为Hello
,并且重写了 toString 方法,用来返回“Hello, ByteBuddy!”。与原始的 ASM 类似,“intercept”会告诉 Byte Buddy 为拦截到的指令提供方法实现:
public static void main(String[] args) throws Exception {
// 使用 ByteBuddy API 创建一个新类
new ByteBuddy()
.subclass(Object.class)
.name("Hello")
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello, ByteBuddy!"))
.make()
.saveIn(new File(ByteBuddyTest.class.getResource("/").getPath()));
}
在target/classes中生成了一个Hello.class文件,反编译如下:
从上面的代码中,我们可以看到 ByteBuddy
要实现一个方法分为两步。首先,编程人员需要指定一个ElementMatcher
,它负责识别一个或多个需要实现的方法。ByteBuddy
提供了功能丰富的预定义拦截器(interceptor),它们暴露在ElementMatchers
类中。在上述的例子中,toString
方法完全精确匹配了名称,但是,我们也可以匹配更为复杂的代码结构,如类型或注解。
当创建子类的时候,ByteBuddy
始终会拦截(intercept)
一个匹配的方法,在生成的类中重写该方法。但是,我们在本文稍后将会看到 ByteBuddy
还能够重新定义已有的类,而不必通过子类的方式来实现。在这种情况下,ByteBuddy
会将已有的代码替换为生成的代码,而将原有的代码复制到另外一个合成的(synthetic)方法中。
通过委托实现 Instrumentation
要实现某个方法,有一种更为灵活的方式,那就是使用 ByteBuddy
的 MethodDelegation
。通过使用方法委托,在生成重写的实现时,我们就有可能调用给定类和实例的其他方法。按照这种方式,我们可以使用如下的委托器(delegator)重新编写上述的样例:
public class ToStringInterceptor {
@RuntimeType
public static String intercept() {
return "Hello, ByteBuddy!";
}
}
借助上面的 POJO 拦截器,我们就可以将之前的 FixedValue 实现替换为 MethodDelegation.to(ToStringInterceptor.class):
new ByteBuddy()
.subclass(Object.class)
.name("Hello")
.method(ElementMatchers.named("toString"))
.intercept(MethodDelegation.to(ToStringInterceptor.class))
.make()
.saveIn(new File(ByteBuddyTest.class.getResource("/").getPath()));
使用上述的委托器,ByteBuddy
会在 toString
方法所给定的拦截目标中,确定最优的调用方法。就ToStringInterceptor.class
来讲,选择过程只是非常简单地解析这个类型的唯一静态方法而已。在本例中,只会考虑一个静态方法,因为委托的目标中指定的是一个 ToStringInterceptor
类。与之不同的是,我们还可以将其委托给某个类的实例,如果是这样的话,ByteBuddy
将会考虑所有的虚方法(virtual method)。如果类或实例上有多个这样的方法,那么 ByteBuddy
首先会排除掉所有与指定 instrumentation
不兼容的方法。在剩余的方法中,库将会选择最佳的匹配者,通常来讲这会是参数最多的方法。我们还可以显式地指定目标方法,这需要缩小合法方法的范围,将ElementMatcher
传递到MethodDelegation
中,就会进行方法的过滤。
此外当我们为拦截器方法设置参数时,就能释放出MethodDelegation
的全部威力。这里的参数通常是带有注解的,用来要求 ByteBuddy
在调用拦截器方法时,注入某个特定的值。例如,通过使用@Origin
注解,ByteBuddy
提供了添加 instrument
功能的方法的实例,将其作为 Java 反射 API 中类的实例:
public class ContextualToStringInterceptor {
@RuntimeType
public static String intercept(@Origin Method m) {
return "Hello World from " + m.getName();
}
}
当拦截toString
方法时,对 instrument
方法的调用将会返回"Hello world from toString"。
常用注解含义
除了@Origin
注解以外,ByteBuddy
提供了一组功能丰富的注解。例如,通过在类型为 Callable
的参数上使用@Super
注解,Byte Buddy 会创建并注入一个代理实例,它能够调用被 instrument
方法的原始代码。如果对于特定的用户场景,所提供的注解不能满足需求或者不太适合的话,我们甚至能够注册自定义的注解,让这些注解注入用户特定的值。
@RuntimeType 注解:
告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
@This 注解
注入被拦截的目标对象。
@AllArguments 注解
注入目标方法的全部参数,是不是感觉与 Java 反射的那套 API 有点类似了?
@Origin 注解
注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
@Super 注解
注入目标对象。通过该对象可以调用目标对象的所有方法。
@SuperCall
这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,
@SuperCall与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。
另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。
方法执行前后增加日志
下面我们使用ByteBuddy
来创建一个动态代理实现在方法调用前后添加日志记录功能,而不希望修改源代码。
假如我们有个接口MyService
,该方法只打印了"RealService.doSomething()"。
public class MyService {
public String doSomething() {
return "RealService.doSomething()";
}
}
下面我们使用bytebuddy
动态生成代理类
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
public class LoggingProxyExample {
public static void main(String[] args) {
MyService proxy = createLoggingProxy();
String result = proxy.doSomething();
System.out.println(result); // Output: Before: RealService.doSomething() After
}
public static MyService createLoggingProxy() {
return new ByteBuddy()
.subclass(MyService.class)
.method(ElementMatchers.named("doSomething"))
.intercept(MethodDelegation.to(LoggingInterceptor.class))
.make()
.load(MyService.class.getClassLoader())
.getLoaded()
.newInstance();
}
}
最后定义一个LoggingInterceptor
类,通过intercept
方法用于在动态代理类的方法执行前后插入自定义逻辑。我们通过在 LoggingInterceptor
的 intercept
方法中调用 superCall.call()
来调用原始方法,然后对原始方法的返回值进行修改。
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.util.concurrent.Callable;
public class LoggingInterceptor {
@RuntimeType
public static String intercept(@SuperCall Callable superCall, @AllArguments Object[] args) throws Exception {
System.out.println("before");
String result = superCall.call(); // 调用原始方法
System.out.println("after");
return "Modified: " + result;
}
}
运行方法输出结果:
在这个示例中,createLoggingProxy
方法使用 ByteBuddy
动态生成了一个 MyService
类的代理类,该代理类在调用 doSomething
方法前后插入了日志记录逻辑。这个示例展示了 ByteBuddy 如何用于创建动态代理,实现了AOP中的日志记录功能。
总结
ByteBuddy
在实际应用中具有广泛的用途。例如,在性能监控方面,我们可以使用 ByteBuddy
在方法调用前后记录执行时间,帮助定位性能瓶颈。在安全增强方面,我们可以使用 ByteBuddy
在敏感方法中添加安全检查,以防止未授权访问。另外,ByteBuddy
也可以用于模块化的框架中,动态生成特定功能的代码,以及在某些框架中实现懒加载和延迟初始化等。
与其他常见的字节码操作库(如ASM、CGLib等)相比,ByteBuddy
具有更加友好的API和更高的性能。ByteBuddy
的API设计更符合面向对象的编程风格,使开发者更容易上手。另外,ByteBuddy
使用 ASM 作为底层的字节码引擎,这使得生成的代码在性能方面有很大的优势。