0

0

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

聖光之護

聖光之護

发布时间:2025-11-28 18:09:06

|

688人浏览过

|

来源于php中文网

原创

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/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文件保存为rewrite.yml在项目根目录,并通过Maven或Gradle插件激活。

Maven配置示例: 在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>

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

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

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

核心挑战:定位与上下文

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

构建增强型命令式配方

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

百度GBI
百度GBI

百度GBI-你的大模型商业分析助手

下载
  1. 该参数同时带有@NotNull注解。
  2. 或者参数类型为java.lang.Number的子类型。
  3. 或者参数名称为"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<?, ExecutionContext> getSingleSourceApplicableTest() {
        // 优化:只有当源文件包含 RequestParam 注解时,才运行此Visitor。
        return new UsesType<>(REQUEST_PARAM_FQ_NAME);
    }

    @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) {
                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;
            }
        };
    }
}

代码解析:

  1. getSingleSourceApplicableTest(): 这是一个优化方法。它通过UsesType检查源文件是否包含@RequestParam注解。如果不存在,则此配方不会运行,从而提高效率。
  2. addAttributeVisitor: 我们首先创建AddOrUpdateAnnotationAttribute配方的一个Visitor实例。这个内部Visitor知道如何添加或更新@RequestParam的required属性。
  3. 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机制,开发者可以实现高度定制化和精准的代码重构任务,极大地提高代码维护和升级的效率。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

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

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

154

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

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

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

1944

2024.04.01

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

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

2118

2024.08.01

xml是什么格式的文件
xml是什么格式的文件

xml是一种纯文本格式的文件。xml指的是可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。想了解更多相关的内容,可阅读本专题下面的相关文章。

1159

2024.11.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

718

2023.08.03

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

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

23

2026.03.06

热门下载

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

精品课程

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

共23课时 | 4.2万人学习

C# 教程
C# 教程

共94课时 | 10.8万人学习

Java 教程
Java 教程

共578课时 | 78.4万人学习

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

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