《Spring实战》学习笔记-第六章:web视图解析

本章主要内容包括:

  • 将model数据展现为HTML
  • JSP视图的使用

在前面的章节中,我们主要关注点在于编写控制来处理web请求,同时也创建了一些简单的视图来展现请求返回的model数据,本章我们将主要讨论在控制器完成请求处理之后和将返回结果展示到用户的浏览器之前,这个过程之间发生了什么。

理解视图解析

在之前章节中所编写的控制器中并没有直接生成HTML的方法,它只是将数据填充到model中,然后将model传送到视图中进行展现。

Spring MVC中定义了一个ViewResolver接口:

public interface ViewResolver {

    View resolveViewName(String viewName, Locale locale) throws Exception;

}

方法resolveViewName(),当给定一个视图名称和一个Locale时就会返回一个View实例,View是另外一个接口:

public interface View {
    String getContentType();
    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

}

View接口的工作是对model、servlet请求、响应对象进行处理,并将结果输出到response中。

看起来很简单啊,我们所需要做的仅仅是编写ViewResolver和View的实现类来将内容输出到response中,并在用户浏览器中进行展示即可,但真的是这样吗?

虽然可以编写自定义的实现类,而且有些时候会需要一些特殊的处理,Spring提供了一些现成的实现类:

视图解析器 描述
BeanNameViewResolver 在Spring的application context中的bean中查找与视图名称相同id
ContentNegotiatingViewResolver 委托给一个或多个人视图解析器,而选择哪一个取决于请求的内容类型
FreeMarkerViewResolver 查找一个基于FreeMarker的模版
InternalResourceViewResolver 在web应用程序的war文件中查找视图
JasperReportsViewResolver 解析为JasperReport报表文件
ResourceBundleViewResolver 根据属性文件(properties file)查找View实现
TilesViewResolver 通过Tiles模版定义的模版解析,模版的名称与视图名称相同
UrlBasedViewResolver 根据视图名称直接解析,当视图名称与物理视图名称匹配时
VelocityLayoutViewResolver 解析为从不同的Velocity模版组成的Velocity布局
VelocityViewResolver 解析为Velocity模版
XmlViewResolver 根据XML文件(/WEB_INF/views.xml)中声明的View实现进行解析,与BeanNameViewResolver类似
XsltViewResolver 基于XSLT视图解析

我们没有足够的时间和篇幅来讨论所有的解析器,上面的每个解析器都对应着一个特定的视图技术。InternalResourceViewResolver主要用于JSP,TilesViewResolver用于 Apache Tiles视图,FreeMarkerViewResolver和VelocityViewResolver分别用于FreeMarker和Velocity模版。

创建JSP视图

Spring对JSP视图有两种支持方式:

  • InternalResourceViewResolver:可以将视图名称解析到JSP文件。另外,对JSP中使用的JSTL(JavaServer Pages Standard Tag Library)标签也提供了支持。
  • Spring提供了两种JSP标签库,一种是form-to-model绑定,另外一种则提供基本的功能。

InternalResourceViewResolver是最简单也是最常用的一个解析器,下面我们来看一下它是如何使用它来完成任务。

配置JSP视图解析器

一些视图解析器(如ResourceBundleViewResolver)是直接的将逻辑视图名称映射到一个特定的View接口实现类上,而InternalResourceViewResolver则采用了另外的比较间接的方式。它采用了一种约定,通过给逻辑视图名称添加前缀和后缀来确定web应用中对应的物理路径。

假设有一个逻辑视图名称为home,如果将所有的JSP文件都存放在/WEB-INF/views/目录下,并且主页的JSP名为home.jsp,那么可以通过为home添加前缀/WEB-INF/views/和后缀.jsp来找到对应的物理视图路径,如图所示。

InternalResourceViewResolver通过为逻辑视图名称添加前缀和后缀来解析视图
InternalResourceViewResolver通过为逻辑视图名称添加前缀和后缀来解析视图

可以在使用@Bean注解的类进行设置:

    // 配置一个JSP视图解析器
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

另外,如果采用基于XML的Spring配置,也可以通过如下方式进行配置:

