likes
comments
collection
share

字节码增强技术-ByteBuddy

作者站长头像
站长
· 阅读数 23

ByteBuddy 简介

前面我们了解了字节码增强技术的ASMJavassist,今天我们看下另一个高效类库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与其他字节码增强工具的对比:

baselineByte BuddycglibJavassistJava proxy
trivial class creation0.003 (0.001)142.772 (1.390)515.174 (26.753)193.733 (4.430)70.712 (0.645)
interface implementation0.004 (0.001)1'126.364 (10.328)960.527 (11.788)1'070.766 (59.865)1'060.766 (12.231)
stub method invocation0.002 (0.001)0.002 (0.001)0.003 (0.001)0.011 (0.001)0.008 (0.001)
class extension0.004 (0.001)885.983 5'408.329** (7.901) (52.437)1'632.730 (52.737)683.478 (6.735)-
super method invocation0.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

<dependency> 
    <groupId>net.bytebuddy</groupId> 
    <artifactId>byte-buddy</artifactId> 
    <version>1.14.6</version> 
</dependency>

生成一个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

从上面的代码中,我们可以看到 ByteBuddy 要实现一个方法分为两步。首先,编程人员需要指定一个ElementMatcher,它负责识别一个或多个需要实现的方法。ByteBuddy 提供了功能丰富的预定义拦截器(interceptor),它们暴露在ElementMatchers类中。在上述的例子中,toString方法完全精确匹配了名称,但是,我们也可以匹配更为复杂的代码结构,如类型或注解。

当创建子类的时候,ByteBuddy 始终会拦截(intercept)一个匹配的方法,在生成的类中重写该方法。但是,我们在本文稍后将会看到 ByteBuddy 还能够重新定义已有的类,而不必通过子类的方式来实现。在这种情况下,ByteBuddy 会将已有的代码替换为生成的代码,而将原有的代码复制到另外一个合成的(synthetic)方法中。

通过委托实现 Instrumentation

要实现某个方法,有一种更为灵活的方式,那就是使用 ByteBuddyMethodDelegation。通过使用方法委托,在生成重写的实现时,我们就有可能调用给定类和实例的其他方法。按照这种方式,我们可以使用如下的委托器(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 方法用于在动态代理类的方法执行前后插入自定义逻辑。我们通过在 LoggingInterceptorintercept 方法中调用 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<String> superCall, @AllArguments Object[] args) throws Exception {  
        System.out.println("before");  
        String result = superCall.call(); // 调用原始方法  
        System.out.println("after");  
        return "Modified: " + result;  
    }  
}

运行方法输出结果:

字节码增强技术-ByteBuddy

在这个示例中,createLoggingProxy 方法使用 ByteBuddy 动态生成了一个 MyService 类的代理类,该代理类在调用 doSomething 方法前后插入了日志记录逻辑。这个示例展示了 ByteBuddy 如何用于创建动态代理,实现了AOP中的日志记录功能。

总结

ByteBuddy 在实际应用中具有广泛的用途。例如,在性能监控方面,我们可以使用 ByteBuddy 在方法调用前后记录执行时间,帮助定位性能瓶颈。在安全增强方面,我们可以使用 ByteBuddy 在敏感方法中添加安全检查,以防止未授权访问。另外,ByteBuddy 也可以用于模块化的框架中,动态生成特定功能的代码,以及在某些框架中实现懒加载和延迟初始化等。

与其他常见的字节码操作库(如ASM、CGLib等)相比,ByteBuddy 具有更加友好的API和更高的性能。ByteBuddy 的API设计更符合面向对象的编程风格,使开发者更容易上手。另外,ByteBuddy 使用 ASM 作为底层的字节码引擎,这使得生成的代码在性能方面有很大的优势。