OpenRewrite 学习笔记(四):使用 JavaTemplate 创建复杂 LST
LST 是 OpenRewrite 的核心,是 OpenRewrite 实现精准、可控代码修改的关键支柱。本文将介绍如何使用 JavaTemplate 创建复杂的 LST。
背景
在操作代码的过程中,可能需要添加、修改、删除代码片段。比如添加一个变量声明、添加一个方法、修改一个方法体等。这些操作都需要创建 LST(Lossless Semantic Tree)。那如何创建一个变量声明的 LST?
我们需要创建变量声明 J.VariableDeclarations 以及变量 J.VariableDeclarations.NamedVariable,并将其添加到方法体中。看起来很简单,但是在使用构造器初始化对象时需要提供很多参数。何况很多参数自身的初始化也是一个复杂的过程,需要保证类型的属性匹配准确。
实际上不需要这么麻烦,也是 官方不推荐的方式。OpenRewrite 提供了 JavaTemplate 来简化这个过程。
JavaTemplate javaTemplate = JavaTemplate.builder("List<String> list = new ArrayList<>();") .build();
上面的代码片段中,通过 JavaTemplate 构建了一个变量声名赋值的 LST。使用 JavaTemplate 可以生成格式准确、类型属性完整的 LST,而不需要手动构造。
构造 JavaTemplate
参数化模板
模板代码是通过 String 代码片段创建的,使用的代码片段必须是合法的代码,可以引用插入点的词法范围内可见的变量、方法和其他符号。
虽然通过 String 片段创建的模板代码是静态的,但是可以通过占位符 #{}
来引用外部变量,进而实现动态的参数化模板。
JavaTemplate 支持两种占位符:无类型占位符 #{}
和有类型占位符 #{any(<type>)}
。使用后者时,要提供 type
类型的 LST。
无类型占位符
无类型占位符通常用于将 String 作为模板参数,也是最常用的占位符。这种通常用来插入上下文中类型不相关的 LST,如注释、类名、方法名、变量名,以及关键字等,不会进行类型检查。
比如下面的例子中定义了一个方法模板,其中的两个 #{}
都是无类型占位符。第一个占位符用于插入方法名,在生成的 LST 中作为 J.Identifier 节点,;第二个占位符作为方法返回的字符串的一部分。
JavaTemplate.builder("public String #{}() { return \"Hello from #{}!\"; }").build();
如果占位符要替换的内容带有类型属性,也是一个 LST,可以使用有类型占位符。比如在要获取 String 的字符数组,可以直接调用其 toCharArray() 方法。但不规范的代码可能会先调用 toString() 方法,再调用 toCharArray() 方法。toString() 方法的调用就显得多余了,要去掉这个多余的调用,将方法调用替换为变量名。
arg.toString().toCharArray()
arg.toCharArray()
要使用的模板就非常简单了:#{},直接使用占位符使用原来的变量即可。如果只是传入一个变量名,就会丢失变量的类型信息。正确的做法是传入一个 LST,也就是带有类型信息的节点,此时就要用到有类型占位符。
有类型占位符
有类型占位符使用带有类型信息的占位符语法 #{any(<type>)}
,使用 LST 作为参数插入,并进行类型检查。type
就是预期的参数类型。要插入数组时,可以使用 #{anyArray(<type>)}
。
boolean b = #{any(boolean)};
foo.acceptsACollection(#{any(java.util.Collection)});
String[] = #{anyArray(java.lang.String)};
比如上面的例子,我们要将 arg.toString() 替换为 arg,就可以使用 #{any(java.lang.String)}。
如果要插入的类型不确定,可以使用通配符 #{any()},这样就不会进行类型检查。下面例子中有定义了两个模板,分别用于生成 getter 和 setter 方法。
JavaTemplate getter = JavaTemplate
.builder("" + "public #{} #{}() {return #{any()};}").contextSensitive().build();
JavaTemplate setter = JavaTemplate
.builder("" + "public void set#{}(#{} #{}) {this.#{} = #{};}").contextSensitive().build()
这里使用了 contextSensitive,是因为代码片段用到了类变量。
当代码片段中引用了插入范围内可见的类、变量、方法或其他符号时,它就是上下文相关的。如果模板是完全独立,则是上下文无关的,可以被缓存,因为其生成的 LST 节点插入到哪里都是一样的。
如
int a = 0
上下文无关的;而int a = b
需要参考上下文才能理解b
,因此是上下文相关的。
类型感知
JavaTemplate 使用的 JavaParser 默认只能感知 Java 运行时提供的类型。不过很多情况下,我们需要从外部库中引用类型。
额外的 import
如果代码片段中引入了新的类型,需要在 JavaTemplate 中指定这些类型。可以通过 JavaParser.Builder.imports()
方法指定要引入的类型,如果是静态 import,可以使用 JavaParser.Builder.staticImports()
方法。
JavaTemplate.builder("List<String> list = new ArrayList<>();")
.imports("java.util.LIst", "java.util.ArrayList")")
.build();
引用外部类型
如果代码片段中引用了其他库的类型,则必须提供配置了这些类型的 JavaParser。
通过 JavaParser.Builder.classpath()
方法,可以将 JavaParser 配置为从项目的运行时类路径中查找库。比如在上一篇文章的 示例 中,我们使用 JavaTemplate 添加 org.springframework.web.bind.annotation.RequestMapping
Annotation 时,我们指定从 spring-web 中查找对应的类型。
JavaTemplate.builder(
"@RequestMapping#{}"
)
.javaParser(JavaParser.fromJavaVersion()
.classpath("spring-web")
)
如果运行时类路径中没有库,可以通过 stubbing 的方式来提供提供对应类型的 stub。
JavaTemplate.builder(
"@" + annotationSimpleName + "#{}"
)
.javaParser(JavaParser.fromJavaVersion()
.dependsOn("package org.springframework.web.bind.annotation;\n" +
"public @interface RequestMapping { }")
)
说了这么多 JavaTemplate 的构造方式,下面看看如何使用 JavaTemplate。
使用
JavaTemplate 在构造完成后,我们可以使用方法 JavaTemplate apply(..)
将其加入到当前的 LST 中。
还是以上一篇文章的 示例 为例,我们要添加 @RequestMapping
注解到类上。
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
//...
return JavaTemplate.builder(/**/).build()
.apply(getCursor(), classDecl.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)), attrStr);
}
调用 JavaTemplate#apply()
方法,需要指定坐标(Coordinates)。
classDecl.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName))
Coordinates 坐标
大多数的 LST 都非常复杂,模板的应用方式不只一种。为了解决这个问题,LST 都会提供一个或者多个坐标,来指示模板的应用方式。比如一个非常简单的类声明:
public class Foo { }
我们可能需要添加或修改注解、修改类型参数(泛型类型)、修改继承类、添加或修改实现接口等操作。我们来看下类声明 LST(ClassDeclaration)提供了哪些坐标:
public abstract class CoordinateBuilder {
...
public static class ClassDeclaration extends Statement {
public JavaCoordinates addAnnotation(Comparator<J.Annotation> idealOrdering) {}
public JavaCoordinates replaceAnnotations() {}
public JavaCoordinates replaceTypeParameters() {}
public JavaCoordinates replaceExtendsClause() {}
public JavaCoordinates replaceImplementsClause() {}
public JavaCoordinates addImplementsClause() {}
}
...
}
通过查看 org.openrewrite.java.tree.CoordinateBuilder,可以查看 Java 中各个不同类型 LST 的坐标。
示例
下面的例子很简单,向类中添加一个 hello
方法,方法返回的字符串中包含类名。在示例中我们用到了之前介绍过的:LST、如何通过 Visitor 操作 LST、通过 Cursor 实现 LST 操作过程中的信息传递。当然,还有本文介绍的 JavaTemplate。
public class AddHelloMethod extends Recipe {
@Override
public String getDisplayName() {
return "Add Hello Method";
}
@Override
public String getDescription() {
return "Add a *hello* method to specified class.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
private final JavaTemplate helloTemplate = JavaTemplate.builder("public String hello() { return \"Hello from #{}!\"; }").build();
private J.ClassDeclaration classDeclaration;
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
this.classDeclaration = classDecl;
return super.visitClassDeclaration(classDecl, executionContext);
}
@Override
public J.Block visitBlock(J.Block b, ExecutionContext executionContext) {
J.Block block = super.visitBlock(b, executionContext);
if (getCursor().getParent().getValue() == this.classDeclaration && !getCursor().getMessage("methodExists", false)) {
block = helloTemplate.apply(getCursor(), block.getCoordinates().lastStatement(), classDeclaration.getType().getClassName());
}
return block;
}
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
if ("hello".equals(method.getSimpleName())) {
getCursor().getPathAsCursors(c -> c.getValue() instanceof J.Block && c.getParent().getValue() == this.classDeclaration)
.next()
.putMessage("methodExists", true);
}
return method;
}
};
}
}