在 Spring Boot GraalVM 原生镜像中使用 Thymeleaf 布局和 Fragment 表达式
在 Spring Boot + Thyemleaf 的应用中,我们可以使用 thymeleaf-layout-dialect 来定义网页的通用布局,效果很好。
但是当我们将 Spring Boot 应用编译到 GraalVM 原生镜像时,却 出现了问题。
GraalVM Native Image: Generating 'demo' (executable)...
========================================================================================================================
[1/7] Initializing... (5,6s @ 0,32GB)
Version info: 'GraalVM 22.3.1 Java 17 CE'
Java version info: '17.0.6+10-jvmci-22.3-b13'
C compiler: gcc (linux, x86_64, 11.3.0)
Garbage collector: Serial GC
2 user-specific feature(s)
- com.oracle.svm.polyglot.groovy.GroovyIndyInterfaceFeature
- org.springframework.aot.nativex.feature.PreComputeFieldFeature
Field org.apache.commons.logging.LogAdapter#log4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#log4jSlf4jProviderPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jApiPresent set to true at build time
Field org.springframework.core.KotlinDetector#kotlinPresent set to true at build time
Field org.springframework.core.KotlinDetector#kotlinReflectPresent set to true at build time
Field org.springframework.core.NativeDetector#imageCode set to true at build time
Field org.springframework.format.support.DefaultFormattingConversionService#jsr354Present set to false at build time
Field org.springframework.cglib.core.AbstractClassGenerator#imageCode set to true at build time
[2/7] Performing analysis... [] (8,5s @ 3,78GB)
9 047 (82,33%) of 10 989 classes reachable
12 287 (66,39%) of 18 507 fields reachable
37 945 (49,30%) of 76 964 methods reachable
612 classes, 363 fields, and 3 625 methods registered for reflection
Error: Classes that should be initialized at run time got initialized during image building:
java.beans.Introspector was unintentionally initialized at build time. To see why java.beans.Introspector got initialized use --trace-class-initialization=java.beans.Introspector
To see how the classes got initialized, use --trace-class-initialization=java.beans.Introspector
Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
我尝试了前面 Issue 中提到的许多建议,但都不奏效。
后来,Oliver Drotbohm 向我推荐了 Flexible layouts(灵活布局)的方法,以创建 Thymeleaf 本身提供的布局支持。这种方法在 GraalVM 原生镜像中也同样适用。
本文将带你了解何在不使用 thymeleaf-layout-dialect 的情况下使用 Thymeleaf 创建布局。
创建 Spring Boot 应用
通过 Spring Initializr 初始化 Spring Boot 应用,添加 Web、Thymeleaf 和 GraalVM Native Support 依赖。
创建布局模板
在 src/main/resources/templates
目录下创建布局模板 layout.html
。
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
th:fragment="layout (title, content, pageScripts)" >
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>MyApp - <th:block th:insert="${title}"/></title>
</head>
<body>
<main>
<div id="app" class="container">
<div th:block th:replace="${content}">
<p>Layout content</p>
</div>
</div>
</main>
<th:block th:replace="${pageScripts}">
</th:block>
</body>
</html>
说明:
html
根标签有th:fragment="layout (title, content, pageScripts)"
属性,该属性为片段(Fragment)命名并定义其接受的参数。- 布局(Layout)有三个参数:
title
、content
和pageScripts
。title
参数用于设置页面标题。content
参数用于包含页面内容。pageScripts
参数用于包含特定页面的脚本。
- 有些应用的标题(
title
)模式是"AppName - PageName"
。这里使用了相同的模式。
创建页面模板
在 src/main/resources/templates
目录下创建一个使用该布局的页面模板 home.html
。
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
th:replace="~{layout :: layout(title=~{::title/text()},
content=~{::#content}, pageScripts=~{})}">
<head>
<title>Home</title>
</head>
<body>
<div id="content">
<h1>This is Home Page</h1>
<a href="/">Home Page</a>
<a href="/about">About Page</a>
</div>
</body>
</html>
说明:
- html 根标签有
th:replace="~{layout :: layout(...)}"
属性,它使用片段表达式指定要替换的片段。这里我们引用了文件名layout.html
和片段名layout
。 - 然后,用命名参数传递所有参数值
title
参数使用片段表达式~{::title/text()}
指定。这指的是当前页面模板中<title>
元素的文本内容。content
参数使用片段表达式~{::#content}
指定。这指的是当前页面模板中<div id="content">
元素的内容。pageScripts
参数使用片段表达式~{}
指定。这指的是一个空片段,指定无标记。
除了使用命名参数指定参数值外,还可以使用默认位置参数指定参数值,如下所示:
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
th:replace="~{layout :: layout(~{::title/text()}, ~{::#content},~{})}">
...
</html>
创建其他页面模板
在 src/main/resources/templates
目录下创建另一个使用该布局的页面模板 about.html
。
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
th:replace="~{layout :: layout(~{::title/text()}, ~{::#content},~{::#pageScripts})}">
<head>
<title>About</title>
</head>
<body>
<div id="content">
<h1>This is About Page</h1>
<a href="/">Home Page</a>
<a href="/about">About Page</a>
</div>
<th:block id="pageScripts">
<script src="/webjars/jquery/3.7.1/jquery.js"></script>
</th:block>
</body>
</html>
说明:
- 我们使用位置参数传递布局片段(Layout Fragment)参数。
pageScripts
参数使用片段表达式~{::#pageScripts}
指定。这指的是当前页面模板中<div id="pageScripts">
元素的内容。在这个特定页面中,我们包含了一个从webjars
中加载 jQuery 的脚本标签。
运行应用
首先,验证一下应用是否运行正常,运行 Spring Boot 应用并访问主页 http://localhost:8080/
。
确定一切 Ok 后,将应用编译为 GraalVM 原生镜像并运行它。
#Maven
./mvnw spring-boot:build-image -Pnative -DskipTests -Dspring-boot.build-image.imageName=thymeleaf-demo
#Gradle
./gradlew bootBuildImage -Pnative -x test --imageName=thymeleaf-demo
docker run -p 8080:8080 thymeleaf-demo
访问主页 http://localhost:8080/
和 About 页面 http://localhost:8080/about
。你应该可以顺利看到这些页面。
总结
与 thymeleaf-layout-dialect 相比,虽然使用片段表达式创建布局略显繁琐,但它在 GraalVM 原生镜像中运行良好。
thymeleaf-layout-dialect
的一个主要问题是它在底层使用了 Groovy,而 Groovy 在 GraalVM 原生镜像中存在一些问题。有人尝试将 thymeleaf-layout-dialect
移植到 Java,github.com/zhanhb/thymeleaf-layout-dialect,如果你感兴趣,可以试试。
Ref:https://www.sivalabs.in/thymeleaf-layouts-using-fragment-expressions/