渲染Web视图
本章主要内容包括:
- 将数据模型渲染为HTML
- 使用JSP视图
1 理解视图解析
将控制器中请求处理的逻辑和视图中的渲染实现解耦是Spring MVC 的一个重要特性。如果控制器中的方法直接负责产生HTML的话,就很难在不影响请求处理逻辑的前提下,维护和更新视图。控制器方法和视图的实现会在模型内容上达成一致,这是两者的最大关联,除此之外,两者应该保持足够的距离。但是,如果控制器只通过逻辑视图来了解视图的话,那Spring该如何确定使用哪一个视图实现来渲染模型呢?这就是Spring视图解析器的任务了。
在之前的章节中,我们使用名为InternalResourceViewResolver的视图解析器。在它的配置中,为了得到视图的名字,会使用”/WEB-INF/views/”前缀和”.jsp”后缀,从而确定来渲染模型的JSP文件的物理位置。现在,我们回过头来看看视图解析器的基础知识以及Spring提供的其他视图解析器。
Spring MVC 定义了一个名为ViewResolver的接口,它大致如下所示:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
当给resolveViewName()方法传入一个视图名和Locale对象时,它会返回一个View实例。View是另外一个接口:
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
View接口的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。
这看起来非常简单。我们所需要做的就是实现ViewResolver和View接口,将要渲染的内容放到response中,进而展现到用户的浏览器中。但是,实际上我们不需要这么麻烦。尽管我们可以自己编写ViewResolver和View的实现,在有些特定的场景下,这样做也是有必要的,但是一般来讲,我们并不需要关心这些接口。Spring提供了多个内置的实现,如下列表所示,它们能够适应大多数的场景。
视图解析器 | 描述 |
---|---|
BeanNameViewResolver | 将视图解析为Spring应用上下文中的bean,其中bean的ID与视图的名字相同 |
ContentNegotiatingViewResolver | 通过考虑客户端需要的内容类型来解析视图,委托给另外一个能够产生对应内容类型的视图解析器 |
FreeMarkerViewResolver | 将视图解析为FreeMarker模板 |
InternalResourceViewResolver | 将视图解析为Web应用的内部资源(一般为JSP) |
JasperReportsViewResolver | 将视图解析为JapserReports定义 |
ResourceBundleViewResolver | 将视图解析为资源bundle(一般为属性文件) |
TilesViewResolver | 将视图解析为ApacheTile定义,其中tile ID与视图名称相同。注意有两个不同的TilesViewResolver实现,分别对应于Tiles2.0和Tiles3.0 |
UrlBasedViewResolver | 直接根据视图的名称解析视图,视图的名称会匹配一个物理视图的定义 |
VelocityLayoutViewResolver | 将视图解析为Velocity布局,从不同的Velocity模板中组合页面 |
VelocityViewResolver | 将视图解析为Velocity模板 |
XmlViewResolver | 将视图解析为特定XML文件中的bean定义。类似于BeanNameViewResolver |
XsltViewResolver | 将视图解析为XSLT转换后的结果 |
Spring4和Spring3.2支持以上所有的视图解析器。Spring3.1支持除Tiles3.0 TilesViewResolver之外的所有视图解析器。对于大部分的视图解析器来讲,每一项都对应Java Web应用中特定的某种视图技术。InternalResourceViewResolver一般会用于JSP,TilesViewResolver会用于Apache Tiles视图,而FreeMarkerViewResolver和VelocityViewResolver分别对应FreeMarker和Velocity模板视图。
2 创建JSP视图
Spring提供了两种支持JSP视图的方式:
- InternalResourceViewResolver会将视图名解析为JSP文件。另外,如果在JSP页面中使用了JSP标准签库(JavaServer Pages Standard Tag library,JSTL)的话,InternalResourceViewResolver能够将视图名解析为JstlView形式的JSP文件,从而将JSTL本地化和资源bundle变量暴露给JSTL的格式化(formatting)和信息(message)标签
- Spring提供了连个JSP标签库,一个用于表单到模型的绑定,另一个提供了通用的工具类特性
2.1 配置使用于JSP的视图解析器
有一些视图解析器,如ResourceBundleViewResolver会直接将逻辑视图名称映射为特定的View接口实现,而InternalResourceViewResolver所采取的方式并不那么直接。它遵循一种约定,会在视图名称上添加前缀和后缀,进而确定一个Web应用中视图资源的物理路径。
在前面的章节中,我们已经使用过Java类配置的方式,如下:
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
//配置JSP视图解析器
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"/>
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视图
到目前为止,我们对InternalResourceViewResolver的配置都很基础和简单。他最终会将逻辑视图名解析为InternalResourceView实例,这个实例会引用JSP文件。但是如果这些JSP使用JSTL标签来处理格式化和信息的话,那么我们会希望InternalResourceViewResolver将视图解析为JstlView。
JSTL的格式化标签需要一个Locale对象,以便于恰当地格式化地域相关的值,如日期和货币。信息标签可以借助Spring的信息资源和Locale,从而选择恰当的信息渲染到HTML中。通过解析JstlView,JSTL能够获得Locale对象以及Spring中配置的信息资源。
如果想让InternalResourceViewResolver将视图解析为JstlView,而不是InternalResourceView的话,那么我们只需要设置它的viewClass属性即可:
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
//配置JSP视图解析器
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setViewClass(JstlView.class);
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
同样的可以使用XML来完成配置:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/views/" p:suffix=".jsp" p:viewClass="org.springframework.web.servlet.view.JstlView"/>
2.2 使用Spring的JSP库
之前已经提到过Spring提供了两个JSP标签库,用来帮助定义Spring MVC Web 的视图。其中一个标签库会用来渲染HTML表单标签,这些标签可以绑定到model中的某个属性。另外一个标签库包含了一些工具类标签。
将表单绑定到模型上
Spring中的表单绑定JSP标签库包含了14个标签,它们中的大多数都是用来渲染HTML中的表单标签。但是,它们与原生HTML表单标签的区别在于它们会绑定模型中的一个对象,能够根据模型中对象的属性填充值。标签库还包含了一个为用户展示错误的标签,它会将错误信息渲染到最终的HTML上。
为了使用表单绑定标签,需要在JSP页面中对其进行声明:
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
这里将前缀指定为”sf”,是”spring form”的缩写。在完成绑定标签库之后,就可以使用14个相关的标签了。
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<label>标签,其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>、<sf:input>和<sf:password>。在注册JSP中使用这些标签后,代码如下:
<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"/>
Password: <sf:input path="password"/>
<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 showRegistrationrForm(Model model){
model.addAttribute(new Spitter());
return "registerForm";
}
回到这个表单,前三个输入域将HTML<input>标签改成了<sf:input>。这个标签会渲染成一个HTML<input>标签,其中type属性设置为”text”。我们在这里设置了path属性,<input>标签的value属性值将会设置为模型对象中path属性所对应的值。对于password输入域,我们使用<sf:password>来代替<sf:input>。<sf:password>与<sf:input>类似,但是它所渲染的HTML<input>标签中,会将type属性设置为password,这样当输入的时候,它的值不会明文显示。
另外需要注意的是,从Spring3.1开始,<sf:form>标签能够允许我们指定type属性,这样的话,除了其他可选的类型外,还能指定HTML5特定类型的文本域,如data、range和email。例如:
Email: <sf:input path="email" tppe="email" /><br/>
这样所渲染的HTML如下所示:
Email: <input id="email" name="email" type="email" value="xxxx"/><br/>
相对于标准的HTML标签,使用Spring的表单绑定能够带来一定的功能提升,在校验失败之后,表单中会预先填充之前输入的值。但是,这依然没有告诉用户错在什么地方。为了指导用户矫正错误,我们需要使用<sf:errors>。
展现错误
如果存在校验错误的话,请求中会包含错误的详细信息,这些信息是与模型数据放到一起的。我们所需要做的就是到模型中将这些数据抽取出来,并展现给用户。<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>的path属性设置成其对应需要展示的<sf:input>的path属性值,也就指定了要显示Spitter模型对象中哪个属性的错误信息。如果绑定的属性没有任何错误的话,那么<sf:errors>不会渲染任何东西。
现在,我们已经可以为用户展现错误信息了,这样他们就能够修正这些错误了。我们还可以更进一步,修改错误的样式,使其更加突出显示。为了做到这一点,可以设置其cssClass属性:
<sf:form method="post" commandName="spitter">
First Name: <sf:input path="firstName"/>
<sf:errors path="firstName" cssClass="error"/><br/>
Last Name: <sf:input path="lastName"/>
<sf:errors path="lastName" cssClass="error"/><br/>
Username: <sf:input path="username"/>
<sf:errors path="username" cssClass="error"/><br/>
Password: <sf:password path="password"/>
<sf:errors path="password" cssClass="error"/><br/>
<input type="submit" value="Register"/>
</sf:form>
并定义一个简单的css样式:
span.error {
color: red;
}
在输入域的旁边展示错误信息是一种很好的方式,这样能够引起用户的注意并及时修正这些问题,但是这样也会带来布局的问题。另外一种处理校验错误方式是将所有的错误信息在同一个地方进行显示。为了做到这一点,我们可以移除每个输入域上的<sf:errors>元素,并将其放到表单的顶部,如下所示:
<sf:form method="post" commandName="spitter">
<sf:errors path="*" element="div" cssClass="errors"/>
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:errors>与之前相比,值得注意的不同之处在于它的path被设置成了”*”。这是一个通配符选择器,会告诉<sf:errors>展示所有属性的所有错误。
同样需要注意的是,我们将elment属性设置为”div”。默认情况下,错误都会渲染在一个HTML<span>标签中,如果只显示一个错误的话,这是一个不错的选择,但是如果要渲染所有输入域的错误的话,很可能要展示不止一个错误,这时候使用<span>标签就不合适了。因此,我们将element属性设置为”div”,这样的话,错误就会渲染在一个<div>标签中。同样我们需要额外添加一个样式:
div.errors {
background-color: #ffcccc;
border: 2px solid red;
}
现在,我们在表单的上方显示所有的错误,这样页面布局可能更加容易以下。但是,我们还没有着重显示需要修正的输入域。通过为每个输入域设置cssErrorClass属性,这个问题很容易解决。我们也可以将每个label都替换为<sf:label>,并且设置它的cssErrorClass属性。
<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: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>
同样为label和input标签添加样式:
label.error {
color: red;
}
input.error {
background-color: #ffcccc;
}
现在,已经有了很好的方式为用户展现错误信息。不过,我们还能够做另外一件事情,能够让这些错误信息更加易读。重新看一下Spitter类,我们可以在校验注解上设置message属性,使其引用对用户更为友好的信息,而这些信息可以定义在属性文件中:
@NotNull
@Size(min = 2, max = 30, message = "{firstName.size}")
private String firstName;
@NotNull
@Size(min = 5, max = 25, message = "{lastName.size}")
private String lastName;
@NotNull
@Size(min = 5, max = 25, message = "{username.size}")
private String username;
@NotNull
@Size(min = 5, max = 25, message = "{password.size}")
private String password;
对于上面的每一个域,我们都将其@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属性。
当用户提交的注册表单校验失败的话,在浏览器可以看到如下界面(请忽略样式):
将这些错误信息抽取到属性文件中还会带来另外一个好处,那就是我们可以同构创建地域相关的属性文件,为用户展示特定语言和地域的信息。我们可以按需创建任意数量的ValidationMessages.properties文件,使其涵盖我们想支持的所有语言和地域。
Spring通用的标签库
除了表单绑定标签库之外,Spring还提供了更为通用的JSP标签库。实际上,这个标签库是Spring中最早的标签库。要使用Spring通用的标签库,我们必须要在页面上对其声明:
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
标签库声明之后,我们就可以使用下表中的十个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,支持URL模板变量以及HTML/XML/JavaScript转义,可以渲染URL(默认行为),也可以将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var和scope属性实现) |
<s:eval> | 计算符合Spring表达式语法(Spring Expression Language,SpEl)的某个表达式的值,然后要么进行渲染(默认行为),要么将其设置为页面作用域、请求作用域、会话作用域或应用作用域的变量(通过使用var和scope属性实现) |
展现国际化信息
到目前为止我们的JSP模板中包含了很多硬编码的文本。这其实也不算什么大问题,但是如果你要修改这些文本的话,就不是那么容易了。而且,没有办法根据用户的语言设置国际化这些文本。例如,不管你选择什么样的欢迎信息,所有的用户都会看到同样的信息。Web是全球性的网络,你所构建的网络很可能会有全球化的用户。因此,最好能够使用用户的语言与其进行交流,而不是只是用某一种语言。
对于渲染文本来说,使用<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后,ResourceBundleMessageSource就会视图在根路径的属性文件中解析信息,这些属性文件的名称是根据这个基础名称衍生得到的。
另外可选的方案是使用ReloadableResourceBundleMessageSource,它的工作方式与ResourceBundleMessageSource非常类似,但是它能够重新加载信息属性,而不必重新编译或重启应用。如下是配置ReloadableResourceBundleMessageSource的样例:
@Bean
public MessageSource messageSource(){
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("file://etc/spittr/messages");
messageSource.setCacheSeconds(10);
return messageSource;
}
这里的关键区别在于basename实行设置为在应用的外部查找(而不是像ResourceBundleMessageSource那样在类路径下查找)。basename属性可以设置为在类路径下(以”classpath:”作为前缀)、文件系统中(以”file:”作为前缀)或Web应用的根路径下(没有前缀)查找属性。
现在我们来创建这些属性文件。首先创建默认的属性文件,名为messages.properties(在这里我们将其放置在根路径下,使用ResourceBundleMessageSource进行加载)。
spittr.welcome=Welcome to Spittr!
创建URL
<s:url>标签的主要任务是创建URL,然后将其赋值给一个变量或者渲染到响应中。它是JSTL中<c:url>标签的替代者,但是它有自己的特殊技巧。
按照其最简单的形式<s:url>会接受一个相当于Servlet上线文的URL,并在渲染的时候预先添加上Servlet上下文路径。例如:
<a href="<s:url value="/spitter/register"/>">Register</a>
如果Servlet的上下文路径spittr,那么在响应中将会渲染如下的HTML:
<a href="/spittr/spitter/register">Register</a>
另外,还可以使用<s:url>创建URL,并将其赋值给一个变量供模板在稍后使用:
<s:url value="/spitter/register" var="registerUrl"></s:url>
<a href="${registerUrl}">Register</a>
默认情况下,URL是在页面作用域内创建的。但是通过设置scope属性,我们可以让<s:url>在应用作用域内、会话作用域内或请求作用域内创建URL:
<s:url value="/spitter/register" var="registerUrl" scope="request"/>
如果希望在URL上添加参数的话,那么你可以使用<s:param>标签。
<s:url value="/spitter/register" var="registerUrl" scope="request">
<s:param name="max" value="60"/>
<s:param name="count" value="20"/>
</s:url>
如果需要创建带有路径(path)参数的URL该怎么办呢?该如何设置href属性使其具有路径变量的占位符呢?我们可以通过使用<s:param>来装配参数:
<s:url value="/spitter/{username}" var="registerUrl">
<s:param name="username" value="godman"/>
</s:url>