OpenRewrite 学习笔记(四):使用 JavaTemplate 创建复杂 LST

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()},这样就不会进行类型检查。下面例子中有定义了两个模板,分别用于生成 gettersetter 方法。

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;
            }
        };
    }
}

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus