Thymeleaf

Spring MVC 视图层:Thymeleaf 与 JSP

在本文中,我们将比较同一个页面(一个订阅表单)在相同 Spring MVC 应用程序中的两个不同实现:一次使用 Thymeleaf,另一次使用 JSP、JSTL 和 Spring 标签库。

此处展示的所有代码都来自一个可运行的应用程序。你可以查看或下载源代码其 GitHub 仓库.

公共需求

我们的客户需要一个用于将新成员订阅到邮件列表的表单,包含以下两个字段:

  • 电子邮件地址
  • 订阅类型(接收所有邮件、每日摘要)

我们还需要该页面支持 HTML5 并完全国际化,从已经在我们的配置中设置好的资源文件中提取所有文本和信息。MessageSource我们 Spring 基础设施中的对象。

我们的应用程序将有两个@Controller控制器方法,它们将包含完全相同的代码,但会转发到不同的视图名称:

  • SubscribeJsp对于 JSP 页面(即subscribejsp视图)。
  • SubscribeTh对于 Thymeleaf 页面(即subscribeth视图)。

我们的模型中将包含以下类:

  • Subscription一个包含两个字段的表单支撑 Bean:String emailSubscriptionType subscriptionType.
  • SubscriptionType一个枚举类,用于表示表单中的subscriptionType字段,其取值为ALL_EMAILSDAILY_DIGEST.

(在本文中我们仅关注 JSP/Thymeleaf 模板代码。如果您想了解控制器代码或 Spring 配置的具体实现细节,请查看可下载包中的源代码)

使用 JSP 实现

这是我们的页面:

The JSP page
JSP 页面

以下是我们的 JSP 代码,它同时使用了 JSTL (core) 和 Spring (tagsform) 的 JSP 标签库:

<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>

<html>

  <head>
    <title>Spring MVC view layer: Thymeleaf vs. JSP</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" href="<s:url value='/css/thvsjsp.css' />"/>
  </head>

  <body>

    <h2>This is a JSP</h2>

    <s:url var="formUrl" value="/subscribejsp" />
    <sf:form modelAttribute="subscription" action="${formUrl}">

      <fieldset>

        <div>
          <label for="email"><s:message code="subscription.email" />: </label>
          <sf:input path="email" />
        </div>
        <div>
          <label><s:message code="subscription.type" />: </label>
          <ul>
            <c:forEach var="type" items="${allTypes}" varStatus="typeStatus">
              <li>
                <sf:radiobutton path="subscriptionType" value="${type}" />
                <label for="subscriptionType${typeStatus.count}">
                  <s:message code="subscriptionType.${type}" />
                </label>
              </li>
            </c:forEach>
          </ul>
        </div>

        <div class="submit">
          <button type="submit" name="save"><s:message code="subscription.submit" /></button>
        </div>

      </fieldset>

    </sf:form>

  </body>

</html>

使用 Thymeleaf 实现

现在,让我们用 Thymeleaf 来实现同样的功能。这是我们的页面:

The Thymeleaf page
Thymeleaf 页面

以下是我们的 Thymeleaf 代码:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Spring MVC view layer: Thymeleaf vs. JSP</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all"
      href="../../css/thvsjsp.css" th:href="@{/css/thvsjsp.css}"/>
  </head>

  <body>

    <h2>This is a Thymeleaf template</h2>

    <form action="#" th:object="${subscription}" th:action="@{/subscribeth}">

      <fieldset>

        <div>
          <label for="email" th:text="#{subscription.email}">Email: </label>
          <input type="text" th:field="*{email}" />
        </div>
        <div>
          <label th:text="#{subscription.type}">Type: </label>
          <ul>
            <li th:each="type : ${allTypes}">
              <input type="radio" th:field="*{subscriptionType}" th:value="${type}" />
              <label th:for="${#ids.prev('subscriptionType')}"
                th:text="#{|subscriptionType.${type}|}">First type</label>
            </li>
            <li th:remove="all"><input type="radio" /> <label>Second Type</label></li>
          </ul>
        </div>

        <div class="submit">
          <button type="submit" name="save" th:text="#{subscription.submit}">Subscribe me!</button>
        </div>

      </fieldset>

    </form>

  </body>

</html>

需要注意的几点:

  • 这个看起来比 JSP 版本更具 HTML 风格 —— 没有奇怪的标签,只有一些有意义的属性。
  • 变量表达式 (${...}) 是 Spring EL,它在模型属性上执行;星号表达式 (*{...}) 在表单支撑 Bean 上执行;哈希表达式 (#{...}) 用于国际化;链接表达式 (@{...}) 用于 URL 重写。(如果您想了解更多信息,请查阅“五分钟快速入门标准方言”教程)。
  • 我们可以在模板中保留原型代码:例如,我们可以给第一个字段的标签设置一个Email:文本,并知道 Thymeleaf 在执行页面时会将其替换为键为subscription.email的国际化文本。
  • 我们甚至可以为了原型设计的目的,在第二个单选按钮旁边添加一个<li>标签。当 Thymeleaf 执行页面时这个标签会被删除。

我们来改变一下页面风格!

假设现在我们的页面已经编写完成,但我们突然决定不再希望提交按钮周围的区域使用绿色,而是改用一种淡蓝色。但我们并不确定哪种蓝色更合适,因此必须尝试多种配色方案后才能最终决定。

我们来看看每种技术需要采取的步骤:

使用 JSP 修改页面样式

步骤 1: 将应用程序部署到我们的开发服务器并启动它。由于 JSP 页面无法在不启动服务器的情况下渲染,所以这是一个必要条件。

步骤 2: 浏览页面直到找到需要修改的那个页面。通常,要修改的页面是我们应用程序中几十个页面之一,要访问到它很可能需要点击链接、提交表单和/或查询数据库。

步骤 3: 启动 Firebug、Dragonfly 或我们喜爱的浏览器内置网页开发工具。这将允许我们直接在浏览器 DOM 上进行样式修改,从而立即看到效果。

步骤 4: 进行颜色更改。可能需要尝试几种不同的蓝色色调,然后选择最满意的那个。

Fine-tuned JSP page
经过微调的 JSP 页面

步骤 5: 将所做的更改复制粘贴到我们的 CSS 文件中.

完成啦!

使用 Thymeleaf 修改页面样式

步骤 1: 双击.html模板文件本身,让浏览器打开它。作为一个 Thymeleaf 模板,它会正常显示,只是使用的是模板/原型数据(请注意订阅类型选项):

Thymeleaf page - valid as a prototype
Thymeleaf 页面 - 可作为原型有效使用

步骤 2: 使用我们喜欢的文本编辑器打开.css文件。模板文件在其<link rel="stylesheet" ...>标签中静态链接到 CSS(带有一个href,在模板执行时 Thymeleaf 会将其替换为由th:href生成的实际路径)。因此我们对该 CSS 所做的任何更改都会应用到浏览器当前显示的静态页面上。

步骤 3: 进行颜色更改。与使用 JSP 类似,我们可能需要尝试多个颜色组合,而这些更改只需按 F5 键即可在浏览器中刷新。

完成啦!

这就是巨大的差异!

步骤数量的差异其实并不是这里的关键(我们也可以对 Thymeleaf 模板使用 Firebug)。真正重要的是 JSP 所需每个步骤的复杂性、工作量和时间成本。必须部署和启动整个应用程序这一点使 JSP 处于劣势。

更进一步地想想,如果以下情况出现的话这种差异会如何演化:

  • 我们的开发服务器不是本地的而是远程的。
  • 改动不仅涉及 CSS,还涉及新增或删除一些 HTML 代码。
  • 我们尚未实现在应用程序中所需的逻辑以实际到达该页面的功能。

最后一点非常重要:如果我们的应用程序还在开发阶段,显示此页面或其他前置页面所需要的 Java 逻辑还未完全正常运行,此时却需要向客户展示新的颜色?(甚至是让他们即时选择颜色!)…

而尝试将 JSP 作为静态原型使用会怎样呢?

好吧,你现在可以说,但为什么我们要启动应用程序来修改 JSP,而不是像处理 Thymeleaf 模板那样直接打开它?我们不能这样做吗?.

简短的回答是:不行。

但我们还是来试一下吧:当然,我们必须重命名我们的文件,使其文件名以.html结尾,.jsp而不是

JSP page directly opened on a browser
就像我们在浏览器中直接打开它时看到的:

什么?我们的页面去哪了?其实页面还在那里,但为了使我们的页面能作为 JSP 正常运行,我们不得不添加了许多 JSP 标签和特性,这些功能在由 Web 服务器执行时非常有效……但在同时,也使得页面不再是纯 HTML。因此浏览器也就无法正确显示它了。

再次回想一下,当我们双击 Thymeleaf 模板时它的样子:

Thymeleaf page directly opened on a browser
在浏览器中直接打开的 Thymeleaf 页面

显然不在同一个级别上,对吧?

支持 HTML5 吗?

不过,嘿——我们一开始就说页面要采用 HTML5,那么……为什么不使用一些酷炫的新 HTML5 表单相关功能呢?

例如,现在有一个<input type="email" ...>输入类型,它会让浏览器检查用户输入的文本是否符合电子邮件地址的格式。此外,所有表单输入还新增了一个属性叫做placeholder当输入框获得焦点(通常由用户点击)时,该属性会在字段中显示一段自动消失的文字提示。

听起来不错,是吧?不幸的是,并非所有浏览器都支持这一特性(截至 2011 年,Opera 11 和 Firefox 4 支持),但无论如何我们可以放心地使用这些特性,因为所有不支持这种输入类型的浏览器都会将其视为一个email类型为text的普通输入框处理,并且会静默忽略placeholder属性,就像它们忽略 Thymeleaf 的属性一样。th:*属性标记。

使用 JSP 实现 HTML5

Spring 3.1 之前版本的情况

直到 Spring 3.1 发布前,Spring MVC 的 JSP 标签库都不完全支持 HTML5,因此在此版本之前,除了使用纯 HTML 编写之外,没有其它方式能写出email 类型的输入标签,比如这样:

<input type="email" id="email" name="email" placeholder="your@email" value="" />

但这并不正确!在 Spring MVC 中,我们永远不应该以这种方式编写 JSP 输入字段,因为我们并没有正确地将输入与表单支持 Bean 的绑定属性相连。为了实现这一点,我们需要使用email property of the form-backing bean. In order to do so, we would need to use an <s:eval/>标签,它将应用所有必要的转换(比如属性编辑器),从而使我们的纯 HTML 标签表现得好像存在<sf:email/>标签一样:

<input type="email" id="email" name="email" placeholder="your@email"
       value="<s:eval expression='subscription.email' />" />

自从 Spring 3.1 开始

在 Spring 3.1 中仍然没有<sf:email ...>标签,但现有的<sf:input ...>标签允许我们指定一个type属性并赋值为email属性,一切就能立即正常运行,不仅能正确绑定我们的属性,还能集成 Spring MVC 的

<sf:input path="email" type="email" />

并且这将正确完成我们的表单绑定:-)

使用 Thymeleaf 实现 HTML5

Thymeleaf 完全支持 HTML5(甚至在 Spring 3.0 下也可以),所以我们只需要更改输入标签的type类型placeholder并添加一个属性编辑器更重要的是,作为原型展示时会被显示为普通的input输入框——而这是sf:input标签所做不到的:

<input type="email" th:field="*{email}" placeholder="your@email" />

完成啦!

Final result with Thymeleaf
使用 Thymeleaf 的最终效果
无噪 Logo
无噪文档
中文文档 · 复刻官网
查看所有 ↗