
本教程详细介绍了如何使用 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";
}
}我们的目标是:
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 代码来精确控制转换逻辑。
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 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; // 不满足条件,返回未修改的注解
}
};
}
}解释:
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 会执行配方并比较结果。
通过本文的指导,您应该能够掌握如何利用 OpenRewrite 的命令式配方,实现对代码的精确控制和有条件的自动化重构。
以上就是OpenRewrite 教程:为特定方法参数精确添加或更新注解属性的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号