User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   正文
Java

Java—Byte Buddy字节码编程

2022-05-04 23:47:23
2048  0 7
参考目录 隐藏
1) HelloWorld
2) 字节码创建类和方法
3) 入门
4) 创建新类型
5) 修改已有类型
6) API 简介
7) 创建类
8) 修改类
9) 操控未加载类
10) 拦截方法
11) 添加字段
12) 总体框架
13) 案例
14) 监控方法执行耗时动态获取出入参类型和值
15) 其他注解汇总
16) 常用核心API
17) 使用委托实现抽象类方法并注入自定义注解信息

阅读完需:约 25 分钟

之前编写的字节码编程; ASM、Javassist 系列,Byte Buddy 玩法上更加高级,你可以完全不需要了解一个类和方法块是如何通过 指令码LDC、LOAD、STORE、IRETURN… 生成出来的。就像它的官网介绍;

https://bytebuddy.net/#/

https://notes.diguage.com/byte-buddy-tutorial/#_官网及版本库

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

  • 无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。
  • 已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
  • 比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。

HelloWorld

每一个程序员,都运行过 N 多个HelloWorld,就像很熟悉的 Java;

public class Hi {

    public static void main(String[] args) {
        System.out.println("Byte-buddy Hi HelloWorld");
    }

}

官方的例子

String helloWorld = new ByteBuddy()
            .subclass(Object.class)
            .method(named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make()
            .load(getClass().getClassLoader())
            .getLoaded()
            .newInstance()
            .toString();    

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

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

接下来的这一段主要是用于加载生成后的 Class 和执行,以及调用方法 toString()。也就是最终我们输出了想要的结果。那么,如果你不能看到这样一段方法块,把我们的代码改造后的样子,心里还是有点虚。那么,我们通过字节码输出到文件,看下具体被改造后的样子,如下;

编译后的Class文件,ByteBuddyHelloWorld.class

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

    public HelloWorld() {
    }
}

在官网来看,这是一个非常简单并且能体现 Byte buddy 的例子。但是与我们平时想创建出来的 main 方法相比,还是有些差异。那么接下来,我们尝试使用字节码编程技术创建出这样一个方法。

字节码创建类和方法

    @Test
    public void test_make() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        // 创建类
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.enmalvi.coroutines.Bytebuddy.bytebuddy01.HelloWorld")                                         // 类名信息
                .defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC) // 定义方法
                .withParameter(String[].class, "args")                                          // 设置参数
                .intercept(MethodDelegation.to(Hi.class))
                .defineField("str", String.class, Modifier.PUBLIC)
                .make();

        // 加载类
        Class<?> clazz = dynamicType.load(GenerateClazzMethod.class.getClassLoader())
                .getLoaded();

        // 输出类字节码
        outputClazz(dynamicType.getBytes());

        // 反射调用
        clazz.getMethod("main", String[].class).invoke(clazz.newInstance(), (Object) new String[1]);
    }

    public static class Hi {
        public static void main(String[] args) {
            System.out.println("helloWorld");
        }
    }
  • defineMethod("main", void.class, Modifier.PUBLIC + Modifier.STATIC),定义方法;名称、返回类型、属性public static
  • withParameter(String[].class, "args"),定义参数;参数类型、参数名称
  • intercept(MethodDelegation.to(Hi.class)),使用了一段委托函数,真正去执行输出的是另外的函数方法。
  • MethodDelegation,需要是 public 类
  • 被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上

这里有一个知识点,Modifier.PUBLIC + Modifier.STATIC,这是一个是二进制相加,每一个类型都在二进制中占有一位。例如 1 2 4 8 ... 对应的二进制占位 1111。所以可以执行相加运算,并又能保留原有单元的属性。

结果

类输出路径:/Users/xujiahui/Me/IDEA/enmalvi/case-study/target/test-classes/ByteBuddyHelloWorld.class
helloWorld

编译输出的类

public class HelloWorld {
    public String str;

    public static void main(String[] args) {
        Hi.main(var0);
    }

    public HelloWorld() {
    }
}

这里需要掌握几个关键信息;创建方法、定义属性、拦截委托、输出字节码,以及最终的运行。这样的一个简单过程,可以很快的了解到如何使用 Byte buddy。

入门

