Thymeleaf

将Thymeleaf和自然模板引入Spring PetClinic

注意:本文涉及的是旧版的Thymeleaf(Thymeleaf 2.1)。

Spring PetClinic应用程序

PetClinic是由SpringSource为Spring框架创建的一个示例应用。它用于显示和管理宠物诊所中的宠物和兽医信息。原始的SpringSource版本位于GitHub上此处,而启用Thymeleaf的版本也位于GitHub上此处.

PetClinic home page
PetClinic主页

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”页面:
    1. 不包含head标签,而是通过JSP include(=> 浏览器无法理解)从另一个JSP中添加了一个。
    2. 头部和尾部内容已被替换为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视图解析器,一个实现SpringThymeleafViewResolver instance implementing Spring’s org.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页面看起来像这样:

Owners page
Owners页面

为了将此页面转换为Thymeleaf,我们将:

  • 重命名ownersList.jspownersList.html.
  • 移除所有<%@ taglib %>指令,因为我们不需要任何JSP标签库
  • 替换jsp:include添加页面head、header和footer的标签,替换成包含thymeleaf属性的标签th:substitutebyth: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模板在静态打开时的样子:

Owners list (JSP), statically opened
所有者列表(JSP),静态打开

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

Owners list (thymeleaf), statically opened
所有者列表(thymeleaf),静态打开

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

无噪 Logo
无噪文档
中文文档 · 复刻官网
查看所有 ↗