首页 > Java > java教程 > 正文

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

聖光之護
发布: 2025-11-28 18:09:06
原创
659人浏览过

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注解:

  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;
            }
        };
    }
}
登录后复制

代码解析:

Noiz Agent
Noiz Agent

AI声音创作Agent平台

Noiz Agent 323
查看详情 Noiz Agent
  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机制,开发者可以实现高度定制化和精准的代码重构任务,极大地提高代码维护和升级的效率。

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

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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