OpenRewrite教程:精准修改特定方法参数上的注解属性

OpenRewrite教程:精准修改特定方法参数上的注解属性

本教程详细介绍了如何利用openrewrite框架有条件地修改java方法参数上的注解属性,特别针对spring的`@requestparam`注解。文章将探讨声明式和命令式两种配方(recipe)的实现方式,并重点演示如何通过命令式java配方结合openrewrite的`cursor`机制,根据参数的特定条件(如是否存在其他注解、参数类型或名称)精准地添加或更新注解属性,从而解决在特定代码片段上应用配方时遇到的常见问题,实现更精细化的代码重构。

OpenRewrite是一个强大的代码重构工具,它允许开发者通过编写配方(Recipe)来自动化修改代码库。在许多场景下,我们需要对代码进行有条件的修改,例如只修改满足特定条件的方法参数上的注解。本文将深入探讨如何实现这种精准的代码重构。

OpenRewrite配方概述

OpenRewrite配方可以分为两种主要类型:声明式(Declarative)和命令式(Imperative)。

1. 声明式配方:快速入门与局限性

声明式配方通常以YAML格式定义,适用于表达相对简单的代码修改逻辑。例如,要为所有@RequestParam注解添加或更新required属性并设置为true,可以使用如下声明式配方:

type: specs.openrewrite.org/v1beta/recipename: org.example.MandatoryRequestParameterdisplayName: Make Spring `RequestParam` mandatorydescription: Add `required` attribute to `RequestParam` and set the value to `true`.recipeList:  - org.openrewrite.java.AddOrUpdateAnnotationAttribute:      annotationType: org.springframework.web.bind.annotation.RequestParam      attributeName: required      attributeValue: "true"

应用方式:将上述YAML文件保存为rewrite.yml在项目根目录,并通过Maven或Gradle插件激活。

Maven配置示例:在pom.xml中添加OpenRewrite Maven插件:

  org.openrewrite.maven  rewrite-maven-plugin  4.38.0             org.example.MandatoryRequestParameter      

局限性:声明式配方虽然简洁,但难以表达复杂的条件逻辑,例如“只修改同时带有@NotNull和@RequestParam注解的参数”。对于这类需求,我们需要借助命令式配方。

2. 命令式配方:实现精准条件控制

命令式配方使用Java编写,提供了更强大的灵活性和控制力,允许开发者通过遍历AST(抽象语法树)并结合Cursor机制来定位和修改代码。

核心挑战:定位与上下文

在OpenRewrite中,对代码进行修改通常涉及TreeVisitor。当我们需要在特定上下文(例如,一个注解的父节点是一个参数声明)中应用另一个子配方时,必须确保子配方在正确的AST节点和Cursor上下文中执行。直接在非注解节点上调用AddOrUpdateAnnotationAttribute的Visitor可能导致UncaughtVisitorException,因为它期望在其Cursor的父级找到一个匹配的注解。

构建增强型命令式配方

以下是一个实现特定条件修改的命令式配方示例。此配方旨在查找同时满足以下条件的@RequestParam注解:

该参数同时带有@NotNull注解。或者参数类型为java.lang.Number的子类型。或者参数名称为”fred”。

