0

0

OpenRewrite:针对特定方法参数应用和定制注解属性的教程

霞舞

霞舞

发布时间:2025-11-28 11:05:35

|

665人浏览过

|

来源于php中文网

原创

OpenRewrite:针对特定方法参数应用和定制注解属性的教程

本文深入探讨了如何使用 openrewrite 框架,针对 java 代码中特定方法参数的注解进行精确修改。文章首先介绍了声明式配方的简洁性及其局限性,随后重点阐述了通过命令式配方结合 `javavisitor` 和 `cursor` 实现细粒度控制的方法。通过具体示例,详细讲解了如何根据参数的类型、名称或其他注解等条件,有选择性地更新或添加注解属性,并提供了测试配方的实践指导。

OpenRewrite 是一个强大的代码重构工具,它允许开发者通过编写“配方”(Recipe)来自动化修改代码库。这些配方可以用于升级依赖、修复安全漏洞、统一代码风格等。在实际开发中,我们经常需要对代码进行有条件的修改,例如,仅针对满足特定条件的方法参数应用或更新注解属性。本文将详细介绍如何实现这一目标。

问题背景与挑战

假设我们需要为 Spring 框架中同时带有 @NotNull 和 @RequestParam 注解的方法参数,将 @RequestParam 的 required 属性设置为 true。例如,将以下代码:

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
    ) {
       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
    ) {
       return "Hello";
    }
}

注意,只有第一个参数 name 需要修改,因为它同时拥有 @NotNull 和 @RequestParam。第二个参数 lang 没有 @NotNull 注解,因此不应被修改。

初次尝试的命令式配方可能如下所示,但它在运行时会抛出 org.openrewrite.UncaughtVisitorException: java.lang.IllegalStateException: Expected to find a matching parent for Cursor{Annotation->root} 错误。这通常是因为 AddOrUpdateAnnotationAttribute 配方被应用到了一个不正确的 AST 上下文(例如,将其直接应用于 Statement 而非其内部的 Annotation 节点,或者 Cursor 没有提供正确的父级信息)。

public class MandatoryRequestParameter extends Recipe {
    @Override
    public @NotNull String getDisplayName() {
        return "Make RequestParam mandatory";
    }

    @Override
    protected @NotNull JavaIsoVisitor<ExecutionContext> getVisitor() {
        return new MandatoryRequestParameterVisitor();
    }

    public class MandatoryRequestParameterVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public @NotNull J.MethodDeclaration visitMethodDeclaration(@NotNull J.MethodDeclaration methodDeclaration, @NotNull ExecutionContext executionContext) {
            J.MethodDeclaration methodDecl = super.visitMethodDeclaration(methodDeclaration, executionContext);
            // 错误的应用方式:直接在参数列表上映射并尝试修改
            return methodDeclaration.withParameters(ListUtils.map(methodDecl.getParameters(), (i, p) -> makeRequestParamMandatory(p, executionContext)));
        }

        private Statement makeRequestParamMandatory(Statement statement, ExecutionContext executionContext) {
            if (!(statement instanceof J.VariableDeclarations methodParameterDeclaration) || methodParameterDeclaration.getLeadingAnnotations().size() < 2) {
                return statement;
            }
            AddOrUpdateAnnotationAttribute addOrUpdateAnnotationAttribute = new AddOrUpdateAnnotationAttribute(
                    "org.springframework.web.bind.annotation.RequestParam", "required", "true", false
            );
            // 错误:直接将 AddOrUpdateAnnotationAttribute 应用到 VariableDeclarations
            return (Statement) methodParameterDeclaration.acceptJava(addOrUpdateAnnotationAttribute.getVisitor(), executionContext);
        }
    }
}

这个错误提示说明 AddOrUpdateAnnotationAttribute 配方期望在其内部访问一个 J.Annotation 节点,并且该注解节点需要在一个正确的 AST 结构中,以便 Cursor 能够找到其父级。直接将 AddOrUpdateAnnotationAttribute 的访问器应用于 J.VariableDeclarations 节点是不正确的,因为它期望直接处理 J.Annotation。

解决方案:声明式与命令式配方

OpenRewrite 提供了两种主要的配方编写方式:声明式(YAML)和命令式(Java)。

1. 声明式配方

