0

0

OpenRewrite 教程:为特定方法参数精确添加或更新注解属性

碧海醫心

碧海醫心

发布时间:2025-11-28 13:24:41

|

203人浏览过

|

来源于php中文网

原创

OpenRewrite 教程:为特定方法参数精确添加或更新注解属性

本教程详细介绍了如何使用 openrewrite 实现对 java 代码中特定方法参数的注解属性进行精确修改。针对需要根据其他注解或参数类型进行条件性修改的场景,文章首先分析了声明式配方的局限性,随后深入讲解了如何通过构建命令式配方,利用 openrewrite 的 ast 遍历机制和 `cursor` 对象,实现对抽象语法树的上下文感知导航和条件判断,最终精准定位并修改目标注解属性,并提供了完整的示例代码和测试方法。

引言

OpenRewrite 是一个强大的代码重构工具,它允许开发者通过定义“配方”(Recipes)来自动化执行代码转换。常见的应用场景包括依赖升级、API 迁移、代码风格统一等。然而,在某些特定情况下,我们可能需要对代码进行更精细的控制,例如,只对满足特定条件的代码片段应用转换,而非整个文件或所有匹配项。本文将聚焦于一个具体的挑战:如何仅对同时带有 @NotNull 和 @RequestParam 注解的方法参数,将其 @RequestParam 注解的 required 属性设置为 true。

问题场景分析

假设我们有以下 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
    ) {
       return "Hello";
    }
}

我们的目标是:

  1. 找到所有方法参数。
  2. 检查这些参数是否同时带有 @NotNull 和 @RequestParam 注解。
  3. 如果满足条件,则将该参数上的 @RequestParam 注解的 required 属性设置为 true。
  4. 预期结果如下:
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";
    }
}

注意,第二个参数 lang 因为没有 @NotNull 注解,所以不应被修改。

解决方案一:声明式配方及其局限性

OpenRewrite 提供了声明式配方,通过 YAML 文件即可定义简单的代码转换。例如,我们可以使用 AddOrUpdateAnnotationAttribute 配方来添加或更新 @RequestParam 的 required 属性:

# 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"

要应用此配方,可以在 Maven 或 Gradle 项目中配置 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>

局限性: 这种声明式配方会无差别地将所有 @RequestParam 注解的 required 属性设置为 true,无法实现我们所需的条件判断(即只针对同时有 @NotNull 的参数)。因此,对于这种需要复杂逻辑判断的场景,我们需要编写命令式配方。

解决方案二:命令式配方实现精确控制

命令式配方允许我们利用 OpenRewrite 的 Java 抽象语法树(AST)遍历机制,编写自定义的 Java 代码来精确控制转换逻辑。

核心概念:AST 遍历与 Cursor

OpenRewrite 的核心是 TreeVisitor,它允许我们遍历代码的 AST。JavaVisitor(或其子类 JavaIsoVisitor)是处理 Java AST 的专用访问器。在遍历过程中,TreeVisitor 提供了一个 Cursor 对象,它代表了当前访问的 AST 节点及其在树中的上下文路径。通过 Cursor,我们可以访问当前节点的父节点、祖先节点,从而获取更丰富的上下文信息,这对于实现条件判断至关重要。

智川X-Agent
智川X-Agent

中科闻歌推出的一站式AI智能体开发平台

下载

实现细节:定位与条件判断

