領先一步
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://: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";
}
我們正在將“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 執行示例,您將在 https://:8080/ 看到主頁。生成的原始碼在 target/generated-sources/rocker/ 中,每個模板生成一個 Java 類。
$ tree target/generated-sources/rocker/
target/generated-sources/rocker/
└── templates
├── demo.java
└── layout.java
原生映象需要一些額外的配置才能允許在渲染期間進行反射。我們為此嘗試了幾次,很快就發現反射在 Rocker 的內部被廣泛使用,要讓它與 GraalVM 配合使用需要付出很大的努力。也許有一天值得回來再看看。
(JTE 示例直接複製自專案文件。本文件中的其他示例之所以具有現在的結構,是因為它們模仿了 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 配置檔案將其關閉,並使用 gg.jte.usePrecompiledTemplates=true。
如果您使用 ./mvnw spring-boot:run 執行示例,您將在 https://: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 在命令列上構建並執行應用程式,並在 https://: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,以及一個 Todo MVC 實現,最初由 Ollie Drotbohm 建立並改編為各種不同的模板引擎。
Dave Syer
倫敦 2024