首页 > 编程语言 >ByteBuddy字节码编程学习(场景、增强方式、类加载器策略、实践)

ByteBuddy字节码编程学习(场景、增强方式、类加载器策略、实践)

时间:2023-12-01 22:04:18浏览次数:31  
标签:username Java String 字节 ByteBuddy Byte 方法 加载

(目录)


ByteBuddy

介绍

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。

除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

官网:https://bytebuddy.net/#/tutorial-cn

ByteBuddy应用场景

运行时生成代码

Java 语言带有相对严格的类型系统。Java 要求所有变量和对象都属于特定类型,任何分配不兼容类型的尝试都会导致错误发生。 当非法转换类型时, 这些错误通常由 Java 编译器或至少由 Java运行时产生。

但是,通过强制执行其严格的类型系统,Java 强加了该语言在其他领域中范围的限制。

例如,在编写供其他 Java 应用程序使用的通用库时, 我们通常无法引用用户应用程序中定义的任何类型,因为在编译我们的库时,这些类型对我们来说是未知的

不过为了解决这个问题,java提供了一套反射的api来帮助使用者感知和修改类的内部。

不幸的是,使用反射 API 有两个明显的缺点

  1. 反射显而易见的缺点是慢。我们在使用反射之前都需要谨慎的考虑他对于当前性能的影响,唯有进过详细的评估,才能够放心的使用。
  2. 反射能够绕过类型安全检查。我们在使用反射的时候需要确保相应的接口不会暴露给外部用户,不然可能造成不小的安全隐患。

ByteBuddy可以帮助我们做到反射能做的事情,而不必受困于他的这些缺点


java编程语言代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:

  • Java Proxy

  • CGLIB

  • Javassist

  • Byte Buddy

推荐使用ByteBuddy,因为Byte Buddy代码生成可的性能最高,ByteBuddy 的主要侧重点在于生成更快速的代码,如下图:

image-20231114162048725


ByteBuddy语法

任何一个由 ByteBuddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,如下代码:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        // 生成 Object的子类
        .subclass(Object.class)
        // 生成类的名称为"com.xxx.Type"
        .name("com.xxx.Type")
        .make();

动态增强代码三种方式 (subclass、rebasing、redefinition)

ByteBuddy 动态增强代码总共有三种方式:

  1. subclass: 对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码

  2. rebasing: 对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用

  3. redefinition: 对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型。

类加载策略(WRAPPER、CHILD_FIRST、INJECTION )

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型,我们可以使用 ClassLoadingStrategy加载此类型。Byte Buddy 提供了几种类加载策略,这些加载策略定义在 ClassLoadingStrategy.Default 中,其中:

  • WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  • CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
  • INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。

官网HelloWorld例子 字节码生成分析

用于输出 HelloWorld

// 创建ByteBuddy对象
String str = new ByteBuddy()
        // subclass增强方式
        .subclass(Object.class)
        // 新类型的类名
        .name("HelloWorld") 
        // 拦截其中的toString()方法
        .method(ElementMatchers.named("toString"))
        // 让toString()方法返回固定值
        .intercept(FixedValue.value("Hello World!"))
        .make()
        // 加载新类型,默认WRAPPER策略
        .load(ByteBuddy.class.getClassLoader())
        .getLoaded()
        // 通过 Java反射创建 HelloWorld实例
        .newInstance()
        // 调用 toString()方法
        .toString(); 

System.out.println(helloWorld);  // Hello World!

他的运行结果就是一行,Hello World!

整个代码块核心功能就是通过 method(named("toString")),找到 toString 方法,再通过拦截 intercept,设定此方法的返回值。FixedValue.value("Hello World!")。到这里其实一个基本的方法就通过 Byte-buddy ,改造完成。

关注这里的 method() 方法:

method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named("toString")即为按照方法名匹配 toString() 方法。

如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:

// 指定方法名称
ElementMatchers.named("toString")
    // 指定方法的返回值
    .and(ElementMatchers.returns(String.class))
    // 指定方法参数
    .and(ElementMatchers.takesArguments(0));

