Skip to content

2020 05 14 字节码编程,Byte buddy篇三《使用委托实现抽象类方法并注入自定义注解信息》

fuzhengwei edited this page May 16, 2020 · 1 revision

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!

一、前言

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

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

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

二、开发环境

  1. JDK 1.8.0
  2. byte-buddy 1.10.9
  3. byte-buddy-agent 1.10.9
  4. 本章涉及源码在:itstack-demo-bytecode-2-03,可以关注公众号bugstack虫洞栈,回复源码下载获取。你会获得一个下载链接列表,打开后里面的第17个「因为我有好多开源代码」,记得给个Star

三、案例目标

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

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

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

四、技术实现

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

1. 创建自定义注解

模拟网关类注解

@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 "";
    
}
  • 这部分你可以创建任何类型的注解,主要是用于模拟类和方法上分别添加注解并获取最终属性值的效果。

2. 创建委托函数

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. 创建方法主体信息

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

4. 将创建的类写入目录

// 输出类信息到目标文件夹下
dynamicType.saveIn(new File(ApiTest.class.getResource("/").getPath()));
  • 这部分内容是 Byte-buddy 提供的 API 方法;saveIn,把字节码信息写成 class 到执行的文件夹下。这样就可以非常方便的验证通过字节码框架创建的方法内容。

字节码方法内容

package org.itstack.demo.bytebuddy;

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

    public UserRepository() {
    }

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

5. 输出自定义注解信息

// 从目标文件夹下加载类信息
Class<Repository<String>> repositoryClass = (Class<Repository<String>>) Class.forName("org.itstack.demo.bytebuddy.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()) 的方式进行直接处理字节码。
  • 最后是读取自定义注解的信息内容,包括类和方法。

6. 测试验证运行

// 实例化对象
Repository<String> repository = repositoryClass.newInstance();
// 测试输出
System.out.println(repository.queryData(10001));
  • 通过 Class.forName 的方式就可以直接调用方法,如果加载字节码的方式就需要通过反射进行处理(以往章节有案例可以对照学习)。

测试结果

RpcGatewayClazz.clazzDesc:查询数据信息
RpcGatewayClazz.aliasdataApi
RpcGatewayClazz.timeOut350
RpcGatewayMethod.methodNamequeryData
RpcGatewayMethod.methodDesc:查询数据
小傅哥博客,查询文章数据:https://bugstack.cn/?id=10001

Process finished with exit code 0
  • 不出意外你会看到以上的结果信息,通过我们使用字节码创建的方法已经可以按照我们的需求进行内容输出。

五、总结

  • 在本章节的学习中需要注意几个知识点的使用,包括;委托方法使用复杂类型的泛型创建类和方法自定义注解的添加以及写入字节码信息到文件中
  • 截至到目前基本我们已经对常用的字节码框架自我学习和分享的基本完成了,另外一些其他的API的使用可以参考官方文档;https://bytebuddy.net
  • 每一段知识都是只有进行系统化的学习才能有完整的收获,只言片语带来的碎片化体验总是不能对一个技术进行全方面的了解。在技术的这条路上,多加油!

📝 首页

🌏 知识星球码农会锁

实战项目:「DDD+RPC分布式抽奖系统」、专属小册、问题解答、简历指导、架构图稿、视频课程

🐲 头条

⛳ 目录

  1. 源码 - :octocat: 公众号:bugstack虫洞栈 文章所涉及到的全部开源代码
  2. Java
  3. Spring
  4. 面向对象
  5. 中间件
  6. Netty 4.x
  7. 字节码编程
  8. 💯实战项目
  9. 部署 Dev-Ops
  10. 📚PDF 下载
  11. 关于

💋 精选

🐾 友链

建立本开源项目的初衷是基于个人学习与工作中对 Java 相关技术栈的总结记录,在这里也希望能帮助一些在学习 Java 过程中遇到问题的小伙伴,如果您需要转载本仓库的一些文章到自己的博客,请按照以下格式注明出处,谢谢合作。

作者:小傅哥
链接:https://bugstack.cn
来源:bugstack虫洞栈

2021年10月24日,小傅哥 的文章全部开源到代码库 CodeGuide 中,与同好同行,一起进步,共同维护。

这里我提供 3 种方式:

  1. 提出 Issue :在 Issue 中指出你觉得需要改进/完善的地方(能够独立解决的话,可以在提出 Issue 后再提交 PR )。
  2. 处理 Issue : 帮忙处理一些待处理的 Issue
  3. 提交 PR: 对于错别字/笔误这类问题可以直接提交PR,无需提交Issue 确认。

详细参考:CodeGuide 贡献指南 - 非常感谢你的支持,这里会留下你的足迹

  • 加群交流 本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!由于微信群人满 100 之后无法加入,请扫描下方二维码先添加作者 “小傅哥” 微信(fustack),备注:加群。
微信:fustack

  • 公众号(bugstack虫洞栈) - 沉淀、分享、成长,专注于原创专题案例,以最易学习编程的方式分享知识,让自己和他人都能有所收获。
公众号:bugstack虫洞栈

感谢以下人员对本仓库做出的贡献或者对小傅哥的赞赏,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR,请与我联系。

Clone this wiki locally