OpenRewrite:实现一个简单的配方(Recipe)

目录

1.重构BeforeAndAfter

2.SayHelloRecipe继承结构

3.编写测试类

4.实现访问器Visitor

[4.1 确定需要改写的LST元素](#4.1 确定需要改写的LST元素)

[4.2 实现访问器(Visitor)](#4.2 实现访问器(Visitor))

[4.2.1 匹配目标类](#4.2.1 匹配目标类)

[4.2.2 过滤已包含hello方法的目标类](#4.2.2 过滤已包含hello方法的目标类)

[4.2.3 目标类中添加hello方法](#4.2.3 目标类中添加hello方法)

[4.2.4 SayHelloRecipe完全体](#4.2.4 SayHelloRecipe完全体)


本文在前面文章大规模自动化重构框架--OpenRewrite浅析理解OpenRewrite的基础之上,通过实现一个简单的配方(Recipe),能够更直观的熟悉OpenRewrite的开发流程,同时起到很好的带入作用,以便更快的上手实践。

下面以配方SayHelloRecipe 如何针对指定目标类添加一个hello()方法进行举例说明;

1.重构BeforeAndAfter

举例说明的类如下,包含重构前和重构后:

Before(重构前) After(重构后)
> package com.yourorg; > class FooBar { > } > package com.yourorg; > class FooBar { > public String hello() { > return "Hello from com.yourorg.FooBar!"; > } > }

2.SayHelloRecipe继承结构

在OpenRewrite框架中,所有配方都需要继承org.openrewrite.Recipe,不包含访问器(Visitor)的最小的配方实现类如下:

java 复制代码
package com.yourorg;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.internal.lang.NonNull;

// Making your recipe immutable helps make them idempotent and eliminates a variety of possible bugs.
// Configuring your recipe in this way also guarantees that basic validation of parameters will be done for you by rewrite.
// Also note: All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
@Value
@EqualsAndHashCode(callSuper = false)
public class SayHelloRecipe extends Recipe {
    @Option(displayName = "Fully Qualified Class Name",
            description = "A fully qualified class name indicating which class to add a hello() method to.",
            example = "com.yourorg.FooBar")
    @NonNull
    String fullyQualifiedClassName;

    // All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
    @JsonCreator
    public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
        this.fullyQualifiedClassName = fullyQualifiedClassName;
    }

    @Override
    public String getDisplayName() {
        return "Say Hello";
    }

    @Override
    public String getDescription() {
        return "Adds a \"hello\" method to the specified class.";
    }

    // TODO: Override getVisitor() to return a JavaIsoVisitor to perform the refactoring
}
  • fullyQualifiedClassName: 表示需要添加hello的目标类全限定名称
  • getDisplayName:定义该配方的展示名称
  • getDescription:定义该配方的描述信息

3.编写测试类

本着测试驱动开发(TDD)的理念,在具体实现添加hello方法之前,首先定义测试类,并考虑不同的测试用例场景覆盖,用例覆盖的越完善、越全面,我们的配方也就更好用。

测试类SayHelloRecipeTest实现如下,这里主要考虑了如下几种情况:

  1. 目标类不包含hello方法,需要添加hello方法
  2. 目标类已经包含hello方法,配方执行后保持不变
  3. 与目标类不匹配,配方执行后保持不变
java 复制代码
package com.yourorg;

import org.junit.jupiter.api.Test;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class SayHelloRecipeTest implements RewriteTest {
    @Override
    public void defaults(RecipeSpec spec) {
        spec.recipe(new SayHelloRecipe("com.yourorg.FooBar"));
    }

    @Test
    void addsHelloToFooBar() {
        rewriteRun(
            java(
                """
                    package com.yourorg;

                    class FooBar {
                    }
                """,
                """
                    package com.yourorg;

                    class FooBar {
                        public String hello() {
                            return "Hello from com.yourorg.FooBar!";
                        }
                    }
                """
            )
        );
    }

    @Test
    void doesNotChangeExistingHello() {
        rewriteRun(
            java(
                """
                    package com.yourorg;
        
                    class FooBar {
                        public String hello() { return ""; }
                    }
                """
            )
        );
    }

    @Test
    void doesNotChangeOtherClasses() {
        rewriteRun(
            java(
                """
                    package com.yourorg;
        
                    class Bash {
                    }
                """
            )
        );
    }
}

如上是通过rewriteRun方法执行测试方法验证的,其中java方法的2个参数分别为:before和after,也即重构前和重构后java源代码定义,只有一个参数表示只有before,重构后保持不变。

4.实现访问器Visitor

在具体实现访问器Visitor之前,我们需要理清如下几个问题:

  1. 匹配目标类全限定名以及添加hello方法,在LST中对应哪个具体元素

  2. 实现访问器逻辑需要覆盖Recipe.getVisitor()方法,重构的核心逻辑包括:

  • 和目标类不匹配不需要进行改写
  • 目标类已经包含hello方法不需要进行改写
  • 匹配目标类且不包含hello方法,添加hello方法

下面分别进行说明;

4.1 确定需要改写的LST元素

参考Java LST examples | OpenRewrite by Moderne,结合我们的目标:匹配目标类全限定名以及添加hello方法,我们可以知道J.ClassDeclaration 包含了需要用到的信息:

  • 通过J.ClassDeclaration->name属性匹配目标类全限定名
  • 通过J.ClassDeclaration->body(J.Block)->statements判断目标类是否包含hello方法声明(J.MethodDeclaration)

4.2 实现访问器(Visitor)

实现访问器逻辑需要覆盖Recipe.getVisitor()方法,同时需要对J.ClassDeclaration进行visit,如下:

java 复制代码
// ...
public class SayHelloRecipe extends Recipe {
    // ...

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        // getVisitor() should always return a new instance of the visitor to avoid any state leaking between cycles
        return new SayHelloVisitor();
    }

    public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // TODO: Filter out classes that don't match the fully qualified name

            // TODO: Filter out classes that already have a `hello()` method

            // TODO: Add a `hello()` method to classes that need it
            return classDecl;
        }
    }
}

在visitClassDeclaration方法实现中,包含了3部分核心逻辑,下面分别进行说明;

4.2.1 匹配目标类

匹配目标类只需要判断当前visit的类的全限定名和目标类全限定名是否相等,不相等则直接返回,不进行改写:

java 复制代码
// ...
public class SayHelloRecipe extends Recipe {
    // ...

    public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // Don't make changes to classes that don't match the fully qualified name
            if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
                return classDecl;
            }

            // TODO: Filter out classes that already have a `hello()` method

            // TODO: Add a `hello()` method to classes that need it
            return classDecl;
        }
    }
}