接下来需要关注的是 intercept() 方法,通过 method()方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强。这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。

Byte Buddy 中可以设置多个 method()Intercept() 方法进行拦截和修改,Byte Buddy 会按照栈的顺序来进行拦截。


接下来的这一段主要是用于加载生成后的 Class 和执行,以及调用方法 toString()

也就是最终我们输出了想要的结果,通过字节码输出到文件,看下具体被改造后的样子,如下:

public class HelloWorld {
    public String toString() {
        return "Hello World!";
    }

    public HelloWorld() {
    }
}

在官网来看,这是一个非常简单并且能体现 Byte buddy 的例子。但是与我们平时想创建出来的 main 方法相比,还是有些差异。


实践 ByteCode

创建一个项目agent-demo,添加ByteBuddy依赖

<dependencies>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.9.2</version>
    </dependency>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy-agent</artifactId>
        <version>1.9.2</version>
    </dependency>
</dependencies>

创建测试类及方法

我们先创建一个普通类,再为该类创建代理类,创建代理对方法进行拦截做处理。

public class UserService {

    //方法1
    public String username(){
        System.out.println("username().....");
        return "张三";
    }

    //方法2
    public String address(String username){
        System.out.println("address(String username).....");
        return username+"来自 【xxxx】";
    }

    //方法3
    public String address(String username,String city){
        System.out.println("address(String username,String city).....");
        return username+"来自 【北京"+city+"】";
    }
}

编写拦截器

2)创建拦截器LogInterceptor,编写拦截器方法:


public class LogInterceptor {

    @RuntimeType //将返回值转换成具体的方法返回值类型,加了这个注解 intercept 方法才会被执行
    public  Object intercept(
            // 被拦截的目标对象 (动态生成的目标对象)
            @This  Object target,
            // 正在执行的方法Method 对象(目标对象父类的Method)
            @Origin Method method,
            // 正在执行的方法的全部参数
            @AllArguments Object[] argumengts,
            // 目标对象的一个代理
            @Super  Object delegate,
            // 方法的调用者对象 对原始方法的调用依靠它
            @SuperCall Callable<?> callable) throws Exception {
        //目标方法执行前执行日志记录
        System.out.println("准备执行Method="+method.getName());
        // 调用目标方法
        Object result = callable.call();
        //目标方法执行后执行日志记录
        System.out.println("方法执行完成Method="+method.getName());
        return result;
    }

}

在程序中我们 用到ByteBuddyMethodDelegation对象,它可以将拦截的目标方法委托给其他对象处理,这里有几个注解我们先进行说明:

  • @RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
  • @This:注入被拦截的目标对象(动态生成的目标对象)。
  • @Origin:注入正在执行的方法Method 对象(目标对象父类的Method)。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
  • @AllArguments:注入正在执行的方法的全部参数。
  • @Super:注入目标对象的一个代理
  • @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用 被代理/增强 的方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

测试

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<? extends UserService> aClass = new ByteBuddy()
                // 创建一个UserService 的子类
                .subclass(UserService.class)
                //指定类的名称
                .name("UserServiceImpl")
                // 指定要拦截的方法
                //.method(ElementMatchers.isDeclaredBy(UserService.class))
        .method(ElementMatchers.named("address").and(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(1))))
                // 为方法添加拦截器 如果拦截器方法是静态的 这里可以传 LogInterceptor.class
                .intercept(MethodDelegation.to(new LogInterceptor()))
                // 动态创建对象,但还未加载
                .make()
                // 设置类加载器 并指定加载策略(默认WRAPPER)
                .load(ByteBuddy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                // 开始加载得到 Class
                .getLoaded();
        UserService userService = aClass.newInstance();

        System.out.println(userService.username());
        System.out.println(userService.address("李四"));
        System.out.println(userService.address("李五","西城区"));
    }

运行测试结果:

准备执行Method=username
username().....
方法执行完成Method=username
张三

准备执行Method=address
address(String username).....
方法执行完成Method=address
李四来自 【xxxx】

