将Thymeleaf和自然模板引入Spring PetClinic
注意:本文涉及的是旧版的Thymeleaf(Thymeleaf 2.1)。
Spring PetClinic应用程序
PetClinic是由SpringSource为Spring框架创建的一个示例应用。它用于显示和管理宠物诊所中的宠物和兽医信息。原始的SpringSource版本位于GitHub上此处,而启用Thymeleaf的版本也位于GitHub上此处.

Pet Clinic最初包含一个使用JSP创建的视图层,我们将使用Thymeleaf替换它:
- 修改将集中在视图层:替换JSP文件并重新配置应用程序。所有Java代码将保持不变。
- 原始的标记将被清理,但应用程序的所有界面必须显示得与原来完全一致。
- 不会更改CSS样式表文件。不会添加、修改或升级JavaScript库。
- Thymeleaf模板文件在浏览器中静态打开时应能正常显示(自然模板)。
PetClinic+Thymeleaf应用程序的所有代码都可以在Thymeleaf项目的文档页面获取。请注意,原始的JSP文件和JSP标签没有从源码树中移除,而是移动到了doc/old_viewlayer
源码树中的
文件夹,以便你仍可以访问它们并与新模板进行比较。所使用的PetClinic应用程序版本基于其GitHub上的master分支
的状态,日期为2013年3月17日。
原始JSP视图层存在一些问题,我们在将视图层转换为Thymeleaf时将尝试解决这些问题:
- JSP中包含了来自JSTL、Spring标签库和其他外部库的标签。这些标签浏览器都无法理解,因此无法静态显示页面(无法静态原型设计)。
- JSTL标签使用JSP EL(表达式语言),而JSP Spring标签库中的标签使用Spring EL。因此,在同一页中混合了两种不同的表达式语言。
- 原始JSP模板不是格式良好的HTML文档。例如,“ownersList”页面:
- 不包含head标签,而是通过JSP include(=> 浏览器无法理解)从另一个JSP中添加了一个。
- 头部和尾部内容已被替换为JSP include标签(=> 浏览器无法理解),因此页面无法在包含头部和尾部的情况下静态显示。即使这些内容存在于页面中,由于页面包含JSP和JSTL标签,我们也无法看到真实的原型。
配置说明
基本项目配置
需要进行一些基本配置步骤:
- 这个
pom.xml
文件将被修改以添加Thymeleaf依赖项并移除与JSP相关的依赖项。 - 这个
web.xml
文件将被修改以移除与JSP相关的servlet和过滤器。
mvc-view-config.xml
我们的下一个配置步骤是将三个必需的Bean添加到Spring Beans配置文件中,mvc-view-config.xml
:
- Thymeleaf模板解析器负责读取需要处理的模板文件。在此应用程序中,我们将使用一个
ServletContextTemplateResolver
. - Thymeleaf模板引擎实例,其类为
SpringTemplateEngine
. - Thymeleaf视图解析器,一个实现Spring
ThymeleafViewResolver
instance implementing Spring’sorg.springframework.web.servlet.ViewResolver
接口的InternalResourceViewResolver
bean which enabled JSP support in the original application.
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
<property name="prefix" value="/WEB-INF/thymeleaf/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
<!-- Template cache is set to false (default is true). -->
<property name="cacheable" value="false" />
</bean>
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
</bean>
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="contentNegotiationManager" ref="cnManager"/>
<property name="viewResolvers">
<list>
<!-- Used here for 'xml' and 'atom' views -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="1"/>
</bean>
<!-- Used for Thymeleaf views -->
<bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
<property name="order" value="2"/>
</bean>
</list>
</property>
</bean>
请注意,与原应用程序不同的是,我们的模板将存储在/WEB-INF/thymeleaf
文件夹中,而不是原来的/WEB-INF/jsp
.
从JSP到Thymeleaf
PetClinic包含超过10个JSP模板,我们将全部使用Thymeleaf重写它们。然而,为了简洁起见,我们只关注owners/ownerslist.jsp
,将其转换为owners/ownersList.html
.
请记住你可以通过文档页面下载的源代码查看所有模板,也可以在doc/old_viewlayer
文件夹中查阅原始的JSP文件。
这个owners/ownersList页面看起来像这样:

