Byte Buddy 委托编程实战:MethodDelegation 实现抽象类方法并注入自定义注解
文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载本文是 CodeGuide 仓库「字节码编程」系列中 Byte Buddy 篇的第三讲围绕如何用MethodDelegation委托机制为一个含泛型的抽象类动态生成实现子类并通过annotateMethod/annotateType向类和方法注入自定义注解模拟 RPC 网关接口暴露场景。读完本文你将掌握 Byte Buddy 的泛型参数化父类构建、委托函数绑定、类/方法注解注入、字节码落盘与运行时读取注解的完整链路并能在实际监控、网关、RPC 中间件开发中复用这套方案。一、前言从改方法到造类注注解在 Byte Buddy 篇一、篇二中我们已经分别体验过如何用字节码创建HelloWorld类篇一基于 Byte Buddy 语法创建的第一个 HelloWorld以及如何用SuperCall、Origin、AllArguments等注解做方法执行监控篇二监控方法执行耗时动态获取出入参类型和值。到本章为止Byte Buddy 大部分常用 API 已经通过案例覆盖接下来要解决一个新问题如何动态实现一个抽象类并给生成的类和方法注入自定义注解。注解注入这部分内容在监控、拦截处理、网关接口暴露等场景下非常常用。例如在 RPC 网关中接口实现类往往需要打上RpcGatewayClazz、RpcGatewayMethod之类的注解用来暴露接口的别名、超时时间、方法描述等元信息。如果这类类和方法是运行时通过字节码动态生成的那么注解也必须随之动态注入。如果你已经读过本系列前两篇这部分内容并不会陌生核心新增点只有两个委托MethodDelegation方法的使用与自定义注解的补充。接下来我们就用一个完整案例把委托 注解串起来学习。二、开发环境依赖版本JDK1.8.0byte-buddy1.10.9byte-buddy-agent1.10.9说明byte-buddy 依赖 ASM 的访问者 API本身无需其他额外依赖byte-buddy-agent用于配合 JavaAgent 场景做运行时 attach本篇案例不强制使用仅作为同版本配套依赖引入。本文所有代码均来自 CodeGuide 仓库docs/md/bytecode/byte-buddy/目录下的系列文章对应工程模块为itstack-demo-bytecode-2-03。三、案例目标模拟网关接口的注解暴露我们首先定义一个含泛型的抽象类RepositoryTpublic abstract class RepositoryT { public abstract T queryData(int id); }接下来案例会使用委托方式实现抽象类方法并加入自定义注解最终效果等价于我们用普通 Java 代码写出的这样一个类RpcGatewayClazz( clazzDesc 查询数据信息, alias dataApi, timeOut 350L ) public class UserRepository extends RepositoryString { RpcGatewayMethod( methodName queryData, methodDesc 查询数据 ) public String queryData(int var1) { // ... } }这里模拟的是一种网关接口的实现和定义通过注解暴露接口信息。如果你在互联网公司做开发类似需求其实蛮多的——接口统一走网关服务由注解承载接口的元数据。本案例的目标就是通过 Byte Buddy 在运行时凭空生成这样一个类。四、技术实现技术实现过程会拆解为若干步骤将用到的知识点逐一展开最终拼装出完整的案例目标。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 ; }两个注解的定义要点Retention(RetentionPolicy.RUNTIME)注解保留到运行时这样生成的类在 JVM 中加载后才能通过反射getAnnotation读取到属性值Target分别限定注解的作用位置ElementType.TYPE类/接口上与ElementType.METHOD方法上每个注解成员都带default默认值实际注入时可通过define覆盖。这部分你可以创建任何类型的注解主要是用于模拟类和方法上分别添加注解并最终获取属性值的效果。2. 创建委托函数public class UserRepositoryInterceptor { public static String intercept(Origin Method method, AllArguments Object[] arguments) { return 小傅哥博客查询文章数据https://bugstack.cn/?id arguments[0]; } }最终我们的字节码操作会通过委托的方式来实现抽象类的功能即把抽象方法queryData(int)的实现转交给这个静态方法执行委托函数中用到的注解在篇二已经完整介绍过这里做两个关键回顾Origin可以绑定到以下类型的参数——Method被调用的原始方法、Constructor被调用的原始构造器、Class当前动态创建的类、MethodHandle、MethodType、String动态类toString()的返回值、int动态方法的修饰符AllArguments绑定所有参数的数组本案例中通过arguments[0]取到queryData(int)的入参id。3. 创建方法主体信息这是整个案例最核心的一段 Byte Buddy 链式 API// 生成含有注解的泛型实现子类 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();逐步拆解这段模板式代码它依次使用了subclass、name、method、intercept、annotateMethod、annotateType六个核心 APIsubclass(...)构建复杂类型的泛型父类设定本方法的父类也就是上面定义的抽象类RepositoryT。这里的关键点是复杂类型的泛型创建——直接传Repository.class只能得到原始类型拿不到泛型参数T的具体化信息因此需要使用TypeDescription.Generic.Builder.parameterizedType(Repository.class, String.class).build()来构建RepositoryString这样的参数化类型。这决定了生成的子类继承的是RepositoryString而非裸的Repository。name(...)设定类名与包路径Repository.class.getPackage().getName().concat(.).concat(UserRepository)拼出的结果是org.itstack.demo.bytebuddy.UserRepository。其中concat是字符串连接符在这里替代号使用多加的这一段类路径信息是为了让生成的类落在指定包名下便于后续Class.forName加载。method(ElementMatchers.named(queryData))匹配要处理的方法通过方法名精确匹配抽象类中的queryData方法。ElementMatchers是 Byte Buddy 提供的元素匹配工具类named/any/nameEndsWith等等类似于 Predicate 的作用可以对类型、方法、字段、注解进行matches判断多个匹配条件还可以通过Junction做and/or组合参见篇二的常用核心 API 总结。intercept(MethodDelegation.to(UserRepositoryInterceptor.class))委托函数绑定这是本案例的核心。MethodDelegation属于 Byte Buddy 的Implementation实现另一个是FixedValue用于返回固定值它支持两种委托方式Class 的 static 方法调用与object 的 instance 方法调用这里使用的是前一种——把queryData的实现委托给UserRepositoryInterceptor.intercept静态方法。配合Origin、AllArguments等注解Byte Buddy 会在生成的方法体内自动编排字节码把原始方法信息与入参数组喂给委托方法。annotateMethod(...)注入方法注解AnnotationDescription.Builder.ofType(RpcGatewayMethod.class) .define(methodName, queryData) .define(methodDesc, 查询数据) .build()define可以为注解成员设置值且可以多次调用连续定义多个属性。annotateType(...)注入类注解AnnotationDescription.Builder.ofType(RpcGatewayClazz.class) .define(alias, dataApi) .define(clazzDesc, 查询数据信息) .define(timeOut, 350L) .build()注意timeOut是long类型定义值时要带上L后缀与注解成员类型保持一致。4. 将创建的类写入目录生成完DynamicType.Unloaded后可以调用saveIn把字节码直接写成.class文件落到磁盘方便我们直接看到框架生成了什么样的类// 输出类信息到目标文件夹下 dynamicType.saveIn(new File(ApiTest.class.getResource(/).getPath()));这部分是 Byte Buddy 提供的 API 方法saveIn把字节码信息写成class文件到指定文件夹下可以非常方便地验证通过字节码框架创建的方法内容。字节码方法内容反编译后的等价 Java 代码package org.itstack.demo.bytebuddy; RpcGatewayClazz( clazzDesc 查询数据信息, alias dataApi, timeOut 350L ) public class UserRepository extends RepositoryString { RpcGatewayMethod( methodName queryData, methodDesc 查询数据 ) public String queryData(int var1) { return UserRepositoryInterceptor.intercept(cachedValue$aGmAjHXh$iha1qv0, new Object[]{var1}); } public UserRepository() { } static { cachedValue$aGmAjHXh$iha1qv0 Repository.class.getMethod(queryData, Integer.TYPE); } }从反编译结果可以清楚看到三件事生成的UserRepository已经正确继承参数化类型RepositoryString说明TypeDescription.Generic.Builder.parameterizedType的泛型构建生效了类上带上了RpcGatewayClazz、方法上带上了RpcGatewayMethod注解注入成功方法体由委托调用构成UserRepositoryInterceptor.intercept(...)其中第一参数cachedValue$aGmAjHXh$iha1qv0是 Byte Buddy 在static{}块中通过Repository.class.getMethod(queryData, Integer.TYPE)缓存下来的Method对象对应Origin的绑定第二参数new Object[]{var1}则是AllArguments绑定的入参数组。5. 输出自定义注解信息类已经生成并落盘接下来通过反射读取类和方法上的注解// 从目标文件夹下加载类信息 ClassRepositoryString repositoryClass (ClassRepositoryString) 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())的方式直接处理字节码二者择一即可最后通过标准的getAnnotation读取自定义注解的信息内容包括类注解与方法注解。6. 测试验证运行加载并实例化生成的类直接调用queryData验证委托是否生效// 实例化对象 RepositoryString repository repositoryClass.newInstance(); // 测试输出 System.out.println(repository.queryData(10001));通过Class.forName的方式可以直接调用方法如果走load(...).getLoaded()的字节码加载方式则需要通过反射来处理篇一、篇二有对照案例可以学习。测试结果RpcGatewayClazz.clazzDesc查询数据信息 RpcGatewayClazz.aliasdataApi RpcGatewayClazz.timeOut350 RpcGatewayMethod.methodNamequeryData RpcGatewayMethod.methodDesc查询数据 小傅哥博客查询文章数据https://bugstack.cn/?id10001 Process finished with exit code 0可以看到类注解的三个属性、方法注解的两个属性全部正确读取最后一次输出?id10001则来自委托函数UserRepositoryInterceptor.intercept说明入参10001通过AllArguments成功传递到了委托方法中方法返回结果也正常返回给了调用方。五、进阶结合仓库源码看委托与注解的底层脉络1. 委托函数的三种绑定形态从同名同参到注解绑定在篇一中委托函数的使用有一个约束被委托的方法与需要与原方法有着一样的入参、出参、方法名否则不能映射上。例如HelloWorld.main委托给Hi.main两者的签名完全一致。而到了本篇委托方法intercept(Origin Method method, AllArguments Object[] arguments)与抽象方法queryData(int id)的签名并不相同为什么也能映射成功答案就在于Origin、AllArguments这类绑定注解它们告诉 Byte Buddy 如何从原始方法上下文中提取对应的值并注入委托参数从而绕开了签名必须一致的限制。这也正是篇二委托编程的核心思想——通过注解自由绑定Method、入参、出参、Callable等运行时信息。2. 委托注解体系速查结合篇二整理除了本篇用到的Origin、AllArgumentsByte Buddy 委托函数还可使用以下注解注解说明Argument绑定单个参数AllArguments绑定所有参数的数组This当前被拦截的、动态生成的那个对象Super当前被拦截的、动态生成的那个对象的父类对象Origin可以绑定到以下类型的参数Method被调用的原始方法、Constructor被调用的原始构造器、Class当前动态创建的类、MethodHandle、MethodType、String动态类的 toString() 的返回值、int动态方法的修饰符DefaultCall调用默认方法而非 super 的方法SuperCall用于调用父类版本的方法Super注入父类型对象可以是接口从而调用它的任何方法RuntimeType可以用在返回值、参数上提示 ByteBuddy 禁用严格的类型检查Empty注入参数的类型的默认值StubValue注入一个存根值。对于返回引用、void 的方法注入 null对于返回原始类型的方法注入 0FieldValue注入被拦截对象的一个字段的值Morph类似于 SuperCall但是允许指定调用参数理解这张表是后续把委托机制用于全链路监控、非入侵插桩的基础。3. 同一目标的两种实现路径Byte Buddy vs Javassist有趣的是仓库中《Javassist 篇五使用 Bytecode 指令码生成含有自定义注解的类和方法》实现了完全相同的案例目标——同样定义RpcGatewayClazz/RpcGatewayMethod注解、同样生成含注解的类和方法、同样读取注解属性输出。但两条技术路线的体验差异巨大Javassist 方式需要手动操作AnnotationsAttribute、Annotation、addMemberValue构建注解用Bytecode指令addGetstatic、addInvokevirtual、addReturn逐条编写方法体还必须记住StringMemberValue、LongMemberValue等不同类型值的前缀差异Byte Buddy 方式一句annotateMethod(...)/annotateType(...)完成注解注入一句MethodDelegation.to(...)完成方法实现全部由高级 API 接管底层字节码编排。对比可见Byte Buddy 对使用者更友好几乎不需要理解LDC、LOAD、STORE、IRETURN之类的指令码而 Javassist 的低级 API 在需要精确控制方法指令如定位TryCatch起始位置、替换某条指令时依然有其价值。选择哪个框架取决于你的场景是需要快速声明式生成还是指令级精细操控。4. 典型应用场景网关暴露与非入侵监控本案例的注解模型类注解 方法注解 运行时反射读取在真实中间件开发中有直接映射RPC/API 网关接口实现类通过clazzDesc接口描述、alias接口别名、timeOut超时时间、methodName方法名、methodDesc方法描述暴露接口元数据网关侧统一扫描并注册路由这正是本案例模拟网关接口注解的现实来源非入侵监控/链路追踪在《调研字节码插桩技术用于系统监控设计和实现》一文中作者将 AOP、ASM、Javassist、Byte Buddy 四种方案放在同一监控场景下对比——为UserController#queryUserInfo采集方法耗时、出入参、异常信息。其中 Byte Buddy 方案的委托函数形态与本篇高度一致用Origin取方法、AllArguments取入参、SuperCall调原方法再包装耗时统计逻辑动态代理与代码生成凡是需要在运行时补齐接口/抽象类实现、同时附加元数据注解的框架层开发如 ORM、RPC、配置中心客户端都可以复用本案例的subclass MethodDelegation annotateType/annotateMethod组合拳。关于整套字节码学习体系仓库中的《字节码编程 | ASM、Javassist、Byte-Buddy》小册提供了完整的脉络梳理JavaAgent与字节码框架结合可以在应用加载时对方法做插桩增强用于构建全链路监控系统三个框架中ASM更偏底层另外两个是对ASM的封装提供更高级的 API。六、总结本章节需要掌握四个知识点委托方法使用MethodDelegation、复杂类型的泛型创建TypeDescription.Generic.Builder.parameterizedType、类和方法自定义注解的添加annotateType/annotateMethod、以及写入字节码信息到文件中saveIn委托函数与抽象方法的签名可以不一致靠Origin、AllArguments等绑定注解建立映射关系生成的类中委托调用所需的方法引用会被 Byte Buddy 缓存到static{}块中注解注入必须配合Retention(RUNTIME)才能在运行时通过反射读取define方法可连续为注解的多个成员赋值与本仓库 Javassist 篇的同类案例对比Byte Buddy 以声明式高级 API显著降低了字节码操作门槛适合网关接口暴露、非入侵监控、动态代理等框架层开发场景。每一段知识只有经过系统化的学习才能形成完整收获只言片语的碎片化体验总不能让一个技术得到全方位的了解。建议将本系列与ASM 系列、Javassist 系列对照阅读再配合 JavaAgent 串成完整的字节码插桩知识闭环。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐ZenML 自定义 Image Builder 开发指南从抽象基类到自定义 Flavor 注册ZenML 自定义 Image Builder 开发指南从抽象基类到自定义 Flavor 注册 本文基于 ZenML 官方文档 Develop a CustoMLOps机器学习后端工作流自动化AI Agent在 ZenML 中开发自定义 Orchestrator从基类抽象到 flavor 注册与源码级实现指南在 ZenML 中开发自定义 Orchestrator从基类抽象到 flavor 注册与源码级实现指南 ZenML 通过 BaseOrchestrator 抽MLOps机器学习后端工作流自动化AI Agent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考