import org.openrewrite.ExecutionContext;import org.openrewrite.Recipe;import org.openrewrite.TreeVisitor;import org.openrewrite.java.AddOrUpdateAnnotationAttribute;import org.openrewrite.java.JavaIsoVisitor;import org.openrewrite.java.JavaVisitor;import org.openrewrite.java.UsesType;import org.openrewrite.java.tree.J;import org.openrewrite.java.tree.JavaType;import org.openrewrite.java.tree.TypeUtils;import org.jetbrains.annotations.NotNull;import java.util.List;public class MandatoryRequestParameter extends Recipe {    private static final String REQUEST_PARAM_FQ_NAME = "org.springframework.web.bind.annotation.RequestParam";    private static final String NOT_NULL_FQ_NAME = "javax.validation.constraints.NotNull"; // 引入NotNull注解的完全限定名    @Override    public @NotNull String getDisplayName() {        return "使Spring `RequestParam`注解强制必填";    }    @Override    public String getDescription() {        return "为满足特定条件的 `RequestParam` 注解添加 `required=true` 属性。";    }    @Override    protected TreeVisitor getSingleSourceApplicableTest() {        // 优化:只有当源文件包含 RequestParam 注解时,才运行此Visitor。        return new UsesType(REQUEST_PARAM_FQ_NAME);    }    @Override    protected @NotNull JavaVisitor getVisitor() {        // 创建一个用于添加或更新注解属性的内部Visitor实例        JavaIsoVisitor addAttributeVisitor = new AddOrUpdateAnnotationAttribute(                REQUEST_PARAM_FQ_NAME, "required", "true", false        ).getVisitor();        return new JavaIsoVisitor() {            @Override            public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {                J.Annotation a = super.visitAnnotation(annotation, ctx);                // 1. 检查当前访问的注解是否为 @RequestParam                if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {                    return a;                }                // 2. 使用 Cursor 向上导航到父节点,获取参数声明                // 当我们访问一个注解时,它的父节点通常是 J.VariableDeclarations (参数声明)                J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();                // 3. 定义条件:                //    a. 检查参数是否带有 @NotNull 注解                boolean hasNotNull = variableDeclaration.getLeadingAnnotations().stream()                        .anyMatch(ann -> TypeUtils.isOfClassType(ann.getType(), NOT_NULL_FQ_NAME));                //    b. 检查参数类型是否为 java.lang.Number 的子类型                JavaType paramType = variableDeclaration.getType();                boolean isNumberType = TypeUtils.isAssignableTo("java.lang.Number", paramType);                //    c. 检查参数名称是否为 "fred"                String paramName = variableDeclaration.getVariables().get(0).getSimpleName();                boolean isFredParam = paramName.equals("fred");                // 4. 应用条件逻辑:如果满足任何一个条件,则委托给 addAttributeVisitor 进行修改                if (hasNotNull || isNumberType || isFredParam) {                    // 将当前的注解 'a' 及其 Cursor 传递给 addAttributeVisitor 进行处理                    // 这是确保子Visitor在正确上下文执行的关键                    return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());                }                return a;            }        };    }}

代码解析:

Waymark Waymark

Waymark是一个视频制作工具,帮助企业快速轻松地制作高影响力的广告。

Waymark 79 查看详情 Waymark getSingleSourceApplicableTest(): 这是一个优化方法。它通过UsesType检查源文件是否包含@RequestParam注解。如果不存在,则此配方不会运行,从而提高效率。addAttributeVisitor: 我们首先创建AddOrUpdateAnnotationAttribute配方的一个Visitor实例。这个内部Visitor知道如何添加或更新@RequestParam的required属性。visitAnnotation(J.Annotation annotation, …): 这是核心逻辑所在。我们在这个方法中拦截所有注解的访问。类型检查: 确保当前注解是@RequestParam。getCursor().getParent().getValue(): 这是OpenRewrite中获取上下文的关键。当visitAnnotation被调用时,getCursor()指向当前注解。通过getParent(),我们可以获取到注解的父节点,通常对于方法参数上的注解来说,其父节点就是J.VariableDeclarations(变量声明,即参数本身)。条件判断:我们通过遍历variableDeclaration.getLeadingAnnotations()来检查参数是否带有@NotNull注解。TypeUtils.isAssignableTo()用于检查参数类型是否为java.lang.Number的子类型。variableDeclaration.getVariables().get(0).getSimpleName()获取参数的名称,并检查是否为”fred”。委托修改: 如果满足任意一个条件,我们就将当前的@RequestParam注解对象a及其当前的Cursor传递给addAttributeVisitor.visit(a, ctx, getCursor())。这确保了AddOrUpdateAnnotationAttribute配方在正确的AST节点和Cursor上下文中执行,避免了UncaughtVisitorException。

示例:源代码转换前后

原始代码:

import org.springframework.web.bind.annotation.RequestParam;import javax.validation.constraints.NotNull;class ControllerClass {    public String sayHello (        @NotNull @RequestParam(value = "name") String name, // 满足 @NotNull 条件        @RequestParam(value = "lang") String lang,           // 不满足条件        @RequestParam(value = "aNumber") Long aNumber,      // 满足 Number 类型条件        @RequestParam(value = "fred") String fred           // 满足名称为 "fred" 条件    ) {       return "Hello";    }}

应用配方后的预期代码:

import org.springframework.web.bind.annotation.RequestParam;import javax.validation.constraints.NotNull;class ControllerClass {    public String sayHello (        @NotNull @RequestParam(required = true, value = "name") String name,        @RequestParam(value = "lang") String lang,        @RequestParam(required = true, value = "aNumber") Long aNumber,        @RequestParam(required = true, value = "fred") String fred    ) {       return "Hello";    }}

配方测试与验证

OpenRewrite提供了一个方便的测试框架,用于验证配方的行为。

import org.junit.jupiter.api.Test;import org.openrewrite.java.JavaParser;import org.openrewrite.test.RecipeSpec;import org.openrewrite.test.RewriteTest;import static org.openrewrite.java.Assertions.java;class MandatoryRequestParameterTest implements RewriteTest {    @Override    public void defaults(RecipeSpec spec) {        spec.recipe(new MandatoryRequestParameter())            .parser(JavaParser.fromJavaVersion().classpath("spring-web", "validation-api")); // 确保classpath包含必要的依赖    }    @Test    void requiredRequestParam() {        rewriteRun(            java(                """                  import org.springframework.web.bind.annotation.RequestParam;                  import javax.validation.constraints.NotNull;                  class ControllerClass {                    public String sayHello (                      @NotNull @RequestParam(value = "name") String name,                      @RequestParam(value = "lang") String lang,                      @RequestParam(value = "aNumber") Long aNumber,                      @RequestParam(value = "fred") String fred                    ) {                      return "Hello";                    }                  }                """,                """                  import org.springframework.web.bind.annotation.RequestParam;                  import javax.validation.constraints.NotNull;                  class ControllerClass {                    public String sayHello (                      @NotNull @RequestParam(required = true, value = "name") String name,                      @RequestParam(value = "lang") String lang,                      @RequestParam(required = true, value = "aNumber") Long aNumber,                      @RequestParam(required = true, value = "fred") String fred                    ) {                      return "Hello";                    }                  }                """            )        );    }}

在测试中,defaults方法用于配置测试环境,包括要运行的配方和Java解析器。rewriteRun方法接收一对字符串,分别代表原始代码和期望修改后的代码,OpenRewrite会执行配方并比较结果。

总结与注意事项

Cursor的重要性: 在OpenRewrite中,Cursor是理解AST上下文的关键。通过getCursor().getParent().getValue(),开发者可以从当前节点导航到其父节点,从而获取更丰富的上下文信息,这对于实现复杂的条件判断至关重要。组合配方: 本文展示了如何在一个命令式配方中嵌套和委托给另一个(声明式或命令式)配方。这种组合能力使得OpenRewrite非常强大,可以将简单的修改逻辑组合成复杂的重构策略。错误处理: 理解UncaughtVisitorException等错误通常与Cursor上下文的丢失或不匹配有关。确保当您将一个Visitor应用于某个AST节点时,该Visitor期望的上下文与实际提供的上下文相符。依赖管理: 在编写和测试配方时,确保OpenRewrite解析器能够访问到所有必要的类路径依赖(例如spring-web、validation-api),否则可能导致类型解析失败。

通过掌握OpenRewrite的命令式配方和Cursor机制,开发者可以实现高度定制化和精准的代码重构任务,极大地提高代码维护和升级的效率。

以上就是OpenRewrite教程:精准修改特定方法参数上的注解属性的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1064489.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月2日 05:55:43
下一篇 2025年12月2日 05:56:01

相关推荐

  • Symfony 怎样把插件配置转为数组

    symfony配置管理的核心逻辑是:1. 定义配置结构(通过configuration类);2. 解析配置文件为原始php数组;3. 在extension类中使用processconfiguration()方法合并、验证并应用默认值,生成规范化配置数组;4. 将处理后的配置通过参数或依赖注入方式注入…

    2025年12月10日
    000
  • Symfony 如何把图片资源转为数组

    获取图片元数据:使用 exif_read_data() 或 getimagesize() 函数提取图片的宽度、高度、mime 类型等信息并存入数组;2. 将图片编码为 base64:通过 file_get_contents() 读取图片内容并用 base64_encode() 转换为字符串,存入数组…

    2025年12月10日
    000
  • PHP怎样优化OPcache?PHP加速配置技巧

    opcache通过缓存php脚本的预编译opcode,避免重复解析和编译,显著提升性能;2. 核心配置包括opcache.enable=1、memory_consumption根据项目设256-512mb、max_accelerated_files设为文件数1.5-2倍、validate_times…

    2025年12月10日
    000
  • Symfony 怎么把数据迁移转为数组

    在symfony中将数据迁移中的数据转换为数组没有一键操作,需根据数据来源选择处理方式;2. 若数据为迁移文件中硬编码的静态数据,可通过手动解析sql或直接在代码中定义数组提取;3. 若数据已执行并存于数据库,则应通过doctrine orm或dbal查询实体后遍历转换为数组,推荐使用symfony…

    2025年12月10日
    000
  • PHP怎样制作虚拟商品交易平台?数字产品交付方案

    虚拟商品的安全存储需将文件置于web根目录外或使用云存储(如s3、oss),并通过数据库记录文件元数据;2. 分发采用“验证-授权-流式传输”模式,php通过download.php验证用户权限后使用readfile()或fpassthru()流式输出文件内容;3. 下载链接应为带加密token的一…

    2025年12月10日
    000
  • PHP怎样处理表单数据? POST/_GET过滤技巧

    <p>php处理表单数据需通过$_post或$_get获取用户输入;2. 必须对数据进行过滤和验证以确保安全性和准确性;3. 使用filter_input()和filter_var()进行数据净化与验证;4. 采用htm<a style=”color:#f60; tex…

    好文分享 2025年12月10日
    000
  • Symfony 怎么把环境变量转为关联数组

    symfony 不需要将环境变量转换为关联数组,因为它已自动加载管理;1. 通过 getparameter() 方法结合 parameterbaginterface 是推荐方式,需在 services.yaml 中定义参数如 app.api_key: ‘%env(app_api_key)…

    2025年12月10日
    000
  • Symfony 怎样将集成数据转为数组

    将 symfony 集成数据转换为数组的核心方法包括:1. doctrine orm 查询结果使用 getarrayresult() 直接获取数组,避免手动遍历对象以提升性能;2. api 响应通过 json_decode($jsonstring, true) 将 json 数据转为关联数组,并检查…

    2025年12月10日
    000
  • Symfony 怎么把SSO凭证转为数组

    要从symfony的安全令牌中获取sso凭证,首先需通过tokenstorageinterface获取当前token,再从中提取用户对象或令牌属性。1. 注入tokenstorageinterface服务以访问当前安全令牌;2. 调用gettoken()获取tokeninterface实例,若无令牌…

    2025年12月10日
    000
  • Symfony 如何将LDAP条目转为数组

    使用php原生ldap_*函数时,需手动遍历ldap_get_entries()返回的嵌套数组,跳过数字索引和count键,将每个属性值(通常为数组)根据其count字段提取为单值或数组,并保留dn,最终构建成干净的关联数组;2. 使用symfony的ldap组件时,通过query执行后得到entr…

    2025年12月10日
    000
  • Symfony 怎样把浏览器Cookies转数组

    在symfony中,通过request对象的cookies属性(parameterbag实例)调用all()方法即可将浏览器发送的cookies直接转换为php关联数组;2. 安全读取和处理cookie数据时,应避免存储敏感信息,仅使用cookie保存标识符,并将在服务器端存储实际数据,同时对输入进…

    2025年12月10日
    000
  • Symfony 怎样把Neo4j节点转为数组

    最直接的方法是调用neo4j节点对象的properties()方法,它会返回包含所有属性的关联数组;2. 对于复杂场景,可通过自定义mapper服务或使用symfony serializer组件处理日期、标签、关系及嵌套结构;3. 为提升性能,应在cypher查询中只返回必要属性,并避免orm的额外…

    2025年12月10日
    000
  • Symfony 怎样将MongoDB文档转数组

    在 symfony 中将 mongodb 文档转换为数组最直接的方式是使用 doctrine odm 提供的 toarray() 方法,适用于简单文档结构;2. 常见应用场景包括构建 restful api 响应、数据导出、日志调试、表单预填充和缓存处理;3. toarray() 方法的主要局限性在…

    2025年12月10日
    000
  • PHP如何创建在线租赁平台?押金与租金计算

    处理租赁期间商品损坏的核心是建立明确的规则与保障机制,1、在租赁协议中清晰界定损坏赔偿标准,如按损坏程度扣除部分或全部押金;2、要求用户租赁前进行实名认证以提高违约成本;3、可引入保险机制,为商品购买保险以分摊平台与用户风险;4、平台应提供便捷的损坏申报与评估流程,确保处理公正透明,最终保障交易双方…

    2025年12月10日
    000
  • PHP数组键值字符串匹配与条件赋值教程

    本教程详细阐述了在PHP中如何遍历数组,并根据其键(key)是否等于特定的字符串值来执行相应的逻辑,例如为变量赋值。文章将深入探讨PHP数组键的特性、foreach循环的正确使用方法,并澄清常见的误区,如array_key_exists()和isset()在遍历场景下的适用性,旨在帮助开发者高效、准…

    2025年12月10日
    000
  • PHP如何创建自动发货系统?虚拟商品卡密生成

    卡密生成需结合随机数、时间戳与哈希算法(如md5(uniqid(rand(), true)))确保唯一性和复杂性,并在数据库中为卡密字段建立唯一索引防止重复;2. 支付成功后,系统通过支付网关的异步回调通知触发发货流程,接收回调数据后需进行验签、核对订单信息,并使用数据库事务保证订单更新、卡密分配与…

    2025年12月10日
    000
  • Symfony 如何将YAML配置转为PHP数组

    symfony通过yaml组件将yaml配置转换为php数组,1. 首先安装symfony/yaml组件;2. 使用yaml::parsefile()或yaml::parse()方法解析文件或字符串;3. 处理解析结果并进行错误捕获;4. 在实际项目中可用于加载自定义配置、处理用户上传、动态生成配置…

    2025年12月10日 好文分享
    000
  • PHP如何开发二级域名分销系统?白标解决方案

    实现动态二级域名解析与路由需配置dns泛解析(*.yourmaindomain.com指向服务器ip)并结合nginx或apache的虚拟主机匹配请求,通过正则捕获二级域名作为租户标识,再由php从$_server[‘http_host’]提取并识别租户;2. 多租户数据管理…

    2025年12月10日
    000
  • Symfony 如何将调试信息转为数组

    要将symfony的dump()函数输出转换为程序可处理的php数组,必须绕过默认渲染机制,直接操作vardumper组件的内部结构;具体步骤是:1. 使用varcloner克隆变量生成data对象;2. 创建自定义arraydumper类继承abstractdumper,递归遍历data对象和st…

    2025年12月10日
    000
  • Symfony 怎么把基准测试结果转数组

    首先使用phpbench生成json格式的基准测试报告,可通过配置phpbench.json文件或命令行参数实现;2. 然后使用php的file_get_contents读取生成的json文件;3. 接着调用json_decode($jsondata, true)将json内容转换为php关联数组;…

    2025年12月10日
    000

发表回复

登录后才能评论
关注微信