对于简单的、无条件的代码修改,声明式配方是一个快速有效的选择。例如,要全局地将所有 @RequestParam 注解的 required 属性设置为 true,可以创建一个 rewrite.yml 文件:

type: specs.openrewrite.org/v1beta/recipe
name: org.example.MandatoryRequestParameter
displayName: Make Spring `RequestParam` mandatory
description: 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 文件放在项目根目录,并通过 Maven 或 Gradle 插件激活该配方。

Maven 示例:

IBM Watson
IBM Watson

IBM Watson文字转语音

下载

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

<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>4.38.0</version>
  <configuration>
    <activeRecipes>
      <recipe>org.example.MandatoryRequestParameter</recipe>
    </activeRecipes>
  </configuration>
</plugin>

局限性: 声明式配方适用于全局性、无条件的修改。然而,它无法实现我们最初的需求——根据其他注解、参数类型或名称等条件来限制修改范围。为了实现这种精细控制,我们需要使用命令式配方。

2. 命令式配方实现精细控制

命令式配方允许我们编写 Java 代码来遍历抽象语法树(AST),并根据复杂的逻辑判断来修改代码。解决上述问题的关键在于正确使用 JavaVisitor 和 Cursor。

核心概念:JavaVisitor 和 Cursor

  • JavaVisitor (或 JavaIsoVisitor): 这是 OpenRewrite 中用于遍历和修改 Java AST 的核心类。通过重写 visit... 方法,我们可以在遇到特定 AST 节点时执行自定义逻辑。
  • Cursor: Cursor 是一个非常强大的工具,它提供了当前正在访问的 AST 节点的上下文信息。最重要的是,它允许我们向上导航到父节点,从而获取当前节点在 AST 中的完整路径和相关信息。

实现步骤:

  1. 定义配方类: 继承 Recipe 类。
  2. 优化适用性检查: 使用 getSingleSourceApplicableTest() 提前判断文件是否包含目标注解,避免不必要的遍历。
  3. 创建主访问器: 继承 JavaIsoVisitor
  4. 重写 visitAnnotation() 方法: 这是关键所在。当访问到任何注解时,我们可以在这里进行判断。
  5. 使用 Cursor 导航: 在 visitAnnotation() 方法中,通过 getCursor().getParent().getValue() 获取当前注解的父节点,通常是 J.VariableDeclarations(对于方法参数而言)。
  6. 应用条件逻辑: 根据父节点(J.VariableDeclarations)的信息,判断是否满足修改条件(例如,是否包含 @NotNull 注解,参数类型是否为 Number,参数名称是否为 fred 等)。
  7. 委托给嵌套访问器: 如果条件满足,则将修改任务委托给 AddOrUpdateAnnotationAttribute 的访问器,并传入正确的 Cursor 上下文。

以下是实现我们需求的命令式配方代码:

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.Statement;
import org.openrewrite.java.tree.TypeUtils;
import org.openrewrite.internal.ListUtils;

import javax.validation.constraints.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 "Make Spring `RequestParam` mandatory conditionally";
    }

    @Override
    public String getDescription() {
        return "Add `required` attribute to `RequestParam` and set the value to `true` only for specific parameters.";
    }

    @Override
    protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
        // 优化:只有当源文件包含 RequestParam 注解时才运行访问器
        return new UsesType<>(REQUEST_PARAM_FQ_NAME);
    }

    @Override
    protected @NotNull JavaVisitor<ExecutionContext> getVisitor() {

        // 创建 AddOrUpdateAnnotationAttribute 的访问器实例,用于实际的注解属性修改
        JavaIsoVisitor<ExecutionContext> addAttributeVisitor = new AddOrUpdateAnnotationAttribute(
                REQUEST_PARAM_FQ_NAME, "required", "true", false
        ).getVisitor();

        return new JavaIsoVisitor<ExecutionContext>() {
            @Override
            public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
                J.Annotation a = super.visitAnnotation(annotation, ctx);

                // 检查当前注解是否为 RequestParam
                if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {
                    return a;
                }

                // 使用 Cursor 获取当前注解的父节点。
                // 对于方法参数上的注解,其父节点通常是 J.VariableDeclarations。
                J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();

                // 实现条件逻辑:
                // 1. 检查参数是否带有 @NotNull 注解
                boolean hasNotNull = variableDeclaration.getLeadingAnnotations().stream()
                        .anyMatch(ann -> TypeUtils.isOfClassType(ann.getType(), NOT_NULL_FQ_NAME));

                // 2. 示例条件:如果参数类型是 java.lang.Number 的子类型
                boolean isNumberType = TypeUtils.isAssignableTo("java.lang.Number", variableDeclaration.getType());

                // 3. 示例条件:如果参数名为 "fred"
                boolean isNamedFred = variableDeclaration.getVariables().get(0).getSimpleName().equals("fred");

                // 满足任一条件(或按实际需求组合条件),则应用修改
                // 原始问题是要求同时有 @NotNull 和 @RequestParam,所以这里组合条件
                if (hasNotNull /* && 其他条件 */) { // 满足原始问题条件
                    // 如果满足条件,将修改委托给 AddOrUpdateAnnotationAttribute 的访问器
                    // 注意:这里传入了当前的 Cursor,确保 AddOrUpdateAnnotationAttribute 在正确的上下文中执行
                    return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
                }
                // 也可以根据其他条件进行修改,例如:
                // if (isNumberType || isNamedFred) {
                //     return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
                // }

                return a;
            }
        };
    }
}