<bean id="viewResolver"
    class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    p:prefix="/WEB-INF/views/" 
    p:suffix=".jsp" />

探索JSTL视图

当JSP页面使用JSTL标签时,就需要配置InternalResourceViewResolver来解析JstlView了。

JSTL格式化标签需要一个Locale来正确地格式化一些特定语言环境的值,如日期和币种。消息标签可以使用Spring消息源和Locale来正确地选中消息并解析到HTML。

要解析JstlView,需要在InternalResourceViewResolver中设置viewClass属性:

resolver.setViewClass(JstlView.class);

同样的,xml配置中也需要进行配置:

p:viewClass="org.springframework.web.servlet.view.JstlView" 

使用Spring的JSP库

Spring提供了两种JSP标签库,一种用于将绑定了model属性的HTML标签进行渲染,其他的一些标签在不同场合下可以用到。

将表单绑定到model

Spring的表单绑定JSP标签库共有14种,与原生HTML标签不同的是,它们可以将一个对象绑定在model,并且可以从model对象的属性中获取填充值。标签库同时可以用来与用户交互错误信息。

要使用表单绑定标签库,需要在JSP页面中进行声明:

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>

注意,这里使用了sf作为前缀。

JSP标签 描述
<sf:checkbox> 生成一个checkbox类型的HTML input标签
<sf:checkboxes> 生成一组checkbox类型的HTML input标签
<sf:errors> 通过一个HTML <span>标签展现字段的错误
<sf:form> 生成一个HTML的<form>标签,同时为内部标签的绑定暴露了一个绑定路径(binding path)
<sf:hidden> 生成一个type为hidden的Html input标签
<sf:input> 生成一个type为text的Html input标签
<sf:label> 生成一个HTML <label>标签
<sf:option> 生成一个HTML的<option>标签,根据绑定的值来设置selected属性
<sf:options> 根据绑定的集合、数组或者map,生成一个option标签列表
<sf:password> 生成password类型的input标签
<sf:radiobutton> 生成radio类型的input标签
<sf:radiobuttons> 生成一组radio类型的input标签
<sf:select> 生成<select>标签
<sf:textarea> 生成<textarea>标签

现在就可以在之前的用户注册界面进行使用:

<sf:form method="POST" commandName="spitter">
    First Name: <sf:input path="firstName" /><br/>
    Last Name: <sf:input path="lastName" /><br/>
    Email: <sf:input path="email" /><br/>
    Username: <sf:input path="username" /><br/>
    Password: <sf:password path="password" /><br/>
    <input type="submit" value="Register" />
</sf:form>

使用Spring的form标签主要有两个作用,第一是它会自动的绑定来自Model中的一个属性值到当前form对应的实体对象,默认是command属性,这样我们就可以在form表单体里面方便的使用该对象的属性了;第二是它支持我们在提交表单的时候使用除GET和POST之外的其他方法进行提交,包括DELETE和PUT等。

这个时候如果Model中存在一个属性名称为command的javaBean,在渲染上面的代码时就会取command的对应属性值赋给对应标签的值。

我们指定form默认自动绑定的是Model的command属性值,那么当我的form对象对应的属性名称不是command的时候,应该怎么办呢?对于这种情况,Spring给我们提供了一个commandName属性,我们可以通过该属性来指定我们将使用Model中的哪个属性作为form需要绑定的command对象。除了commandName属性外,指定modelAttribute属性也可以达到相同的效果。

这里将commandName设置为spitter,因此model中必然存在一个key为spitter的对象,否则表单将不能渲染。这意味着需要对SpitterController进行简单的改动,以保证model中存在一个Spitter的对象:

// 处理来自/spitter/register的get请求
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegistrationForm(Model model) {
    model.addAttribute(new Spitter());
    return "registerForm";
}

回到表单代码中,使用<sf:input>代替了<input>标签,这个标签会生成一个HTML的<input>标签,并且将它的attribute属性设为text,它的value属性会根据<sf:input>标签的path属性设置的值去model对象中对应的属性值进行设置。如model中的Spitter对象有一个firstName属性为Jack,那么<sf:input path="firstName" />会被解析为含有value="Jack"的input标签。

