渲染Web视图
理解视图解析
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接口的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。
尽管我们可以编写ViewResolver和View的实现,在有些特定的场景下,这样做也是有必要的,但是一般来讲,我们并不需要关心这些接口。我在这里提及这些接口只是为了让你对视图解析内部如何工作有所了解。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转换后的结果 |
Spring 4和Spring 3.2支持表中的所有视图解析器。Spring 3.1支持除Tiles 3 TilesViewResolver之外的所有视图解析器。
对于表中的大部分视图解析器来讲,每一项都对应Java Web应用中特定的某种视图技术。InternalResourceViewResolver一般会用 于JSP,TilesViewResolver用于Apache Tiles视图,而FreeMarkerViewResolver和VelocityViewResolver分别对应 FreeMarker和Velocity模板视图。
创建JSP视图
Spring提供了两种支持JSP视图的方式:
- InternalResourceViewResolver会将视图名解析为JSP文件。另外,如果在你的JSP页面中使用了JSP标准标签库(JavaServer Pages Standard Tag Library,JSTL)的话,InternalResourceViewResolver能够将视图名解析为JstlView形式的JSP文件,从而将JSTL本地化和资源bundle变量暴露给JSTL的格式化(formatting)和信息(message)标签。
- Spring提供了两个JSP标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性。
不管使用JSTL,还是准备使用Spring的JSP标签库,配置解析JSP的视图解析器都是非常重要的。尽管Spring还有其他的几个视图解析器都能将视图名映射为JSP文件,但就这项任务来讲,InternalResourceViewResolver是最简单和最常用的视图解析器。
配置适用于JSP的视图解析器
有一些视图解析器,如ResourceBundleViewResolver会直接将逻辑视图名映射为特定的View接口实现,而InternalResourceViewResolver所采取的方式并不那么直接。它遵循一种约定,会在视图名上添加前缀和后缀,进而确定一个Web应用中视图资源的物理路径。
通用的实践是将JSP文件放到Web应用的WEB-INF目录下,防止对它的直接访问。如果我们将所有的JSP文件都放在“/WEB-INF/views/”目录下,并且home页的JSP名为home.jsp,那么我们可以确定物理视图的路径就是逻辑视图名home再加上“/WEB-INF/views/”前缀和“.jsp”后缀。
当使用@Bean注解的时候,我们可以按照如下的方式配置Internal-ResourceView Resolver,使其在解析视图时,遵循上述的约定。
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
作为替代方案,如果更喜欢使用基于XML的Spring配置,那么可以按照如下的方式配置InternalResourceViewResolver:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:perfix="/WEB-INF/views/" p:suffix=".jsp" />
InternalResourceViewResolver配置就绪之后,它就会将逻辑视图名解析为JSP文件,如下所示:
- home将会解析为“/WEB-INF/views/home.jsp”
- productList将会解析为“/WEB-INF/views/productList.jsp”
- books/detail将会解析为“/WEB-INF/views/books/detail.jsp”
当逻辑视图名中包含斜线时,这个斜线也会带到资源的路径名中。因此,它会对应到prefix属性所引用目录的子目录下的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完成这一任务:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:perfix="/WEB-INF/views/" p:suffix=".jsp" p:ViewClass="org.springframework.web.servlet.view.JstlView" />
不管使用Java配置还是使用XML,都能确保JSTL的格式化和信息标签能够获得Locale对象以及Spring中配置的信息资源。
使用Spring的JSP库
当为JSP添加功能时,标签库是一种很强大的方式,能够避免在脚本块中直接编写Java代码。Spring提供了两个JSP标签库,用来帮助定义Spring MVC Web的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定model中的某个属性。另外一个标签库包含了一些工具类标签,随时都可以非常便利地使用它们。
在这两个标签库中,可能会发现表单绑定的标签库更加有用。所以,就从这个标签库开始学习Spring的JSP标签。我们将会看到如何将Spittr应用的注册表单绑定到模型上,这样表单就可以预先填充值,并且在表单提交失败后,能够展现校验错误。
将表单绑定到模型上
Spring的表单绑定JSP标签库包含了14个标签,它们中的大多数都用来渲染HTML中的表单标签。但是,它们与原生HTML标签的区别在于它们会绑定模型中的一个对象,能够根据模型中对象的属性填充值。标签库中还包含了一个为用户展现错误的标签,它会将错误信息渲染到最终的HTML之中。
为了使用表单绑定库,需要在JSP页面中对其进行声明:
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
需要注意,我将前缀指定为“sf”,但通常也可能使用“form”前缀。你可以选择任意喜欢的前缀,我之所以选择“sf”是因为它很简洁、易于输入,并且还是Spring form的简写形式。
| JSP标签 | 描 述 |
|---|---|
| <sf:checkbox > | 渲染成一个HTML input 标签,其中type属性设置为checkbox |
| <sf:checkboxes > | 渲染成多个HTML input 标签,其中type属性设置为checkbox |
| <sf:errors > | 在一个HTML span 中渲染输入域的错误 |
| <sf:form > | 渲染成一个HTML 标签,并为其内部标签暴露绑定路径,用于数据绑定 |
| <sf:hidden > | 渲染成一个HTML input 标签,其中type属性设置为hidden |
| <sf:input > | 渲染成一个HTML input 标签,其中type属性设置为text |
| <sf:label > | 渲染成一个HTML label 标签 |
| <sf:option > | 渲染成一个HTML 标签,其selected属性根据所绑定的值进行设置 |
| <sf:options > | 按照绑定的集合、数组或Map,渲染成一个HTML 标签的列表 |
| <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 标签 |
<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>
<sf:form >会渲染会一个HTML 标签,但它也会通过commandName属性构建针对某个模型对象的上下文信息。在其他的表单绑定标签中,会引用这个模型对象的属性。
在之前的代码中,将commandName属性设置为spitter。因此,在模型中必须要有一个key为spitter的对象,否则的话,表单不能正常渲染(会出现JSP错误)。这意味着需要修改一下SpitterController,以确保模型中存在以spitter为key的Spitter对象:
@RequestMapping(value="/register", method=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属性,标签的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,这样当输入的时候,它的值不会直接明文显示。
<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:
<password id="password" name="password" type="password" value="" /><br/>
<input type="submit" value="Register" />
</form>
值得注意的是,从Spring 3.1开始,<sf:input >标签能够允许我们指定type属性,这样的话,除了其他可选的类型外,还能指定HTML 5特 定类型的文本域,如date、range和email。例如,我们可以按照如下的方式指定email域:
Email: <sf:input path="email" type="email" /><br/>
这样所渲染得到的HTML如下所示:
Email:
<input id="email" name="email" type="text" value="jack" /><br/>
相对于标准的HTML标签,使用Spring的表单绑定标签能够带来一定的功能提升,在校验失败后,表单中会预先填充之前输入的值。但是,这 依然没有告诉用户错在什么地方。为了指导用户矫正错误,我们需要使用<sf:errors >。
展现错误
如果存在校验错误的话,请求中会包含错误的详细信息,这些信息是与模型数据放到一起的。我们所需要做的就是到模型中将这些数据抽取出来,并展现给用户。<sf:errors >能够让这项任务变得很简单。
看一下将<sf:errors >用到registerForm.jsp中的代码片段:
<sf:form method="POST" commandName="spitter" >
First Name:
<sf:input path="firstName" />
<sf:errors path="firstName" /><br/>
</sf:form>
尽管只展现了将<sf:errors >用到First Name输入域的场景,但是它可以按照同样简单的方式用到注册表单的其他输入域中。在这里,它的path属性设置成了firstName,也就是指定了要显示Spitter模型对象中哪个属性的错误。如果firstName属性没有错误的话,那么<sf:errors >不会渲染任何内容。但如果有校验错误的话,那么它将会在一个HTML 标签中显示错误信息。
First Name:
<input id="firstName" name="firstName" type="text" value="J" />
<span id="firstName.errors">size must be between 2 and 30</span>
现在,已经可以为用户展现错误信息,这样就能修正这些错误了。可以更进一步,修改错误的样式,使其更加突出显示。为了做到这一点,可以设置cssClass属性:
<sf:form method="POST" commandName="spitter" >
First Name:
<sf:input path="firstName" />
<sf:errors path="firstName" cssClass="error" /><br/>
</sf:form>
同样,简单起见,只会展现如何为firstName输入域<sf:errors >的设置cssClass属性。可以将其用到其他的输入域上。
现在errors的会有一个值为error的class属性。剩下需要做的就是为这个类定义CSS样式。如下就是一个简单的CSS样式,它会将错误信息渲染为红色:
span.error {
color: 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
private String email;
对于上面每个域,我们都将其@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.
email.valid=The email address must be valid.
ValidationMessages.properties文件中每条信息的key值对应于注解中message属性占位符的值。同时,最小和最大长度没有硬编码在ValidationMessages.properties文件中,在这个用户友好的信息中也有自己的占位符——{min}和{max}——它们会引用@Size注解上所设置的min和max属性。
将这些错误信息抽取到属性文件中还会带来一个好处,那就是我们可以通过创建地域相关的属性文件,为用户展现特定语言和地域的信息。例如,如果用户的浏览器设置成了西班牙语,那么就应该用西班牙语展现错误信息,我们需要创建一个名为Validation- Errors_es.properties的文件,内容如下:
firstName.size=Nombre debe ser entre {min} y {max} caracteres largo.
lastName.size=El apellido debe ser entre {min} y {max} caracteres largo.
username.size=Nombre de usuario debe ser entre {min} y {max} caracteres largo.
password.size=Contrase�a debe estar entre {min} y {max} caracteres largo.
email.valid=La direcci�n de email no es v�lida
我们可以按需创建任意数量的ValidationMessages.properties文件,使其涵盖我们想支持的所有语言和地域。
Spring通用的标签库
除了表单绑定标签库之外,Spring还提供了更为通用的JSP标签库。实际上,这个标签库是Spring中最早的标签库。这么多年来,它有所变化,但是在最早版本的Spring中,它就已经存在了。
要使用Spring通用的标签库,我们必须要在页面上对其进行声明:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
与其他JSP标签库一样,prefix可以是任意你所喜欢的值。在这里,通用的做法是将这个标签库的前缀设置为spring。但是,我将其设置为“s”,因为它更加简洁,更易于阅读和输入。
| JSP标签 | 描 述 |
|---|---|
| <s:bind > | 将绑定属性的状态导出到一个名为status的页面作用域属性中,与组合使用获取绑定属性的值 |
| <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属性实现) |
表6中的一些标签已经被Spring表单绑定标签库淘汰了。例如,<s:bind >标签就是Spring最初所提供的表单绑定标签,它比我们在前面所介 绍的标签复杂得多。
展现国际化信息
Web是全球性的网络,所构建的应用很可能会有全球化用户。因此,最好能够使用用户的语言与其进行交流,而不是只使用某一种语言。
对于渲染文本来说,是很好的方案,文本能够位于一个或多个属性文件中。借助<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后,ResourceBundle-MessageSource就会试图在根路径的属性文件中解析信息,这些属性文件的名称是根据这个基础名称衍生得到的。
另外的可选方案是使用ReloadableResourceBundleMessageSource,它的工作方式与ResourceBundleMessageSource非常类似,但是它能够重新加载信息属性,而不必重新编译或重启应用。如下是配置ReloadableResourceBundle-MessageSource的样例:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("file:///Users/habuma/messages");
messageSource.setCacheSeconds(10);
return messageSource;
}
这里的关键区别在于basename属性设置为在应用的外部查找(而不是像ResourceBundleMessageSource那样在类路径下查找)。basename属性可以设置为在类路径下(以“classpath:”作为前缀)、文件系统中(以“file:”作为前缀)或Web应用的根路径下(没有前缀)查找属性。在这里,我将其配置为在服务器文件系统的“/etc/spittr”目录下的属性文件中查找信息,并且基础的文件名为“message”。
现在,创建这些属性文件。首先,创建默认的属性文件,名为messages. properties。它要么位于根类路径下(如果使用ResourceBundleMessageSource的话),要么位于pathname属性指定的路径下(如果使用ReloadableResourceBundle-MessageSource的话)。对spittr.welcome信息来讲,它需要如下的条目:
spittr.welcome=Welcome to Spittr!
如果不再创建其他信息文件的话,那么所做的事情就是将JSP中硬编码的信息抽取到了属性文件中,依然作为硬编码的信息。它能够一站式地修改应用中的所有信息,但是它所完成的任务并不限于此。
已经具备了对信息进行国际化的重要组成部分。例如,如果想要为语言设置为西班牙语的用户展现西班牙语的欢迎信息,那么需要创建另外一个名为messages_es. properties的属性文件,并包含如下的条目:
spittr.welcome=Bienvenidos a Spittr!
创建url
按照其最简单的形式,<s:url >会接受一个相对于Servlet上下文的URL,并在渲染的时候,预先添加上Servlet上下文路径。例如,考虑如下<s:url >的基本用法:
<a href="<s:url href="/spitter/register" />">Register</a>
如果应用的Servlet上下文名为spittr,那么在响应中将会渲染如下的HTML:
<a href="/spittr/spitter/register">Register</a>
这样,我们在创建URL的时候,就不必再担心Servlet上下文路径是什么了,<s:url >将会负责这件事
另外,我们还可以使用<s:url >创建URL,并将其赋值给一个变量供模板在稍后使用:
<s:url href="/spitter/register" var="registerUrl" />
<a hreaf="${registerUrl}">Register</a>
默认情况下,URL是在页面作用域内创建的。但是通过设置scope属性,我们可以让在<s:url >应用作用域内、会话作用域内或请求作用域内创建URL:
<s:url href="/spitter/register" var="registerUrl" scope="request" />
如果希望在URL上添加参数的话,那么你可以使用标签。比如,如下的<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>
假设需要为特定用户的基本信息页面创建一个URL。那没有问题,<s:param >标签可以承担此任:
<s:url href="/spitter/{username}" var="spitterUrl">
<s:param name="username" value="jbauer" />
</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的话,那么应该将javaScript-Escape属性设置为true:
<s:url href="/spittles" var="spittlesUrl" javaScriptEscape>
<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代码片段。为了正确显示,我们需要将“<”和“>”字符替换为“<”和“>”,否则的话,浏览器将会像解析页面上其他HTML那样解析这段HTML内容。
当然,没有人禁止我们手动将其转义为“<”和“>”,但是这很烦琐,并且代码难以阅读。我们可以使用,并让Spring完成这项任务:
<s:escapeBody htmlEscape="true">
<h1>Hello</h1>
</s:escapeBody>
它将会在响应体中渲染成如下的内容:
<h1>Hello</h1>
虽然转义后的格式看起来很难读,但浏览器会很乐意将其转换为未转义的HTML,也就是我们希望用户能够看到的样子。
通过设置javaScriptEscape属性,<s:escapeBody >标签还支持JavaScript转义:
<s:escapeBody javaScriptEscape="true">
<h1>Hello</h1>
</s:escapeBody>
<s:escapeBody >只完成一件事,并且完成得非常好。与<s:url >不同,它只会渲染内容,并不能将内容设置为变量。
使用Apache Tiles视图定义布局
使用布局引擎,如Apache Tiles,定义适用于所有页面的通用页面布局。Spring MVC以视图解析器的形式为Apache Tiles提供了支持,这个视图解析器能够将逻辑视图名解析为Tile定义。
配置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 tilesConfigurer() {
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/views/**/tiles.xml"
});
使用了Ant风格的通配符(**),所以TilesConfigurer会遍历“WEB-INF/”的所有子目录来查找Tile定义。
接下来,配置TilesViewResolver,可以看到,这是一个很基本的bean定义,没有什么要设置的属性:
@Bean
public ViewResolver viewResolver() {
return new TilesViewResolver();
}
如果更喜欢XML配置的话,那么可以按照如下的形式配置TilesConfigurer和TilesViewResolver:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/layout/tiles.xml</value>
<value>/WEB-INF/views/**/tiles.xml</value>
</list>
</property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.tile3.TilesViewResolver" />
TilesConfigurer会加载Tile定义并与Apache Tiles协作, 而TilesViewRe-solver会将逻辑视图名称解析为引用Tile定义的视图。它是通过查找与逻辑视图名称相匹配的Tile定义实现该功能的。我们需要创建几个Tile定义以了解它是如何运转的。
定义Tiles
Apache Tiles提供了一个文档类型定义(document type definition,DTD),用来在XML文件中指定Tile的定义。每个定义中需要包含一 个元素,这个元素会有一个或多个元素。例如,如下的XML文档为Spittr应用定义了几个Tile。
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!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>
<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>
每个元素都定义了一个Tile,它最终引用的是一个JSP模板。在名为base的Tile中,模板引用的是“/WEB-INF/layout/page.jsp”。某个Tile可能还会引用其他的JSP模板,使这些JSP模板嵌入到主模板中。对于base Tile来讲,它引用的是一个头部JSP模板和一个底部JSP模板
base Tile所引用的page.jsp模板如下面程序所示。
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="t" %>
<%@ page session="false" %>
<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" />
</div>
<div id="content">
<t:insertAttribute name="body" />
</div>
<div id="footer">
<t:insertAttribute name="footer" />
</div>
</body>
</html>
需要重点关注的事情就是如何使用Tile标签库中<t:insert Attribut >的 JSP标签来插入其他的模板。在这里,用它来插入名为header、body和footer的模板。
base Tile不会期望单独使用。它会作为基础定义(这是其名字的来历),供其他的Tile定义扩展。在上述程序的其余内容中,我们可以看到其他的Tile定义都是扩展自base Tile。它意味着它们会继承其header和footer属性的设置(当然,Tile定义中也可以覆盖掉这些属性),但是每一个都设置了body属性,用来指定每个Tile特有的JSP模板。
关注一下home Tile,它扩展了base。因为它扩展了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>
属性所引用的每个模板是很简单的,如下是header.jsp模板:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
<a href="<s:url value="/" />"><img
src="<s:url value="/resources" />/images/spitter_logo_50.png"
border="0"/></a>
footer.jsp模板更为简单:
Copyright © Craig Walls
每个扩展自base的Tile都定义了自己的主体区模板,所以每个都会与其他的有所区别。但是为了完整地了解home Tile,如下展现了home.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<h1>Welcome to Spitter</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模板中不再包含这部分内容。这使得它们能够跨页面重用,这些元素的维护也得以简化。
使用Thymeleaf
标签库和JSP缺乏良好格式的一个副作用就是它很少能够与其产生的HTML类似。所以,在Web浏览器或HTML编辑器中查看未经渲染的JSP模板是非常令人困惑的,而且得到的结果看上去也非常丑陋。这个结果是不完整的——在视觉上这简直就是一场灾难!因为JSP并不是真正的HTML,很多浏览器和编辑器展现的效果都很难在审美上接近模板最终所渲染出来的效果。
同时,JSP规范是与Servlet规范紧密耦合的。这意味着它只能用在基于Servlet的Web应用之中。JSP模板不能作为通用的模板(如格式化Email),也不能用于非Servlet的Web应用。
配置Thymeleaf视图解析器
为了要在Spring中使用Thymeleaf,需要配置三个启用Thymeleaf与Spring集成的bean:
- ThymeleafViewResolver:将逻辑视图名称解析为Thymeleaf模板视图
- SpringTemplateEngine:处理模板并渲染结果
- TemplateResolver:加载Thymeleaf模板
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
@Bean
public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
@Bean
public TemplateResolver templateResolver() {
TemplateResolver templateResolver = new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}
使用XML来配置bean,那么如下的声明能够完成该任务
<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" />
不管使用哪种配置方式,Thymeleaf都已经准备就绪了,它可以将响应中的模板渲染到Spring MVC控制器所处理的请求中。
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输出。
定义Thymeleaf模板
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"></link>
</head>
<body>
<h1>Welcome to Spitter</h1>
<a th:href="@{/spittles}">Spittles</a> |
<a th: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 >标签类似)。
尽管home.html是一个相当简单的Thymeleaf模板,但是它依然很有价值,这在于它与纯HTML模板非常接近。唯一的区别之处在于th:href属性,否则的话,它就是基础且功能丰富的HTML文件。
这意味着Thymeleaf模板与JSP不同,它能够按照原始的方式进行编辑甚至渲染,而不必经过任何类型的处理器。当然,我们需要Thymeleaf来处理模板并渲染得到最终期望的输出。即便如此,如果没有任何特殊的处理,home.html也能够加载到Web浏览器中,并且看上去与完整渲染的效果很类似。
借助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。
<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属性和“*{}”表达式,为所有的表单域绑定后端对象。这其实重复了我们在First Name域中所做的事情。
但是,需要注意我们在表单的顶部了也使用了Thymeleaf,它会用来渲染所有的错误。
在
想知道{}表达式(如{spitter}这个例子中,它会解析为key为spitter的model属性。
而对于*{}表达式,它们是选择表达式(selection expression)。变量表达式是基于整个SpEL上下文计算的,而选择表达式是基于某一个选中对象计算的。在本例的表单中,选中对象就是标签中th:object属性所设置的对象:模型中的Spitter对象。因此*{firstName}表达式就会计算为Spitter对象的firstName属性。