第七章 使用 Spring MVC 构建 Web 应用程序
注:需要添加 Spring-webmvc.jar 包
7.1 Spring MVC 起步
Spring MVC 基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮助你构建像 Spring 框架那样灵活和松耦合的 Web 应用程序。
7.1.1 跟踪 Spring MVC 的请求
在 Spring MVC 中,Spring 会将请求在调度 Servlet、处理器映射(hander mapping)、控制器以及视图解析器(view resolver)之间移动。每一个 Spring MVC 中的组件都有特定的目的。
请求从离开浏览器开始到获取响应返回,它会经理好多站,在每站都会留下一些信息同事也会带上其他信息。
在请求离开浏览器时,会带有用户所请求内容的信息。请求路程的第一站是 Spring 的 DispatcherServlet。Spring MVC 所有的请求都会通过一个前端控制器 Servlet — DispatcherServlet。前端控制器是常用的 Web 应用程序模式,这个单实例的 Servlet 会将请求委托给应用程序的其他组件来执行实际的处理。
DispatcherServlet 的任务是将请求发送给 Spring MVC 控制器。控制器是一个用于处理请求的 Spring 组件。DispatcherServlet 会查询一个或多个处理器映射(hander mapping)来确定请求的下一站控制器在哪里。处理器映射会根据请求所携带的 URL 信息来进行决策。
请求到达了控制器后,请求会卸载下其负载(用户提交的信息)并等待控制器处理这些信息(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象)。
控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的,这些信息需要以用户友好的方式进行格式化,一般是HTML。所以,信息需要发送给一个视图(view),通常会是 JSP。
控制器所做的最后一件事是将模型数据打包,并且标示出用于渲染输出的视图名称。它接下来会将请求联通模型和视图名称发送回 DispatcherServlet 。
传递给 DispatcherServlet 的视图名称并不直接表示某个特定的 JSP。实际上,它甚至并不能确定视图是 JSP。相反,它仅仅传递了一个逻辑名,这个名字将会用来查找用来产生结果的真正视图。DispatcherServlet 将会使用视图解析器来讲逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是 JSP。
请求的最后一站式视图的实现,在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,并通过这个输出将响应对象传递给客户端。
7.1.2 搭建 Spring MVC
Spring MVC 的核心是 DispatcherServlet 。在应用程序中使用 Spring MVC 的第一件事就是将 DispatcherServlet 在 web.xml 文件中进行配置。
<servlet>
<servlet-name>spitter</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spitter</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
为 DispatcherServlet 设置 servlet-name 是很重要的。默认情况下,DispatcherServlet在加载时会从一个基于这个 Servlet 名字的 XML 文件中加载 Spring 应用上下文。在上面的示例中,因为servlet-name 为 spitter,DispatcherServlet 将尝试从一个名为 spitter-servlet.xml 的文件(位于应用程序的WEB-INF目录下)来加载应用上下文。
注:如果需要指定特定的配置文件可以进行如下配置:
<servlet>
<servlet-name>spitter</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spitter-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
配置 DispatcherServlet 的 servlet-mapping,比较常见的 DispatcherServlet 匹配模式是 *.htm、/* 或者 /app,但这些模式都存在一些问题:
- *.htm:隐式表名响应始终是 HTML 格式的(我们将会在第11章了解到,并不会总是这样的)。
- /*:没有映射特定类型的响应,它表名 DispatcherServlet 将处理所有的请求。这会处理所有的请求。这会在处理图片或者样式表这样的静态资源时带来不必要的麻烦。
- /app:该模型帮助我们区分了 DispatcherServlet 处理的内容和其他内容,但这样就会在 URL 中暴露实现的细节(具体来说,就是 /app 路径)。为了隐藏 /app 路径通常需要复杂的 URL 重写技巧。
通过将 DispatcherServlet 映射到 / ,声明了它会作为默认的 servlet 并且会处理所有的请求,包括对静态资源的请求。
Spring 的 mvc 命名空间包含了一个新的<mvc:resource>
元素,它会处理静态资源的请求。你所要做的就是在 Spring 配置文件中对其进行配置。<mvc:resource>
是 Spring 3.0.4 新增的。如果使用之前版本的 Spring ,它是不可用的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd" default-lazy-init="true">
<!-- 处理对静态资源的请求 -->
<!-- <mvc:resources>建立了一个服务于静态资源的处理器。
属性 mapping 被设置为 /resources/**,它包含了 Ant 风格的通配符以表明路径必须以 /resources 开始,而且也包括它的任意子路径。
属性 location 表明了要提供服务的文件位置。 -->
<!-- 以下配置表明,所有以 /resources 路径开头的请求都会自动由应用程序根目录下的 /resources 目录提供服务。
因此,我们的所有图片、样式表、JavaScript 以及其他的静态资源都必须放在应用程序的 /resources 目录下。 -->
<mvc:resources mapping="/resources/**" location="/resources/*" />
</beans>
7.2 编写基本的控制器
正如前面所述,DispatcherServlet 需要咨询一个或过个处理器映射来明确地将请求分发给哪个控制器。Spring 自带了多个处理映射实现供我们选择,具体如下:
- BeanNameUrlHandlerMapping:根据控制器 Bean 的名字将控制器映射到 URL。
- ControllerBeanNameHandlerMapping:与 BeanNameUrlHandlerMapping 类似,根据控制器 Bean 的名字将控制器映射到 URL。使用该处理器映射实现,Bean 的名字不需要遵循 URL 的约定。
- ControllerClassNameHandlerMapping:通过使用控制器的类名作为 URL 基础将控制器映射到 URL。
- DefaultAnnotationHandlerMapping:将请求映射给使用 @RequestMapping 注解的控制器和控制器方法。
- SimpleUrlHandlerMapping:使用定义在 Spring 应用上下文的属性集合将控制器映射到 URL。
使用如上致谢处理器映射通常只需在 Spring 中配置一个Bean。如果没有找到处理器映射 Bean,DispatcherServlet 将创建并使用 BeanNameUrlHandlerMapping 和 DefaultAnnotationHandlerMapping。
7.2.1 配置注解驱动的 Spring MVC
DefaultAnnotationHandlerMapping 将请求映射到使用 @RequestMapping 注解的方法。但是,实现注解驱动的 Spring MVC 并不仅仅是将请求映射到方法上。在构建控制器的时候,我们还需要使用注解将请求参数绑定到控制器的方法参数上进行校验以及信息转换。
使用<mvc:annotation-driven />
来开启 Spring MVC 所提供的注解驱动特性。无需再创建 DefaultAnnotationHandlerMapping 的Bean。
7.2.2 定义首页控制器(控制器示例)
编写名为 HomeController 的控制器,用于处理首页的请求。
import java.util.Map;
import javax.inject.Inject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.spring.springdemo.spittermvc.service.SpitterService;
//声明为控制器,需在Spring配置文件中配置<context:component-scan />
//注解详情查看第三章 最小化SpringXML配置
@Controller
public class HomeController {
public static final int DEFAULT_SPITTER_PER_PAGE = 25;
private SpitterService spitterService;
//注入相应服务类对象,在服务类中实现具体的后台业务逻辑
//@Inject 注解详情查看第三章 最小化SpringXML配置
@Inject
public HomeController(SpitterService spitterService){
this.spitterService = spitterService;
}
/**
* 注解为请求处理方法,处理对首页的请求。具体处理 "/" 或者 "/home" 路径的请求
* 请求处理方法的签名可以将任何事物作为参数。例如:httpServletRequest、HttpServletResponse、String 或者数字参数
* 这里将一个Map<String,Object>作为参数,这个Map代表了模型----控制器和视图之间传递的数据。
* @param model
* @return
*/
@RequestMapping({"/","/home"})
public String showHomePage(Map<String,Object> model){
//将 Spittle 放入模型中,这样当视图渲染的时候,它就能够展现出来了。
model.put("spittles",spitterService.getRecentSpittles(DEFAULT_SPITTER_PER_PAGE));
//返回视图名称
return "home";
}
}
7.2.3 解析视图
处理请求的最后一件必须要做的是将就是为用户渲染输出。这个任务落在了视图实现上—通常会是 JSP(JavaServer Page),但是其他的视图技术也可以使用。
为了确定指定的请求需要使用哪个视图,DispatcherServlet 会查找一个视图解析器来讲控制器返回的逻辑视图名称转换成渲染结果的实际视图。
实际上,视图解析器的工作是将逻辑视图的名字与 org.springframework.web.servlet.View 的实现相匹配。但现在我们可以认为,视图解析器所做的就是将视图名称与 JSP 进行匹配。
Spring 自带了多个视图解析器实现供选择:
视图解析器 | 描述 |
---|---|
BeanNameViewResolver | 查找<bean> 元素的ID 与逻辑视图名称相同的 View 的实现 |
ContentNegotiatingViewResolver | 委托给一个或多个视图解析器,而选择哪一个取决于请求的内容类型(在第11章将介绍这个视图解析器) |
FreeMarkerViewResolver | 查找一个基于FreeMarker的模板,它的路径根据加完前缀和后缀的逻辑视图名称来确定 |
InternalResourceViewResolver | 在Web应用程序的WAR文件中查找视图模板,视图模板的路径根据加完前缀和后缀的逻辑视图名称来确定 |
JasperReportsViewResolver | 根据加完前缀和后缀的逻辑视图名称来查找一个 Jasper Report 报表文件 |
ResourceBundleViewResolver | 根据属性文件(properties file)来查找View实现 |
TilesViewResolver | 查找通过Tiles模板定义的视图。模板的名字与逻辑视图名称相同 |
UrlBasedViewResolver | 这是一些其他视图解析器(如InternalResourceViewResolver)的基类。它可以单独使用,但是没有它的子类强大。例如,UrlBasedViewResolver不能基于当前的语言环境来解析视图 |
VelocityLayoutViewResolver | 它是VelocityViewResolver的子类,它支持通过Spring的VelocityLayoutView(模仿VelocitylayoutServlet的View实现)来进行页面的组合 |
VelocityViewResolver | 解析基于Velocity的视图,Velocity模板的路径根据加完前缀和后缀的逻辑视图名来确定 |
XmlViewResolver | 查找在XML文件(/WEB-INF/view.xml)中声明的View实现。这个视图解析器与BeanNameViewResolver很类似,但在这里视图<bean> 的声明与应用程序 Spring 上下文的其他<bean> 是分开的 |
XsltViewResolver | 解析基于XSLT的视图,XSLT样式表的路径根据加完前缀和后缀的逻辑视图名称来确定 |
7.2.3.1 解析内部视图(InternalResourceViewResolver)
在 Spring MVC 中,大量使用了约定优于配置的开发模式。InternalResourceViewResolver 就是一个面向约定的元素。它将逻辑视图的名称解析为 View 对象,而该对象将渲染的任务委托给 Web 应用程序上下文中的一个模板(通常是JSP)。
InternalResourceViewResolver 通过为逻辑视图名称添加特定的前缀和后缀来得到视图模板的路径。
假设我们已经将 Spitter 应用程序的所有 JSP 放在 “/WEB-INF/views/”目录下。基于这样的安排,我们需要在 Spring 配置文件中配置 InternalResourceViewResolver :
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
当 DispatcherServlet 要求 InternalResourceViewResolver 解析视图的时候,它将获取一个逻辑视图名称,添加指定的前缀和后缀。得到的结果就是渲染输出的 JSP 路径。在内部,InternalResourceViewResolver 接下来会将这个路径传递给 View 对象,View 对象将请求传递(dispatch)给 JSP。所以,当 HomeContriller 返回 home 作为逻辑视图名称时,它最终会被解析成“/WEB-INF/views/home.jsp”路径。
默认情况下,InternalResourceViewResolver 创建的 View 对象是 InternalResourceView 的实例,它只会简单地将请求传递给要渲染的 JSP。但因为 “home.jsp” 使用了一些 JSTL 标签,因此需要通过设置 viewClass 属性来将 InternalResourceView 替换为 JstlView,如下所示:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
JstlView 将请求传递给 JSP,就像 InternalResourceView 一样。但是,它还会公布特定的 JSTL 的请求属性,这样你就可以使用 JSTL 的国际化支持了。
FreeMarkerViewResolver、JasperReportsViewResolver、VelocityViewResolver、VelocityLayoutViewResolver 以及 XsltViewResolver 与 InternalResourceViewResolver 类似,解析视图都是为逻辑视图名称添加前缀和后缀,然后查找视图模板。
7.2.3.2 解析 Tiles 视图(TilesViewResolver)
Apache Tiles 是一个模板框架,它将页面分成片段并在运行时组装成完整的页面。尽管它最初是 Struts 框架的一部分,但是 Tiles 在其他的 Web 框架中一样能够发挥作用。
为了在 Spring MVC 中使用 Tiles,第一件事就是在 Spring 配置文件中,将 TilesViewResolver 注册为一个Bean:
<bean class="org.springframework.web.servlet.view.tiles2.TilesViewResolver" />
这个简单的 Bean 声明会建立一个视图解析器,它会查找逻辑视图名称与 Tiles 定义名称相同的 Tiles 模板定义,并将其作为视图。
TilesViewResolver 本身并不了解 Tiles 定义的任何事情,而是依靠 TilesConfigurer 来记录这个信息。所以我们需要在 Spring 配置文件中添加 TilesConfigurer 类型的Bean:
<bean class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/views/**/views.xml</value>
</list>
</property>
</bean>
TilesConfigurer 会加载一个或多个的 Tiles 定义,并使得 TilesViewResolver 可以通过它来解析视图。对于 Spitter 应用程序,我们会需要一些名为 views.xml 的 Tiles 定义文件,它们都分散在 /WEB-INF/views 目录下。所以我们将 /WEB-INF/views/**/views.xml 装配到 definitions 属性中。Ant 风格的 ** ,模式表名要在 /WEB-INF/views 下的所有目录查找名为 views.xml 的文件。
<!-- views.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
"http://tiles.apache.org/dtdstiles-config_2_1.dtd">
<tiles-definitions>
<!-- main_template.jsp 文件描述了 Spitter 应用程序所有页面的通用布局 -->
<definition name="template"
template="/WEB-INF/views/main_template.jsp">
<!-- spittleForm.jsp 和 signinsignup.jsp 提供了一些通用的附加元素 -->
<put-attribute name="top"
value="/WEB-INF/views/tiles/spittleForm.jsp" />
<put-attribute name="side"
value="/WEB-INF/views/tiles/signinsignup.jsp" />
</definition>
<!-- home.jsp 显示了首页的主要内容 -->
<definition name="home" extends="template">
<put-attribute name="content" value="/WEB-INF/views/home.jsp" />
</definition>
</tiles-definitions>
关于 view.xml 的内容,在本章中我们从能够渲染主页开始逐渐完成这个文件。上示 views.xml 文件的代码中定义了 home Tiles 定义以及通用的 template 定义,而 template 定义将会用到其他的 Tile 定义中。
home 定义扩展了 template 定义,使用 home.jsp 作为渲染页面主要内容的 JSP 并依赖 template 来展现页面的通用特性。
TilesViewResolver 在解析逻辑视图名称时(由 HomeController 的 showHomePage() 方法返回)时,将会找到 home 模板。DispatcherServlet 将会把请求传递给 Tiles,并用 home 定义来渲染。
7.2.4 定义首页的视图
在 7.2.2 小节的示例代码中,HomeController 将得到的数据放在了模型中,该模型使用了 spittles 作为键,所以在 JSP 中要使用 ${spittles}
来引用它。
在 HomeController 完成其任务之后以及 home.jsp 被调用之前,DispatcherServlet 将所有的模型成员以及相同的名字复制到请求属性中。
<!-- home.jsp -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="t" uri="http://tiles.apache.org/tags-tiles" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<div>
<h2>.......</h2>
<h3>....</h3>
<ol class="spittle-list">
<c:forEach var="spittle" items="${spittles}">
<!-- <s:url> 是 Spring 3.0 新增的标签,它与JSTL的<c:url>标签类似 -->
<!-- <s:url>创建一个相对于Servlet上下文的URl,来指向每个Spittle所属的Spitter -->
<!-- 如果 Spitter 的用户名是 habuma 而 Servlet 上下文名为 Spitter,那最终的路径将会是/spitter/spitters/habuma -->
<s:url value="/spitters/{spitterName}" var="spitter_url" >
<s:param name="spitterName" value="${spittle.spitter.username }" />
</s:url>
<li>
<span class="spittleListImage">
<img src="http://s3.amazonws.com/spitterImages/${spittle.spitter.id}.jpg }" />
</span>
<span class="spittleListText">
<a href="${spitter_url }">
<c:out value="${spittle.spitter.username }"/>
</a>
- <c:out value="${spittle.text }" /></br>
<small><fmt:formatDate value="${spittle.when }"
pattern="hh:mma MMM d, yyyy" /></small>
</span>
</li>
</c:forEach>
</ol>
</div>
7.2.5 完成 Spring 应用上下文
正如前面所提到的(7.1.2),DispatcherServlet 会根据一个 XML 文件来加载其 Spring 应用上下文,而这个文件的名字基于它的 <servlet-name>
属性来确定。
将 Spring 配置文件组织到多个文件中,一个用于服务层、一个用于持久层还有一个用于数据源配置层。DispatcherServlet 会加载指定的 Spring 配置文件,但是我们还需要一种方式来加载其他的配置文件。这就是 ContextLoaderListener 能够发挥作用的地方了。
ContextLoaderListener 是一个 Servlet 监听器,除了 DispatcherServlet 创建的应用上下文以外,它能够加载其他的配置文件到一个 Spring 应用上下文中,为了使用 ContextLoaderListener ,需要在 web.xml 文件中添加如下的 <listener>
声明。
<listener>
<listener-class>org.springframework.web.context.ContextloaderListener</listener-class>
</listener>
<!-- ContextloaderListener 默认会查找 /WEB-INF/applicationContext.xml 这个配置文件 -->
<!-- 给 ContextloaderListener 指定一个或多个 Spring 配置文件,需要在 servlet 上下文中配置 contextConfigLocation 参数-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spitter-security.xml
classpath:service-context.xml
classpath:persistence-context.xml
classpath:dataSource-context.xml
</param-value>
</context-param>
7.3 处理控制器的输入
HomeContriller 很简单,它不用处理用户输入和任何参数。它只处理了一个基本的请求并填充模型供视图渲染所用。但控制器往往需要基于 URL 参数或表单数据所传递进来的信息执行一些逻辑。
7.3.1 编写处理输入的控制器
import javax.inject.Inject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.spring.springdemo.spittermvc.entity.Spitter;
import com.spring.springdemo.spittermvc.service.SpitterService;
@Controller
@RequestMapping("/spitter") // 根 URL 路径
public class SpitterController {
private final SpitterService spitterService;
@Inject
public SpitterController(SpitterService spitterService){
this.spitterService = spitterService;
}
//处理针对 "/spitter/spittles" 的 Http Get 请求
@RequestMapping(value="/spittles",method=RequestMethod.GET)
public String listSpittlesForSpitter
/*参数 username 使用了 @RequestParam 注解
* 表明它的值应该根据请求中名为 spitter 的查询参数来获得。
*/
(@RequestParam("spitter") String username,Model model){
Spitter spitter = spitterService.getSpitter(username);
/*
* 填充模型
* 当添加 Spitter 对象到模型中的时候,addAttribute() 赋予它的名字是 spitter,
* 这个名字是将 JavaBean 的属性命名规则应用到对象的类名上得到的。
* 当添加一个 Spittle 的 List 时,它将 "List" 添加到这个列表的成员类型上,这样就将这个属性命名为 spittleList
*/
model.addAttribute(spitter);
model.addAttribute(spitterService.getRecentSpittlesForSpitter(username));
//返回的视图是 前缀+返回值+后缀 的文件,如 7.2.3.1 小节所示
return "spittles/list";
}
}
@RequestParam 注解并不是严格需要的。在查询参数与方法参数的名字不匹配的时候,@RequestParam 是有用的。基于约定,如果处理方法的所有参数没有使用注解的话,将绑定到同名的查询参数上。
7.3.2 渲染视图
上小节代码示例中,listSpittlesForSpitter() 返回 “spittles/list” 作为其逻辑视图名称,所以如下的 Tile 定义就能够满足要求了。
<!-- views.xml -->
<definition name="spittles/list" extends="template">
<put-attribute name="content" value="/WEB-INF/views/spittles/list.jsp" />
</definition>
<!-- list.jsp -->
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div>
<h2>....</h2>
<table cellspacing="15">
<c:forEach item="${spittleList }" var="spittle">
<tr>
<td>
<img src="<s:url value='/resources/images/spitter_avatar.png' />"
with="48" height="48" /></td>
</td>
<td>
<a href="<s:url value='/spitters/${spittle.spitter.username }' />">
${spittle.spitter.username }
</a>
<c:out value="${spittle.text }" /></br>
<c:out value="${spittle.when }" />
</td>
</tr>
</c:forEach>
</table>
</div>
7.4 处理表单
在 Web 应用程序中,处理表单涉及两个操作:展现表单与处理表单提交。
7.4.1 展现注册表单
当展现注册表单的时候,它需要将一个 Spitter 对象绑定到表单域上。以下的 createSpitterProfile() 处理方法将创建一个新的 Spitter 对象并将其放入模型中。
/*
* 注解中没有指定路径,所以这个方法会处理类级别 @RequestMapping 所指定的路径
* 注解中 method 属性为 RequestMethod.GET,且将 params 属性设置为 new,
* 意味着这个方法只处理 HTTP GET 请求并要求请求中必须包含名为 new 的查询参数,
* 即处理的URL类型为:http://localhost:8080/Spitter/spitters?new
* Spitter 为Servlet上下文(项目名),spitters为类注解的路径
*/
@RequestMapping(method=RequestMethod.GET, params="new")
public String createSpitterProfile(Model model){
model.addAttribute(new Spitter());
return "spitters/edit";
}
渲染用来捕获注册信息的表单视图:
<!-- edit.jsp -->
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<div>
<h2>...</h2>
<!-- 将表单绑定到模型属性 -->
<!-- 表单没有指定URL,默认被提交到展现表单的URL路径,这里的路径就是/spitters -->
<sf:form merhod="POST" modelAttribute="spitter">
<fieldset>
<table cellspacing="0">
<tr>
<th><label for="user_full_name">Full name:</label></th>
<!--用户输入 -->
<td><<sf:input path="fullName" size="15" id="user_full_name" /></td>
</tr>
<tr>
<th><label for="user_screen_name">Username:</label></th>
<!--用户输入 -->
<td><sf:input path="username" size="15" maxlength="15"
id="user_screen_name" />
<small id="username_msg">........</small>
</td>
</tr>
<tr>
<th><label for="user_password">Password:</label></th>
<!--用户输入 -->
<td>
<sf:password path="password" size="30"
showPassword="true" id="user_password" />
<small>.........</small>
</td>
</tr>
</table>
</fieldset>
</sf:form>
</div>
上示 JSP 代码中使用了 Spring 的表单绑定库。<sf:form>
标签将 createSpitterProfile() 方法所放入模型的 Spitter 对象(通过 modelAttribute 属性来标识)绑定到表单中的各个输入域。
<sf:input>
、<sf:password>
或是 <sf:checkbox>
等标签都有一个 path 属性,用于指定绑定的对象属性。当提交表单时,这些输入域中的值将会放到 Spitter 对象中并提交到服务器进行处理。
7.4.2 处理表单输入
在表单提交之后,我们需要一个处理方法来获得 Spitter 对象(包含了表单上的数据)并对其进行保存,最后还需要重定向到用户基本信息页面。
/**
* 处理类注解的路径的POST请求
* 当表单提交时,请求中的输入域将绑定到 Spitter 对象中,这个对象会作为参数传递给该方法
* @Valid注解(7.4.3小节介绍)表明传入的参数在传入之前需要校验,使用该注解需要添加 javax.validation 包
*
*/
@RequestMapping(method=RequestMethod.POST)
public String addSpitterFromForm(@Valid Spitter spitter,BindingResult bindingResult){
// spitter 校验失败,返回注册表单页面
if(bindingResult.hasErrors()){
return "spitters/edit";
}
spitterService.saveSpitter(spitter);
//重定向路径,使用户刷新页面不会重复提交表单
return "redirect:/spitters/" + spitter.getUsername();
}
处理带有路径变量的请求
上面示例代码中,方法重定向的路径带有username变量,可以在控制器中创建该路径 “/spitters/{username}” 的处理方法。
/*
* @RequestMapping的value属性包含花括号,而且username参数使用了@PathVariable注解,使得方法能够处理URL路径中包含参数请求。
* 路径中{username}部分实际上是占位符,它对应了使用@PathVariable注解的username方法参数,在第11章会详细介绍 @PathVariable
*
*/
@RequestMapping(value="/{username}",method=RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username,Model model){
model.addAttribute(spitterService.getSpitter(username));
return "spitters/view";
}
7.4.3 校验输入
当用户在 Spitter 应用程序上进行注册的时候,我们对注册有一些特定的要求。
@Valid 注解是防御错误表单输入的第一道防线。@Valid 实际上示 JavaBean 校验规范(JSR-303)的一部分。Spring 提供了对 JSR-303 的支持。在上小节示例代码中,我们使用 @Valid 来告诉 Spring 在将表单输入绑定到 Spitter 对象的时候,需要进行校验。
如果校验对象出错,检验错误将会作为第二个参数以 BindingResult 的形式传递给 addSpitterFromForm() 方法。
定义校验规则
通过 JSR-303 定义的一些注解,在校验类的属性上配置校验规则
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
public class Spitter {
private long id;
//限制长度
@Size(min=3, max=20, message="Username must be between 3 and 20 characters long.")
//确保没有空格及特殊字符
@Pattern(regexp="^[a-zA-Z0-9]+$", message="Username must be alphanumeric with no spaces")
private String username;
@Size(min=6, max=20, message="........")
private String password;
//匹配 Email 模式
@Pattern(regexp="[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}",message="........")
private String email;
//getters and setters ....
}
展现校验错误
在校验注解上设置了 message 属性,该属性值应该在校验失败时显示在表单上,以便用户知道如何改正它。
为用户展现这些错误的一种方式就是通过 BindingResult 的 getFieldError() 方法来获取字段的错误。但更好的一种方式是使用 Spring 的表单绑定 JSP 标签库来展现错误。更具体来讲,<sf:errors>
能够渲染字段按的校验错误。我们所需要做的就是在表单 JSP 中添加几个<sf:errors>
标签
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
...
<sf:errors path="username" cssClass="error"/>
...
<sf:errors>
标签的 path 属性指明了要显示哪个表单域的错误。如果只需要在一个地方显示所有错误信息,只需要将 path 属性设置为 * ;如果一个输入域有多个错误,那么它们都会显示出来。默认多个错误会用</br>
分隔,如果想使用其他方式进行分隔,那么可以使用 delimiter 属性指定。
<sf:errors path="username" delimiter="," cssClass="error"/>
7.5 处理文件上传
7.5.1 在表单上添加文件上传域
大多数的表单域都是文本数据,所以能够容易地通过名称-值的格式提交到服务器上。事实上,典型的提交会带有一个 “application/x-www-form-urlencoded” 这样的内容类型并将表单上的名称 - 值以 & 符号分隔。
但是文件与大多数表单输入域所提交的类型不同,上传的内容一般都是二进制的文件,所以当提交带有文件的表单时,要选择的内容类型是 “multipart/form-data”。配置方式是设置表单标签的 enctype 属性。
//文件上传的表单,method 方法必须为 POST
<sf:form method="POST" modelAttribute="spitter"
enctype="multipart/form-data" >
<input name="image" type="file" />
</sf:form>
7.5.2 接收上传的文件
修改 addSpitterFromForm() 方法使其能够处理图片文件的上传。
@RequestMapping(method=RequestMethod.POST)
public String addSpitterFromForm(@Valid Spitter spitter
, BindingResult bindingResult
//使用注解标注该参数不是必须的
, @RequestParam(value="image",required=false) MultipartFile image){
if(bindingResult.hasErrors()){
return "spitters/edit";
}
spitterService.saveSpitter(spitter);
try{
if(!image.isEmpty()){
//校验图片,确保上传的图片格式为jpeg
validateImage(image);
//保存图片文件
saveImage(spitter.getId() + ".jpg",image);
}
}catch(Exception e){
bindingResult.reject(e.getMessage());
return "spitters/edit";
}
return "redirect:/spitters/" + spitter.getUsername();
}
private void validateImage(MultipartFile image) throws ImageUploadException{
if(!image.getContentType().equals("image/jpeg")){
// 自定义的RuntimeException
throw new ImageUploadException("Only JPG image accepted");
}
}
private void saveImage(String filename, MultipartFile image){
String path = this.getClass().getResource("/").toString();
try{
File file = new File(path + "/resources/" + filename);
// FileUtils 在 commons-io 包中
FileUtils.writeByteArrayToFile(file, image.getBytes());
}catch (IOException e){
throw new ImageUploadException("Unable to save image",e);
}
}
7.5.3 配置 Spring 支持文件上传
DispatcherServlet 本身并不知道如何处理 multipart 的表单数据。我们需要一个 multipart 解析器把 POST 请求中的 multipart 数据抽取出来,这样 DispatcherServlet 就能将其传递给我们的控制器了。
为了在 Spring 中注册 multipart 解析器,我们需要声明一个实现了 MultipartResolver 接口的 Bean。选择 multipart 解析器其实很简单,因为 Spring 只提供了一个:CommonmultipartResolver。
它的 Spring的配置如下:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:maxUploadSize="500000" />
要注意的是,multipart 解析器的 Bean ID 是有意义的。当 DispatcherServlet 查找 multipart 解析器的时候,它将会查找 ID 为 multipartResolver 的解析器Bean,如果 multipart 解析器是其他 ID 的话,DispatcherServlet 将会忽略它。