4.2.2 过滤已包含hello方法的目标类

判断是否已包含hello方法的决策路径为:J.ClassDeclaration->body(J.Block)->statements,判断statements中是否已包含hello方法声明J.MethodDeclaration,如下:

java 复制代码
// ...
public class SayHelloRecipe extends Recipe {
    // ...

    public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // Don't make changes to classes that don't match the fully qualified name
            if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
                return classDecl;
            }

            // Check if the class already has a method named "hello"
            boolean helloMethodExists = classDecl.getBody().getStatements().stream()
                    .filter(statement -> statement instanceof J.MethodDeclaration)
                    .map(J.MethodDeclaration.class::cast)
                    .anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));

            // If the class already has a `hello()` method, don't make any changes to it.
            if (helloMethodExists) {
                return classDecl;
            }

            // TODO: Add a `hello()` method to classes that need it
            return classDecl;
        }
    }
}

4.2.3 目标类中添加hello方法

向目标类中添加方法可以使用Java Template,传入Java代码片段自动构造LST元素,免去手动构造的复杂性。添加hello方法的用法如下:

java 复制代码
// ...
public class SayHelloRecipe extends Recipe {
    // ...

    public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
        private final JavaTemplate helloTemplate =
                JavaTemplate.builder( "public String hello() { return \"Hello from #{}!\"; }")
                        .build();

        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // Don't make changes to classes that don't match the fully qualified name
            if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
                return classDecl;
            }

            // Check if the class already has a method named "hello"
            boolean helloMethodExists = classDecl.getBody().getStatements().stream()
                    .filter(statement -> statement instanceof J.MethodDeclaration)
                    .map(J.MethodDeclaration.class::cast)
                    .anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));

            // If the class already has a `hello()` method, don't make any changes to it.
            if (helloMethodExists) {
                return classDecl;
            }

            // Interpolate the fullyQualifiedClassName into the template and use the resulting LST to update the class body
            classDecl = classDecl.withBody( helloTemplate.apply(new Cursor(getCursor(), classDecl.getBody()),
                    classDecl.getBody().getCoordinates().lastStatement(),
                    fullyQualifiedClassName ));

            return classDecl;
        }
    }
}

