2个月前 (07-22)  Java系列 |   抢沙发  40 
文章评分 0 次,平均分 0.0

我们可以在JDK 21中使用字符串模板https://openjdk.org/jeps/430)作为预览功能。我发现的一个很好的用例是生成Java源代码。

因此,在这篇博客文章中,我们将编写一个适合生成Java源代码的字符串模板处理器。此外,它应该作为Java开发人员对字符串模板的介绍。

关于这篇博客文章中提出的实施,请注意以下注意事项:

  • 它的行为就像一个字符串插值器,因为它不验证输入
  • 它是有状态的

简而言之,它是一种具有副作用的插值器。而且,如果我理解正确的话,这并不是JEP 430(https://openjdk.org/jeps/430)设计师心目中最好的使用方式。

迭代01:没有表达式的模板

字符串模板允许我们在其中嵌入正则Java表达式。在以下示例中:

String what = "Hi!";
String greeting = STR."Say \{what}";

字符串模板包含\{what}嵌入表达式。它指的是之前声明的局部变量。

字符串模板也可以没有嵌入表达式:

String greeting = STR."Say Hi!";

这可能不是字符串模板的最佳用途。无论如何,当我们不声明嵌入式表达式时,我们的处理器实现必须工作。

我们从以下测试用例开始:

@Test(description = "no embedded expressions")
public void testCase01() {
  Code code;
  code = new Code();

  String java;
  java = code."public class TestCase01 {}";

  assertEquals(java, "public class TestCase01 {}");
}

首先,我们通过调用处理器的构造函数来创建处理器的实例。我们的模板处理器实现名为Code

接下来,使用我们的处理器实例,我们评估一个字符串模板。模板不包含任何嵌入表达式。我们的处理器产生一个String值。

最后,在方法的底部,我们验证了产生的值。我们希望生成的值等于模板,就像它是一个普通字符串一样。

首先,我们的Code类必须是StringTemplate。处理器实例:

public class Code 
    implements 
    StringTemplate.Processor<String, RuntimeException> {

  @Override
  public final String process(
  	  StringTemplate template) throws RuntimeException {
    ...
  }
  
}

它是一个声明两个类型参数的函数接口。

第一个类型参数是处理器结果的类型。从我们的测试用例中,我们希望我们的处理器生成一个String值。

第二个类型参数是流程方法抛出的异常的类型。从我们的测试用例来看,我们不清楚处理器应该抛出哪种类型的异常。但我们必须声明一个类型。因此,目前,我们已经声明它只会抛出RuntimeException

以下是我们的流程方法实现:

@Override
public final String process(StringTemplate template) throws RuntimeException {
  List<String> fragments;
  fragments = template.fragments();

  int fragmentsSize;
  fragmentsSize = fragments.size();

  if (fragmentsSize == 1) {
    return fragments.getFirst();
  }

  return process0(template, fragments);
}

protected String process0(StringTemplate template, List<String> fragments) {
  throw new UnsupportedOperationException("Implement me");
}

在运行时,字符串模板被分解为片段。

片段的数量是模板中声明的嵌入表达式数量的函数:

  • 没有嵌入表达式的模板将只有一个片段;
  • 具有一个嵌入表达式的模板将恰好有两个片段;
  • 具有两个嵌入表达式的模板将恰好有三个片段;
  • 等等。

我们的特定测试用例属于第一种情况。因此,我们只需返回运行时模板的第一个也是唯一一个片段。

迭代02:单个嵌入式表达式

让我们处理带有嵌入式表达式的模板。

我们将从一个嵌入式表达式开始,但代码应该适用于任何数量的表达式。

以下是我们的测试用例:

@Test(description = "single embedded expression")
public void testCase02() {
  Code code;
  code = new Code();

  String message;
  message = "Hello world!";

  String java;
  java = code."""
  public class TestCase02 {
    public static void main(String[] args) {
      System.out.println("\{message}");
    }
  }
  """;

  assertEquals(java, """
  public class TestCase02 {
    public static void main(String[] args) {
      System.out.println("Hello world!");
    }
  }
  """);
}

我们的模板包含\{message}嵌入表达式。它指的是消息局部变量。

我们验证生成的结果是否正确包含消息局部变量的值。

以下是一个使我们的测试通过的实现:

public class Code extends iter01.Code {
  @Override
  protected final String process0(StringTemplate template, List<String> fragments) {
    Object[] values;
    values = values(template);

    StringBuilder out;
    out = new StringBuilder();

    for (int idx = 0, len = values.length; idx < len; idx++) {
      String fragment;
      fragment = fragments.get(idx);

      out.append(fragment);

      Object value;
      value = values[idx];

      out.append(value);
    }

    String lastFragment;
    lastFragment = fragments.getLast();

    out.append(lastFragment);

    return out.toString();
  }

  protected Object[] values(StringTemplate template) {
    List<Object> values;
    values = template.values();

    return values.toArray();
  }
}

它扩展了前一次迭代的实现,并覆盖了process0方法。

它首先从模板中获取所有值。StringTemplate::values方法返回一个对象列表。每个对象都是按遇到顺序计每个嵌入表达式的结果。

接下来,在for循环中,它将一个片段和一个值附加到结果字符串中。

最后,添加最后一个片段并返回结果。

因此,总之,它只是插值片段和值。

迭代03:导入声明

我认为,为了有用,Java源代码生成器应该自动生成导入声明。

因此,让我们在字符串模板处理器中添加这样的功能。

此外,我们应该测试一个包含多个嵌入式表达式的模板。

这就是我们希望进口设施的工作方式:

@Test
public void testCase03() {
  Code code;
  code = new Code();

  ClassName LIST;
  LIST = ClassName.of("java.util", "List");

  String packageName;
  packageName = "com.example";

  ImportList imports;
  imports = code.importList(packageName);

  String java;
  java = code."""
  package \{packageName};
  \{imports}
  public class TestCase02 {
    public static void main(String[] args) {
      \{LIST}<String> msgs = \{LIST}.of("Foo", "Bar");
      for (var msg : msgs) {
        System.out.println(msg);
      }
    }
  }
  """;

  assertEquals(java, """
  package com.example;

  import java.util.List;

  public class TestCase02 {
    public static void main(String[] args) {
      List<String> msgs = List.of("Foo", "Bar");
      for (var msg : msgs) {
        System.out.println(msg);
      }
    }
  }
  """);
}

首先,我们介绍一个ClassName类。它表示生成的Java代码中引用的类型的完全限定名。

接下来,我们介绍ImportList类。它表示一个(可能为空)进口申报列表。它绑定到我们处理器的特定实例。它还绑定到特定的包名称;它不应该为来自同一包的类型生成导入声明。
此外,如声明中所述,ImportList应在进口申报的上方和下方生成空行。

此外,如声明中所述,ImportList应在进口申报的上方和下方生成空行。

下面是一个使我们的测试通过的实现:

public class Code extends iter02.Code {
  private final ImportList importList = new ImportList();

  public final ImportList importList(String packageName) {
    Objects.requireNonNull(packageName, "packageName == null");
    importList.set(packageName);
    return importList;
  }

  @Override
  protected final Object[] values(StringTemplate template) {
    List<Object> values;
    values = template.values();

    int size;
    size = values.size();

    Object[] result;
    result = new Object[size];

    for (int idx = 0; idx < size; idx++) {
      Object value;
      value = values.get(idx);

      if (value instanceof ClassName className) {
        value = importList.process(className);
      } else {
        value = processValue(value);
      }

      result[idx] = value;
    }

    return result;
  }

  protected Object processValue(Object value) {
    return value;
  }
}

它扩展了前一次迭代的实现,并覆盖了values方法。

它首先声明一个ImportList私有实例。importList(String)方法返回实例,同时将其绑定到指定的包名。
values方法迭代模板的值。当特定值是ClassName实例时,它将由ImportList实例进一步处理。如果值的类型不同,则值保持不变。

接下来,让我们仔细看看ImportList类。

以下测试用例指定了ImportList类的工作方式:

@Test(description = "Happy path")
public void testCase01() {
  ImportList imports;
  imports = new ImportList("com.example");

  ClassName list;
  list = ClassName.of("java.util", "List");

  ClassName awt;
  awt = ClassName.of("java.awt", "List");

  ClassName inputStream;
  inputStream = ClassName.of("java.io", "InputStream");

  ClassName foo;
  foo = ClassName.of("com.example", "Foo");

  assertEquals(imports.process(list), "List");
  assertEquals(imports.process(awt), "java.awt.List");
  assertEquals(imports.process(inputStream), "InputStream");
  assertEquals(imports.process(foo), "Foo");
  assertEquals(imports.toString(), """

  import java.io.InputStream;
  import java.util.List;
  """);
}

首先,将ImportList实例绑定到特定的包名称。

  • java.util.List类型被处理为其简单名称List,并被导入。
  • java.awt.List类型被处理为其全名,不会被导入。
  • java.io.InputStream类型被处理为其简单名称InputStream并被导入。
  • com.example.Foo类型被处理为其简单名称Foo,并且不会被导入。

迭代04:string literals

以下操作将生成无效的Java代码:

String s = """
The message has "quotes".
And new lines
""";

String invalid;
invalid = code."String s = \"\{s}\";"

我们应该正确地转义将在生成的Java代码中用作字符串文字的字符串值。

结论

在这篇博客文章中,我们开发了一个用于生成Java源代码的字符串模板处理器。

正如引言中所指出的,人们应该了解实施注意事项:

  • 它几乎不进行处理。它的行为更像一个插值器
  • 它通过ImportList类产生副作用

尽管如此,它本应作为字符串模板的入门。特别地:

  • 处理器必须实现StringTemplate.Processor接口
  • 我们使用新的字符串模板文字语法调用处理器
  • 它允许在其中嵌入正则Java表达式
  • 在运行时,模板被分解为片段和值

它没有涵盖与实现模板处理器相关的所有主题:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StringTemplate.Processor.Linkage.html

原文地址:https://www.objectos.com.br/blog/generating-java-source-code-with-string-templates.html

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2993.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册