Spring MVC提供的更多功能
除了直接实现Controller接口外,Spring还提供了许多功能更多的Controller的实现,可以选择继承一个合适类型的Controller来简化编码。相对于Struts或 WebWork,Spring提供的Controller层次极为丰富,如图7-24所示。
图7-24
AbstractController提供了一个最顶层的Controller模板,用来完成一些基本的任务。AbstractController可以注入以下属性。
(1)supportedMethods:设定允许的HTTP请求方式,默认为GET和POST。如果需要PUT、HEAD之类的请求,则需要覆盖默认的设定,不过,通常不需要设定其他HTTP请求方式。
(2)requireSession:设定是否需要Session支持,默认为false。如果设定为true,则要求当前请求必须和Session关联,这样可以保证子类在任何时候调用request.getSession()时不会得到null。
(3)cacheSeconds:设定HTTP响应头的缓存,默认值为-1,表示不添加任何缓存指令到HTTP响应头;如果设为0,表示完全不缓存;如果设为大于0,表示应当缓存的秒数。
(4)synchronizeOnSession:表示来自同一用户的请求是否能并行处理,默认值为false,表示允许同一用户同时向服务器发送多个请求。如果设定为true,则同一用户的请求只能被依次处理,这个设置可以有效控制同一用户对服务器的并发请求,例如,禁止使用多线程下载由Controller生成的文件。
由于AbstractController位于Controller继承体系的上端,其他子类也可以非常方便地设定上述属性。
为了演示如何使用Spring内置的常用的Controller,我们在SpringMVC工程的基础上扩展。在Eclipse中新建SpringMVC_Controllers工程,结构如图7-25所示。
图7-25
Spring提供了一套标签库,能大大简化表单的绑定和验证任务,为了使用Spring内置的Tag和JSP标准标签库,需要将c.tld和spring-form.tld复制到/web/WEB-INF/目录下,并且在web.xml中声明。
<taglib>
<taglib-uri>http://java.sun.com/jsp/jstl/core</taglib-uri>
<taglib-location>/WEB-INF/c.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://www.springframework.org/tags/form</taglib-uri>
<taglib-location>/WEB-INF/spring-form.tld</taglib-location>
</taglib>
前面讲到了使用BeanNameUrlHandlerMapping能极大地简化从URL到Controller的映射,在实际的项目中,完全可以采用 Ant+XDoclet自动生成Spring Web应用程序所需的XML配置文件,这样,对URL映射的配置就变成了在相应的Controller类的源代码中简单地添加一个XDoclet注释,极大地降低了手动维护XML配置文件带来的成本。
在第3章中我们已经介绍了如何使用XDoclet生成Spring的XML配置文件,并对XDoclet做了一定的扩展,使其支持Spring 2.0的XML配置文件。对于这个SpringMVC_ Controllers Web应用程序也同样适用,先将XDoclet和Ant的相关文件复制到工程的lib目录下,然后编写Ant的build.xml脚本。
<?xml version="1.0" encoding="UTF-8"?>
<project name="SpringMVC_Controllers" default="gen-spring-conf" basedir=".">
<property name="src.dir" value="src" />
<property name="conf.dir" value="conf" />
<property name="web.dir" value="web" />
<property name="xdoclet.dir" value="xdoclet" />
<property name="template.dir" value="template" />
<property name="lib.dir" value="${web.dir}/WEB-INF/lib" />
<property name="build.dir" value="${web.dir}/WEB-INF/classes" />
<!-- 定义编译期的classpath -->
<path id="master-classpath">
<!-- 包含${lib.dir} -->
<fileset dir="${lib.dir}">
<include name="**/*.jar" />
</fileset>
<!-- 包含${xdoclet.dir} -->
<fileset dir="${xdoclet.dir}">
<include name="**/*.jar" />
</fileset>
<!-- 包含${src.dir} -->
<pathelement path="${src.dir}"/>
</path>
<!-- 清理自动生成的资源 -->
<target name="clean">
<delete>
<fileset dir="${build.dir}" />
<filename>${web.dir}/WEB-INF/dispatcher-servlet.xml</filename>
</delete>
</target>
<!-- 编译源代码 -->
<target name="compile">
<mkdir dir="${build.dir}"/>
<javac destdir="${build.dir}" target="1.5" debug="on" debuglevel="lines">
<classpath refid="master-classpath"/>
<src path="${src.dir}"/>
</javac>
</target>
<!-- 生成Spring配置文件 -->
<target name="gen-spring-conf" depends="compile">
<!-- 定义Ant任务 -->
<taskdef name="springdoclet"
classname="xdoclet.modules.spring.SpringDocletTask"
classpathref="master-classpath"
/>
<!-- 生成配置文件 -->
<springdoclet
destDir="${web.dir}/WEB-INF"
mergeDir="${conf.dir}"
force="true"
excludedtags="@version,@author,@todo"
>
<fileset dir="${src.dir}" includes="**/*.java" />
<springxml
xmlencoding="UTF-8"
templateFile="${template.dir}/custom_spring_xml.xdt"
destinationFile="dispatcher-servlet.xml"
/>
</springdoclet>
</target>
</project>
不熟悉XDoclet的读者请参考第3章关于如何使用XDoclet自动生成Spring配置文件的相关章节,这里不再做更多的介绍。下面我们要讨论的是Spring提供的几种非常有用的控制器。
7.4.1 SimpleFormController
SimpleFormController可以处理简单的表单,这和Struts的ActionForm类似,但是Spring对表单类不要求实现某个特定的接口。此外,SimpleFormController可以同时完成显示表单和提交表单两个功能。显示表单无须编写代码,Spring会自动处理,我们只需处理提交表单。
SimpleFormController主要通过以下几个属性来决定如何显示和提交表单。
(1)commandClass:表单类(或称为命令类),Spring据此来实例化一个表单类,请读者注意,在Spring中,表单对象被称为Command对象,这和Struts中的 ActionForm类似,但Spring的Command对象不要求实现任何特定接口。
(2)formView:显示表单的视图名称。
(3)successView:提交表单成功后的视图名称。
以用户登录为例,我们设计一个LoginController,从SimpleFormController派生。当用户以GET方式请求 /login.do时,LoginController就会向用户显示一个登录表单,该视图的名称就是由formView指定的“login.jsp”,显示表单不需要编写任何代码,Spring替我们自动完成这一步骤,唯一需要处理的是覆写onSubmit方法,处理表单提交。
/**
* 处理用户登录表单
* @spring.bean name="/login.do"
* @spring.property name="commandClass" value="example.chapter7.User"
* @spring.property name="formView" value="login"
* @spring.property name="successView" value="loginSuccess"
*/
public class LoginController extends SimpleFormController {
private UserService userService;
/**
* @spring.property ref="userService"
*/
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
protected ModelAndView onSubmit(HttpServletRequest request, HttpServlet Response response, Object command, BindException errors) throws Exception {
User user = (User)command;
try {
userService.login(user.getUsername(), user.getPassword());
// 登录成功,在Session中标记:
request.getSession().setAttribute("USERNAME", user.getUsername());
// 然后返回successView:
Map model = new HashMap();
model.put("username", user.getUsername());
return new ModelAndView(getSuccessView(), model);
}
catch(RuntimeException e) {
// 登录失败,返回formView让用户重新填写表单:
Map model = new HashMap();
model.put("command", command);
model.put("error", e.getMessage());
return new ModelAndView(getFormView(), model);
}
}
}
由于使用XDoclet在注释中就配置好了LoginController,因此运行Ant,生成的XML配置片断最终如下。
<bean name="/login.do" class="example.chapter7.LoginController">
<property name="userService" ref="userService" />
<property name="commandClass" value="example.chapter7.User" />
<property name="formView" value="login" />
<property name="successView" value="loginSuccess" />
</bean>
对于作为formView的login.jsp视图文件,为了让Spring自动绑定表单的内容,使用Spring的<form:>标签库即可完成此功能。
<form:form method="post" action="login.do">
用户名:<form:input path="username" />
口令:<form:password path="password" />
<input type="submit" name="Submit" value="登录" />
</form:form>
<form:form> 用于定义一个表单,和HTML的<form>标签对应,<form:input>对应一个INPUT,其path属性指定了这个文本输入框对应Command对象的username属性,<form:password>对应一个类型为PASSWORD的INPUT,其 path属性指定了这个口令输入框对应Command对象的password属性。
运行Ant编译,然后启动Resin,输入http://localhost:8080/login.do,就可以看到Spring自动绑定表单的效果,如图7-26所示。
可以看到,整个处理流程非常清晰。 commandClass 必须是具有默认构造方法,可以被实例化的类,通常都是简单的JavaBean对象,这里我们使用的commandClass是User类,但是仅使用了 username和password这两个属性,其余属性由于不出现在登录表单中,将直接被Spring忽略。
7.4.2 验证表单
对于用户输入的表单,通常需要在服务器端进行验证,以确保数据的完整性和一致性。Spring提供了一个Validation框架来验证用户输入的表单,并可以将错误信息绑定到合适的字段上。
以登录表单为例,我们可以编写一个LoginValidator来验证登录表单。
public class LoginValidator implements Validator {
public boolean supports(Class clazz) {
return clazz==User.class;
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "error. username.required", "必须填写用户名");
ValidationUtils.rejectIfEmpty(errors, "password", "error.password. required", "必须填写口令");
}
}
所有的验证器都必须实现Validator接口,该接口需要实现两个方法。
(1)boolean supports(Class clazz):返回是否支持该类型。
(2)void validate(Object target, Errors errors):验证表单对象。
Spring提供了一个ValidationUtils来简化验证,如果某个字段未通过验证,就将其放入Errors对象中,也可以直接使用Errors的rejectValue()方法来加入一个验证错误。
有了验证器后,就可以将其注入到BaseCommandController及其子类中。LoginController也是从BaseCommandController继承而来的,一旦注入了Validator,Spring对表单的处理流程就如图7-27所示。
加入了验证功能的login.jsp由<form:errors>标签显示验证错误,其内容如下。
<form:form method="post" action="login.do">
用户名:<form:input path="username" /> <form:errors path="username" />
口令:<form:password path="password" /> <form:errors path="password" />
<input type="submit" name="Submit" value="登录" />
</form:form>
其中,path属性必须和ValidationUtils.rejectIf Empty(Errors errors, String field, String errorCode)或Errors.rejectValue(String field, String errorCode, String defaultMessage)中的field参数一致,这样,Spring的<form:errors>标签就可以自动绑定错误信息。
对于用户登录表单,如果没有输入用户名或口令,就会显示验证错误的信息,如图7-28所示。
7.4.3 MultiActionController
如果需要处理多个类似的请求,可以考虑使用一个MultiActionController来实现,而不必分别编写多个单一功能的Controller。例如,在显示用户详细资料时,考虑到用户资料的内容较多,可以分为“基本资料”、“联系方式”和“详细地址”3大类显示给用户,由于这3个页面都从User对象中获取数据,因此,将3个页面的显示放到一个MultiActionController中不仅更方便,也便于减少Controller的数量。
ViewProfileController便是从MultiActionController派生的,能够查看3个页面。
/**
* @spring.bean name="/*Profile.do"
*/
public class ViewProfileController extends MultiActionController {
public class ViewProfileController extends MultiActionController {
private UserService userService;
/**
* @spring.property ref="userService"
*/
public void setUserService(UserService userService) {
this.userService = userService;
}
private String getUsername(HttpServletRequest request) {
String username = (String) request.getSession().getAttribute("USERNAME");
if(username==null)
throw new NeedLoginException();
return username;
}
public ModelAndView basicProfile(HttpServletRequest request, HttpServlet Response response) throws Exception {
String username = getUsername(request);
User user = userService.query(username);
Map model = new HashMap();
model.put("username", user.getUsername());
model.put("role", user.getRole()==User.ADMIN ? "Admin" : "User");
return new ModelAndView("basicProfile", model);
}
public ModelAndView contactProfile(HttpServletRequest request, HttpServlet Response response) throws Exception {
String username = getUsername(request);
User user = userService.query(username);
Map model = new HashMap();
model.put("email", user.getEmail());
model.put("blog", user.getBlog());
model.put("website", user.getWebsite());
return new ModelAndView("contactProfile", model);
}
public ModelAndView addressProfile(HttpServletRequest request, HttpServlet Response response) throws Exception {
String username = getUsername(request);
User user = userService.query(username);
Map model = new HashMap();
model.put("province", user.getProvince());
model.put("city", user.getCity());
model.put("zip", user.getZip());
return new ModelAndView("addressProfile", model);
}
}
其中,basicProfile()、contactProfile()和addressProfile()这3个方法都能分别独立地处理用户请求,并返回 User对象中对应的部分数据,那么,Spring如何根据URL来确定应该调用ViewProfileController的哪个方法来处理用户请求呢?答案是使用methodName Resolver的设定。在使用ViewProfileController之前,我们还需要配置一个MethodName Resolver。
MultiActionController 默认使用InternalPathMethodNameResolver,从URL中提取方法名,然后调用相应的方法来处理请求。对于上面的 ViewProfileController,传入“http://localhost:8080/basicProfile.do”、“http:// localhost:8080/contactProfile.do”和“http:// localhost:8080/addressProfile.do”就可以分别调用basicProfile()、contactProfile()和 addressProfile()方法来处理用户请求。
由于要从URL中提取methodName,所以配置ViewProfileController的name为“/*Profile.do”,使用通配符“*”来匹配这3个方法名,因此,方法命名一定要符合一定的规则,才便于使用URL映射。
现在,我们在一个ViewProfileController中就同时实现了3个页面的处理。启动服务器,登录后可以看到ViewProfileController的效果如图7-29~图7-31所示。
图7-30 图7-31
如果不喜欢通过URL来调用方法,Spring同样提供了多种MethodNameResolver的实现,最常见的一种是ParameterMethodNameResolver,它可以从参数中提取方法名。
<bean id="methodNameResolver" class="org.springframework.web.servlet.mvc. multiaction.ParameterMethodNameResolver">
<property name="paramName" value="action" />
<property name="defaultMethodName" value="basicProfile" />
</bean>
在上面的XML配置片断中,ParameterMethodNameResolver根据URL的action参数来确定方法名称。将 methodNameResolver注入到ViewProfileController后,若用户请求 “/viewProfile.do?action=basicProfile”,则methodNameResolver将根据参数action= basicProfile来决定调用ViewProfileController的basicProfile()方法处理用户请求,若用户请求 “/viewProfile.do?action=contactProfile”,则调用contactProfile()方法,若没有找到 action参数,则methodNameResolver根据defaultMethodName属性来调用basicProfile()方法。
事实上,我们自己也可以手动编写代码来确定调用哪个方法。
public ModelAndView handleRequest(HttpServletRequest request, HttpServlet Response response) throws Exception {
String methodName = request.getParameter("action");
if("basicProfile".equals(methodName))
return basicProfile(request, response);
if("contactProfile".equals(methodName))
return contactProfile(request, response);
if("addressProfile".equals(methodName))
return addressProfile(request, response);
// default method:
return basicProfile(request, response);
}
Spring 为我们所做的工作不过是将上述代码以配置文件的形式更灵活地实现而已。除了ParameterMethodNameResolver外,Spring还提供了PropertiesMethodNameResolver,不过,最简单也最有用的还是ParameterMethodNameResolver。
7.4.4 AbstractWizardFormController
从名字上就可以看出,AbstractWizardFormController能够实现向导式的页面。如果用户需要填写的表单内容很多,就有必要将其拆为几个页面,使用户能通过“上一步”和“下一步”按钮方便地在向导页面间导航,例如,设计一个在线调查的向导,就可以方便地引导用户一步一步完成调查表单的填写。
我们以注册新用户为例,RegisterController需要用户填写基本资料、联系方式和详细地址,由于表单内容较多,我们让用户分3个页面分步完成注册。
我们无须处理“下一步”和“上一步”按钮,Spring会自动显示正确的页面,我们只需要处理最后用户单击“完成”按钮提交的整个表单对象。
/**
* 用户注册向导
* @spring.bean name="/register.do"
* @spring.property name="commandClass" value="example.chapter7.User"
* @spring.property name="pages" list="registerStep0,registerStep1, registerStep2"
*/
public class RegisterController extends AbstractWizardFormController {
private UserService userService;
/**
* @spring.property ref="userService"
*/
public void setUserService(UserService userService) {
this.userService = userService;
}
// 当用户单击"_finish"按钮时,触发processFinish()方法:
protected ModelAndView processFinish(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
User user = (User)command;
userService.register(user);
Map model = new HashMap();
model.put("username", user.getUsername());
return new ModelAndView("registerSuccess", model);
}
}
那么,Spring是如何知道下一个或上一个需要显示的页面呢?除了指定command Class为User对象外,我们还需要将几个View的逻辑名称注入到RegisterController的pages属性中,注意到 AbstractWizardController的pages是从下标0开始计数的,因此,我们将注册的3个页面依次命名为 registerStep0.jsp、registerStep1.jsp和registerStep2.jsp。
除了指定pages属性外,我们还需要按照一定的规则来编写JSP页面,才能告诉Spring如何显示下一页或上一页。在表单的提交按钮上,必须以_target+索引命名按钮,例如:
<input type="submit" name="_target1" value="下一步">将前进到索引为1的页面,即registerStep1.jsp。
<input type="submit" name="_target0" value="上一步">将返回到索引为0的页面,即registerStep0.jsp。
最后一个“完成”按钮必须以“_finish”命名。
<input type="submit" name="_finish" value="完成">
当用户单击“完成”按钮后,Spring将调用processFinish()方法处理表单。
如果需要验证表单,在AbstractWizardController中,就无法使用Validator来进行验证,因为用户在每个页面仅填写了部分内容,直到用户单击“完成”按钮时,整个表单对象才被填充完毕,因此,在任何一个页面中验证Command都将失败,为此,验证必须在 AbstractWizardController的validatePage()方法中进行,Spring将传入page参数,我们就根据这个参数对 command对象进行部分验证。
// 每当用户单击"_target?"准备前进到下一步时,触发validatePage()来验证当前页:
protected void validatePage(Object command, Errors errors, int page) {
User user = (User)command;
if(page==0) {
// 验证username,password:
if(!user.getUsername().matches("[a-zA-Z0-9]{3,20}"))
errors.rejectValue("username", "error.username", "用户名不符合要求");
if(userService.isExist(user.getUsername()))
errors.rejectValue("username", "error.username", "用户名已存在");
if(user.getPassword()==null || user.getPassword().length()<6)
errors.rejectValue("password", "error.password", "口令至少为6个字符");
}
else if(page==1) {
// 验证email,blog,website:
if(user.getEmail()==null)
errors.rejectValue("email", "error.email.empty", "电子邮件不能为空");
else if(!user.getEmail().matches("[a-zA-Z0-9\\_\\-]+\\@[a-zA-Z0-9\\_ \\-]+[\\.[a-zA-Z0-9\\_\\-]+]{1,2}"))
errors.rejectValue("email", "error.email", "电子邮件地址无效");
if(user.getBlog()==null || user.getBlog().trim().equals(""))
errors.rejectValue("blog", "error.blog", "博客地址不能为空");
if(user.getWebsite()==null || user.getWebsite().trim().equals(""))
errors.rejectValue("website", "error.website", "网址地址不能为空");
}
else if(page==2) {
// 验证province,city,zip:略过
}
}
若验证未通过,则将停留在当前页,并可以通过<form:errors>显示相应的错误信息,待用户更正后,才可以继续前进到下一页。编译工程,启动Resin服务器,可以看到整个用户注册的流程如图7-32~图7-35所示。
图7-32 图7-33
图7-34 图7-35
读者也许注意到了,第一个页面有两个口令框,其中,第二个口令框名称为password2,在User对象中并没有对应的属性,Spring不会自动绑定它。那么,如何验证用户两次输入的口令是否一致呢?我们一般不愿意更改User对象,因为User对象很可能对应数据库中的某个表,而数据库表不会存储同一用户的两份相同的口令。此时,可以通过 JavaScript来验证,既方便,又能避免修改User对象。因此,在Web应用程序的设计中,不要仅仅拘泥于JavaEE框架,对于 JavaScript、AJAX等技术也要充分利用。
为了让读者能更方便地看到Controller和对应的JSP视图,我们将Controller的源码也放入到每个页面中。在每一个页面中,读者都可以通过单击“查看源码”非常方便地阅读对应的Controller代码,如图7-36所示。
图7-36
7.4.5 输出二进制内容
虽然大多数时候用户请求的都是HTML页面,不过,某些情况下仍然需要向用户发送动态生成的二进制内容,例如,图片认证码、Excel报表等,从HTTP协议上看来,向用户发送二进制内容只需要设置好输出响应的MIME类型,然后直接写入二进制流即可。对应到JavaEE Web应用程序,就是设置HttpServletResponse对象的ContentType,然后将二进制数据写入OutputStream即可。
我们以动态生成一个图片认证码为例,实现一个ImageController如下。
public class ImageController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 在内存中绘图:
String code = String.valueOf((int)(Math.random() * 9000) + 1000);
BufferedImage image = new BufferedImage
(100, 50, BufferedImage. TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GRAY);
g.fillRect(0, 0, 100, 50);
g.setColor(Color.RED);
g.drawRect(0, 0, 99, 49);
g.setColor(Color.BLACK);
g.drawString(code, 20, 20);
g.dispose();
image.flush();
// 设置ContentType:
response.setContentType("image/jpeg");
response.setHeader("Cache-Control", "no-cache");
// 输出到ServletOutputStream:
ServletOutputStream output = response.getOutputStream();
ImageIO.write(image, "jpeg", output);
return null;
}
}
上述代码的运行效果如图7-37所示。
图7-37
从上面的代码可以看出,输出二进制内容的关键是设置正确的ContentType及ContentLength,然后获得输出流,将二进制内容写入即可。常见的ContentType类型如表7-2所示。
表7-2
文件类型
ContentType
JPEG
image/jpeg
MS Word
application/msword
MS Excel
application/vnd.ms-excel
PDF
application/pdf
MP3
audio/mpeg
未知文件类型
application/octet-stream
虽然可以将所有的二进制内容都设置为application/octet-stream,但是浏览器会根据ContentType做不同的处理。例如,若一个 Word文档被设置为application/octet-stream,则浏览器将直接提示将文件另存到本地,而设置为application /msword时,如果用户计算机上已装有Word,就可以直接在浏览器中打开该Word文档。
在Spring中,生成二进制内容还可以用 AbstractView来实现,即Controller仍返回一个ModelAndView,最终输出由View实现,Spring提供了好几种这样的 View,例如,AbstractPdfView可以输出PDF文档,AbstractExcelView可以输出Excel文档。使用 AbstractView来输出二进制内容的关键是正确实现下面几个方法。
(1)String getContentType():返回二进制内容的ContentType。
(2)void renderMergedOutputModel (Map model, HttpServletRequest request, Http ServletResponse response):输出二进制内容,所需的数据可以从model中获得,最终的输出仍然是通过调用 response.getOutputStream()获得输出流并写入。
使用Spring提供的 AbstractView虽然也能实现输出二进制内容,并且能保持Spring MVC的一致性,不过,我个人认为,这样做反而增加了应用程序的复杂性。由于通常Web应用程序需要动态生成二进制内容的地方不多,完全可以自己在 Controller中将二进制内容写入response对象,然后直接返回null结束处理,这样反而使流程更清晰,配置更少,代码更容易维护。有些时候,Spring过于严密的封装反而增加了代码的复杂性。当然,如果需要输出Excel或PDF文档,则仍可以选择AbstractExcelView和 AbstractPdfView,因为它们提供了一些额外的辅助方法来简化文档的生成。
7.4.6 重定向URL
重定向URL会使服务器向客户端发送一个 Redirect响应,并包含一个目标URL。客户端接收到Redirect响应后,会立刻重新请求新的URL,这一点和Forward不同。前者使客户端发送了两次独立的HTTP请求,而后者请求是在服务器内部处理的,客户端并不知道服务器端对Request是否做了Forward处理。
重定向功能的主要用途是为了在服务器端修改了某一资源的URL后,原有客户仍可以继续通过原来的URL访问该资源。由于重定向会使客户端发送两次请求,所以降低了网络效率,并且不便于用户在浏览器中单击“后退”按钮返回上一个页面。对于Web应用程序而言,决不能大量使用重定向功能。
在Controller中实现 Redirect也非常容易。最简单的方法是直接调用HttpServlet Response对象的sendRedirect()方法,然后返回null。一旦返回的ModelAndView为null,Spring就认为 Controller自己已经完成了请求处理,不再按照常规的MVC流程继续处理请求。
例如,对于用户注销登录的操作,在清理了Session的内容后,就可以将用户重定向到登录页面。LogoutController代码如下。
/**
* @spring.bean name="/logout.do"
*/
public class LogoutController extends AbstractController {
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
request.getSession().removeAttribute("USERNAME");
response.sendRedirect("login.do");
return null;
}
}
另一种实现重定向的方法不用直接调用HttpServletResponse对象的sendRedirect()方法,而是返回一个带有 “redirect:”前缀的View,这样,ViewResolver就知道这是一个重定向操作,于是不再渲染视图,而是直接向客户端发送 Redirect响应。
return new ModelAndView("redirect:login.do");
Spring还提供了一个RedirectView对象,也可以实现重定向操作,不过使用RedirectView使Controller和View的耦合稍微紧密了一点,推荐的方法是使用“redirect:”前缀。
使用重定向要注意的一点是,重定向的资源不可位于/WEB-INF/目录下,因为用户无法通过URL直接访问位于/WEB-INF/目录下的资源,而使用MVC流程通过forward调用/WEB-INF/目录下的资源是允许的。
7.4.7 处理异常
如果Controller在处理用户请求时发生了异常,自己捕获异常并跳转到出错页面会使核心逻辑混乱。Spring的MVC框架提供了一个HandlerExceptionResolver,为所有的Controller抛出的异常提供一个统一的入口。
/**
* @spring.bean id="handlerExceptionResolver"
*/
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
private Log log = LogFactory.getLog(getClass());
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
log.warn("Handle exception: " + ex.getClass().getName());
if(ex instanceof NeedLoginException)
return new ModelAndView("redirect:login.do");
Map model = new HashMap();
model.put("ex", ex.getClass().getSimpleName());
model.put("message", ex.getMessage());
return new ModelAndView("error", model);
}
}
MyHandlerExceptionResolver 根据Exception类型判断如何处理异常,如果是NeedLoginException,说明系统要求用户登录,这时直接将用户导向到登录页面;对于其他类型的异常,则直接将异常的错误信息显示给用户,注意返回的视图名称为“error”,实际的视图文件即为“/error.jsp”。
使用 HandlerExceptionResolver可以避免在应用程序的每一个Controller中都去处理异常,将异常统一放到 HandlerExceptionResolver中可以极大地简化异常处理逻辑,也便于在一个统一的地方记录异常日志。对于无法处理的异常,可以给用户显示一个友好的出错页面。
7.4.8 拦截请求
在本章的前几节,我们已经看到了使用 Filter可以拦截用户请求,并实现相应的处理。Spring的MVC框架也提供了一个拦截器链,可以由多个HandlerInterceptor构成,允许在Controller处理用户请求的前后有机会处理请求。和Filter相比,HandlerInterceptor是在Spring的IoC 容器中配置的,可以注入任意的组件,而Filter定义在Spring容器之外,因此,注入IoC组件比较困难,或者难以得到一个优雅的设计。
HandlerInterceptor接口定义了以下3个方法。
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
(1)preHandle()方法在Controller执行前调用,其返回值指定了是否应当继续处理请求。若返回false,Spring MVC框架将不再继续调用下一个拦截器,也不会将请求交给Controller处理,整个请求处理将到此结束。
(2)postHandler()方法在Controller执行完毕后调用,此时Controller仅返回了ModelAndView对象,还没有对视图进行渲染,在这个方法中有机会对ModelAndView进行修改。
(3)afterCompletion()方法在整个请求全部完成后调用,通过判断参数ex是否为null就可以判断是否产生了异常。
通过HandlerInterceptor,就有机会在一个请求执行的3个阶段对其进行拦截。例如,为了统计Web应用程序的性能,我们设计了一个性能拦截器,将每个用户请求的处理时间记录下来。PerformanceHandlerInterceptor实现如下。
/**
* @spring.bean id="performanceHandler"
*/
public class PerformanceHandlerInterceptor implements HandlerInterceptor {
private final Log log = LogFactory.getLog(PerformanceHandlerInterceptor. class);
private static final String START_TIME = "PERF_START";
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute(START_TIME, System.currentTimeMillis());
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 不需要处理postHandler, 保留空方法即可
}
public void afterCompletion(HttpServletRequest request, HttpServlet Response response, Object handler, Exception ex) throws Exception {
Long startTime = (Long)request.getAttribute(START_TIME);
if(startTime!=null) {
long last = System.currentTimeMillis() - startTime.longValue();
String uri = request.getRequestURI();
String query = request.getQueryString();
if(query!=null)
uri = uri + '?' + query;
log.info("URL: " + uri);
log.info("Execute: " + last + "ms.");
}
}
}
由于我们必须保证PerformanceHandlerInterceptor是线程安全的,因此,绝不可将起始时间记录在PerformanceHandlerInterceptord 的成员变量中。由于每个请求都对应一个独立的HttpServletRequest实例,因此,将起始时间放入HttpServletRequest实例中就保证了线程安全。
然后,将其添加到handlerMapping中的interceptor列表中。
<bean id="handlerMapping" class="org.springframework.web.servlet.handler. BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="performanceHandler" />
</list>
</property>
</bean>
运行应用程序,在浏览器中请求/login.do,查看控制台输出如下。
[2006/11/20 22:33:41.857] URL: /login.do
[2006/11/20 22:33:41.857] Execute: 3781ms.
[2006/11/20 22:33:45.325] URL: /login.do
[2006/11/20 22:33:45.325] Execute: 0ms.
可以看到PerformanceHandlerInterceptor记录的处理时间。首次执行/login.do请求时,耗时3秒多,这是因为服务器需要编译JSP文件,随后刷新页面,由于可以跳过JSP的编译步骤,/login.do请求在1ms内就完成了。
7.4.9 处理文件上传
文件上传是Web应用程序中常见的功能。本质上,浏览器在向服务器发送文件时,其HTTP请求必须以multipart/form-data的形式发送,该规范定义在RFC 2388(http://www.ietf.org/rfc/rfc2388.txt)中,可以实现一次上传一个或多个文件。不过,JavaEE的Web 规范并没有内置处理multipart请求的功能,因此,要实现文件上传,就必须借助于第三方组件,或者自己手动编码解析 HttpServletRequest。
Apache Commons FileUpload(http://jakarta.apache.org/commons/fileupload)组件和COS FileUpload(http://www.servlets.com/cos)组件都是常见的处理文件上传的组件,Spring很好地对这两种组件进行了封装。在Spring中处理文件上传时,根本无须与这两个组件的API打交道,只需用到Spring提供的 MultipartHttpServletRequest对象,就可以轻松实现文件上传的功能。
下面的例子演示了如何在Spring中实现文件上传。我们在Eclipse中建立如下的WebUpload工程,如图7-38所示。
图7-38
默认地,Spring不会处理文件上传,即所有的以multipart/form-data形式发送的请求都不被处理,如果要处理Multipart请求,需要在Spring的XML配置文件中申明一个MultipartResolver。
<bean id="multipartResolver" class="org.springframework.web.multipart. commons.CommonsMultipartResolver">
<!-- 最大允许上传文件大小:1M -->
<property name="maxUploadSize" value="1048576" />
</bean>
maxUploadSize属性指定了最大所能上传的文件大小,若超出了最大范围,Spring将会直接抛出异常。
如果一个请求不是Multipart请求,它就会按照正常的流程处理;如果一个请求是Multipart请求,Spring就会自动调用MultipartResolver,然后将 HttpServletRequest请求变为MultipartHttpServletRequest请求,开发者只需要处理 MultipartHttpServletRequest对象就可以了。
如何得知一个请求是否是MultipartHttpServletRequest类型呢?通过instanceof操作就能非常简单地判断出来。我们在UploadController中实现文件上传的代码如下。
public class UploadController implements Controller {
private Log log = LogFactory.getLog(getClass());
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 判断request是不是multipart请求:
if(request instanceof MultipartHttpServletRequest) {
MultipartHttpServletRequest multipart = (MultipartHttpServlet Request)request;
MultipartFile file = multipart.getFile("file");
if(file==null || file.isEmpty()) {
// 文件不存在:
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
String filename = file.getOriginalFilename();
log.info("Upload file name: " + filename);
// 获取文件扩展名:
String ext = "";
int pos;
if((pos = filename.lastIndexOf('.'))!=(-1)) {
ext = URLEncoder.encode(filename.substring(pos).trim(), "UTF-8");
}
InputStream input = null;
OutputStream output = null;
// 确定服务器端写入文件的文件名:
String uploadFile = request.getSession()
.getServletContext()
.getRealPath("/upload" + System.currentTimeMillis() + ext);
try {
// 获得上传文件的输入流:
input = file.getInputStream();
// 写入到服务器的本地文件:
output = new BufferedOutputStream(new FileOutputStream(uploadFile));
byte[] buffer = new byte[1024];
int n;
while((n=input.read(buffer))!=(-1)) {
output.write(buffer, 0, n);
}
}
finally {
// 必须在finally中关闭输入/输出流:
if(input!=null) {
try {
input.close();
}
catch(IOException ioe) {}
}
if(output!=null) {
try {
output.close();
}
catch(IOException ioe) {}
}
}
// 告诉浏览器文件上传成功:
Writer writer = response.getWriter();
writer.write("File uploaded successfully!");
writer.flush();
}
else {
// 非multipart/form-data请求,发送一个错误:
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return null;
}
}
仔细查看上面的代码,读者可能会发现,我们根本没有调用Commons FileUpload或COS FileUpload组件的API,Spring已经完全为我们封装好了。那么,Spring如何确定使用Commons FileUpload还是使用COS FileUpload呢?答案是发现哪个就用哪个。如果在/WEB-INF/lib目录下放置Commons FileUpload的jar包,Spring就会自动使用Commons FileUpload,COS FileUpload也是如此,这样带来的好处是完全屏蔽了底层组件的API,如果需要替换底层组件,只需要替换相应的jar包,甚至连XML配置文件都不用改动。
在 WebUpload工程中,我们使用的是Commons FileUpload,只需将commons- fileupload.jar和commons-io.jar放到/WEB-INF/lib目录下,剩下的事情就由Spring处理了。使用任何文本编辑器编写一个最简单的上传文件的index.html页面。
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Upload File</title>
</head>
<body>
<form action="upload.do" method="post" enctype="multipart/form-data" name="form1">
<p>请选择需要上传的文件:<input type="file" name="file"></p>
<p><input type="submit" name="Submit" value="上传"></p>
</form>
</body>
</html>
配置好DispatcherServlet后,运行这个Web应用程序,打开index.html,选择待上传的文件,如图7-39所示。
文件上传成功后,就可以在服务器的Web应用的根目录下找到已上传的文件,如图7-40所示。
图7-39 图7-40
对于非file类型的表单字段,仍可以调用MultipartHttpServletRequest的getParameter()方法获得相应的字段值,因为MultipartHttpServletRequest也实现了HttpServletRequest接口。
也可以在SimpleFormController中将表单中上传的文件绑定到byte[]类型的属性中,不过,如果上传文件较大,则将消耗较大的服务器内存,因此,采用何种解决方案需要视情况而定。