address(String username,String city).....
李五来自 【北京西城区】

标签:username,Java,String,字节,ByteBuddy,Byte,方法,加载
From: https://blog.51cto.com/panyujie/8649112

相关文章

  • 汇编-Lea将内存地址加载到寄存器
     它的功能主要是将一个有效地址加载到寄存器中,让程序通过寄存器来访问相应的内存地址 lea指令的应用场景在汇编语言中,lea指令应用广泛,它通常用来完成以下几个任务:1.计算地址偏移量:当程序需要访问数组、结构体等数据结构中的某个元素时,我们可以使用lea指令来计算该元素相对......
  • axios(ajax)发送请求响应码200,但获取不到数据,无法加载响应数据: No datafound for res
    问题截图:没有响应数据控制台报错其实是由于浏览器的跨域资源共享(CORS)策略导致,前后端跨域请求是不行的。什么是域,看页面的url,比如https://www.baidu.com/下的网页都是属于baidu.com这个域。如果你是和我一样是从本地文件打开html的方式来调试ajax,那么一定会出现这个问题,因为本......
  • @Autowired作用在方法上 @autowired注解放在方法上,如果方法没有参数,spring容器会在类
    @Autowired作用在方法上@autowired注解放在方法上,如果方法没有参数,spring容器会在类加载完后执行一次这个方法;如果方法中有参数的话,还会从容器中自动注入这个方法的参数,然后执行一次这个方法。@autowired用于测试的实体类User,并将这个类注入到Spring容器中,为@autowired注解......
  • npm install 报pnpm无法加载文件 (解决方法 )
    例如:我现在要运行一个TS的项目,我的电脑上没有安装pnpm,导致我的vscode一直报错无法加载 Pnpm安装npminstall-gpnpmpnpm:无法加载文件pnpm:无法加载文件C:\Users\HP\AppData\Roaming\npm\pnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅https:/go.micro......
  • 成品直播源码推荐,实现文字加载效果 文字跳动
    成品直播源码推荐,实现文字加载效果文字跳动实现<viewv-elseclass="status-working"><textclass="letterletter1">充</text><textclass="letterletter2">电</text><textclass="letterletter3">中</text>......
  • SpringBoot的配置文件application.yml及加载顺序详解
    SpringBoot配置文件application.yml及加载顺序配置文件分类自定义配置文件配置文件总结Springboot中application.yml、application.properties和bootStrap.yml加载顺序SpringApplication位于项目根目录以jar包发布springboot项目时若application.yml和bootStra......
  • 在web-view加载的本地及远程HTML中调用uni的API及网页和vue页面通讯
    转载于在web-view加载的本地及远程HTML中调用uni的API及网页和vue页面通讯-DCloud问答uni-app的web-view组件,支持加载远程网页,在app环境下,还支持加载本地HTML页面。在web-view加载页面中,会涉及wx、plus、uni等对象的使用。在小程序下使用wx的api,需要引入微信提供的https......
  • C语言 主机字节序和网络字节序互换
    #include<stdio.h>#include<arpa/inet.h>intmain(){//10.11.12.13uint32_thost_ip=168496141;uint32_tnetwork_ip=htonl(168496141);//13.12.11.10printf("network_ipis%u\n",network_ip);//10.11.1......
  • 手机直播源码,js实现懒加载、vue实现图片懒加载指令
    手机直播源码,js实现懒加载、vue实现图片懒加载指令图片懒加载监听滚动条滚动事件,当视口的高度+滚动高度,大于图片所在位置举例顶部的偏移量时(也就是距离),加载图片资源 index.html<!DOCTYPEhtml><html> <head>  <metacharset="UTF-8">  <metaname="viewport"conte......
  • react native FlatList 下拉加载更多分页数据
    一.官方文档flatlist文档onendreached方法二、相关代码importReact,{useState,useEffect}from'react';import{View,FlatList,Text,ActivityIndicator}from'react-native';constMyFlatListWithLoadMore=()=>{const[data,setData]=u......