为了将此页面转换为Thymeleaf,我们将:
- 重命名
ownersList.jsp
为ownersList.html
. - 移除所有
<%@ taglib %>
指令,因为我们不需要任何JSP标签库 - 替换
jsp:include
添加页面head、header和footer的标签,替换成包含thymeleaf属性的标签th:substituteby
或th:include
。这些页面片段已保存在fragments
文件夹中,并也已转换为Thymeleaf
<!-- ownersList.jsp -->
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="datatables" uri="http://github.com/dandelion/datatables" %>
<html lang="en">
<jsp:include page="../fragments/headTag.jsp"/>
<body>
<div class="container">
<jsp:include page="../fragments/bodyHeader.jsp"/>
<!-- ... -->
<jsp:include page="../fragments/footer.jsp"/>
</div>
</body>
</html>
<!-- ownersList.html -->
<!DOCTYPE html>
<html lang="en">
<head th:substituteby="fragments/headTag :: headTag">
<!-- ============================================================================ -->
<!-- This <head> is only used for static prototyping purposes (natural templates) -->
<!-- and is therefore entirely optional, as this markup fragment will be included -->
<!-- from "fragments.html" at runtime. -->
<!-- ============================================================================ -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>PetClinic :: a Spring Framework demonstration</title>
<link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap.min.css"
th:href="@{/webjars/bootstrap/2.3.0/css/bootstrap.min.css}" rel="stylesheet" />
<link href="../../../resources/css/petclinic.css"
th:href="@{/resources/css/petclinic.css}" rel="stylesheet" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
th:src="@{/webjars/jquery/1.9.0/jquery.js}"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"
th:src="@{/webjars/jquery-ui/1.9.2/js/jquery-ui-1.9.2.custom.js}"></script>
<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/themes/smoothness/jquery-ui.css"
th:href="@{/webjars/jquery-ui/1.9.2/css/smoothness/jquery-ui-1.9.2.custom.css}"
rel="stylesheet" />
</head>
<body>
<div class="container">
<div th:include="fragments/bodyHeader" th:remove="tag">
<!-- =========================================================================== -->
<!-- This div is only used for static prototyping purposes (natural templates) -->
<!-- and is therefore entirely optional, as this markup fragment will be included -->
<!-- from "fragments.html" at runtime. -->
<!-- =========================================================================== -->
<img th:src="@{/resources/images/banner-graphic.png}"
src="../../../resources/images/banner-graphic.png"/>
<div class="navbar" style="width: 601px;">
<div class="navbar-inner">
<ul class="nav">
<li style="width: 100px;">
<a href="../welcome.html" th:href="@{/}">
<i class="icon-home"></i>Home
</a>
</li>
<li style="width: 130px;">
<a href="../owners/findOwners.html" th:href="@{/owners/find.html}">
<i class="icon-search"></i>Find owners
</a>
</li>
<li style="width: 140px;">
<a href="../vets/vetList.html" th:href="@{/vets.html}">
<i class="icon-th-list"></i>Veterinarians
</a>
</li>
<li style="width: 90px;">
<a href="../exception.html" th:href="@{/oups.html}"
title="trigger a RuntimeException to see how it is handled">
<i class="icon-warning-sign"></i>Error
</a>
</li>
<li style="width: 80px;">
<a href="#" title="not available yet. Work in progress!!">
<i class=" icon-question-sign"></i>Help
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- ... -->
<table th:substituteby="fragments/footer :: footer" class="footer">
<!-- =========================================================================== -->
<!-- This table section is only used for static prototyping purposes (natural -->
<!-- templates) and is therefore entirely optional, as this markup fragment will -->
<!-- be included from "fragments.html" at runtime. -->
<!-- =========================================================================== -->
<tr>
<td></td>
<td align="right">
<img src="../../../resources/images/springsource-logo.png"
th:src="@{/resources/images/springsource-logo.png}"
alt="Sponsored by SpringSource" />
</td>
</tr>
</table>
</div>
</body>
</html>
注意我们的ownersList.html
在head、header和footer部分相比原始JSP文件包含更多代码。这样做纯粹是可选的,唯一目的是允许ownersList.html
启用Thymeleaf的模板作为原型静态显示(这是JSP几乎无法实现的)。
这些额外代码值得吗?如果你需要或想使用设计原型,确实值得!你会清楚地看到本文最后一节中的大不同之处。不管怎样……记住这种原型设计代码是可选的!
- 更改页面主体。原始代码如下所示:
<!-- ownersList.jsp -->
<datatables:table id="owners" data="${selections}" cdn="true" row="owner" theme="bootstrap2"
cssClass="table table-striped" paginate="false" info="false" export="pdf">
<datatables:column title="Name" cssStyle="width: 150px;" display="html">
<spring:url value="owners/{ownerId}.html" var="ownerUrl">
<spring:param name="ownerId" value="${owner.id}"/>
</spring:url>
<a href="${fn:escapeXml(ownerUrl)}"><c:out value="${owner.firstName} ${owner.lastName}"/></a>
</datatables:column>
<datatables:column title="Name" display="pdf">
<c:out value="${owner.firstName} ${owner.lastName}"/>
</datatables:column>
<datatables:column title="Address" property="address" cssStyle="width: 200px;"/>
<datatables:column title="City" property="city"/>
<datatables:column title="Telephone" property="telephone"/>
<datatables:column title="Pets" cssStyle="width: 100px;">
<c:forEach var="pet" items="${owner.pets}">
<c:out value="${pet.name}"/>
</c:forEach>
</datatables:column>
<datatables:export type="pdf" cssClass="btn btn-small" />
</datatables:table>
我们将替换为:
<!-- ownersList.html -->
<h2>Owners</h2>
<table class="table table-striped">
<thead>
<tr>
<th style="width: 150px;">Name</th>
<th style="width: 200px;">Address</th>
<th>City</th>
<th>Telephone</th>
<th style="width: 100px;">Pets</th>
</tr>
</thead>
<tbody>
<tr th:each="owner : ${selections}">
<td>
<a href="ownerDetails.html"
th:href="@{|/owners/${owner.id}|}"
th:text="|${owner.firstName} ${owner.lastName}|">Mary Smith</a>
</td>
<td th:text="${owner.address}">45, Oxford Street</td>
<td th:text="${owner.city}">Cambridge</td>
<td th:text="${owner.telephone}">555-555-555</td>
<td>
<span th:each="pet : ${owner.pets}" th:text="${pet.name}" th:remove="tag">
Rob
</span>
</td>
</tr>
</tbody>
</table>
- 在上面的代码中可以看到,我们使用HTML代码代替了来自外部库的一堆JSP标签。这不仅使我们的代码更清晰、更具可读性,而且更加标准化和能被浏览器理解,这将允许我们将此模板用作静态原型。同样,我们将在下一节中看到这样做的优势。
那么自然模板呢?
在开始此次迁移之前,我们设定了一个目标:我们的新 Thymeleaf 模板能够在浏览器中静态打开时(不启动应用服务器)也能正确显示,这要归功于自然模板的功能。
好的,让我们看看原始owners/ownersList.jsp
模板在静态打开时的样子:

……现在让我们看看我们基于 Thymeleaf 的新owners/ownersList.html
:

看到了吧。数据虽然无效,因为它只是一个原型,但看起来效果不错!