领先一步
VMware 提供培训和认证,助您快速进步。
了解更多最近出现了一些使用文本模板但在构建时编译成 Java 类的 Java 库。因此,它们在一定程度上可以声称是“无反射”的。除了潜在的运行时性能优势外,它们还承诺易于使用并与 GraalVM 本机镜像编译集成,因此对于刚开始在 Spring Boot 3.x 中使用该技术栈的人来说非常有趣。我们将介绍一些库(JStachio、Rocker、JTE 和 ManTL)以及如何运行它们。
示例的源代码位于 GitHub 上,并且每个模板引擎都有自己的分支。示例故意保持非常简单,没有使用所有模板引擎的功能。重点在于如何将它们与 Spring Boot 和 GraalVM 集成。
由于它是我的最爱,我将从 JStachio 开始介绍。它非常易于使用,占用空间非常小,并且运行时速度也非常快。模板是使用 Mustache 编写的纯文本文件,这些文件在构建时被编译成 Java 类,并在运行时进行渲染。
在示例中,有一个用于主页 (index.mustache
) 的模板,它只打印问候语和访客计数
{{<layout}}{{$body}}
Hello {{name}}!
<br>
<br>
You are visitor number {{visits}}.
{{/body}}
{{/layout}}
它使用了一个简单的“布局”模板 (layout.mustache
)
<html>
<head></head>
<body>{{$body}}{{/body}}
</body>
</html>
(布局并非严格必需,但它是展示如何组合模板的好方法)。
JStachio APT 处理器将为它找到的每个带有 @JStache
注解的模板生成一个 Java 类,该注解用于在源代码中标识模板文件。在此示例中,我们有
@JStache(path = "index")
public class DemoModel {
public String name;
public long visits;
public DemoModel(String name, long visits) {
this.name = name;
this.visits = visits;
}
}
@JStache
注解的 path
属性是模板文件的名称,不包含扩展名(关于如何将它们组合在一起,请参见下文)。您也可以为模型使用 Java record,这很简洁,但由于其他模板引擎不支持它,我们将忽略此用法,以使示例更具可比性。
要将其编译成 Java 类,您需要在 pom.xml
中的编译器插件中添加一些配置
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>io.jstach</groupId>
<artifactId>jstachio-apt</artifactId>
<version>${jstachio.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</plugin>
JStachio 提供了一些 Spring Boot 集成,因此您只需将其添加到类路径中即可
<dependency>
<groupId>io.jstach</groupId>
<artifactId>jstachio-spring-boot-starter-webmvc</artifactId>
<version>${jstachio.version}</version>
</dependency>
您可以在控制器中使用该模板,例如
@GetMapping("/")
public View view() {
visitsRepository.add();
return JStachioModelView.of(new DemoModel("World", visitsRepository.get()));
}
此控制器返回一个由 DemoModel
构建的 View
。它也可以直接返回 DemoModel
,Spring Boot 会自动将其包装在 JStachioModelView
中。
在 DemoApplication
类中也有全局配置
@JStachePath(prefix = "templates/", suffix = ".mustache")
@SpringBootApplication
public class DemoApplication {
...
}
以及一个指向它的 package-info.java
文件(每个包含 @JStache
模型的 Java 包都需要一个这样的文件)
@JStacheConfig(using = DemoApplication.class)
package demo;
...
使用 ./mvnw spring-boot:run
(或从 IDE 中的 main
方法)运行应用程序,您应该能在 http://localhost:8080/
看到主页。
编译后生成的源代码位于 target/generated-sources/annotations
中,您可以在那里看到为 DemoModel
生成的 Java 类
$ tree target/generated-sources/annotations/
target/generated-sources/annotations/
└── demo
└── DemoModelRenderer.java
示例还包含一个 测试主方法,因此您可以使用命令行 ./mvnw spring-boot:test-run
运行,或者通过 IDE 中的测试主方法运行,并且当您在 IDE 中进行更改时,应用程序将重启。构建时编译的缺点之一是,您必须强制重新编译才能看到模板中的更改。IDE 不会自动执行此操作,因此您可能需要使用其他工具来触发重新编译。我曾成功使用以下方法来强制模型类在模板更改时重新编译
$ while inotifywait src/main/resources/templates -e close_write; do \
sleep 1; \
find src/main/java -name \*Model.java -exec touch {} \;; \
done
inotifywait
命令是一个工具,它等待文件在写入后关闭。它易于在任何 Linux 发行版或 Mac 上安装和使用。
使用 ./mvnw -P native spring-boot:build-image
(或直接使用 native-image
插件)无需额外配置即可生成本机镜像。镜像启动时间不到 0.1 秒
$ docker run -p 8080:8080 demo:0.0.1-SNAPSHOT
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.4)
2024-03-22T12:23:45.403Z INFO 1 --- [ main] demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.10 with PID 1 (/workspace/demo.DemoApplication started by cnb in /workspace)
2024-03-22T12:23:45.403Z INFO 1 --- [ main] demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2024-03-22T12:23:45.418Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2024-03-22T12:23:45.419Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-03-22T12:23:45.419Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2024-03-22T12:23:45.429Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-03-22T12:23:45.429Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 26 ms
2024-03-22T12:23:45.462Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2024-03-22T12:23:45.462Z INFO 1 --- [ main] demo.DemoApplication : Started DemoApplication in 0.069 seconds (process running for 0.073)
Rocker 的使用方式与 JStachio 类似。模板是用一种自定义语言编写的,类似于带有额外 Java 特性的 HTML(有点像 JSP)。主页看起来像这样 (demo.rocker.html
)
@import demo.DemoModel
@args(DemoModel model)
@templates.layout.template("Demo") -> {
<h1>Demo</h1>
<p>Hello @model.name!</p>
<br>
<br>
<p>You are visitor number @model.visits.</p>
}
它导入了 DemoModel
对象 - 实现与 JStachio 示例相同。模板还直接引用其布局(调用 templates.layout
上的静态方法)。布局是一个独立的模板文件 (layout.rocker.html
)
@args (String title, RockerBody content)
<html>
<head>
<title>@title</title>
</head>
<body>
@content
</body>
</html>
Rocker 需要一个 APT 处理器,并且需要手动将生成的源代码添加到构建输入中。所有这些都可以在 pom.xml
中配置
<plugin>
<groupId>com.fizzed</groupId>
<artifactId>rocker-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<?m2e execute onConfiguration,onIncremental?>
<id>generate-rocker-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<javaVersion>${java.version}</javaVersion>
<templateDirectory>src/main/resources</templateDirectory>
<outputDirectory>target/generated-sources/rocker</outputDirectory>
<discardLogicWhitespace>true</discardLogicWhitespace>
<targetCharset>UTF-8</targetCharset>
<postProcessing>
<param>com.fizzed.rocker.processor.LoggingProcessor</param>
<param>com.fizzed.rocker.processor.WhitespaceRemovalProcessor</param>
</postProcessing>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/rocker</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
控制器的实现非常常规——它构造一个模型并返回“demo”视图的名称
@GetMapping("/")
public String view(Model model) {
visitsRepository.add();
model.addAttribute("arguments", Map.of("model", new DemoModel("mystérieux visiteur", visitsRepository.get())));
return "demo";
}
我们使用命名约定将“arguments”作为特殊的模型属性。这是我们稍后将看到的 View
实现的细节。
Rocker 没有自带 Spring Boot 集成,但这不难实现,而且只需实现一次。示例包含一个 View
实现,以及一个 ViewResolver
和 RockerAutoConfiguration
中的一些配置
@Configuration
public class RockerAutoConfiguration {
@Bean
public ViewResolver rockerViewResolver() {
return new RockerViewResolver();
}
}
RockerViewResolver
是一个使用 Rocker 模板引擎渲染模板的 ViewResolver
。View
实现是 Rocker 模板类的包装器
public class RockerViewResolver implements ViewResolver, Ordered {
private String prefix = "templates/";
private String suffix = ".rocker.html";
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RockerView view = new RockerView(prefix + viewName + suffix);
return view;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
}
如果查看 RockerView
的实现,您会发现它是 Rocker 模板类的包装器,并且它包含一些反射代码来查找模板参数名称。这对于本机镜像可能是一个问题,因此它并不理想,但我们稍后会看到如何解决它。Rocker 内部也使用反射将模板参数绑定到模型,因此它无论如何也不是完全无反射的。
如果使用 ./mvnw spring-boot:run
运行示例,您将看到主页在 http://localhost:8080/
。生成的源代码在 target/generated-sources/rocker/
中,每个模板生成一个 Java 类
$ tree target/generated-sources/rocker/
target/generated-sources/rocker/
└── templates
├── demo.java
└── layout.java
本机镜像需要一些额外配置来允许渲染期间的反射。我们对此进行了一些尝试,很快就发现反射在 Rocker 内部被广泛使用,使其与 GraalVM 配合工作需要大量精力。或许有一天值得再回来研究。
(JTE 示例直接复制自项目文档。本文档中的其他示例之所以采用目前的结构,是因为它们模仿了此示例。)
与 Rocker 类似,JTE 具有类似于 HTML 并带有额外 Java 特性的模板语言。项目文档中的模板位于 java
旁边的 jte
目录中,因此我们采用相同的约定。主页看起来像这样 (demo.jte
)
@import demo.DemoModel
@param DemoModel model
Hello ${model.name}!
<br>
<br>
You are visitor number ${model.visits}.
此示例中没有布局模板,因为 JTE 不显式支持模板组合。DemoModel
与我们用于其他示例的类似。
在 pom.xml
中,您需要添加 JTE 编译器插件
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>${jte.version}</version>
<configuration>
<sourceDirectory>${basedir}/src/main/jte</sourceDirectory>
<contentType>Html</contentType>
<binaryStaticContent>true</binaryStaticContent>
</configuration>
<executions>
<execution>
<?m2e execute onConfiguration,onIncremental?>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
以及一些源代码和资源复制
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/jte</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>process-classes</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<resources>
<resource>
<directory>${basedir}/target/generated-sources/jte</directory>
<includes>
<include>**/*.bin</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
运行时依赖是
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte</artifactId>
<version>${jte.version}</version>
</dependency>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-spring-boot-starter-3</artifactId>
<version>${jte.version}</version>
</dependency>
控制器的实现非常常规——事实上,它与我们用于 Rocker 的实现完全相同。
JTE 自带 Spring Boot 自动配置(我们已在 pom.xml
中添加),因此您几乎不需要做任何其他事情。要使其与 Spring Boot 3.x 配合工作,您只需做一件小事,即在 application.properties
文件中添加一个属性。在开发时,特别是如果您使用 Spring Boot Devtools,您会希望设置
gg.jte.developmentMode=true
在生产环境中,通过 Spring profile 关闭它,并改用 gg.jte.usePrecompiledTemplates=true
。
如果使用 ./mvnw spring-boot:run
运行示例,您将看到主页在 http://localhost:8080/
。生成的源代码在 target/generated-sources/jte/
中,每个模板生成一个 Java 类
$ tree target/generated-sources/jte/
target/generated-sources/jte/
└── gg
└── jte
└── generated
└── precompiled
├── JtedemoGenerated.bin
└── JtedemoGenerated.java
.bin
文件是文本模板的有效二进制表示形式,在运行时使用,因此需要将其添加到类路径中。
可以通过一些额外配置生成本机镜像。我们需要确保 .bin
文件可用,并且生成的 Java 类可以被反射
@SpringBootApplication
@ImportRuntimeHints(DemoRuntimeHints.class)
public class DemoApplication {
...
}
class DemoRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.resources().registerPattern("**/*.bin");
hints.reflection().registerType(JtedemoGenerated.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS);
}
}
因此,JTE 并非完全无反射,但可以相当容易地配置它与 GraalVM 本机一起工作。
ManTL (Manifold Template Language) 是另一个具有 Java 类似语法的模板引擎。模板在构建时编译成 Java 类,与其他示例一样。主页看起来像这样 (Demo.html.mtl
)
<%@ import demo.DemoModel %>
<%@ params(DemoModel model) %>
Hello ${model.name}!
<br>
<br>
You are visitor number ${model.visits}.
其中 DemoModel
与其他示例中的相同。
Manifold 与其他示例有点不同,它使用 JDK 编译器插件,而不是 APT 处理器。pom.xml
中的配置稍微复杂一些。有 maven-compiler-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<compilerArgs>
<arg>-Xplugin:Manifold</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>systems.manifold</groupId>
<artifactId>manifold-templates</artifactId>
<version>${manifold.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
和运行时依赖
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-templates-rt</artifactId>
<version>${manifold.version}</version>
</dependency>
此示例中的控制器更像 JStachio 的控制器,而不是 Rocker/JTE 的控制器
@GetMapping("/")
public View view(Model model, HttpServletResponse response) {
visitsRepository.add();
return new StringView(() -> Demo.render(new DemoModel("mystérieux visiteur", visitsRepository.get())));
}
其中 StringView
是一个方便的类,用于包装模板并渲染它
public class StringView implements View {
private final Supplier<String> output;
public StringView(Supplier<String> output) {
this.output = output;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
String result = output.get();
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentLength(result.getBytes().length);
response.getOutputStream().write(result.getBytes());
response.flushBuffer();
}
}
您可以使用命令行 ./mvnw spring-boot:run
构建并运行应用程序,并在 http://localhost:8080
上查看结果。生成的源代码每个模板包含一个类和一些辅助文件
$ tree target/classes/templates/
target/classes/templates/
├── Demo$LayoutOverride.class
├── Demo.class
└── Demo.html.mtl
ManTL 只能在安装特殊插件后在 IntelliJ 中工作,完全不能在 Eclipse、NetBeans 或 VSCode 中工作。您或许可以从这些 IDE 运行 main 方法,但引用模板的代码会出现编译错误,因为编译器插件缺失。
GraalVM 不支持此编译器插件,因此您无法将 ManTL 与 GraalVM 本机镜像一起使用。
我们在此介绍的所有模板引擎在构建时将模板编译成 Java 类,因此在这一点上是无反射的。它们都易于使用并与 Spring 集成,并且都具有或可以提供某种 Spring Boot 自动配置。JStachio 是运行时最轻量、最快的,并且对 GraalVM 本机镜像的支持最好。Rocker 在运行时也非常快,但它内部使用反射,并且不易使其与 GraalVM 配合工作。JTE 配置稍微复杂一些,但运行时也非常快,并且易于使其与 GraalVM 配合工作。ManTL 配置最复杂,并且完全无法与 GraalVM 配合工作。它也只能在 IntelliJ 作为 IDE 时工作。
如果您想查看更多示例,每个模板引擎都有自己的文档,请点击上面的链接。我在 JStachio 方面的工作产生了一些额外的示例,例如 Mustache PetClinic,以及由 Ollie Drotbohm 最初实现的 Todo MVC,并已适配到各种不同的模板引擎。
Dave Syer
伦敦 2024