字节码Byte-buddy 使用委托实现抽象类方法并注入自定义 注解信息

Posted 九师兄

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了字节码Byte-buddy 使用委托实现抽象类方法并注入自定义 注解信息相关的知识,希望对你有一定的参考价值。

1.概述

上一篇文章:【字节码】Byte-buddy 监控方法执行耗时动态获取出入参类型 和值

转载:https://github.com/fuzhengwei/itstack-demo-bytecode

截至到本章节关于字节码框架 Byte-buddy 的大部分常用 API 的使用已经通过案例介绍比较全面了,接下来介绍关于如何去实现一个抽象类以及创建出相应注解(包括类的注解和方法的注解)的知识点。而注解的这部分内容在一些监控或者拦截处理的场景下还是比较常用的,所以在这章节我们会通过一个例子来创建出含有自定义注解的类和方法。

如果你已经阅读了之前的系列文章,这部分学习的内容并不会有太多的陌生,主要是关于 委托(MethodDelegation) 方法的使用以及补充自定义注解。

那么,接下来我们就使用委托和注解方式来创建这样的案例进行学习。

2. 案例目标

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

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) 
	// ...
	

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

3.技术实现

在技术实现的过程中会逐步的去实现我们需要的功能,将需要的用到知识点信息拆开讲解,以达到最终的案例目标。

3.1. 创建自定义注解

模拟网关类注解

package com.bytebuddy.service.demo3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

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



模拟网关方法注解

package com.bytebuddy.service.demo3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

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


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

3.2 创建委托函数

package com.bytebuddy.service.demo3;

import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;

import java.lang.reflect.Method;

public class UserRepositoryInterceptor 

    public static String intercept(@Origin Method method, @AllArguments Object[] arguments) 
        return "小傅哥博客,查询文章数据:https://bugstack.cn/?id=" + arguments[0];
    



最终我们的字节码操作会通过委托的方式来实现抽象类的功能。

在委托函数中的用到注解已经在上一章节中完整的介绍了,可以回顾参考。

@Origin 可以绑定到以下类型的参数:Method 被调用的原始方法 Constructor 被调用的原始构造器 Class 当前动态创建的类 MethodHandle MethodType String 动态类的toString()的返回值int 动态方法的修饰符.

@AllArguments 绑定所有参数的数组

3.3 创建方法主体信息

/**
     * 测试点:测试注解的方法
     *
     * 运行结果如下
     *
     * RpcGatewayClazz.clazzDesc:查询数据信息
     * RpcGatewayClazz.alias:dataApi
     * RpcGatewayClazz.timeOut:350
     * RpcGatewayMethod.methodName:queryData
     * RpcGatewayMethod.methodDesc:查询数据
     * 小傅哥博客,查询文章数据:https://bugstack.cn/?id=10001
     *
     * @throws Exception
     */
    @Test
    public void test_byteBuddy() throws Exception 

        TypeDescription.Generic build = TypeDescription.Generic
                .Builder.parameterizedType(Repository.class, String.class)
                .build();

        // 生成含有注解的泛型实现字类
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(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();

        String path = UserRepositoryInterceptorTest.class.getResource("/").getPath();
        System.out.println("保存位置:"+path);
        // 输出类信息到目标文件夹下
        dynamicType.saveIn(new File(path));

        // 从目标文件夹下加载类信息
        Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("com.bytebuddy.service.demo3.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));
    

3.3.1 分析

  TypeDescription.Generic build = TypeDescription.Generic
                .Builder.parameterizedType(Repository.class, String.class)
                .build();

        // 生成含有注解的泛型实现字类
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(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();

这部分基本是 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 设定值(可以多次使用)。

2.3.2 将创建的类写入目录

这部分内容是 Byte-buddy 提供的 API 方法; saveIn ,把字节码信息写成 class 到执行的文件夹下。这样就可以非常方便的验证通过字节码框架创建的方法内容。

  String path = UserRepositoryInterceptorTest.class.getResource("/").getPath();
        System.out.println("保存位置:"+path);
        // 输出类信息到目标文件夹下
        dynamicType.saveIn(new File(path));

字节码方法内容


package com.bytebuddy.service.demo3;

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

    public UserRepository() 
    

    static 
        cachedValue$oVUSNXHx$uh8nsd3 = Repository.class.getMethod("queryData", Integer.TYPE);
    


从上可以看出来我们的自定义类已经实现了抽象类,同时也添加了类和方法的注解信息。而在实现的类中有一步是使用委托函数进行处理方法的内容。

2.3.3 输出自定义注解信息

 		// 从目标文件夹下加载类信息
        Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("com.bytebuddy.service.demo3.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());

在这里我们使用的是 Class.forName ,进行加载类信息。也可以像以前的章节一样使用;unloadedType.load(XXX.class.getClassLoader()) 的方式进行直接处理字节码。

最后是读取自定义注解的信息内容,包括类和方法。

2.3.4 测试验证运行

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

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

通过 Class.forName 的方式就可以直接调用方法,如果加载字节码的方式就需要通过反射进行处理(以往章节有案例可以对照学习)。

以上是关于字节码Byte-buddy 使用委托实现抽象类方法并注入自定义 注解信息的主要内容,如果未能解决你的问题,请参考以下文章

cglib的动态代理

10-java安全基础——javassist字节码编程

10-java安全基础——javassist字节码编程

10-java安全基础——javassist字节码编程

代理模式(Proxy)

动态代理Dynamic Proxy