领先一步
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 记录作为模型,这很简洁,但是由于其他模板引擎不支持它,我们将忽略它,使示例更具可比性。
要将其编译为 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
方法)运行应用程序,您应该在 https://127.0.0.1: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 类似。模板是用一种自定义语言编写的,这种语言类似于 HTML,并具有附加的 Java 功能(有点像 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";
}
我们使用命名约定将“参数”作为特殊模型属性。这是我们稍后将看到的 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
运行示例,你将在https://127.0.0.1:8080/
看到主页。生成的源代码以每个模板一个Java类的形式输出到target/generated-sources/rocker/
目录。
$ 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
运行示例,你将在https://127.0.0.1:8080/
看到主页。生成的源代码以每个模板一个Java类的形式输出到target/generated-sources/jte/
目录。
$ 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模板语言)是另一个具有类似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
在命令行中构建并运行应用程序,并在https://127.0.0.1:8080
上检查结果。生成的源代码以每个模板一个类和辅助内容的形式输出。
$ tree target/classes/templates/
target/classes/templates/
├── Demo$LayoutOverride.class
├── Demo.class
└── Demo.html.mtl
ManTL仅在安装特殊插件后才能在IntelliJ中工作,而在Eclipse、NetBeans或VSCode中则完全无法工作。你或许可以从这些IDE运行主方法,但引用模板的代码将出现编译错误,因为缺少编译器插件。
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