为了更好的理解,在一次注册失败后,会重定向到注册页面,对应的HTML的form标签如下所示:

<form id="spitter" action="/spitter/spitter/register" method="POST">
    First Name:
        <input id="firstName" name="firstName" type="text" value="J"/><br/>
    Last Name:
        <input id="lastName" name="lastName" type="text" value="B"/><br/>
    Email:
        <input id="email" name="email" type="text" value="jack"/><br/>
    Username:
        <input id="username" name="username" type="text" value="jack"/><br/>
    Password:
        <input id="password" name="password" type="password" value=""/><br/>
    <input type="submit" value="Register" />
</form>

值得注意的是,从Spring3.1开始,<sf:input>标签允许使用type属性来声明一些特殊的HTML5类型,如data、range和email等,例如,可以这样来声明email:
Email: <sf:input path="email" type="email" /><br/>

这样就会解析为:
Email: <input id="email" name="email" type="email" value="jack"/><br/>

错误信息展示

当存在验证错误时,错误的详细信息会被存放model数据中并被request携带,所要做的就是对model中的错误信息进行展示,使用<sf:errors>标签即可。

例如:

First Name: <sf:input path="firstName" />
  <sf:errors path="firstName" cssClass="error"/><br/>

这里将<sf:errors>的path属性设置为firstName,那么就会展示Spitter model对象的firstName的验证错误信息,如果没有错误,那么就不会对其进行解析。如果有,会将其解析为<span>标签。

First Name: <input id="firstName"
    name="firstName" type="text" value="J"/>
<span id="firstName.errors">size must be between 2 and 30</span>

另外一种展示错误信息的方式是将它们放在一起进行展示,如:

<sf:form method="POST" commandName="spitter">
    <sf:errors path="*" element="div" cssClass="errors" />
    ...
</sf:form>  

这里的path属性使用了*,这表明<sf:errors>标签会解析所有属性的错误信息。需要注意的是,这里设置了属性element为div,默认情况下errors会被解析为<span>标签,适用于只有一条错误信息时。但是当有多条错误信息时,就需要使用<div>,这样错误信息就会解析为<div>标签。

现在还需要对需要更正的属性进行高亮显示,可以通过使用<sf:label>标签以及它的cssErroeClass属性来实现:

<sf:form method="POST" commandName="spitter">
    <sf:errors path="*" element="div" cssClass="errors" />
    <sf:label path="firstName" cssErrorClass="error">First Name</sf:label>:
        <sf:input path="firstName" cssErrorClass="error"/><br/>
...
</sf:form>

<sf:label>标签也有一个path属性,用来显示对于的model对象中的属性,如果没有验证错误,那么会将其解析为<label>标签:
<label for="firstName">First Name</label>

如果出现了验证错误消息,那么就会解析成:
<label for="firstName" class="error">First Name</label>

类似的,<sf:input>将其cssErrorClass属性设置为error,如果出现验证错误,那么解析后的<input>标签的class属性会被设置为error。可以自定义属性信息:

span.error {
    color: red;
}

label.error {
    color: red;
}

input.error {
    background-color: #ffcccc;
}

div.errors {
    background-color: #ffcccc;
    border: 2px solid red;
}

现在可以为用户展示一个比较美观的验证错误信息,另外还可以在Spitter类中为验证信息设置message属性,从而可以得到比较友好的验证信息:

@NotNull
@Size(min = 5, max = 16, message = "{username.size}")
private String username;

@NotNull
@Size(min = 5, max = 25, message = "{password.size}")
private String password;

@NotNull
@Size(min = 2, max = 30, message = "{firstName.size}")
private String firstName;

@NotNull
@Size(min = 2, max = 30, message = "{lastName.size}")
private String lastName;

@NotNull
@Email(message = "{email.valid}")
private String email;

对于每一个属性,为@Size标注的message设置了一个字符串,其中的值使用了大括号包括,那么大括号之间的值对应的真实内容可以通过properties文件进行设置:

firstName.size=First name must be between {min} and {max} characters long.
lastName.size=Last name must be between {min} and {max} characters long.
username.size=Username must be between {min} and {max} characters long.
password.size=Password must be between {min} and {max} characters long.
email.valid=The email address must be valid.

其中的min和max是@Size标注中设置的。

当用户提交了一个不合法的注册信息时,可以得到如下图这样的错误提示信息:


对验证错误信息进行友好地展示
对验证错误信息进行友好地展示

Spring基础标签库

处理表单绑定标签库之外,Spring还提供了一个跟基本的JSP标签库。要使用该标签库,需要在页面中做以下声明:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>

声明了之后,就可以在页面中使用如下的JSP标签了:

JSP标签 描述
<s:bind> 通常和form一起用,用以指明表单要提交到哪个类或类的属性中去
<spring:escapeBody> 对标签中的内容进行转义处理
<spring:hasBindErrors> 用于将特定对象(request属性中)中绑定的的errors解析出来
<spring:htmlEscape> 设置当前页面的默认的HTML转义值
<spring:message> 根据code取得消息资源,并将其解析为一个page、request、session或者application范围的变量(由var或者scope属性指定)
<spring:nestedpath> <spring:bind>配置嵌套路径
<s:theme> <spring:message>相同,只不过处理的是theme消息
<spring:transform> 来转换表单中不与bean中的属性一一对应的那些属性
<s:url> <spring:message>相同,只不过处理的是URI模版变量
<s:eval> <spring:message>相同,只不过处理的是SpEL表达式

展示消息的国际化支持

使用<s:message>标签可以对引用外部属性文件的文件进行完美地解析,如:
<h1><s:message code="spittr.welcome" /></h1>

这里<s:message>标签会从某个属性文件中根据key值spittr.welcome读取对应的文本并解析到页面中,在这之前需要对这个key-value进行配置。

Spring有一些实现自MessageSource接口的消息源类,其中一个比较常用的就是ResourceBundleMessageSource,它可以从properties文件中加载消息,下面的@Bean方法对该类进行了配置:

@Bean
public MessageSource messageSource() {
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages");
    return messageSource;
}

其中的关键在于设置了basename属性,之后ResourceBundleMessageSource就可以对classpath根路径下相对应的的properties文件进行解析。

另外,还可以使用ReloadableResourceBundleMessageSource,它可以在不重新编译或者重启项目的情况下重新加载消息属性:

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("file:///etc/spittr/messages");
    messageSource.setCacheSeconds(10);
    return messageSource;
}

与ResourceBundleMessageSource主要的区别在于basename的设置,这里可以将其设置为classpath(需要前缀classpath:)、文件系统(file:)或者项目的根目录(不加任何前缀)。上面代码中设置的就是在文件系统中的/etc/spittr目录中查找文件名称为messages的文件。

下面创建一个名为messages.properties的文件,并添加如下内容:
spittr.welcome=Welcome to Spittr!

构建URL:<s:url>

<s:url>标签的主要功能就是创建URL,并将其分配给一个变量或者解析到响应中。作为JSTL的<c:url>标签的简单替换,它也有这一些新功能。

<s:url>标签需要一个servlet上下文相关的URL,并对其进行解析。例如:
<a href="<s:url href="/spitter/register" />">Register</a>

如果servlet上下文是spittr,那么上面的链接将会被解析为:
<a href="/spittr/spitter/register">Register</a>

将servlet上下文作为链接前缀添加到目标链接中。

另外也可以使用<s:url>标签构建URL并分配到变量中:

<s:url href="/spitter/register" var="registerUrl" />
<a href="${registerUrl}">Register</a>

默认情况下,URL变量是page范围内的。但是也可以通过设置scope属性将其设置为application、session或者request范围内的:
<s:url href="/spitter/register" var="registerUrl" scope="request" />

如果想为URL添加参数,可以通过<s:param>标签进行添加。例如,下面的<s:url>标签通过<s:param>为/spittles设置了max和count属性:

<s:url href="/spittles" var="spittlesUrl">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

现在看起来<s:url><c:url>没有什么区别嘛。但是如果需要创建一个含有路径参数的URL时怎么处理?如何让一个href中有一个可以替换的path参数?

使用<s:param>就可以处理:

<s:url href="/spitter/{username}" var="spitterUrl">
    <s:param name="username" value="jbauer" />
</s:url>

href中有一个占位符,通过<s:param>来指定这个占位符的值。

另外,<s:url>也可以实现URL的转义,通过设置htmlEscape属性可以完成URL中的HTML转义:

<s:url value="/spittles" htmlEscape="true">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

上面的标签将会被解析为:
/spitter/spittles?max=60&count=20

另一方面,如果想在JavaScript代码中使用URL,那么可以设置javaScriptEscape属性为true。

<s:url value="/spittles" var="spittlesJSUrl" javaScriptEscape="true">
    <s:param name="max" value="60" />
    <s:param name="count" value="20" />
</s:url>

<script>
    var spittlesUrl = "${spittlesJSUrl}"
</script>

上述代码会被解析为:

<script>
    var spittlesUrl = "\/spitter\/spittles?max=60&count=20"
</script>

内容转义:<s:escapeBody>

有时想在页面展示一段HTML代码,一般的要在页面显示字符<>需要用<>代替,但是这种做法明显的很笨重而且难读。这种情况下可以使用<s:escapeBody>标签:

<s:escapeBody htmlEscape="true">
<h1>Hello</h1>
</s:escapeBody>

上述代码会被解析为:
<h1>Hello</h1>

当然,该标签页支持JavaScript代码,只需将其javaScriptEscape属性设为true即可:

<s:escapeBody javaScriptEscape="true">
<h1>Hello</h1>
</s:escapeBody>

使用Apache Tiles视图

假设要为所有页面添加一个通用的页首和页尾,一般的做法是为每个JSP页面添加HTML代码,显然这种方法在后期不方便进行维护。

刚好的方法是使用排版引擎,例如Apache Tiles,来定义所有页面中的通用页面排版。

配置Tiles视图解析器

为了在Spring中使用Tiles,必须配置一些bean。需要一个TilesConfigurer,它主要用来定位以及加载tile定义。另外还需要TilesViewResolver来解析tile定义中的逻辑视图。

对于这两个组件,Apache Tiles 2和3中使用了不同的包:org.springframework.web.servlet
.view.tiles2和org.springframework.web.servlet
.view.tiles3,这里我们使用3版本。

下面添加TilesConfigurer的bean定义:

@Bean
public TilesConfigurer tilesConfigurer() {
    TilesConfigurer tiles = new TilesConfigurer();
    // 指定tile定义的位置
    tiles.setDefinitions(new String[] { "/WEB-INF/layout/tiles.xml" });
    // 开启刷新
    tiles.setCheckRefresh(true);
    return tiles;
}

在配置TilesConfigurer时,最重要的属性就是definitions,该属性使用一个String数组作为参数,用来指定tile定义文件的位置。可以指定多个位置,还可以使用通配符。例如可以使用如下的配置来指定TilesConfigurer寻找/WEB-INF目录下的任意名为tiles.xml的文件:

tiles.setDefinitions(new String[] {
    "/WEB-INF/**/tiles.xml"
});

Ant风格的**模式表明要在/WEB-INF/下的所有目录查找名为tiles.xml的文件。

下面来配置TilesViewResolver

@Bean
public ViewResolver tilesViewResolver(){
    return new TilesViewResolver();
}

另外,也可以使用XML的方式配置:

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/layout/tiles.xml.xml</value>
            <value>/WEB-INF/views/**/tiles.xml</value>
        </list>
    </property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.tiles3.TilesViewResolver" />

定义tile配置文件

Apache Tiles提供了一个DTD(document type definition)用来指定XML中tile的定义。每个定义由<definition>元素组成,该元素又有一个或多个<put-attribute>,如:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">

<tiles-definitions>

    <!-- 定义一个基础tile -->
    <definition name="base" template="/WEB-INF/layout/page.jsp">
        <put-attribute name="header" value="/WEB-INF/layout/header.jsp" />
        <put-attribute name="footer" value="/WEB-INF/layout/footer.jsp" />
    </definition>

    <definition name="home" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/home.jsp" />
    </definition>
    <definition name="registerForm" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/registerForm.jsp" />
    </definition>
    <definition name="profile" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/profile.jsp" />
    </definition>
    <definition name="spittles" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/spittles.jsp" />
    </definition>
    <definition name="spittle" extends="base">
        <put-attribute name="body" value="/WEB-INF/views/spittle.jsp" />
    </definition>
</tiles-definitions>

每个<definition>元素定义了一个tile,代表着一个JSP模版。一个tile同时也可以代表其他在主模版中被嵌入的模版。对应base tile,它表示一个header JSP模版和footer JSP模版。

下面是page.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="t"%>
<%@ page session="false"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Spittr</title>
<link rel="stylesheet" type="text/css"
    href="<s:url value="/resources/style.css" />">
</head>
<body>
    <!-- 头部 -->
    <div id="header">
        <t:insertAttribute name="header" />
    </div>
    <!-- 正文 -->
    <div id="content">
        <t:insertAttribute name="body" />
    </div>
    <!-- 尾部 -->
    <div id="footer">
        <t:insertAttribute name="footer" />
    </div>
</body>
</html>

其中的关键在于如何使用<t:insertAttribute>标签从Tile标签库插入到其他模版中。使用该标签来引入header、body、footer属性,最终的布局如下图所示:

基本布局
基本布局

其中,header和footer属性在tile定义文件中分别指明了,但是body属性呢?它在哪里设置呢?

base tile的主要作用是作为一个基础模版用来作为其他tile定义扩展使用的。那么扩展了base的tile就继承了base的header和footer属性(也可以进行重写)。它们自己也设置了一个body属性用来引用一个JSP模版。

如home tile,它继承自base,所以它继承了base的所有属性。即使home tile的定义非常简单,但是它相当于如下定义:

<definition name="home" template="/WEB-INF/layout/page.jsp">
    <put-attribute name="header" value="/WEB-INF/layout/header.jsp" />
    <put-attribute name="footer" value="/WEB-INF/layout/footer.jsp" />
    <put-attribute name="body" value="/WEB-INF/views/home.jsp" />
</definition>

hsader.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
<a href="<s:url value="/" />"><img
    src="<s:url value="/resources" />/images/spittr_logo_50.png" border="0" /></a>

footer.jsp:

Copyright © Craig Walls

每一个继承自base的tile都定义了自己的body模版:

home.jsp:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<h1>Welcome to Spittr</h1>
<a href="<c:url value="/spittles" />">Spittles</a> |
<a href="<c:url value="/spitter/register" />">Register</a>

这里的关键在于一个页面的公共部分已经在page.jsp、header.jsp、footer.jsp中捕获,这样就可以在所有页面里进行重复利用,简化后期的维护工作。

如下图所示,页面中包含一些样式和图片,但是这些与Tiles是没有关联的,因此这里就不再进行详细的阐述。但是,从这个页面可以看出页面是如何通过各个不同的tile组件组成的。

通过Tile加载的Spittr的首页
通过Tile加载的Spittr的首页

使用Thymeleaf

虽然JSP已经使用了较长的时间,并且在Java web中使用的也很广泛,但是它也有自身的一些缺陷。明显的就是JSP是以HTML或者XML的形式展现的。大多数的JSP模版都使用HTML的格式,并使用各种JSP标签库。虽然这些标签库可以在JSP中进行动态的解析,但是却很难有一个格式良好的页面。比如,可以在HTML中使用下面的JSP标签:
<input type="text" value="<c:out value="${thing.name}"/>" />

当阅读一个没有解析的JSP页面时,常常很难读懂,简直就是一场视觉灾难!因为JSP并不是真正的HTML,很多web浏览器和编辑器很难对JSP进行解析。

另外,JSP与servlet规范是紧密耦合的,这就意味着它只能使用在以servlet为基础的web应用中。