前面体验过了现在可以开始入门了!!

依赖

        <!-- Byte-buddy -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.10.9</version>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.10.9</version>
        </dependency>

创建新类型

Class<?> dynamicType = new ByteBuddy()
  // 指定父类
  .subclass(Object.class)
   // 根据名称来匹配需要拦截的方法
  .method(ElementMatchers.named("toString"))
  // 拦截方法调用,返回固定值
  .intercept(FixedValue.value("Hello World!"))
  // 产生字节码
  .make()
  // 加载类
  .load(getClass().getClassLoader())
  // 获得Class对象
  .getLoaded();
 
assertThat(dynamicType.newInstance().toString(), is("Hello World!"));

ByteBuddy利用Implementation接口来表示一个动态定义的方法,FixedValue.value就是该接口的实例。

完全实现Implementation比较繁琐,因此实际情况下会使用MethodDelegation代替。使用MethodDelegation,你可以在一个POJO中实现方法拦截器:

public class GreetingInterceptor {
  // 方法签名随意
  public Object greet(Object argument) {
    return "Hello from " + argument;
  }
}
 
Class<? extends java.util.function.Function> dynamicType = new ByteBuddy()
  // 实现一个Function子类
  .subclass(java.util.function.Function.class)
  .method(ElementMatchers.named("apply"))
  // 拦截Function.apply调用,委托给GreetingInterceptor处理
  .intercept(MethodDelegation.to(new GreetingInterceptor()))
  .make()
  .load(getClass().getClassLoader())
  .getLoaded();
 
assertThat((String) dynamicType.newInstance().apply("Byte Buddy"), is("Hello from Byte Buddy"));

编写拦截器时,你可以指定一些注解,ByteBuddy会自动注入:

public class GeneralInterceptor {
  // 提示ByteBuddy根据被拦截方法的实际类型,对此拦截器的返回值进行Cast
  @RuntimeType
  //                      所有入参的数组
  public Object intercept(@AllArguments Object[] allArguments,
  //                      被拦截的原始方法
                          @Origin Method method) {
  }
}

修改已有类型

上面的两个例子中,我们利用ByteBuddy创建了指定接口的新子类型,ByteBuddy也可以用来修改已存在的。

ByteBuddy提供了便捷的创建Java Agent的API,本节的例子就是通过Java Agent方式来修改已存在的Java类型:public class TimerAgent {

public static void premain(String arguments, 
                             Instrumentation instrumentation) {
    new AgentBuilder.Default()
      // 匹配被拦截方法
      .type(ElementMatchers.nameEndsWith("Timed"))
      .transform(
          (builder, type, classLoader, module) -> 
              builder.method(ElementMatchers.any()) .intercept(MethodDelegation.to(TimingInterceptor.class))
      ).installOn(instrumentation);
  }
}
 
public class TimingInterceptor {
  @RuntimeType
  public static Object intercept(@Origin Method method, 
                                 // 调用该注解后的Runnable/Callable,会导致调用被代理的非抽象父方法
                                 @SuperCall Callable<?> callable) {
    long start = System.currentTimeMillis();
    try {
      return callable.call();
    } finally {
      System.out.println(method + " took " + (System.currentTimeMillis() - start));
    }
  }
}

API 简介

创建类

subclass

调用此方法可以创建一个目标类的子类:

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