如上,通过在body中statements的最后添加hello方法,并支持传入参数进行占位符替换。

4.2.4 SayHelloRecipe完全体

至此,SayHelloRecipe配方核心逻辑编写完毕,其完全体如下;执行测试类验证不同场景的测试用例,可以保证测试通过。后续可以利用rewrite-maven-plugin插件进行真实Java项目的自动化重构。

java 复制代码
package com.yourorg;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.internal.lang.NonNull;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;

// Making your recipe immutable helps make them idempotent and eliminates categories of possible bugs.
// Configuring your recipe in this way also guarantees that basic validation of parameters will be done for you by rewrite.
// Also note: All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
@Value
@EqualsAndHashCode(callSuper = false)
public class SayHelloRecipe extends Recipe {
    @Option(displayName = "Fully Qualified Class Name",
            description = "A fully qualified class name indicating which class to add a hello() method to.",
            example = "com.yourorg.FooBar")
    @NonNull
    String fullyQualifiedClassName;

    // All recipes must be serializable. This is verified by RewriteTest.rewriteRun() in your tests.
    @JsonCreator
    public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
        this.fullyQualifiedClassName = fullyQualifiedClassName;
    }

    @Override
    public String getDisplayName() {
        return "Say Hello";
    }

    @Override
    public String getDescription() {
        return "Adds a \"hello\" method to the specified class.";
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        // getVisitor() should always return a new instance of the visitor to avoid any state leaking between cycles
        return new SayHelloVisitor();
    }

    public class SayHelloVisitor extends JavaIsoVisitor<ExecutionContext> {
        private final JavaTemplate helloTemplate =
                JavaTemplate.builder( "public String hello() { return \"Hello from #{}!\"; }")
                        .build();

        @Override
        public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
            // Don't make changes to classes that don't match the fully qualified name
            if (classDecl.getType() == null || !classDecl.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
                return classDecl;
            }

            // Check if the class already has a method named "hello".
            boolean helloMethodExists = classDecl.getBody().getStatements().stream()
                    .filter(statement -> statement instanceof J.MethodDeclaration)
                    .map(J.MethodDeclaration.class::cast)
                    .anyMatch(methodDeclaration -> methodDeclaration.getName().getSimpleName().equals("hello"));

            // If the class already has a `hello()` method, don't make any changes to it.
            if (helloMethodExists) {
                return classDecl;
            }

            // Interpolate the fullyQualifiedClassName into the template and use the resulting LST to update the class body
            classDecl = classDecl.withBody( helloTemplate.apply(new Cursor(getCursor(), classDecl.getBody()),
                    classDecl.getBody().getCoordinates().lastStatement(),
                    fullyQualifiedClassName ));

            return classDecl;
        }
    }
}

至此,我们通过"在指定目标类中添加一个hello()方法"的实例讲解了配方(Recipe)的实际开发过程,能够对OpenRewrite的开发流程有一个简要的了解,方便后续大家针对实际项目需要进行OpenRewrite开发实践。

相关推荐
芒果披萨10 分钟前
El表达式和JSTL
java·el
许野平36 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
Tassel_YUE1 小时前
网络自动化04:python实现ACL匹配信息(主机与主机信息)
网络·python·自动化
duration~1 小时前
Maven随笔
java·maven
zmgst1 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp1 小时前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong2 小时前
Java反射
java·开发语言·反射
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb