領先一步
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
方法)執行應用程式,您應該會在 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";
}
我們正在使用「arguments」的命名慣例作為一個特殊的模型屬性。這是我們稍後將看到的 View
實作的詳細資訊。
Rocker 沒有自己的 Spring Boot 整合,但實作起來並不難,而且您只需要做一次。該範例包含一個 View
實作,加上一個 ViewResolver
和 RockerAutoConfiguration
中的一些設定
@Configuration
public class RockerAutoConfiguration {
@Bean
public ViewResolver rockerViewResolver() {
return new RockerViewResolver();
}
}
RockerViewResolver
是一個 ViewResolver
,它使用 Rocker 樣板引擎來呈現樣板。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 模板類別的一個封裝器,並且它包含一些反射程式碼來尋找模板參數名稱。這對於 native image 而言可能是一個問題,所以它並不是最理想的選擇,但我們稍後會看到如何修正它。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
Native image 需要一些額外的設定才能允許在渲染期間進行反射。我們嘗試了幾次,很快就發現 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 設定檔將其關閉,並改為使用 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
檔案是文字模板的有效二進位表示法,用於執行階段,因此需要將其添加到類別路徑中。
可以使用一些額外的設定產生 native image。我們需要確保 .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 native 協同工作。
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 執行 main 方法,但引用模板的程式碼會出現編譯器錯誤,因為缺少編譯器外掛程式。
GraalVM 不支援編譯器外掛程式,因此你無法將 ManTL 與 GraalVM native image 搭配使用。
我們在這裡研究的所有模板引擎都是無反射的,意思是模板會在建置時編譯成 Java 類別。它們都很容易使用,並且可以與 Spring 整合,而且它們都有或可以提供某種 Spring Boot 自動配置。JStachio 是最輕量級且運行時速度最快的,並且它對 GraalVM native images 有最佳的支援。Rocker 在運行時也很快,但它在內部使用反射,而且不容易讓它與 GraalVM 協同工作。JTE 的配置有點複雜,但它在運行時也很快,而且很容易讓它與 GraalVM 協同工作。ManTL 的配置最複雜,而且它根本不能與 GraalVM 協同工作。它也只能與 IntelliJ 作為 IDE 使用。
如果您想看到更多的範例,那麼每個模板引擎都有自己的文檔,請點擊上面的連結。我自己在 JStachio 上的工作產生了一些額外的範例,例如 Mustache PetClinic,還有一個由 Ollie Drotbohm 原創並改編為各種不同模板引擎的 Todo MVC 實現。
Dave Syer
倫敦 2024