OpenRewrite教程:如何添加方法注解属性
IT行业相对于一般传统行业,发展更新速度更快,一旦停止了学习,很快就会被行业所淘汰。所以我们需要踏踏实实的不断学习,精进自己的技术,尤其是初学者。今天golang学习网给大家整理了《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";
}
}我们的目标是:
- 找到所有方法参数。
- 检查这些参数是否同时带有 @NotNull 和 @RequestParam 注解。
- 如果满足条件,则将该参数上的 @RequestParam 注解的 required 属性设置为 true。
- 预期结果如下:
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 配置示例:
org.openrewrite.maven rewrite-maven-plugin 4.38.0 org.example.MandatoryRequestParameter
局限性: 这种声明式配方会无差别地将所有 @RequestParam 注解的 required 属性设置为 true,无法实现我们所需的条件判断(即只针对同时有 @NotNull 的参数)。因此,对于这种需要复杂逻辑判断的场景,我们需要编写命令式配方。
解决方案二:命令式配方实现精确控制
命令式配方允许我们利用 OpenRewrite 的 Java 抽象语法树(AST)遍历机制,编写自定义的 Java 代码来精确控制转换逻辑。
核心概念:AST 遍历与 Cursor
OpenRewrite 的核心是 TreeVisitor,它允许我们遍历代码的 AST。JavaVisitor(或其子类 JavaIsoVisitor)是处理 Java AST 的专用访问器。在遍历过程中,TreeVisitor 提供了一个 Cursor 对象,它代表了当前访问的 AST 节点及其在树中的上下文路径。通过 Cursor,我们可以访问当前节点的父节点、祖先节点,从而获取更丰富的上下文信息,这对于实现条件判断至关重要。
实现细节:定位与条件判断
我们将创建一个继承自 Recipe 的类 MandatoryRequestParameter,并在其中定义一个 JavaIsoVisitor 来实现我们的逻辑。
定义配方类:
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."; }优化:getSingleSourceApplicableTest()
为了提高性能,我们可以使用 getSingleSourceApplicableTest() 方法来预先检查源文件是否包含我们感兴趣的类型。如果文件不包含 org.springframework.web.bind.annotation.RequestParam 类型,则无需运行主访问器。
@Override protected TreeVisitor, ExecutionContext> getSingleSourceApplicableTest() { return new UsesType<>(REQUEST_PARAM_FQ_NAME); }核心逻辑:getVisitor()
在 getVisitor() 方法中,我们将创建一个 JavaIsoVisitor。此访问器将覆盖 visitAnnotation 方法,因为我们主要关注注解的修改。
@Override protected @NotNull JavaVisitorgetVisitor() { // 这是一个嵌套的 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) { // 首先调用父类的 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 getVisitor() {
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);
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 的命令式配方,实现对代码的精确控制和有条件的自动化重构。
以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。
钉钉群文件下载失败怎么解决
- 上一篇
- 钉钉群文件下载失败怎么解决
- 下一篇
- 追书大全官网小说资源实时更新
-
- 文章 · java教程 | 12小时前 | map · 并发安全 · 缓存设计 · Java教程 · java optional concurrenthashmap computeIfAbsent Map缓存
- Java computeIfAbsent 缓存初始化实战:少写判断、避开空值和并发坑
- 236浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · 异步编程 · 后端开发 · CompletableFuture · 接口聚合 · java 结果合并 completablefuture 并行调用 超时兜底
- Java CompletableFuture 多接口聚合完整流程:并行调用、超时兜底和结果合并
- 428浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · 线程安全 · DateTimeFormatter · 日期处理 · 并发问题 · java 线程安全 日期格式化 threadlocal SimpleDateFormat DateTimeFormatter
- Java SimpleDateFormat 日期偶发错乱怎么办:从共享实例到线程安全一步步排查
- 481浏览 收藏
-
- 文章 · java教程 | 3天前 | http接口 · httpclient · Java教程 · 接口调试 · 超时处理 · java 接口调用 httpclient 超时控制 状态码 响应体
- Java HttpClient 调接口实战:超时、状态码和响应体这样处理
- 224浏览 收藏
-
- 文章 · java教程 | 3天前 | 时间处理 · instant · Java教程 · 时区转换 · DateTimeFormatter · java DateTimeFormatter java.time 时区处理 ZoneId INSTANT
- Java 时间与时区处理实战:Instant、ZoneId 和 DateTimeFormatter 怎么配
- 461浏览 收藏
-
- 文章 · java教程 | 3天前 | Java · Stream · 集合统计 · 分组聚合 · Collectors · java Stream Collectors groupingBy counting summarizingInt
- Java Stream 分组统计实战:groupingBy、counting 和 summarizingInt 怎么用
- 478浏览 收藏
-
- 文章 · java教程 | 3天前 | Java · 文件读取 · 异常处理 · 资源管理 · try-with-resources · java 异常处理 try-with-resources 资源关闭 AutoCloseable 文件流
- Java try-with-resources 资源关闭实战:文件流和目录扫描这样写更稳
- 268浏览 收藏
-
- 文章 · java教程 | 4天前 | Java教程 · 后端开发 · BigDecimal · 金额计算 · java 舍入 bigdecimal 浮点误差 金额计算 RoundingMode
- Java BigDecimal 金额计算实战:避免浮点误差和舍入问题
- 324浏览 收藏
-
- 文章 · java教程 | 4天前 | 异步编程 · Java教程 · 超时治理 · CompletableFuture · java 异步任务 超时处理 completablefuture orTimeout completeOnTimeout
- Java CompletableFuture 超时处理实战:orTimeout 和兜底结果怎么选
- 421浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 161次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 177次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 159次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 315次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 318次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