我们将创建一个继承自 Recipe 的类 MandatoryRequestParameter,并在其中定义一个 JavaIsoVisitor 来实现我们的逻辑。

  1. 定义配方类:

    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.openrewrite.marker.Markers;
    
    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";
    
        @Override
        public @NotNull String getDisplayName() {
            return "Make Spring `RequestParam` mandatory with @NotNull";
        }
    
        @Override
        public String getDescription() {
            return "Add `required` attribute to `RequestParam` and set the value to `true` for parameters also annotated with @NotNull.";
        }
  2. 优化:getSingleSourceApplicableTest()

    为了提高性能,我们可以使用 getSingleSourceApplicableTest() 方法来预先检查源文件是否包含我们感兴趣的类型。如果文件不包含 org.springframework.web.bind.annotation.RequestParam 类型,则无需运行主访问器。

        @Override
        protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
            return new UsesType<>(REQUEST_PARAM_FQ_NAME);
        }
  3. 核心逻辑:getVisitor()

    在 getVisitor() 方法中,我们将创建一个 JavaIsoVisitor。此访问器将覆盖 visitAnnotation 方法,因为我们主要关注注解的修改。

        @Override
        protected @NotNull JavaVisitor<ExecutionContext> getVisitor() {
            // 这是一个嵌套的 visitor,用于实际添加或更新注解属性
            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) {
                    // 首先调用父类的 visitAnnotation,确保 AST 正常遍历
                    J.Annotation a = super.visitAnnotation(annotation, ctx);
    
                    // 1. 检查当前注解是否是 @RequestParam
                    if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {
                        return a; // 如果不是,则直接返回
                    }
    
                    // 2. 使用 Cursor 导航到父节点,获取方法参数声明
                    // 当前 cursor 指向 Annotation,其父节点是 J.VariableDeclarations
                    J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();
    
                    // 3. 检查该方法参数是否也带有 @NotNull 注解
                    boolean hasNotNull = false;
                    for (J.Annotation leadingAnnotation : variableDeclaration.getLeadingAnnotations()) {
                        if (TypeUtils.isOfClassType(leadingAnnotation.getType(), NOT_NULL_FQ_NAME)) {
                            hasNotNull = true;
                            break;
                        }
                    }
    
                    // 4. 如果同时满足 @RequestParam 和 @NotNull,则委托给 addAttributeVisitor 进行修改
                    if (hasNotNull) {
                        // 关键:将当前的注解和上下文传递给 addAttributeVisitor
                        // 这样 addAttributeVisitor 就能在正确的 AST 位置上操作
                        return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
                    }
                    return a; // 不满足条件,返回未修改的注解
                }
            };
        }
    }

解释:

  • addAttributeVisitor:我们创建了一个 AddOrUpdateAnnotationAttribute 的实例,并获取其内部的 Visitor。这个 Visitor 知道如何添加或更新注解属性。
  • visitAnnotation(J.Annotation annotation, ExecutionContext ctx):这是我们自定义访问器的核心。当 OpenRewrite 遍历到任何注解时,都会调用此方法。
  • super.visitAnnotation(annotation, ctx):确保 AST 的正常遍历,允许子节点被访问。
  • TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME):检查当前注解的完全限定名是否为 org.springframework.web.bind.annotation.RequestParam。
  • getCursor().getParent().getValue():这是实现精确控制的关键。getCursor() 返回当前 AST 节点的 Cursor。getParent() 导航到父节点的 Cursor。getValue() 则获取父节点对应的 AST 元素。对于一个注解,它的父节点通常是 J.VariableDeclarations(即变量声明,这里是方法参数)。
  • variableDeclaration.getLeadingAnnotations():获取该方法参数声明上所有前置注解的列表。
  • TypeUtils.isOfClassType(leadingAnnotation.getType(), NOT_NULL_FQ_NAME):遍历这些注解,检查是否存在 javax.validation.constraints.NotNull。
  • addAttributeVisitor.visit(a, ctx, getCursor()):如果条件满足,我们不直接修改 a,而是将修改任务委托给 addAttributeVisitor。重要的是,我们将当前的注解 a、执行上下文 ctx 和当前 Cursor 一并传递过去。这样,addAttributeVisitor 就能在正确的上下文(即当前 @RequestParam 注解的位置)执行其修改逻辑,避免了原始问题中因上下文丢失而导致的 UncaughtVisitorException。

完整命令式配方代码

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.TypeUtils;

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";

    @Override
    public @NotNull String getDisplayName() {
        return "Make Spring `RequestParam` mandatory with @NotNull";
    }

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

    @Override
    protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
        return new UsesType<>(REQUEST_PARAM_FQ_NAME);
    }

    @Override
    protected @NotNull JavaVisitor<ExecutionContext> getVisitor() {
        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);

                if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {
                    return a;
                }

                J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();

                boolean hasNotNull = false;
                for (J.Annotation leadingAnnotation : variableDeclaration.getLeadingAnnotations()) {
                    if (TypeUtils.isOfClassType(leadingAnnotation.getType(), NOT_NULL_FQ_NAME)) {
                        hasNotNull = true;
                        break;
                    }
                }

                if (hasNotNull) {
                    return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
                }
                return a;
            }
        };
    }
}

测试配方

