我们可以在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
暂无评论