上篇文章我们讲了SpringMVC 的响应数据和结果视图,现在来讲SpringMVC 文件上传、SpringMVC 中的异常处理和SpringMVC 中的拦截器。
文章目录
SpringMVC 文件上传
再讲文件上传之前,我们先来了解几个知识点:
-
文件上传的必要前提
form
表单的enctype
取值必须是:multipart/form-data
(默认值是:application/x-www-form-urlencoded
)enctype
:是表单请求正文的类型
method
属性取值必须是Post
- 提供一个文件选择域
<input type=”file” />
-
文件上传的原理分析
- 当
form
表单的enctype
取值不是默认值后,request.getParameter()
将失效。enctype=”application/x-www-form-urlencoded”
时,form
表单的正文内容是:key=value&key=value&key=value
- 当
form
表单的enctype
取值为Mutilpart/form-data
时,请求正文内容就变成:每一部分都是MIME
类型描述的正文
- 当
传统方式文件上传
- 使用传统方式进行文件上传时,我们需要依赖
Commons-fileupload jar
包,所以需要先导入依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
- 编写index.jsp 代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>传统文件上传</h3>
<form action="user/fileupload1" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload">
<input type="submit" value="上传">
</form>
</body>
</html>
- UserController控制器代码
@Component
@RequestMapping("/user")
public class UserController {
@RequestMapping("/fileupload1")
public String fileupload1(HttpServletRequest request) throws Exception{
System.out.println("文件上传...");
// 使用fileupload组件完成文件上传
// 上传的位置
String path = request.getSession().getServletContext().getRealPath("/uploads/");
// 判断,该路径是否存在
File file = new File(path);
if (!file.exists()){
//创建文件夹
file.mkdirs();
}
// 解析request对象,获取上传文件项
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
// 解析request
List<FileItem> items = upload.parseRequest(request);
// 遍历
for (FileItem item : items){
// 进行判断,当前item对象是否是上传文件项
if (item.isFormField()){
// 说明普通表单向
}else {
// 说明上传文件项
// 获取上传文件的名称
String filename = item.getName();
// 把文件的名称设置唯一值,uuid
String uuid = UUID.randomUUID().toString().replace("-","");
filename = uuid+"_"+filename;
// 完成文件上传
item.write(new File(path,filename));
// 删除临时文件
item.delete();
}
}
return "success";
}
}
SpringMVC 方式文件上传
-
从上面的例子可以看出,如果使用传统方式进行文件上传,我们需要自己解析
request
,分辨每一项究竟是普通表单项还是文件项,特别繁琐。而如果使用 SpringMVC,我们只需要配置一个文件解析器,由文件解析器来替我们解析,然后我们在控制器使用MultipartFile
对象接收即可(该对象表示要上传的文件)。 -
控制器代码
/**
* springmvc文件上传
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/fileupload2")
public String fileupload2(HttpServletRequest request, MultipartFile upload) throws Exception{
System.out.println("springmvc文件上传...");
// 使用fileupload组件完成文件上传
// 上传的位置
String path = request.getSession().getServletContext().getRealPath("/uploads/");
// 判断,该路径是否存在
File file = new File(path);
if (!file.exists()){
//创建文件夹
file.mkdirs();
}
// 说明上传文件项
// 获取上传文件的名称
String filename = upload.getOriginalFilename();
// 把文件的名称设置唯一值,uuid
String uuid = UUID.randomUUID().toString().replace("-","");
filename = uuid+"_"+filename;
// 完成文件上传
upload.transferTo(new File(path,filename));
return "success";
}
- 使用该方式上传文件时,要确保
MultipartFile
对象名称与表单中 file 组件的名称保持一致。如果不一致,则需要使用@RequestParam
进行参数的绑定。譬如表单为:<input type="file" name="upload"/>
,则控制器方法应该为upload2(HttpServletRequest request, @RequestParam("upload") MultipartFile upload)
- 获取上传文件的文件名时,应该使用
getOriginalFilename()
,不能使用getName()
,getName()
是获取表单中file
组件的名称。 - 如果要想实现多文件上传,那么需要在 file 组件加上属性
multiple="multiple"
(HTML5 的新属性),然后控制器用List<MultipartFile>
接收即可。 - 在 SpringMVC 配置文件中配置文件解析器
<!--配置文件解析器对象-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="10485760"></property>
</bean>
文件解析器的
id
必须为multipartResolver
,文件上传的解析器 id 是固定的,不能起别的名称,否则无法实现请求参数的绑定。(不光是文件,其他
字段也将无法绑定),maxUploadSize
属性以Byte
为单位。
- 最后来看看SpringMVC上传文件的原理:
SpringMVC 跨服务器方式文件上传
- 在实际开发中,我们可能会有好几台服务器,有的服务器用于部署应用,而有的服务器用于存储用户上传的文件。那么,此时我们如果需要跨服务器上传文件,就需要借助
jersey-client
的 jar 包。
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-core</artifactId>
<version>1.18.1</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
<version>1.18.1</version>
</dependency>
-
想要测试跨服务器文件上传,需要部署两个
Tomcat
服务器,一个服务器部署应用,一个服务器用于接收文件。这里将存放文件的路径指定为接收文件项目的根目录下的uploads
文件夹。所以得新建一个项目当做是文件服务器,然后在部署另外一个Tomcat
最后把新建的项目部署到新建的服务器上就好了。 -
jsp代码:
<h3>SpringMVC跨服务器文件上传</h3>
<form action="user/fileupload3" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="upload">
<input type="submit" value="上传">
</form>
- 控制器代码:
/**
* springmvc跨服务器文件上传
* @param upload
* @return
* @throws Exception
*/
@RequestMapping("/fileupload3")
public String fileupload3(MultipartFile upload) throws Exception{
System.out.println("springmvc跨服务器文件上传...");
// 定义上传文件服务器路径
String path = "http://localhost:9090/uploads";
// 说明上传文件项
// 获取上传文件的名称
String filename = upload.getOriginalFilename();
// 把文件的名称设置唯一值,uuid
String uuid = UUID.randomUUID().toString().replace("-","");
filename = uuid+"_"+filename;
// 创建客户端的对象
Client client = Client.create();
// 和图片服务器进行连接
WebResource webResource = client.resource(path+"/"+filename);
// 上传文件
webResource.put(upload.getBytes());
return "success";
}
关于SpringMVC跨服务器报错的,大家可以看看我的这篇文章。传送门
SpringMVC 中的异常处理
- 系统中异常包括两类:预期异常和运行时异常
RuntimeException
,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。 - 在开发中,一般我们都是
Controller
调用Service
,Service
调用Dao
,在这个过程中,如果有某个地方出现了异常,那么异常都会被往上一层一层throws Exception
(抛出)。Dao
抛给Service
,Service
抛给DispatcherServlet
,最后由DispatcherServlet
将异常交给HandlerExceptionResolver
(异常处理器)进行处理。如下图:
操作步骤
- 编写自定义异常类,记得继承
Exception
类
/**
* @Author: zhang
* @Descripetion: 自定义异常类
**/
public class SysException extends Exception {
//存储信息的
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public SysException(String message) {
this.message = message;
}
}
- 编写错误页面
error.jsp
:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${errorMsg}
</body>
</html>
- 编写自定义异常处理器,需要实现
HandlerExceptionResolver
接口
/**
* @Author: zhang
* @Descripetion: 异常处理器
**/
public class SysExceptionResolver implements HandlerExceptionResolver {
/**
* 处理异常业务逻辑
* @param request
* @param response
* @param handler
* @param ex
* @return
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 获取到异常对象
SysException sysException;
if(ex instanceof SysException){
sysException = (SysException) ex;
}else{
// 如果抛出的不是自定义异常,那么就创建一个新的自定义异常
sysException = new SysException("系统正在维护....");
}
// 创建ModelAndView对象
ModelAndView mv = new ModelAndView();
mv.addObject("errorMsg",sysException.getMessage());
mv.setViewName("error");
return mv;
}
}
- 在 SpringMVC 配置文件中配置异常处理器
<!-- 配置异常处理器 -->
<bean id="sysExceptionResolver" class="com.cz.exception.SysExceptionResolver"></bean>
- 测试代码
/**
* @Author: zhang
* @Descripetion: 测试
**/
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/testException")
public String testException() throws SysException {
System.out.println("testException执行了...");
try {
//模拟异常
int a = 10/0;
}catch (Exception e){
// 打印异常信息
e.printStackTrace();
// 抛出自定义异常信息
throw new SysException("查询所有用户出现错误了...");
}
return "success";
}
}
SpringMVC 中的拦截器
拦截器的概述
Spring MVC 的处理器拦截器类似于 Servlet 开发中的过滤器 Filter
,用于对处理器进行预处理和后处理。 和过滤器类似,过滤器可以定义过滤链,拦截器也可以定义拦截器链(Interceptor Chain
),也就是将拦截器按一定的顺序联结成一条链,在访问被拦截的方法之前,拦截器链中的拦截器就会按之前定义的顺序进行拦截。
过滤器和拦截器虽然比较类似,但是还是有区别的:
- 过滤器是 Servlet 规范中的,也就是说,只要是 JavaWeb 工程就可以使用;而拦截器是 SpringMVC 框架的,只有使用了 SpringMVC 框架的工程才可以使用
- 过滤器可以拦截所有的资源,也就是不仅可以拦截
Controller
,其它的所有资源(像 jsp、js 这些静态资源)它都可以拦截;而拦截器是针对 Controller 的,它只能拦截控制器。 - 拦截器是
AOP
思想的一种实现方式
如果想自定义拦截器,那么就需要实现HandlerInterceptor
接口,该接口中有 3 个 default
方法:
-
boolean preHandle()
方法是controller
方法执行前拦截的方法- 可以使用
request
或者response
跳转到指定的页面 return true
放行,执行下一个拦截器,如果没有拦截器,执行controller中的方法。return false
不放行,不会执行controller中的方法。此时可以使用转发或者重定向跳转到指定的页面。- 该方法是按拦截器定义顺序进行调用的。
- 可以使用
-
void postHandle()
是controller
方法执行后执行的方法,在JSP视图执行前。- 可以通过
ModelAndView
(模型和视图对象)来对模型数据或视图进行处理 - 也可以通过
HttpServletRequest
和HttpServletResponse
进行转发或者重定向 - 该方法是按拦截器定义逆序进行调用的。
- 可以通过
-
void afterCompletion(..)
: 该方法在整个请求处理完毕后调用(也就是在视图渲染完毕后)。- 可以在该方法中进行一些资源清理的操作(不能在该方法中进行页面的跳转了),相当于
try...catch...finally
中的finally
。 - 该方法是按拦截器定义逆序进行调用的,而且只有 preHandle() 返回 true 才调用。
- 可以在该方法中进行一些资源清理的操作(不能在该方法中进行页面的跳转了),相当于
操作步骤
先来看看原理:
- 创建自定义拦截器,实现
Interceptor
接口并重写其中的方法
/**
* @Author: zhang
* @Descripetion: 自定义拦截器
**/
public class MyInterceptor1 implements HandlerInterceptor {
/**
* 预处理,controller方法执行前
* return true 放行,执行下一个拦截器,如果没有,执行controller中的方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("第 1 个拦截器中的preHandle方法执行了...");
//跳转页面 return false
// request.getRequestDispatcher("/WEB-INF/pages/error.jsp").forward(request,response);
return true;
}
/**
* 后处理方法,controller方法执行后,success.jsp执行之前
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("第 1 个拦截器中的postHandle方法执行了...");
//跳转页面 return false
// request.getRequestDispatcher("/WEB-INF/pages/error.jsp").forward(request,response);
}
/**
* success.jsp页面执行后,该方法会执行
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("第 1 个拦截器中的afterCompletion方法执行了...");
}
}
- 在 SpringMVC 配置文件中配置拦截器
<!--配置拦截器-->
<mvc:interceptors>
<!--配置拦截器-->
<mvc:interceptor>
<!--要拦截的具体的方法-->
<mvc:mapping path="/user/*"/>
<!--不要拦截的方法
<mvc:exclude-mapping path=""/>
-->
<!--配置拦截器对象-->
<bean class="com.cz.interceptor.MyInterceptor1" />
</mvc:interceptor>
</mvc:interceptors>
<mvc:mapping>
标签用于指定要拦截的路径,而<mvc:exclude-mapping>
标签用于指定不拦截的路径,两者选其一即可。
- 编写控制器代码
@Component
@RequestMapping("/user")
public class UserController {
@RequestMapping("/testInterceptor")
public String testInterceptor() {
System.out.println("testInterceptor执行了...");
return "success";
}
}
多个拦截器
以下内容引自这篇博客
-
如果有多个拦截器,此时拦截器 1 的
preHandle
方法返回true
,但是拦截器 2 的preHandle
方法返回false
,那么拦截器 1 的afterCompletion
的方法是否执行?- 因为我们上面说过,
afterCompletion
方法只有当preHandle
返回true
的时候执行。所以,拦截器 1 的afterCompletion
方法会执行,而拦截器 2 的afterCompletion
方法不会执行。
- 因为我们上面说过,
-
如果某个拦截器在
postHandle
方法中使用request
和response
进行转发,那么最终的结果视图(这里是success.jsp
)还会不会执行?- 我们上面说过,
postHandle
方法是在控制器执行后,视图渲染前被调用。所以,当我们转发到某个页面时,浏览器会把这个页面展示出来,但是postHandle
方法还是会继续执行,也就是说 success.jsp
还是会被执行到,只不过不会再被渲染展示了。
- 我们上面说过,
-
如果某个拦截器在
postHandle
方法中不通过request
进行转发,而是使用ModelAndView.setViewName("error")
,那么最终的结果视图(这里是success.jsp
)还会不会执行?- 因为
ModelAndView
中存储的是结果视图,也就是控制器方法的返回值success
,此时我们直接调用setViewName()
方法,就相当于修改了控制器方法的返回值,所以最终只会跳转到error.jsp
而不是success.jsp
- 因为