目录
[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实现如下,这里主要考虑了如下几种情况:
- 目标类不包含hello方法,需要添加hello方法
- 目标类已经包含hello方法,配方执行后保持不变
- 与目标类不匹配,配方执行后保持不变
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之前,我们需要理清如下几个问题:
-
匹配目标类全限定名以及添加hello方法,在LST中对应哪个具体元素
-
实现访问器逻辑需要覆盖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开发实践。