近年内有涌现出很多要替代JSP作为java应用的视图技术,其中一个有力的竞争者就是:Thymeleaf。Thymeleaf不需要依赖标签库,并且是可编辑的、可以解析到HTML中。另外,它与servlet规范是没有耦合的,因此它可以在JSP不能使用的环境进行使用。下面,我们来看一下如何在Spring MVC中使用Thymeleaf

配置Thymeleaf视图解析器

为了在Spring中使用Thymeleaf,需要配置3个bean:

  • ThymeleafViewResolver:用来从逻辑视图中解析出Thymeleaf模版;
  • SpringTemplateEngine:对模版进行处理,并给出结果;
  • TemplateResolver:用来加载Thymeleaf模版;

下面使用Java类的方式进行声明:

// Thymeleaf视图解析器
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
    ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
    viewResolver.setTemplateEngine(templateEngine);
    return viewResolver();
}

// Thymeleaf驱动
@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    return templateEngine;
}

// 模版解析器
@Bean
public TemplateResolver templateResolver() {
    TemplateResolver templateResolver = new ServletContextTemplateResolver();
    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setTemplateMode("HTML5");
    return templateResolver;
}

也可以使用XML配置文件的方式进行配置:

<bean id="viewResolver" class="org.thymeleaf.spring3.view.ThymeleafViewResolver"
    p:templateEngine-ref="templateEngine" />
<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"
    p:templateResolver-ref="templateResolver" />
<bean id="templateResolver"
    class="org.thymeleaf.templateresolver.ServletContextTemplateResolver"
    p:prefix="/WEB-INF/templates/" p:suffix=".html" p:templateMode="HTML5" />

ThymeleafViewResolver是Spring MVC视图解析器ViewResolver的一个实现,和其他视图解析器一样,它会对一个逻辑视图名称进行解析,此时最终的视图会是一个Thymeleaf模版。

注意,ThymeleafViewResolver中注入了一个SpringTemplateEngine的bean类,SpringTemplateEngine可以用来对模版进行转换和解析。

TemplateResolver用来定位最终的模版。

定义Thymeleaf模版

Thymeleaf模版主要是HTML文件,并没有一些特殊的标签或者标签库。它是通过自定义命名空间的方式来向标准HTML中添加Thymeleaf属性的。比如下面的例子:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spittr</title>
<link rel="stylesheet" type="text/css" th:href="@{/resources/style.css}"></link>
</head>
<body>
    <h1>Welcome to Spittr</h1>
    <a th:href="@{/spittles}">Spittles</a> |
    <a th:href="@{/spitter/register}">Register</a>
</body>
</html>

主页模版非常简单,只使用了th:href属性。该属性就像HTML的href属性,使用起来也是一样的。它的特别之处在于它可以包含Thymeleaf表达式。它会对href属性进行解析,这就是Thymeleaf表达式的工作原理:它们对应着标准的HTML属性,并且会解析一些计算值。这种情况下,所有的th:href属性会使用@{}表达式来得出相关的上下文URL路径(类似于JSTL中的<c:url>标签)。

Thymeleaf模版不像JSP,它是可以编辑甚至可以自然的解析,不需要准备其他任何处理过程。当然,需要Thymeleaf对模版进行处理从而获取到预想的输出。但是不需要做其他特殊的处理,home.html就可以装载到浏览器中,如图所示:

Thymeleaf模版可以像处理HTML文件一样进行解析
Thymeleaf模版可以像处理HTML文件一样进行解析

如上图所示,JSP文件的标签库声明也会显示出来,并且在超链接前面会有一些奇怪的标记。

相反,Thymeleaf模版解析得比较完美,唯一的不足就是超链接的解析。浏览器没有将th:href解析为href,所有link没有解析为超链接的样式。

Spring的JSP标签擅长使用绑定,如果摒弃使用JSP,那么该如何使用属性绑定呢?

使用Thymeleaf进行绑定

表单绑定是Spring MVC的一个重要特性,没有正确的表单绑定,你就必须保证HTML的表单字段是正确命名的,并且是和后台的对象属性是一一映射的。同时还要保证当验证失败对表单进行展示时属性值可以正确地set到相应的对象属性中去。

