6.1 理解视图解析
上一章我们所编写的控制器方法都没有直接产生浏览器中渲染所需的HTML。这些方法只是将一些数据填充到模型中,然后将模型传递给一个用来渲染的视图。这些方法会返回一个String类型的值,这个值是视图的逻辑名称,不会直接引用具体的视图实现。尽管我们也编写了几个简单的JavaServer Page(JSP)视图,但是控制器并不关心这些。
将控制器中请求处理的逻辑和视图中的渲染实现解耦是Spring MVC的一个重要特性。如果控制器中的方法直接负责产生HTML的话,就很难在不影响请求处理逻辑的前提下,维护和更新视图。控制器方法和视图的实现会在模型内容上达成一致,这是两者的最大关联,除此之外,两者应该保持足够的距离。
但是,如果控制器只通过逻辑视图名来了解视图的话,那Spring该如何确定使用哪一个视图实现来渲染模型呢?这就是Spring视图解析器的任务了。在第5章中,我们使用名为InternalResourceViewResolver的视图解析器。在它的配置中,为了得到视图的名字,会使用/WEBINF/views/
前缀和.jsp
后缀,从而确定来渲染模型的JSP文件的物理位置。现在,我们回过头来看一下视图解析的基础知识以及Spring提供的其他视图解析器。
Spring MVC定义了一个名为ViewResolver的接口,它大致如下所示:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
当给resloveViewName()
方法传入一个视图名和Locale对象时,它会返回一个View实例。View接口如下:
public interface View {
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
View接口的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。
Spring提供了多个内置View实现:
表6.1 Spring视图解析实现:
视图解析器 | 描述 |
---|---|
BeanNameViewResolver | 将视图解析为Spring应用上下文中的bean,其中bean的ID与视图的名字相同 |
ContentNegotiatingViewResolver | 通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器 |
FreeMarkerViewResolver | 将视图解析为FreeMarker模板 |
InternalResourceViewResolver | 将视图解析为Web应用的内部资源(一般为JSP) |
JasperReportsViewResolver | 将视图解析为JasperReports定义 |
ResourceBundleViewResolver | 将视图解析为资源bundle(一般为属性文件) |
TilesViewResolver | 将视图解析为Apache Tile定义,其中tile ID与视图名称相同。注意有两个不同的TilesViewResolver实现,分别对应于Tiles 2.0和Tiles 3.0 |
UrlBasedViewResolver | 直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义 |
VelocityLayoutViewResolver | 将视图解析为Velocity布局,从不同的Velocity模板中组合页面 |
VelocityViewResolver | 将视图解析为Velocity模板 |
XmlViewResolver | 将视图解析为特定XML文件中的bean定义。类似于BeanName-ViewResolver |
XsltViewResolver | 将视图解析为XSLT转换后的结果 |
对于表6.1中的大部分视图解析器来讲,每一项都对应Java Web应用中特定的某种视图技术。InternalResourceViewResolver一般会用于JSP,TilesViewResolver用于Apache Tiles视图,而FreeMarkerViewResolver和VelocityViewResolver分别对应FreeMarker和Velocity模板视图。
6.2 创建JSP视图
Spring提供了两种支持JSP视图的方式:
- InternalResourceViewResolver会将视图名解析为JSP文件。另外,如果在你的JSP页面中使用了JSP标准标签库(JavaServer Pages Standard Tag Library,JSTL)的话,InternalResourceViewResolver能够将视图名解析为JstlView形式的JSP文件,从而将JSTL本地化和资源bundle变量暴露给JSTL的格式化(formatting)和信息(message)标签
- Spring提供了两个JSP标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性
6.2.1 配置适用于JSP的视图解析器
有一些视图解析器,如ResourceBundleViewResolver会直接将逻辑视图名映射为特定的View接口实现,而InternalResourceViewResolver所采取的方式并不那么直接。它遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个Web应用中视图资源的物理路径。
作为样例,考虑一个简单的场景,假设逻辑视图名为home
。通用的实践是将JSP文件放到Web应用的WEB-INF
目录下,防止对它的直接访问。如果我们将所有的JSP文件都放在/WEB-INF/views/
目录下,并且home
页的JSP名为home.jsp
,那么我们可以确定物理视图的路径就是逻辑视图名home
再加上/WEB-INF/views/
前缀和.jsp
后缀。
当使用@Bean
注解时,配置方式如下:
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
XML的配置方式为:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/views/" p:suffix=".jsp" />
解析JSTL视图
JSTL的格式化标签需要一个Locale对象,以便于恰当地格式化地域相关的值,如日期和货币。信息标签可以借助Spring的信息资源和Locale,从而选择适当的信息渲染到HTML之中。通过解析JstlView,JSTL能够获得Locale对象以及Spring中配置的信息资源。
如果想让InternalResourceViewResolver将视图解析为JstlView,而不是InternalResourceView的话,那么我们只需设置它的viewClass属性即可:
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
return resolver;
}
XML中同样加入:
p:viewClass="org.springframework.web.servlet.view.JstlView"
不管使用Java配置还是使用XML,都能确保JSTL的格式化和信息标签能够获得Locale对象以及Spring中配置的信息资源。
6.2.2 使用Spring的JSP库
当为JSP添加功能时,标签库是一种很强大的方式,能够避免在脚本块中直接编写Java代码。*Spring提供了两个JSP标签库,用来帮助定义Spring MVC Web的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定model中的某个属性。另外一个标签库包含了一些工具类标签*,我们随时都可以非常便利地使用它们。
将表单绑定到模型上
Spring的表单绑定JSP标签库包含了14个标签,它们中的大多数都用来渲染HTML中的表单标签。但是,它们与原生HTML标签的区别在于它们会绑定模型中的一个对象,能够根据模型中对象的属性填充值。标签库中还包含了一个为用户展现错误的标签,它会将错误信息渲染到最终的HTML之中。
为了使用表单绑定库,需要在JSP页面中对其进行声明:
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
表6.2 Spring标签
JSP标签 | 描述 |
---|---|
<sf:checkbox> | 渲染成一个HTML <input> 标签,其中type 属性设置为checkbox |
<sf:checkboxes> | 渲染成多个HTML <input> 标签,其中type 属性设置为checkbox |
<sf:errors> | 在一个HTML <span> 中渲染输入域的错误 |
<sf:form> | 渲染成一个HTML <form> 标签,并为其内部标签暴露绑定路径,用于数据绑定 |
<sf:hidden> | 渲染成一个HTML <input> 标签,其中type 属性设置为hidden |
<sf:input> | 渲染成一个HTML <input> 标签,其中type 属性设置为text |
<sf:label> | 渲染成一个HTML <label> 标签 |
<sf:option> | 渲染成一个HTML <option> 标签,其selected 属性根据所绑定的值进行设置 |
<sf:options> | 按照绑定的集合、数组或Map,渲染成一个HTML <option> 标签的列表 |
<sf:password> | 渲染成一个HTML <input> 标签,其中type 属性设置为password |
<sf:radiobutton> | 渲染成一个HTML <input> 标签,其中type 属性设置为radio |
<sf:radiobuttons> | 渲染成多个HTML <input> 标签,其中type 属性设置为radio |
<sf:select> | 渲染为一个HTML <select> 标签 |
<sf:textarea> | 渲染为一个HTML <textarea> 标签 |
Spittr注册表单可以改写为:
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" /> <br />
Last Name: <sf:input path="lastName" /> <br />
Username: <sf:input path="username" /> <br />
Password: <sf:password path="password" /> <br />
<input type="submit" value="Register">
</sf:form>
<sf:form>
会渲染会一个HTML <form>
标签,但它也会通过commandName
属性构建针对某个模型对象的上下文信息。在其他的表单绑定标签中,会引用这个模型对象的属性。
在之前的代码中,我们将commandName
属性设置为spitter
。因此,在模型中必须要有一个key
为spitter
的对象,否则的话,表单不能正常渲染(会出现JSP错误)。这意味着我们需要修改一下SpitterController,以确保模型中存在以spitter
为key
的Spitter对象:
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegistrationForm(Model model) {
model.addAttribute(new Spitter());
return "registerForm";
}
修改后的showRegistrationForm()
方法中,新增了一个Spitter实例到模型中。模型中的key是根据对象类型推断得到的,也就是spitter
,与我们所需要的完全一致。
回到这个表单中,前四个输入域将HTML <input>
标签改成了<sf:input>
。这个标签会渲染成一个HTML <input>
标签,并且type
属性将会设置为text
。我们在这里设置了path
属性,<input>
标签的value属性值将会设置为模型对象中path
属性所对应的值。例如,如果在模型中Spitter对象的firstName
属性值为Jack
,那么<sf:input path="firstName"/>
所渲染的<input>
标签中,会存在value="Jack"
。对于password
输入域,我们使用<sf:password>
来代替<sf:input>
。<sf:password>
与<sf:input>
类似,但是它所渲染的HTML <input>
标签中,会将type
属性设置为password
,这样当输入的时候,它的值不会直接明文显示。
展现错误
使用<sf:errors>
会很简单:
<sf:form method="POST" commandName="spitter">
First Name: <sf:input path="firstName" />
<sf:errors path="firstName" />
<br />
Last Name: <sf:input path="lastName" />
<sf:errors path="lastName" />
<br />
Username: <sf:input path="username" />
<sf:errors path="username" />
<br />
Password: <sf:password path="password" />
<sf:errors path="password" />
<br />
<input type="submit" value="Register">
</sf:form>
如果属性没有错误的话,那么<sf:errors>
不会渲染任何内容。但如果有校验错误的话,那么它将会在一个HTML <span>
标签中显示错误信息。
我们还可以更改css样式:
<sf:errors path="firstName" cssClass="error" />
css文件中增添:
span.error {
color: red;
}
我们还可以把错误信息放在同一个地方显示:
<sf:form method="POST" commandName="spitter">
<sf:errors path="*" element="div" cssClass="error" />
First Name: <sf:input path="firstName" />
<br />
Last Name: <sf:input path="lastName" />
<br />
Username: <sf:input path="username" />
<br />
Password: <sf:password path="password" />
<br />
<input type="submit" value="Register">
</sf:form>
css文件中增加:
div.error {
background-color: #ffcccc;
border: 2px solid red;
}
为了能告诉用户什么地方错了,我们需要修改表单:
<sf:form method="POST" commandName="spitter">
<sf:errors path="*" element="div" cssClass="error" />
<sf:label path="firstName" cssErrorClass="error">First Name:</sf:label>
<sf:input path="firstName" cssErrorClass="error" />
<br />
<sf:label path="lastName" cssErrorClass="error">Last Name:</sf:label>
<sf:input path="lastName" cssErrorClass="error" />
<br />
<sf:label path="username" cssErrorClass="error">Username:</sf:label>
<sf:input path="username" cssErrorClass="error" />
<br />
<sf:label path="password" cssErrorClass="error">Password:</sf:label>
<sf:password path="password" cssErrorClass="error" />
<br />
<input type="submit" value="Register">
</sf:form>
然后继续修改css文件:
label.error {
color: red;
}
input.error {
background-color: #ffcccc;
}
现在,我们有了很好的方式为用户展现错误信息。不过,我们还可以做另外一件事情,能够让这些错误信息更加易读。重新看一下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 = 5, max = 30, message="{firstName.size}")
private String firstName;
@NotNull
@Size(min = 5, max = 30, message="{lastName.size}")
private String lastName;
对于上面每个域,我们都将其@Size
注解的message
设置为一个字符串,这个字符串是用大括号括起来的。如果没有大括号的话,message
中的值将会作为展现给用户的错误信息。但是使用了大括号之后,我们使用的就是属性文件中的某一个属性,该属性包含了实际的信息。
接下来需要做的就是创建一个名为ValidationMessages.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.
ValidationMessages.properties文件中每条信息的key值对应于注解中message
属性占位符的值。同时,最小和最大长度没有硬编码在ValidationMessages.properties文件中,在这个用户友好的信息中也有自己的占位符——{min}
和{max}
——它们会引用@Size
注解上所设置的min
和max
属性。
将这些错误信息抽取到属性文件中还会带来一个好处,那就是我们可以通过创建地域相关的属性文件,为用户展现特定语言和地域的信息。例
如,如果用户的浏览器设置成了西班牙语,那么就应该用西班牙语展现错误信息,我们需要创建一个名为ValidationErrors_es.properties的文
件。
Spring通用的标签库
要使用Spring通用的标签库,我们需要进行声明:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
表6.3 Spring的JSP标签库标签:
JSP标签 | 描述 |
---|---|
<s:bind> | 将绑定属性的状态导出到一个名为status 的页面作用域属性中,与<s:path> 组合使用获取绑定属性的值 |
<s:escapeBody> | 将标签体中的内容进行HTML和/ 或JavaScript转义 |
<s:hasBindErrors> | 根据指定模型对象(在请求属性中)是否有绑定错误,有条件地渲染内容 |
<s:htmlEscape> | 为当前页面设置默认的HTML转义值 |
<s:message> | 根据给定的编码获取信息,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var 和scope 属性实现) |
<s:nestedPath> | 设置嵌入式的path ,用于<s:bind> 之中 |
<s:theme> | 根据给定的编码获取主题信息,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var 和scope 属性实现) |
<s:transform> | 使用命令对象的属性编辑器转换命令对象中不包含的属性 |
<s:url> | 创建相对于上下文的URL,支持URI模板变量以及HTML/XML/JavaScript转义。可以渲染URL(默认行为),也可以将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var 和scope 属性实现) |
<s:eval> | 计算符合Spring表达式语言(Spring Expression Language,SpEL)语法的某个表达式的值,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var 和scope 属性实现) |
展现国际化信息
对于渲染文本,<s:message>
是很好的方案,文本能够位于一个或多个属性文件中。借助<s:message>
,我们可以将硬编码的信息替换为:
<h1><s:message code="spittr.welcome" /></h1>
按照这里的方式,<s:message>
将会根据key
为spittr.welcome
的信息源来渲染文本。因此,如果我们希望<s:message>
能够正常完成任务的话,就需要配置一个这样的信息源。
Spring有多个信息源的类,它们都实现了MessageSource接口。在这些类中,更为常见和有用的是ResourceBundleMessageSource。它会从一个属性文件中加载信息,这个属性文件的名称是根据基础名称(base name)衍生而来的。如下的@Bean
方法配置了ResourceBundleMessageSource:
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
return messageSource;
}
在这个bean声明中,核心在于设置basename
属性。你可以将其设置为任意你喜欢的值,在这里,我将其设置为message
。将其设置为message
后,ResourceBundleMessageSource就会试图在根路径的属性文件中解析信息,这些属性文件的名称是根据这个基础名称衍生得到的。
另外的可选方案是使用ReloadableResourceBundleMessageSource,它的工作方式与ResourceBundleMessageSource非常类似,但是它能够重新加载信息属性,而不必重新编译或重启应用。如下是配置ReloadableResourceBundleMessageSource的样例:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("file:///etc/spitter/messages");
messageSource.setCacheSeconds(10);
return messageSource;
}
这里的关键区别在于basename
属性设置为在应用的外部查找(而不是像ResourceBundleMessageSource那样在类路径下查找)。basename
属性可以设置为在类路径下(以classpath:
作为前缀)、文件系统中(以file:
作为前缀)或Web应用的根路径下(没有前缀)查找属性。在这里,我将其配置为在服务器文件系统的/etc/spittr
目录下的属性文件中查找信息,并且基础的文件名为message
。
现在,我们来创建这些属性文件。首先,创建默认的属性文件,名为messages. properties
。它要么位于根类路径下(如果使用ResourceBundleMessageSource的话),要么位于pathname
属性指定的路径下(如果使用ReloadableResourceBundleMessageSource的话)。对spittr.welcome
信息来讲,它需要如下的条目:
spitter.welcome=Welcome to Spittr!
如果你不再创建其他信息文件的话,那么我们所做的事情就是将JSP中硬编码的信息抽取到了属性文件中,依然作为硬编码的信息。它能够让我们一站式地修改应用中的所有信息,但是它所完成的任务并不限于此。
我们已经具备了对信息进行国际化的重要组成部分。例如,如果你想要为语言设置为西班牙语的用户展现西班牙语的欢迎信息,那么需要创建另外一个名为messages_es. properties的属性文件。
创建URL
<s:url>
的主要任务是创建URL,然后将其赋值给一个变量或者渲染到响应中。它是JSTL中<c:url>
标签的替代者。
按照其最简单的形式,<s:url>
会接受一个相对于Servlet上下文的URL,并在渲染的时候,预先添加上Servlet上下文路径。例如,考虑如下<s: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是在页面内创建的,但是可以通过设置scope
属性,我们可以让<s:url>
在作用域内、会话作用域内或请求作用域内创建URL:
<s:url href="/spitter/register" var="registerUrl" scope="request" />
如果想在URL上添加参数,可以使用<s:param>
、
<s:url href="/spitter/register" var="registerUrl">
<s:param name="max" value="60" />
<s:param name="count" value="20" />
</s:url>
还可以创建带有路径参数的URL:
<s:url href="/spitter/{username}" var="spitterUrl">
<s:param name="username" value="john" />
</s:url>
当href属性中的占位符匹配<s:param>
中所指定的参数时,这个参数将会插入到占位符的位置中。如果<s:param>
参数无法匹配href
中的任何占位符,那么这个参数将会作为查询参数。
<s:url>
标签还可以解决URL的转义需求。例如,如果你希望将渲染得到的URL内容展现在Web页面上(而不是作为超链接),那么你应该要求<s:url>
进行HTML转义,这需要将htmlEscape
属性设置为true
。例如,如下的<s:url>
将会渲染HTML转义后的URL:
<s:url href="/spittles" htmlEscape="true">
<s:param name="max" value="60" />
<s:param name="count" value="20" />
</s:url>
所渲染的URL结果如下:
/spitter/spittles?max=60&count=20
如果想在JavaScript中使用URL,那么应该将javaScriptEscape
设置为true
。
<s:url href="/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>
是一个通用转义标签。他会将特殊符号进行转义从而让浏览器正确解析。
<s:escapeBody htmlEscape="true">
...
</s:escapeBody>
<s:escapeBody javaScriptEscape="true">
...
</s:escapeBody>
6.3 使用Apache Tiles视图定义布局
6.3.1 配置Tiles视图解析器
为了在Spring中使用Tiles,需要配置几个bean。我们需要一个TilesConfigurer bean,它会负责定位和加载Tile定义并协调生成Tiles。除此之外,还需要TilesViewResolver bean将逻辑视图名称解析为Tile定义。
这两个组件又有两种形式:针对Apache Tiles 2和Apache Tiles 3分别都有这么两个组件。这两组Tiles组件之间最为明显的区别在于包名。针对Apache Tiles 2的TilesConfigurer/TilesViewResolver位于org.springframework.web.servlet.view.tiles2
包中,而针
对Tiles 3的组件位于org.springframework.web.servlet.view.tiles3
包中。对于该例子来讲,假设我们使用的是Tiles 3。
首先,配置TilesConfigurer来解析Tile定义。
@Bean
public TilesConfigurer tilesConfiger() {
TilesConfigurer tiles = new TilesConfigurer();
tiles.setDefinitions(new String[]{"/WEB-INF/layout/tiles.xml"});
tiles.setCheckRefresh(true);
return tiles;
}
当配置TilesConfigurer的时候,所要设置的最重要的属性就是definitions
。这个属性接受一个String类型的数组,其中每个条目都指定一个Tile定义的XML文件。对于Spittr应用来讲,我们让它在/WEB-INF/layout/
目录下查找tiles.xml。
其实我们还可以指定多个Tile定义文件,甚至能够在路径位置上使用通配符,当然在上例中我们没有使用该功能。例如,我们要求TilesConfigurer加载/WEB-INF/
目录下的所有名字为tiles.xml的文件,那么可以按照如下的方式设置definitions
属性:
tiles.setDefinitions(new String[]{"/WEB-INF/**/tiles.xml"});
我们使用了Ant风格的通配符**
,所以TilesConfigurer会遍历WEB-INF/
的所有子目录来查找Tile定义。
接下来,让我们来配置TilesViewResolver,可以看到,这是一个很基本的bean定义,没有什么要设置的属性:
@Bean
public ViewResolver viewResolver() {
return new TilesViewResolver();
}
XML中配置为:
<bean id="titlesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/layout/tiles.xml</value>
<value>/WEB-INF/views/**/tites.xml</value>
</list>
</property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.tiles3.TilesViewResolver" />
TilesConfigurer会加载Tile定义并与Apache Tiles协作, 而TilesViewResolver会将逻辑视图名称解析为引用Tile定义的视图。它是通过查找与逻辑视图名称相匹配的Tile定义实现该功能的。
定义Tiles
Apache Tiles提供了一个文档类型定义(Document type definition, DTD),用来在XML文件中指定Tile的定义。每个定义中需要包含一个<definition>
元素,这个元素会有一个或多个<put-attribute>
元素。如titles.xml
:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTA Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
<definition name="base" template="/WEB-INF/layout/page.jsp">
<put-attribute name="header" value="/WEB-INF/layout/header.jsp"></put-attribute>
<put-attribute name="footer" value="/WEB-INF/layout/footer.jsp"></put-attribute>
</definition>
<definition name="home" extends="base">
<put-attribute name="body" value="/WEB-INF/views/home.jsp"></put-attribute>
</definition>
<definition name="profile" extends="base">
<put-attribute name="body" value="/WEB-INF/views/profile.jsp"></put-attribute>
</definition>
<definition name="spittles" extends="base">
<put-attribute name="body" value="/WEB-INF/views/spittles.jsp"></put-attribute>
</definition>
<definition name="spittle" extends="base">
<put-attribute name="body" value="/WEB-INF/views/spittle.jsp"></put-attribute>
</definition>
</tiles-definitions>
每个<definition>
元素都定义了一个Tile,它最终引用的是一个JSP模板。在名为base的Tile中,模板引用的/WEBINF/layout/page.jsp
。某个Tile可能还会引用其他的JSP模板,使这些JSP模板嵌入到主模板中。对于base
Tile来讲,它引用的是一个头部JSP模板和一个底部JSP模板。
page.jsp
:
<%@ 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">
<html>
<head>
<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"></t:insertAttribute>
</div>
<div id="content">
<t:insertAttribute name="body"></t:insertAttribute>
</div>
<div id="footer">
<t:insertAttribute name="footer"></t:insertAttribute>
</div>
</body>
</html>
header.jsp
:
<%@ 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都定义了自己的主体区模板,所以每个都会与其他的有所区别。但是为了完整地了解home
Tile,如下展现了home.jsp
:
<%@ page session="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<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
中,其他的Tile模板中不再包含这部分内容。这使得它们能够跨页面重用,这些元素的维护也得以简化。
下面是效果图:
6.4 使用Thymeleaf
6.4.1 配置Thymeleaf视图解析器
为了在Spring中使用Thymeleaf,我们需要配置三个启用Thymeleaf与Spring集成的bean:
- ThymeleafViewResolver:将逻辑视图名称解析为Thymeleaf模板视图
- SpringTemplateEngine:处理模板并渲染结果
- TemplateResolver:加载Thymeleaf模板
Java配置如下:
package spittr.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolver;
@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public TemplateResolver templateResolver() {
TemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
/**
* 配置JSP视图解析器
* @return
*/
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
return resolver;
}
/**
* 配置静态资源的处理
*/
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
XML配置:
<bean id="viewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver" p:templateEngine-ref="templateEngine" />
<bean id="templateEngine" class="org.thymeleaf.spring4.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 bean中注入了一个对SpringTemplate Engine bean的引用。SpringTemplateEngine会在Spring中启用Thymeleaf引擎,用来解析模板,并基于这些模板渲染结果。可以看到,我们为其注入了一个TemplateResolver bean的引用。
TemplateResolver会最终定位和查找模板。与之前配置InternalResource-ViewResolver类似,它使用了prefix和suffix属性。前缀和后缀将会与逻辑视图名组合使用,进而定位Thymeleaf引擎。它的templateMode属性被设置成了HTML 5,这表明我们预期要解析的模板会渲染成HTML 5输出。
6.4.2 定义Thymeleaf模版
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xthml"
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 href="@{/spitter/register}">Register</a>
</body>
</html>
首页模板相对来讲很简单,只使用了th:href
属性。这个属性与对应的原生HTML属性很类似,也就是href
属性,并且可以按照相同的方式来使用。th:href
属性的特殊之处在于它的值中可以包含Thymeleaf表达式,用来计算动态的值。它会渲染成一个标准的href
属性,其中会包含在渲染时动态创建得到的值。这是Thymeleaf命名空间中很多属性的运行方式:它们对应标准的HTML属性,并且具有相同的名称,但是会渲染一些计算后得到的值。在本例中,使用th:href
属性的三个地方都用到了@{}
表达式,用来计算相对于URL的路径(就像在JSP页面中,我们可能会使用的JSTL <c:url>
标签或Spring<s:url>
标签类似)。
借助Thymeleaf实现表单绑定
表单绑定是Spring MVC的一项重要特性。它能够将表单提交的数据填充到命令对象中,并将其传递给控制器,而在展现表单的时候,表单中也会填充命令对象中的值。如果没有表单绑定功能的话,我们需要确保HTML表单域要映射后端命令对象中的属性,并且在校验失败后展现表单的时候,还要负责确保输入域中值要设置为命令对象的属性。但是,如果有表单绑定的话,它就会负责这些事情了。
我们可以使用:
<label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>
<input type="text" th:field="*{firstName}" th:class="${#fields.hasErrors('firstName')}? 'error'" /><br />
在这里,我们不再使用Spring JSP标签中的cssClassName
属性,而是在标准的HTML标签上使用th:class
属性。th:class
属性会渲染为一个class
属性,它的值是根据给定的表达式计算得到的。在上面的这两个th:class
属性中,它会直接检查firstName
域有没有校验错误。如果有的话,class
属性在渲染时的值为error
。如果这个域没有错误的话,将不会渲染class
属性。
<input>
标签使用了th:field
属性,用来引用后端对象的firstName
域。这可能与你的预期有点差别。在Thymeleaf模板中,我们在很多情况下所使用的属性都对应于标准的HTML属性,因此貌似使用th:value
属性来设置<input>
标签的value
属性才是合理的。
其实不然,因为我们是在将这个输入域绑定到后端对象的firstName
属性上,因此使用th:field
属性引用firstName
域。通过使用th:field
,我们将value
属性设置为firstName
的值,同时还会将name
属性设置为firstName
。
为了阐述Thymeleaf是如何实际运行的,如下的程序清单展示了完整的注册表单模板:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xthml"
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>
<form method="POST" th:object="${spitter}">
<div class="error" 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('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>
</body>
</html>
另外,把SpitterController里改为:
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String showRegistrationForm(Model model) {
model.addAttribute("spitter", new Spitter());
return "registerForm";
}
程序使用了相同的Thymeleaf属性和*{}
表达式,为所有的表单域绑定后端对象。这其实重复了我们在firstName
域中所做的事情。但是,需要注意我们在表单的顶部了也使用了Thymeleaf,它会用来渲染所有的错误。<div>
元素使用th:if
属性来检查是否有校验错误。如果有的话,会渲染<div>
,否则的话,它将不会渲染。在<div>
中,会使用一个无顺序的列表来展现每项错误。<li>
标签上的th:each
属性将会通知Thymeleaf为每项错误都渲染一个<li>
,在每次迭代中会将当前错误设置到一个名为err
的变量中。<li>
标签还有一个th:text
属性。这个命令会通知Thymeleaf计算某一个表达式(在本例中,也就是err
变量)并将它的值渲染为<li>
标签的内容体。实际上的效果就是每项错误对应一个<li>
元素,并展现错误的文本。
${}
表达式(如${spitter}
)是变量表达式(variable expression)。一般来讲,它们会是对象图导航语言(Object-Graph Navigation Language,OGNL)表达式。但在使用Spring的时候,它们是SpEL表达式。在${spitter}
这个例子中,它会解析为key
为spitter
的model
属性。
而对于*{}
表达式,它们是选择表达式(selection expression)。变量表达式是基于整个SpEL上下文计算的,而选择表达式是基于某一个选中对象计算的。在本例的表单中,选中对象就是<form>
标签中th:object
属性所设置的对象:模型中的Spitter对象。因此,*{firstName}
表达式就会计算为Spitter对象的firstName
属性。