如果不指定子类名称,Byte Buddy会有一套自动的策略来生成。你还可以指定子类命名策略:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    public String subclass(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

加载类

上节创建的DynamicType.Unloaded,代表一个尚未加载的类,你可以通过ClassLoadingStrategy来加载这种类。 

如果不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中:

  1. WRAPPER:创建一个新的Wrapping类加载器
  2. CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
  3. INJECTION:利用反射机制注入动态类型

示例:

Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

修改类

redefine

重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。新添加的方法,如果签名和原有方法一致,则原有方法会消失。

rebase

类似于redefine,但是原有的方法不会消失,而是被重命名,添加后缀 $original,例如类:

class Foo {
  String bar() { return "bar"; }
}

在rebase之后,会变成:

class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

重新加载类

得益于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具有限制:

  1. 类再重新载入前后,必须具有相同的Schema,也就是方法、字段不能减少(可以增加)
  2. 不支持具有静态初始化块的类

操控未加载类

Byte Buddy提供了类似于Javassist的、操控未加载类的API。它在TypePool中维护类型的元数据TypeDescription:

// 获取默认类型池
TypePool typePool = TypePool.Default.ofClassPath();
new ByteBuddy()
  .redefine(typePool.describe("foo.Bar").resolve(), // 根据名称进行解析类
            // ClassFileLocator用于定位到被修改类的.class文件
            ClassFileLocator.ForClassLoader.ofClassPath())
  .defineField("qux", String.class) // 定义一个新的字段
  .make()
  .load(ClassLoader.getSystemClassLoader());
assertThat(Bar.class.getDeclaredField("qux"), notNullValue());

拦截方法

匹配方法

Byte Buddy提供了很多用于匹配方法的DSL:

class Foo {
  public String bar() { return null; }
  public String foo() { return null; }
  public String foo(Object o) { return null; }
}
 
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();

委托方法

使用MethodDelegation可以将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致:

class Source {
  public String hello(String name) { return null; }
}
 
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 hello(String name) {
    return "Hello " + name + "!";
  }
} 

也可以如下:  

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遵循一个最接近原则:

  1. intercept(int)因为参数类型不匹配,直接Pass
  2. 另外两个方法参数都匹配,但是 intercept(String)类型更加接近,因此会委托给它

参数绑定

你可以在Target的方法中使用注解进行参数绑定:

void foo(Object o1, Object o2)
// 等价于
void foo(@Argument(0) Object o1, @Argument(1) Object o2)

添加字段

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
  .defineField("interceptor", Interceptor.class, Visibility.PRIVATE);

方法调用也可以委托给字段(而非外部对象):

Class<? extends UserType> dynamicUserType = new ByteBuddy()
  .subclass(UserType.class)
    .method(not(isDeclaredBy(Object.class)))
    .intercept(MethodDelegation.toField("interceptor")); 

总体框架

  1. wave-framework : 多种代理框架或者修改字节码框架的对比
  2. classLoader: bytebudd使用classLoader的加载策略,wrap ,child-first, injection
  3. class-wave: 构造新类的方式,redefine,rebase, subclass后面会介绍
  4. matcher: 运行时判断一个类是不是目标类,这个就是matcher的作用。bytebuddy提供一堆内置的matcher,比如匹配方法异常的MethodExceptionTypeMatcher。
  5. implementation: 实现,目的是描述字节码如何转化。比如我想为类新加一个方法,使用methodCall,定义一个实现。

bytebuddy的一个范式

  1. 我想为Object.class 构造一个子类,那好就使用subclass
  2. 子类叫什么呢,使用name(example.Type)
  3. 子类想要覆盖Object.class的toString方法,我需要做什么呢
    1. a. 定义匹配的规则—matcher,named(“toString”)就是生成一个matcher
    2. b. 修改成什么呢—implementation, FixedValue.value生成一个 FixedValue.value
    3. c. intercept 拼接
  4. 这个定义好的类,该被什么classloader加载呢,这里传入的是当前类的加载器。
  5. 后续就是生成示例测试了。


案例

监控方法执行耗时动态获取出入参类型和值

实现一款非入侵的全链路最终监控系统,那么这里就会包括一些基本的核心功能点;方法执行耗时、出入参获取、异常捕获、添加链路ID等等。而这些一个个的功能点,最快的掌握方式就是去实现他最基本的功能验证,这个阶段基本也是技术选型的阶段,验证各项技术点是否可以满足你后续开发的需求。否则在后续开发中,如果已经走了很远的时候再发现不适合,那么到时候就很麻烦了。

在前面的ASM、Javassist 章节中也有陆续实现过获取方法的出入参信息,但实现的方式还是偏向于字节码控制,尤其ASM,更是需要使用到字节码指令将入参信息压栈操作保存到局部变量用于输出,在这个过程中需要深入了解Java虚拟机规范,否则很不好完成这一项的开发。但!ASM也是性能最牛的。其他的字节码编程框架都是基于它所开发的。

案例目标

public class BizMethod {

    public String queryUserInfo(String uid, String token) throws InterruptedException {
        Thread.sleep(new Random().nextInt(500));
        return "xjh测试,bytebuddy02,bytebuddy02,bytebuddy02,bytebuddy02,bytebuddy02";
    }

}

我们定义一个类并创建出等待被监控的方法,当方法执行时监控方法的各项信息;执行耗时、出入参信息等。

这里模拟监控并没有使用 Javaagent 去做字节码加载时的增强,主要为了将最核心的内容体现出来。

监控方法耗时

public class MonitorDemo {

    /**
     * @Origin,用于拦截原有方法,这样就可以获取到方法中的相关信息。
     * @RuntimeType:定义运行时的目标方法。
     * @SuperCall:用于调用父类版本的方法。
     * 在这里我们会用到新的注解;@AllArguments 、@Argument(0),一个用于获取全部参数,一个获取指定的参数。
     *
     * 下面有一个 callable.call(); 这个方法是调用原方法的内容,返回结果。而前后包装的。
     * 最后在finally中,打印方法的执行耗时。System.currentTimeMillis() - start
     *
     * @param method
     * @param args
     * @param arg0
     * @param callable
     * @return
     * @throws Exception
     */
    @RuntimeType
    public static Object intercept(@Origin Method method, @AllArguments Object[] args, @Argument(0) Object arg0, @SuperCall Callable<?> callable) throws Exception {
        long start = System.currentTimeMillis();
        Object resObj = null;
        try {
            resObj = callable.call();
            return resObj;
        } finally {
            System.out.println("方法名称:" + method.getName());
            System.out.println("入参个数:" + method.getParameterCount());
            System.out.println("入参类型:" + method.getParameterTypes()[0].getTypeName() + "、" + method.getParameterTypes()[1].getTypeName());
            System.out.println("入参内容:" + arg0 + "、" + args[1]);
            System.out.println("出参类型:" + method.getReturnType().getName());
            System.out.println("出参结果:" + resObj);
            System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
        }
    }

}
  • 这里面包括几个核心的知识点;@RuntimeType:定义运行时的目标方法。@SuperCall:用于调用父类版本的方法。
  • 定义好方法后,下面有一个 callable.call(); 这个方法是调用原方法的内容,返回结果。而前后包装的。
  • 最后在finally中,打印方法的执行耗时。System.currentTimeMillis() - start
  • @Origin,用于拦截原有方法,这样就可以获取到方法中的相关信息。
  • 这一部分的信息相对来说比较全,尤其也获取到了参数的个数和类型,这样就可以在后续的处理参数时进行循环输出。
  • 在一段方法执行的过程中,如果可以在必要的时候拿到当时入参的信息,那么就可以非常方便的进行排查异常快速定位问题。在这里我们会用到新的注解;@AllArguments 、@Argument(0),一个用于获取全部参数,一个获取指定的参数。

测试方法

public class ApiTest {

    @Test
    public void test_byteBuddy() throws Exception {

        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(BizMethod.class)
                .method(ElementMatchers.named("queryUserInfo"))
                .intercept(MethodDelegation.to(MonitorDemo.class))
                .make();

        // 加载类
        Class<?> clazz = dynamicType.load(ApiTest.class.getClassLoader())
                .getLoaded();

        // 反射调用
        clazz.getMethod("queryUserInfo", String.class, String.class).invoke(clazz.newInstance(), "10001", "Adhl9dkl");

    }

}

结果

方法名称:queryUserInfo
入参个数:2
入参类型:java.lang.String、java.lang.String
入参内容:10001、Adhl9dkl
出参类型:java.lang.String
出参结果:xjh测试,bytebuddy02,bytebuddy02,bytebuddy02,bytebuddy02,bytebuddy02
方法耗时:227ms

其他注解汇总

除了以上为了获取方法的执行信息使用到的注解外,Byte Buddy 还提供了很多其他的注解。

注解 说明
@Argument 绑定单个参数
@AllArguments 绑定所有参数的数组
@This 当前被拦截的、动态生成的那个对象
@Super 当前被拦截的、动态生成的那个对象的父类对象
@Origin 可以绑定到以下类型的参数:
Method 被调用的原始方法
Constructor 被调用的原始构造器
Class 当前动态创建的类
MethodHandle MethodType
String 动态类的toString()的返回值
int 动态方法的修饰符
@DefaultCall 调用默认方法而非super的方法
@SuperCall 用于调用父类版本的方法
@Super 注入父类型对象,可以是接口,从而调用它的任何方法
@RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
@Empty 注入参数的类型的默认值
@StubValue 注入一个存根值。对于返回引用、void的方法,注入null;对于返回原始类型的方法,注入0
@FieldValue 注入被拦截对象的一个字段的值
@Morph 类似于@SuperCall,但是允许指定调用参数

常用核心API

  1. ByteBuddy
    • 流式API方式的入口类
    • 提供Subclassing/Redefining/Rebasing方式改写字节码
    • 所有的操作依赖DynamicType.Builder进行,创建不可变的对象
  2. ElementMatchers(ElementMatcher)
    • 提供一系列的元素匹配的工具类(named/any/nameEndsWith等等)
    • ElementMatcher(提供对类型、方法、字段、注解进行matches的方式,类似于Predicate)
    • Junction对多个ElementMatcher进行了and/or操作
  3. DynamicType(动态类型,所有字节码操作的开始,非常值得关注)
    • Unloaded(动态创建的字节码还未加载进入到虚拟机,需要类加载器进行加载)
    • Loaded(已加载到jvm中后,解析出Class表示)
    • Default(DynamicType的默认实现,完成相关实际操作)
  4. Implementation(用于提供动态方法的实现)
    • FixedValue(方法调用返回固定值)
    • MethodDelegation(方法调用委托,支持两种方式: Class的static方法调用、object的instance method方法调用)
  5. Builder(用于创建DynamicType,相关接口以及实现后续待详解)
    • MethodDefinition
    • FieldDefinition
    • AbstractBase

当我们学会了监控的核心功能,在后续与Javaagent结合使用时就可以很容易扩展进去,而不是看到了陌生的代码。


使用委托实现抽象类方法并注入自定义注解信息

如何去实现一个抽象类以及创建出相应注解(包括类的注解和方法的注解)的知识点。而注解的这部分内容在一些监控或者拦截处理的场景下还是比较常用的

案例目标

在这里我们定义了一个抽象并且含有泛型的接口类,如下

public abstract class Repository<T> {

    public abstract T queryData(int id);

}

那么接下来的案例会使用到委托的方式进行实现抽象类方法并加入自定义注解,也就相当于我们使用代码进行编程实现的效果。

@RpcGatewayClazz( clazzDesc = "查询数据信息", alias = "dataApi", timeOut = 350L )
public class UserRepository extends Repository<String> {      

    @RpcGatewayMethod( methodName = "queryData", methodDesc = "查询数据" )
    public String queryData(int var1) {
        // ...
    }

}

这里就是最终效果,我们模拟是一种网关接口的实现和定义注解暴漏接口信息(如果你是在互联网中做开发,类似这样的需求还是蛮多的,接口统一走网关服务)。

模拟网关类注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RpcGatewayClazz {

    String clazzDesc() default "";
    String alias() default "";
    long timeOut() default 350;

}

模拟网关方法注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RpcGatewayMethod {

    String methodName() default "";
    String methodDesc() default "";
    
}

这部分你可以创建任何类型的注解,主要是用于模拟类和方法上分别添加注解并获取最终属性值的效果。

创建委托函数

public class UserRepositoryInterceptor {

    public static String intercept(@Origin Method method, @AllArguments Object[] arguments) {
        return "查询数据:https://127.0.0.1:1234/?id=" + arguments[0];
    }

}
  • 最终我们的字节码操作会通过委托的方式来实现抽象类的功能。
  • 在委托函数中的用到注解已经在上一章节中完整的介绍了,可以回顾参考。
  • @Origin 可以绑定到以下类型的参数:Method 被调用的原始方法 Constructor 被调用的原始构造器 Class 当前动态创建的类 MethodHandle MethodType String 动态类的toString()的返回值 int 动态方法的修饰符.
  • @AllArguments 绑定所有参数的数组。

测试方法

public class ApiTest {

    @Test
    public void test_byteBuddy() throws Exception {

        // 生成含有注解的泛型实现字类
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build()) // 创建复杂类型的泛型注解
                .name(Repository.class.getPackage().getName().concat(".").concat("UserRepository"))                  // 添加类信息包括地址
                .method(ElementMatchers.named("queryData"))                                                          // 匹配处理的方法
                .intercept(MethodDelegation.to(UserRepositoryInterceptor.class))                                     // 交给委托函数
                .annotateMethod(AnnotationDescription.Builder.ofType(RpcGatewayMethod.class).define("methodName", "queryData").define("methodDesc", "查询数据").build())
                .annotateType(AnnotationDescription.Builder.ofType(RpcGatewayClazz.class).define("alias", "dataApi").define("clazzDesc", "查询数据信息").define("timeOut", 350L).build())
                .make();

        // 输出类信息到目标文件夹下
        dynamicType.saveIn(new File(ApiTest.class.getResource("/").getPath()));

        // 从目标文件夹下加载类信息
        Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("com.enmalvi.ByteBuddy.bytebuddy03.UserRepository");

        // 获取类注解
        RpcGatewayClazz rpcGatewayClazz = repositoryClass.getAnnotation(RpcGatewayClazz.class);
        System.out.println("RpcGatewayClazz.clazzDesc:" + rpcGatewayClazz.clazzDesc());
        System.out.println("RpcGatewayClazz.alias:" + rpcGatewayClazz.alias());
        System.out.println("RpcGatewayClazz.timeOut:" + rpcGatewayClazz.timeOut());

        // 获取方法注解
        RpcGatewayMethod rpcGatewayMethod = repositoryClass.getMethod("queryData", int.class).getAnnotation(RpcGatewayMethod.class);
        System.out.println("RpcGatewayMethod.methodName:" + rpcGatewayMethod.methodName());
        System.out.println("RpcGatewayMethod.methodDesc:" + rpcGatewayMethod.methodDesc());

        // 实例化对象
        Repository<String> repository = repositoryClass.newInstance();

        // 测试输出
        System.out.println(repository.queryData(10001));
    }

}
  • 这部分基本是Byte-buddy的模板方法,通过核心API;subclass、name、method、intercept、annotateMethod、annotateType 的使用构建方法。
  • 首先是定义复杂类型的自定义注解,设定为本方法的父类,这部分内容也就是抽象类。Repository<T>,通过 TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build() 来构建。
  • 设定类名称在我们之前就已经使用过,这里多加类的路径信息。concat 函数是字符串的连接符,替换 + 号。
  • method,设定匹配处理方法名称。
  • MethodDelegation.to(UserRepositoryInterceptor.class),最终的核心是关于委托函数的使用。这里的使用也就可以调用到我们上面定义的委托函数,等最终我们通过字节码生成的 class 类进行查看。
  • annotateMethod、annotateType,定义类和方法的注解,通过 define 设定值(可以多次使用)。

生成的类

@RpcGatewayClazz(
    timeOut = 350L,
    alias = "dataApi",
    clazzDesc = "查询数据信息"
)
public class UserRepository extends Repository<String> {
    @RpcGatewayMethod(
        methodName = "queryData",
        methodDesc = "查询数据"
    )
    public String queryData(int id) {
        return UserRepositoryInterceptor.intercept(cachedValue$uVVNM5UP$s2736l0, new Object[]{var1});
    }

    public UserRepository() {
    }

    static {
        cachedValue$uVVNM5UP$s2736l0 = Repository.class.getMethod("queryData", Integer.TYPE);
    }
}
  • 从上可以看出来我们的自定义类已经实现了抽象类,同时也添加了类和方法的注解信息。
  • 而在实现的类中有一步是使用委托函数进行处理方法的内容。

结果

通过我们使用字节码创建的方法已经可以按照我们的需求进行内容输出。

RpcGatewayClazz.clazzDesc:查询数据信息
RpcGatewayClazz.alias:dataApi
RpcGatewayClazz.timeOut:350
RpcGatewayMethod.methodName:queryData
RpcGatewayMethod.methodDesc:查询数据
查询数据:https://127.0.0.1:1234/?id=10001

需要注意几个知识点的使用,包括;委托方法使用、复杂类型的泛型创建、类和方法自定义注解的添加以及写入字节码信息到文件中。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

7 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593480浏览

随机文章
PostgreSQL—逻辑复制
11个月前
Caffeine—缓存学习
2年前
MyBatis笔记14—一对多查询
5年前
Redis笔记—String
5年前
SpringCloud—Zuul(一)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593480 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付