代码解析:

  1. REQUEST_PARAM_FQ_NAME 和 NOT_NULL_FQ_NAME: 定义了 @RequestParam 和 @NotNull 注解的全限定名,方便引用。
  2. getSingleSourceApplicableTest(): 这是一个优化点。它通过 UsesType 判断源文件是否包含 org.springframework.web.bind.annotation.RequestParam 类型,只有包含时才会进一步运行 getVisitor(),从而避免不必要的 AST 遍历,提高性能。
  3. addAttributeVisitor: 我们首先创建了一个 AddOrUpdateAnnotationAttribute 的实例,并获取其内部的 JavaIsoVisitor。这个访问器负责实际的属性添加或更新操作。
  4. visitAnnotation(J.Annotation annotation, ExecutionContext ctx):
    • super.visitAnnotation(annotation, ctx):首先调用父类的 visitAnnotation 方法,确保 AST 遍历的连续性。
    • !TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME):检查当前访问的注解是否为 @RequestParam。如果不是,则直接返回,不进行处理。
    • J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();:这是核心。getCursor() 获取当前节点的 Cursor,getParent() 获取父级 Cursor,getValue() 获取父级 Cursor 所指向的 AST 节点。对于方法参数上的注解,其父节点就是 J.VariableDeclarations(变量声明)。
    • 条件判断:
      • hasNotNull:通过遍历 variableDeclaration.getLeadingAnnotations(),检查 J.VariableDeclarations 是否包含 javax.validation.constraints.NotNull 注解。这是解决原始问题的关键逻辑。
      • isNumberType 和 isNamedFred:这些是示例条件,展示了如何根据参数类型或名称进行判断。你可以根据实际需求组合或替换这些条件。
    • addAttributeVisitor.visit(a, ctx, getCursor()):如果条件满足,我们将当前注解 a、执行上下文 ctx 和当前的 Cursor 传递给 addAttributeVisitor。这确保了 AddOrUpdateAnnotationAttribute 在正确的 AST 上下文(即,直接作用于 J.Annotation 节点)中执行其修改逻辑,从而避免了之前遇到的 IllegalStateException。

测试配方

OpenRewrite 提供了强大的测试工具来验证配方的行为。在 JUnit 测试中,你可以使用 rewriteRun 方法来定义输入代码和期望的输出代码。

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;
import static org.openrewrite.test.RewriteTest.rewriteRun;

class MandatoryRequestParameterTest implements RewriteTest {

    @Override
    public void defaults(RecipeSpec spec) {
        spec.recipe(new MandatoryRequestParameter())
            .parser(JavaParser.fromJavaVersion().classpath("spring-web")); // 确保 classpath 包含 Spring Web 依赖
    }