为了验证配方是否按预期工作,我们可以使用 OpenRewrite 的测试工具。在 src/test/java 目录下创建一个测试类:

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 requiredRequestParamWithNotNull() {
        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,
                      @NotNull @RequestParam(value = "id") Long id,
                      @RequestParam(value = "age") Integer age
                    ) {
                      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 = "id") Long id,
                      @RequestParam(value = "age") Integer age
                    ) {
                      return "Hello";
                    }
                  }
                """
            )
        );
    }

    @Test
    void noNotNullAnnotation() {
        rewriteRun(
            java(
                """
                  import org.springframework.web.bind.annotation.RequestParam;

                  class ControllerClass {
                    public String sayHello (
                      @RequestParam(value = "name") String name
                    ) {
                      return "Hello";
                    }
                  }
                """
            )
        ); // 预期没有变化,所以只提供一个参数
    }

    @Test
    void otherAnnotationsPresent() {
        rewriteRun(
            java(
                """
                  import org.springframework.web.bind.annotation.RequestParam;
                  import org.springframework.lang.Nullable; // 假设有其他注解

                  class ControllerClass {
                    public String sayHello (
                      @Nullable @RequestParam(value = "param1") String param1,
                      @RequestParam(value = "param2") String param2
                    ) {
                      return "Hello";
                    }
                  }
                """
            )
        ); // 预期没有变化,因为没有 @NotNull
    }
}

在 defaults 方法中,我们通过 classpath 配置了 spring-web 和 validation-api,确保 OpenRewrite 解析器能够正确识别 @RequestParam 和 @NotNull 注解。rewriteRun 方法接受原始代码和期望转换后的代码,OpenRewrite 会执行配方并比较结果。

总结与注意事项

  • 命令式配方的强大之处: 当声明式配方无法满足复杂的条件判断和上下文感知转换时,命令式配方提供了无与伦比的灵活性。
  • Cursor 的重要性: Cursor 是 OpenRewrite 中进行 AST 上下文导航的核心工具。理解如何使用 getCursor()、getParent() 和 getValue() 是编写高级配方的关键。
  • 委托模式: 在自定义访问器中,如果已经有现成的配方或访问器可以完成部分工作(如 AddOrUpdateAnnotationAttribute),可以将其作为嵌套访问器,并在满足条件时委托给它执行,同时传递正确的 Cursor,以保持 AST 上下文的正确性。
  • 性能优化: 使用 getSingleSourceApplicableTest() 可以有效减少不必要的 AST 遍历,提升配方执行效率。
  • 依赖管理: 确保 OpenRewrite 解析器在测试和实际应用时,能够访问到所有相关的库依赖(例如 Spring Web 和 Validation API),否则可能导致类型解析失败。

通过本文的指导,您应该能够掌握如何利用 OpenRewrite 的命令式配方,实现对代码的精确控制和有条件的自动化重构。

热门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

Java Maven专题
Java Maven专题

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

0

2025.09.15

PHP 高并发与性能优化
PHP 高并发与性能优化

本专题聚焦 PHP 在高并发场景下的性能优化与系统调优,内容涵盖 Nginx 与 PHP-FPM 优化、Opcode 缓存、Redis/Memcached 应用、异步任务队列、数据库优化、代码性能分析与瓶颈排查。通过实战案例(如高并发接口优化、缓存系统设计、秒杀活动实现),帮助学习者掌握 构建高性能PHP后端系统的核心能力。

112

2025.10.16

PHP 数据库操作与性能优化
PHP 数据库操作与性能优化

本专题聚焦于PHP在数据库开发中的核心应用,详细讲解PDO与MySQLi的使用方法、预处理语句、事务控制与安全防注入策略。同时深入分析SQL查询优化、索引设计、慢查询排查等性能提升手段。通过实战案例帮助开发者构建高效、安全、可扩展的PHP数据库应用系统。

99

2025.11.13

JavaScript 性能优化与前端调优
JavaScript 性能优化与前端调优

本专题系统讲解 JavaScript 性能优化的核心技术,涵盖页面加载优化、异步编程、内存管理、事件代理、代码分割、懒加载、浏览器缓存机制等。通过多个实际项目示例,帮助开发者掌握 如何通过前端调优提升网站性能,减少加载时间,提高用户体验与页面响应速度。

36

2025.12.30

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

98

2026.03.06

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

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

76

2026.03.11

热门下载

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

精品课程

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

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 80.9万人学习

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

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