比如registration.jsp中的First Name属性:

<sf:label path="firstName" cssErrorClass="error">First Name</sf:label>:
    <sf:input path="firstName" cssErrorClass="error" />
<br />

这里<sf:input>标签会被解析为HTML的<input>标签,并且其value属性会根据后台对象的firstName属性进行设置。同时使用了<sf:label>和cssErrorClass属性用来在出现验证错误时解析该标签。

但是本节中我们要讨论的是如何在Thymeleaf中使用动态绑定,而不是JSP,比如下面的代码:

<label th:class="${#fields.hasErrors('firstName')}? 'error'">
    First Name</label>:
<input type="text" th:field="*{firstName}"
    th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>

这里使用了th:class属性,该属性会被解析为一个class属性,并且其值是使用给定的表达式计算而来。该属性会对firstName值进行检查是否存在校验错误,如果存在,那么class属性解析后就会包含error,如果没有错误,那么class属性就不会进行解析。

<input>标签使用了th:field属性来从后台对象中解析出firstName属性。这里使用了th:field属性与后台对象的firstName属性进行了绑定,这样可以同时得到为firstName设置的value属性和name属性。

下面的代码中验证了Thymeleaf的数据绑定:

      <form method="POST" th:object="${spitter}">
        <div class="errors" th:if="${#fields.hasErrors('*')}">
          <ul>
            <li th:each="err : ${#fields.errors('*')}" 
                th:text="${err}">Input is incorrect</li>
          </ul>
        </div>
        <label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>: 
          <input type="text" th:field="*{firstName}"  
                 th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>: 
          <input type="text" th:field="*{lastName}"
                 th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>: 
          <input type="text" th:field="*{email}"
                 th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>: 
          <input type="text" th:field="*{username}"
                 th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
  
        <label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>: 
          <input type="password" th:field="*{password}"  
                 th:class="${#fields.hasErrors('password')}? 'error'" /><br/>
        <input type="submit" value="Register" />

      </form>

上述代码使用了相同的Thymeleaf属性和*{}表达式来对后台对象进行绑定。值得注意的是,我们在form的顶部使用Thymeleaf来解析所有的异常。<div>元素使用了th:if属性来对是否存在错误进行校验,如果有错误,那么就会对<div>进行解析,否则就不解析。

<div>中的列表是无序的,针对变量err中的每一个error,每个<li>中的th:each会解析为一个<li>标签。<li>标签也有一个th:text属性,该属性会对表达式的值进行计算,并将结果解析到对应的<li>标签中。最终,针对每个error,都会有一个<li>进行展示。

你也许会对${}*{}包含的表达式有疑惑。${}表达式是变量表达式,如${spitter}。一般的,都是一些OGNL(Object-Graph Navigation Language,对象图导航语言)表达式,但是当使用Spring时,它们就是SpEL表达式。比如${spitter}会解析为key是spitter的model对象。

*{}表达式则是选择表达式,变量表达式根据整个的SpEL上下文进行计算,而选择表达式则是根据指定的对象进行计算。在上述表单中,选中的对象是在<form>标签中根据th:object属性指定的,即来自model的spitter对象,因此,*{password}表达式会被解析为spitter对象的password属性。

总结

对请求的处理仅仅是Spring MVC的一半内容,如果来自控制器的结果准备进行展示,那么所产生的model数据需要解析到views中并在用户的浏览器中进行展示。Spring在试图解析方面是非常灵活的,并且可以提供一些创造性的选项,包括传统的JSP和较为高级的Apache Tile页面引擎。

本章大致介绍了视图以及Spring提供的视图解析,同时对如何使用JSP和Apache Tile进行了研究,另外还有Thymeleaf。

不知道是我见识少还是我没怎么关注前端技术,好像还没有见人使用过Tile和Thymeleaf,因此本文翻译过程中显得很单薄,代码也不完善,读完之后只能有一个大体的了解,请见谅。


如果觉得有用,欢迎关注我的微信,有问题可以直接交流:

你的关注是对我最大的鼓励!
你的关注是对我最大的鼓励!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容