    @Test
    void requiredRequestParamWithNotNullAndOtherConditions() {
        rewriteRun(
            java(
                """
                  import org.springframework.web.bind.annotation.RequestParam;
                  import javax.validation.constraints.NotNull; // 导入 NotNull 注解

                  class ControllerClass {
                    public String sayHello (
                      @NotNull @RequestParam(value = "name") String name, // 期望被修改
                      @RequestParam(value = "lang") String lang, // 不期望被修改
                      @NotNull @RequestParam(value = "aNumber") Long aNumber, // 期望被修改 (同时有 NotNull)
                      @RequestParam(value = "fred") String fred, // 期望被修改 (如果配方中包含 name="fred" 的条件)
                      @NotNull String justNotNull // 不期望被修改 (没有 RequestParam)
                    ) {
                      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,
                      @NotNull @RequestParam(required = true, value = "aNumber") Long aNumber,
                      @RequestParam(value = "fred") String fred, // 根据配方条件,此项可能不变或改变
                      @NotNull String justNotNull
                    ) {
                      return "Hello";
                    }
                  }
                """
            )
        );
    }
}

在上述测试中,defaults 方法用于配置测试规范,包括要运行的配方和 Java 解析器。requiredRequestParamWithNotNullAndOtherConditions 测试方法则定义了输入代码和期望的输出代码,用于验证配方在不同场景下的行为。请注意,classpath("spring-web") 是为了确保解析器能够正确解析 Spring 相关的注解。

总结

通过本文的讲解,我们了解了 OpenRewrite 声明式配方和命令式配方的不同应用场景。当需要对代码进行精细的、条件性的修改时,命令式配方结合 JavaVisitor 和 Cursor 是实现这一目标的关键。特别是 Cursor 允许我们获取 AST 节点的上下文信息,从而能够根据父节点(如 J.VariableDeclarations)的属性(如其他注解、类型、名称)来决定是否应用修改。掌握这一技术,将大大提升 OpenRewrite 在复杂代码重构任务中的灵活性和效率。在编写配方时,务必注意 AST 结构和 Cursor 的正确使用,并通过完善的测试来验证配方的行为。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

156

2025.08.06

Java Spring Security 与认证授权
Java Spring Security 与认证授权

本专题系统讲解 Java Spring Security 框架在认证与授权中的应用,涵盖用户身份验证、权限控制、JWT与OAuth2实现、跨站请求伪造(CSRF)防护、会话管理与安全漏洞防范。通过实际项目案例,帮助学习者掌握如何 使用 Spring Security 实现高安全性认证与授权机制,提升 Web 应用的安全性与用户数据保护。

88

2026.01.26

Java Maven专题
Java Maven专题

本专题聚焦 Java 主流构建工具 Maven 的学习与应用,系统讲解项目结构、依赖管理、插件使用、生命周期与多模块项目配置。通过企业管理系统、Web 应用与微服务项目实战,帮助学员全面掌握 Maven 在 Java 项目构建与团队协作中的核心技能。

0

2025.09.15

软件测试常用工具
软件测试常用工具

软件测试常用工具有Selenium、JUnit、Appium、JMeter、LoadRunner、Postman、TestNG、LoadUI、SoapUI、Cucumber和Robot Framework等等。测试人员可以根据具体的测试需求和技术栈选择适合的工具,提高测试效率和准确性 。

463

2023.10.13

java测试工具有哪些
java测试工具有哪些

java测试工具有JUnit、TestNG、Mockito、Selenium、Apache JMeter和Cucumber。php还给大家带来了java有关的教程,欢迎大家前来学习阅读,希望对大家能有所帮助。

313

2023.10.23

Java 单元测试
Java 单元测试

本专题聚焦 Java 在软件测试与持续集成流程中的实战应用,系统讲解 JUnit 单元测试框架、Mock 数据、集成测试、代码覆盖率分析、Maven 测试配置、CI/CD 流水线搭建(Jenkins、GitHub Actions)等关键内容。通过实战案例(如企业级项目自动化测试、持续交付流程搭建),帮助学习者掌握 Java 项目质量保障与自动化交付的完整体系。

29

2025.10.24

pdf怎么转换成xml格式
pdf怎么转换成xml格式

将 pdf 转换为 xml 的方法:1. 使用在线转换器;2. 使用桌面软件(如 adobe acrobat、itext);3. 使用命令行工具(如 pdftoxml)。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1946

2024.04.01

xml怎么变成word
xml怎么变成word

步骤:1. 导入 xml 文件;2. 选择 xml 结构;3. 映射 xml 元素到 word 元素;4. 生成 word 文档。提示:确保 xml 文件结构良好,并预览 word 文档以验证转换是否成功。想了解更多xml的相关内容,可以阅读本专题下面的文章。

2119

2024.08.01

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号