第五章 文件上传与下载
文件上传的含义是指:用户在页面上传文件给服务器后,会将文件存入本地位置。实现文件上传的前提:
- method=”post”。因为提交的数据会比较大,所以要使用post提交。
- 必须使用要有name属性。
- 在input标签中设置属性
encType=”Multipart/form-data”
(encType是指表单请求正文的类型) - 需要导入
Common-fileupload
和Common-io
包,其中Common-io
不需要手动导入,maven会自动导入对应版本的jar。
一 文件上传
1. 涉及的接口和类
在WEB学习中我们用到的是 Apache fileupload
这个组件来实现上传,在SpringMVC中对它进行了封装,让我们使用起来比较方便,但是底层还是由Apache fileupload来实现的。SpringMVC中由MultipartFile
接口来实现文件上传。
public interface MultipartResolver {
//判断是否包含文件,是就返回true.
boolean isMultipart(HttpServletRequest var1);
//对请求进行解析并将其封装成MultipartHttpServletRequest对象
MultipartHttpServletRequest resolveMultipart(HttpServletRequest var1) throws MultipartException;
void cleanupMultipart(MultipartHttpServletRequest var1);
}
流程图大概如下:
该接口有两个实现类:CommonsMultipartResolver
和StandardServletMultipartResolver
。
public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {}
其中CommonsMultipartResolver
使用commons Fileupload
来处理multipart请求,所以在使用时,必须要引入相应的 jar 包(即Common-fileupload和Common-io)。对应的依赖为:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>
而 StandardServletMultipartResolver
是基于 Servlet 3.0来处理 multipart 请求的,所以不需要引用其他 jar 包,但是必须使用支持 Servlet 3.0的容器才可以。
2 单服务器单文件上传
2.1配置 CommonsMultipartResolver
在此文件中进行配置的时候,bean id
必须固定为multipartResolver
,只有这样SpringMVC才会根据此id找解析器。可配置的属性如下:
defaultEncoding
:请求的编码格式,默认为iso-8859-1。一般我们需要更改为UTF-8。maxUploadSize
:设定允许上传的文件大小。单位为字节。axInMemorySize
:设定文件上传时写入内存的最大值,如果小于这个参数不会生成临时文件。默认为10240字节uploadTempDir
:上传文件的临时路径。上传完成后,就会将临时文件删除。注意:这不是保存文件的路径!maxUploadSizePerFile
:跟maxUploadSize
差不多,不过maxUploadSizePerFile
是限制每个上传文件的大小,而maxUploadSiz
是限制总的上传文件大小。preserveFilename
:保存文件名。
如果没其他特殊要求,我们配置以下几点即可:
<!-- 定义文件上传解析器 此bean的id必须固定,springMVC会根据此id找解析器。-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!--编码格式-->
<property name="defaultEncoding" value="UTF-8"></property>
<!--上传的文件大小-->
<property name="maxUploadSize" value="5242880"></property>
<!--临时路径-->
<property name="uploadTempDir" value="fileUpload/temp"></property>
</bean>
2.2 配置Controller类
在Controller方法中,我们需要传入MultipartFile
类型的形参。该形参是用来操作上传文件相关的:
getOriginalFilename()transferTo()
:取用户上传的文件名(包括后缀)。得到该名字后,可以通过判断后缀来规定用户上传的文件必须是什么类型的,也可以通过拼接随机数来保证重复上交的文件不会被覆盖等等。transferTo()
:将上传的文件写入到指定文件中。因此我们必须先创建好一个空文件,这样文件上传成功之后就会填充这个空文件。
在此例中,我们通过request
对象获取当前项目的相对路径,通过new File(path, fileName)
创建空文件。
/**
* 该类用于测试文件上传!
*/
@Controller
@RequestMapping("/file")
public class FileController {
@RequestMapping("/fileUploadOne")
public String upload(MultipartFile upload,HttpServletRequest request) throws Exception {
//获取开始时间,测试上传文件效率用!
long startTime=System.currentTimeMillis();
// 判断用户上传的文件是否为空,空则返回失败页面
if (upload.isEmpty()) {
return "failed";
}
// 获取文件存储路径(此行代码也是获取项目的相对路径)。
//如果是指定存储路径就直接写:String path="D:\Code\资料";
String path = request.getSession().getServletContext().getRealPath("/WEB-INF/file");
//获取用户上传的文件名(包括后缀)
String fileName = upload.getOriginalFilename();
// 创建文件实例
File filePath = new File(path, fileName);
// 如果本机存放文件的目录不存在,则创建目录
if (!filePath.getParentFile().exists()) {
filePath.getParentFile().mkdirs();
System.out.println("创建目录" + filePath);
}
// 写入文件(核心方法)
upload.transferTo(filePath);
//结束时间,测试上传文件效率用!
long endTime=System.currentTimeMillis();
System.out.println("上传的文件名:"+fileName);
System.out.println("上传文件共使用时间:"+(endTime-startTime));
return "success";
}
}
2.3 配置index.jsp页面
别忘了enctype="multipart/form-data
,并且我们需要将input
的type设置为file。
<form action="/file/fileUploadOne" method="post" enctype="multipart/form-data">
测试文件上传!
用户名:<input type="text" name="username"> <br>
文件: <input type="file" name="upload"> <br>
<input type="submit" value="上传">
</form>
注意:
测试过程:
第一次点击上传按钮:
第二次点击上传同样的文件:
我们通过String path = request.getSession().getServletContext().getRealPath("/WEB-INF/file")
;是获取到了Tomcat所在的位置,并不是这个项目的位置。这是由于我们在选择的时候是采用War的方式(具体见下图)
当我们选择war exploded的打包方式后,再次上传文件就可以看见:
可以看出最终文件储存的位置是这个项目的位置,其实也就是这个项目target的位置。
具体见: 徐刘根:Tomcat部署时war和war exploded区别以及平时踩得坑
3. 单服务器多文件上传
SpringMVC.xml配置文件中关于 CommonsMultipartResolver类的配置不变。
3.1 index.jsp中
<h2>单服务器多个文件上传!</h2>
<form action="/file/fileUploadTwo" method="post" enctype="multipart/form-data">
<p>选择文件:<input type="file" name="uploads"></p>
<p>选择文件:<input type="file" name="uploads"></p>
<p><input type="submit" value="提交"></p>
</form>
3.2 Controller类中
当上传多个文件的时候,MultipartFile形参是一个数组,通过循环遍历来创建文件然后保存到本地。
@RequestMapping("/fileUploadTwo")
public String uploadTwo(MultipartFile[] uploads,HttpServletRequest request) throws IOException {
// 获取文件存储的路径(此行代码也是获取项目的相对路径)
String path = request.getSession().getServletContext().getRealPath("/WEB-INF/file");
if (uploads!=null&&uploads.length>0){
for (MultipartFile upload:uploads){
String fileName = upload.getOriginalFilename();
File filePath = new File(path, fileName);
if (!filePath.getParentFile().exists()) {
filePath.getParentFile().mkdirs();
System.out.println("创建目录" + filePath);
}
upload.transferTo(filePath);
System.out.println("上传的文件名:"+fileName);
}
}
return "success";
}
测试结果:
点击提交按钮后,控制台输出:
二 文件下载
文件下载从某种意义上来说不算是SpringMVC的特有功能,在javaWeb中就可以实现。一共有两种方式,方式一是使用ResponseEntity,另一种就是使用javaWeb的方式。此处直接上代码吧。
1.1 index.jsp页面
<a href="/file/fileDownOne">测试文件下载:方式一</a>
<a href="/file/fileDownTwo">测试文件下载:方式二</a>
1.2 方式一
在方式一种,我们使用到了ResponseEntity
类,它位于org.springframework.http
包下。该类将响应头、文件数据(以字节存储)、状态封装在一起交给浏览器处理以实现浏览器的文件下载。
除此之外,我们还应该设置响应头header
的Content-Disposition
属性。该属性是作为对下载文件的一个标识字段,它有两种取值方式:
inline
:将文件内容直接显示在页面attachment
:弹出对话框让用户下载。
@RequestMapping("/fileDownOne")
public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException {
//设置用户能下载的文件路径。该路径为项目路径的upload目录下。
String path = request.getSession().getServletContext().getRealPath("/WEB-INF/file");
//用户下载的文件名字
String fileName = "123.jpg";
//设置编码 为了解决中文名称乱码问题
String downloadFileName = new String(filename.getBytes("UTF-8"), "iso-8859-1");
// 创建文件实例
File file = new File(path,downloadFileName);
//用流来处理文件
byte[] body = null;
InputStream is = new FileInputStream(file);
body = new byte[is.available()];
is.read(body);
HttpHeaders headers = new HttpHeaders();
//添加Content-Disposition属性信息
headers.add("Content-Disposition", "attchement;filename=" + file.getName());
HttpStatus statusCode = HttpStatus.OK;
//封装响应头、文件数据(以字节存储)、状态
ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(body, headers, statusCode);
return entity;
}
基于ResponseEntity的实现的局限性还是很大:
- 从代码中可以看出这种下载方式是一种一次性读取的下载方式,在文件较大的时候会直接抛出内存溢出。
- 无法统计在下载失败的情况已完成下载量,因此限制了对下载的功能扩展。
1.3 方式二
方式二就是单纯的基于JAVA来实现了,和SpringMVC已经没有太大的关系。
@RequestMapping(value="/fileDownTwo")
public String downloads(HttpServletResponse response , HttpServletRequest request) throws Exception{
//设置用户能下载的文件路径。该路径为项目路径的upload目录下。
String path = request.getSession().getServletContext().getRealPath("/WEB-INF/file");
//用户下载的文件名字
String fileName = "123.jpg";
// 创建文件实例
File file = new File(path,fileName);
//1、设置response 响应头,设置页面不缓存清空buffer
response.reset();
//设置response字符编码
response.setCharacterEncoding("UTF-8");
//设置response采用二进制传输数据
response.setContentType("multipart/form-data");
//设置响应头
response.setHeader("Content-Disposition", "attachment;fileName="+ URLEncoder.encode(fileName, "UTF-8"));
//2、 读取文件--输入流
InputStream input=new FileInputStream(file);
//3、 写出文件--输出流
OutputStream out = response.getOutputStream();
byte[] buff =new byte[1024];
int index=0;
//4、执行 写出操作
while((index= input.read(buff))!= -1){
out.write(buff, 0, index);
out.flush();
}
out.close();
input.close();
return null;
}
java通用实现在功能上比第一种实现更加丰富:
- 对下载的文件大小无限制。循环读取一定量的字节写入到输出流中,因此不会造成内存溢出。
- 因为是这种实现方式是基于循环写入的方式进行下载,在每次将字节块写入到输出流中的时都会进行输出流的合法性检测,在因为用户取消或者网络原因造成
Socket
断开的时候,系统会抛出SocketWriteException
,系统可以捕捉这个过程中抛出的异常,当捕捉到异常的时候我们可以记录当前已经传输的数据量,这样就可以完成下载状态和对应状态下载量和速度之类的数据记录。 - 这种方式实现方式还可以实现一种断点续载的功能。
文件存放的位置:
进入测试,页面内容:
点击超链接过后,弹出下载框,此时可以看见文件名字正确为:123.jpg。
第六章 拦截器
拦截器类似于Servlet中的过滤器Filter
,用于对处理器进行预处理和后处理。它采用的是AOP思想。只会拦截访问控制器方法,如果访问的是jsp,html,css,image
是不会拦截的。拦截器可以配置多个,当有多个拦截器时,运行流程图如下:
SpringMVC要使用拦截器比较简单直接定义一个类实现HandlerInterceptor
接口并实现该接口的方法即可。
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
default boolean preHandle()
:该方法将在请求处理之前进行调用。由于最先执行的都是Interceptor中的preHandle方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean类型的,当它返回为false 时,表示请求结束,后续的拦截器和Controller 都不会再执行;当返回值为true
时就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller 方法。default void postHandle()
:该方法会在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用。所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。只能是在当前所属的Interceptor 的preHandle 方法的返回值为true 时才能被调用。default void afterCompletion()
:该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行(即:将模型数据填充至视图中,也可以说是来到目标页面之后!)。这个方法的主要作用是用于进行资源清理工作的。该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。
以上三个方法的作用描述均来自:SpringMVC中使用Interceptor拦截器
一 简单测试案例
我们创建两个拦截器来验证拦截器方法的执行流程。
1. 创建两个拦截器
拦截器1:
public class MyInterceptorOne implements HandlerInterceptor {
public MyInterceptorOne() {
super();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptorOne中的预处理 preHandle方法执行了");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptorOne中的后处理 postHandle方法执行了");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptorOne中的最终处理 afterCompletion方法执行了");
}
}
拦截器2:
public class MyInterceptorTwo implements HandlerInterceptor {
public MyInterceptorTwo() {
super();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptorTwo中的预处理 preHandle方法执行了");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptorTwo中的后处理 postHandle方法执行了");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptorTwo中的最终处理 afterCompletion方法执行了");
}
}
2. 配置文件
在SpringMVC.xml文件中在<mvc:interceptors>
标签内部对拦截器进行配置,内部使用<mvc:interceptor>
标签对每个拦截器进行单独配置,我们常配置以下属性:
<mvc:mapping path>
:配置拦截器作用的路径,/**
表示拦截所有路径。<mvc:exclude-mapping path>
:配置不需要拦截器作用的路径。例如/admin
表示放行所有以/admin结尾的请求路径。<bean class>
:自定义拦截器的全限定类路径。表示对匹配路径的请求进行拦截。
<mvc:interceptors>
<!-- 拦截器1 -->
<mvc:interceptor>
<!-- 配置拦截器作用的路径 -->
<mvc:mapping path="/**"/>
<!-- 配置不需要拦截器作用的路径 -->
<mvc:exclude-mapping path="/admin"/>
<!-- 自定义拦截器的全限定类路径 -->
<bean class="com.itachi.pojo.MyInterceptorOne"/>
</mvc:interceptor>
<!-- 拦截器2 -->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.itachi.pojo.MyInterceptorTwo"/>
</mvc:interceptor>
<!-- 后面可以配置多个拦截器-->
</mvc:interceptors>
我们也可以不使用<mvc:interceptor>
标签,直接配置<bean class>
表示拦截所有请求。
<mvc:interceptors>
<!-- Interceptor将拦截所有请求-->
<bean class="com.itachi.AllInterceptor"/>
</mvc:interceptors>
3. index.jsp
<a href="/upstart/uphello">测试简单的拦截器</a>
测试结果:
点击超链接后,页面显示操作成功!;控制台显示:
二 拦截器的实际运用
拦截器其实也是一种AOP思想的具体体现,在实际项目中,一般有如下几个运用:
- 日志记录:记录请求信息的来源时间,对请求信息进行监视和统计。日志,以便进行信息监控、信息统计、计算PV(Page View)等。
- 权限检查:登录检测,进入页面之前检测是否是登录状态,如果没有直接返回到登录页面;
- 函数增强(削弱):对于用户传入的数值的合法性进行检测,对于大量的信息进行过滤传入,甚至对函数进行权限检查。也可以对函数的性能进行检测,在拦截器中记录该函数执行完毕后需要多少时间!
- 缓存行为:用户登录后,可以提前读取cookie中的信息,方便用户操作。
最常见的就是用户登录验证(其实就是笔者只会这一个0.0)。设计这样一种页面:用户开始的时候会在login.jsp
登录页面,登录成功之后会进入main
页面并保存。如果客户直接进入main.jsp
页面会检查是否已经登录,未登录的话就直接跳转到login.jsp
页面让客户先登录。
1.创建User实体类
public class Users {
private int id;
private String userName;
private String password;
//以下省略get、set、toString方法
}
2. 创建UserController 类
在Controller中用到了前面学到的POJO类的数据绑定,并且我们需要登录页面、登录操作页面、主页面和登出操作页面。
login登录页面
没有特殊的地方,该页面主要是为了和登录操作和登出操作配合使用。
登录操作
分两部分逻辑。首先我们通过数据绑定的方式获得了前端传来的数据,与数据库中的数据进行判断:
- 如果相同,就
重定向
到main主页面并且存储到session域中。存储到session域中的目的就是为了客户在未登出的情况下关闭页面,下次直接访问主页面的时候不会被提示要登录。 - 否则就是密码错误,还是回到login登录页面
main主页面
与登录操作配合使用,但是也引发了一系列问题,如果用户直接访问main主页面那么login登录页面就无意义了,所以该处需要拦截器进行拦截!
登出操作
用户登出后,应该要清楚Session域中的数据,并且 重定向
到login登录页面。
@Controller
@RequestMapping("/user")
public class UserController {
//登录界面
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String toLogin(){
System.out.println("toLogin()方法执行了!");
return "login";
}
//登录操作
@RequestMapping(value = "/login",method = RequestMethod.POST)
//通过pojo类的数据绑定,将页面用户输入的用户名和密码绑定到Users的属性上
public String login(Users user, Model model, HttpSession session){
String username=user.getUserName();
String password=user.getPassword();
//模拟与数据库中的数据做比较!
if(username!=null&&username.equals("Alice")&&password!=null&&password.equals("123")){
System.out.println("login()方法执行了!用户验证通过");
//如果正确就存储到session域中!并重定向到主页面.
session.setAttribute("USER_SESSION",user);
return "redirect:main";
}
System.out.println("login()方法执行了!用户验证错误");
//否则提示密码错误,并且依旧在登录页面
model.addAttribute("msg","用户名或密码错误,请重新登录!");
return "login";
}
//直接访问主页面
@RequestMapping(value = "/main")
public String toMain(){
System.out.println("toMain()方法执行了!");
return "main";
}
//登出操作,重定向到登录页面!
@RequestMapping(value = "/logout")
public String logout(HttpSession session) {
System.out.println("logout()方法执行了!");
//清空session域
session.invalidate();
return "redirect:login";
}
}
3. 创建UserInterceptor类
先前说过要防止用户直接访问main主页面,因此需要配置拦截器,定义preHandle()
方法即可。拦截器的逻辑判断分三部分:
- 获取请求的RUI后判断用户是否访问的是login登录页面,因为除了
login.jsp
是可以公开访问的,其他的URL都进行拦截控制。如果是就返回true放行,后续逻辑不再执行。所谓RUI,就是去除http:localhost:8080
后剩下的部分。(此处似乎可以在SpringMVC.xml中进行配置,通过配置<mvc:exclude-mapping path="/login"/>
达到同样效果) - 如果用户直接访问main主页面,那么就判断session域中是否有账户密码,如果有说明用户已登录,就返回true放行。
- 以上两种情况都不是,那么就转发到登录页面,让用户登录
public class UserInterceptor implements HandlerInterceptor {
public UserInterceptor() {
super();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求的RUi
String uri = request.getRequestURI();
System.out.println("preHandle()方法执行了----1!");
//通过判断时候有login,有返回位置数值,没有返回-1
if (uri.indexOf("/login") >= 0) {
System.out.println("preHandle()方法执行了----2!");
return true;
}
//获取session
HttpSession session = request.getSession();
Users user = (Users) session.getAttribute("USER_SESSION");
System.out.println("preHandle()方法执行了----3!");
//判断session中是否有用户数据,如果有,则返回true,继续向下执行
if (user != null) {
System.out.println("preHandle()方法执行了----4!");
return true;
}
//不符合条件的给出提示信息,并转发到登录页面
request.setAttribute("msg", "您还没有登录,请先登录!");
request.getRequestDispatcher("/WEB-INF/pages/login.jsp").forward(request, response);
System.out.println("preHandle()方法执行了----5!");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
4. jsp页面和配置
4.1 jsp页面
mian.jsp
中给用户一个登出按钮。
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false"%>
<html>
<head>
<title>模拟主页,登录过后才能到达此页!</title>
</head>
<body>
当前用户:${USER_SESSION.userName}
<a href="/user/logout">退出</a>
</body>
login.jsp
页面中我们需要设置isELIgnored="false"
从而开启EL表达式的支持。
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>用户登录</title>
</head>
<body>
${msg}
<form action="/user/login" method="post">
用户名:<input type="text" name="userName"><br>
密 码:
<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
4.2 配置文件
在springMVC.xml中配置拦截器,对所有请求都进行拦截。
<mvc:interceptors>
<!--登录拦截器-->
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.itachi.pojo.UserInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
点击测试,若直接访问登录页面,http://localhost:8080/user/login:
若直接访问主页面,http://localhost:8080/user/main.
登录成功的页面为:
不点退出,直接关闭页面,下次直接访问主页面的时候可以直接进入!
5. 其他细节(个人的疑惑)
5.1 拦截器方法的执行次数
第4点的登录案例,控制台会输出一些东西,我们来具体分析:
直接输入http://localhost:8080/user/login
,进行正常登录输入正确的用户名和密码,控制台输出如下:
可以看到System.out.println("preHandle()方法执行了----1!")
执行了两次!原因是在return “redirect:main”。
redirect:main
是属于重定向,URL从http://localhost:8080/user/login
变换成http://localhost:8080/user/main
,在个人看来其实是再次调用了Controller类中的toMain()
方法,不管是第几次调用Controller类中的方法都是拦截器中的方法先执行,所以执行了两次!
5.2 是否会对所有Controller有效?
笔者在测试的时候,输入了以下URL:http://localhost:8080/file/fileDownTwo
(即前文方式二文件下载路径),得到以下界面:
刚开始是以为对所有Controller都有效,后来将SpringMVC.xml中关于拦截器的配置更改了,得到如下结果:
<mvc:interceptors>
<!--登录拦截器-->
<mvc:interceptor>
<mvc:mapping path="/user/**"/>
<bean class="com.itachi.pojo.UserInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
输入了以下URL:http://localhost:8080/file/fileDownTwo后发现不会被拦截了!!所以拦截器不是对所有的Controller有效,通过设置mvc:mapping path的值来限制拦截器!
第七章 补充内容
一 @ControllerAdvice详解
@ControllerAdvice,标注了该注解的类用于处理全部Controller的请求。它主要有三个作用:全局异常处理、全局数据绑定、全局数据预处理!
@ControllerAdvice可以定义属性值,主要有以下四种定义方式:
项目 | Value |
---|---|
@ControllerAdvice(basePackages={"com.itachi.allcontroller"}) | 只对allcontroller包下的所有Controller类起效! |
@ControllerAdvice(basePackageClasses={AllDataController.class}) | 只对allcontroller包下的所有Controller类起效! |
@ControllerAdvice(assignableTypes={AllDataController.class,AllExController.class}) | 只对AllDataController和AllExController类起效! |
@ControllerAdvice(annotations=TestException.class) | 只对带有@TestException注解的Controller类有效! |
1. 全局数据绑定
在某些Controller类中可能存在请求共同数据的情况,可以将该数据提取出来进行全局处理,减少重复代码,在每一个 Controller 的接口中,就都能够访问导致这些数据!
1.1 全局数据绑定代码
@ControllerAdvice
注解与@ModelAttribute
配合使用,利用@ModelAttribute
的 name 属性给返回的对象一个唯一标识符作为 key 。
@ControllerAdvice
public class AllDataHandler {
@ModelAttribute(name = "dataOne")
public Map<String,Object> mydataOne() {
HashMap<String, Object> mapOne = new HashMap<>();
mapOne.put("name", "张三");
mapOne.put("gender", "男");
return mapOne;
}
@ModelAttribute(name = "dataTwo")
public Map<String,Object> mydataTwo(){
HashMap<String, Object> mapTwo = new HashMap<>();
mapTwo.put("name", "李四");
mapTwo.put("gender", "女");
return mapTwo;
}
}
在此例中,两个方法中分别定义了一个Map集合变量,变量名字分别为mapOne
和mapTwo
,并分别将这个map集合返回。@ModelAttribute
的name属性是指定返回的数据key值为dataOne
和dataTwo
。
简单理解为就是返回了一个Map集合,内部有两个元素分别为mapOne
和mapTwo
(也就是方法的返回值) 而这两个元素的key为@ModelAttribute
注解的name属性。
1.2 Controller方法
在Controller方法中通过model.asMap()
方法就可以取出上述的Map集合。
@RequestMapping("/all")
@Controller
public class AllDataController {
@RequestMapping("/allData")
public String hello(Model model){
Map<String, Object> map = model.asMap();
System.out.println(map);
return "success";
}
}
测试后控制台输出如下:
2. 全局数据预处理
在前文数据绑定的时候,Controller方法中的参数只有一个实体类变量,自然能够成功绑定。但是如果有参数为两个实体类变量,此时是否还能成功绑定?
创建实体类:
public class Student {
private String name;
private int age;
private String studentSex;
//省略get、set、toString方法
}
public class Teacher {
private String name;
private int age;
private String teacherSex;
//省略get、set、toString方法
}
创建Controller方法:
@RequestMapping("/all")
@Controller
public class AllDataController {
@RequestMapping("/allPred")
public String saveBook(Teacher teacher, Student student){
System.out.println("老师:"+teacher);
System.out.println("学生:"+student);
return "success";
}
}
index.jsp页面中:
<h1>测试POJO类型的数据绑定</h1>
<form action="/all/allPred" method="post">
教师名称:<input type="text" name="name" ><br/>
教师年龄:<input type="text" name="age" ><br/>
教师性别:<input type="text" name="teacherSex" ><br/>
<input type="submit" value=" 保存 ">
</form>
进入测试,控制台会输出:
可以看到由于Teacher类和Student类由于具有相同的属性(name和age),在进行数据绑定的时候,两个实体类中都绑定了相同的数据,这是相当不友好的事情。此时@ControllerAdvice
就起作用了。
2.1 创建AllPreHandler类
要实现准确绑定,除了@ControllerAdvice
注解以外还需要@InitBinder()
注解和WebDataBinder
对象。
@ControllerAdvice
public class AllPreHandler {
@InitBinder("t")
public void teacherBinder(WebDataBinder binder) {
binder.setFieldDefaultPrefix("teacher.");
}
@InitBinder("s")
public void studentBinder(WebDataBinder binder) {
binder.setFieldDefaultPrefix("student.");
}
}
@InitBinder("t")
:t 相当于一个标识符。与Controller类中的@ModelAttribute
注解配合使用。
binder.setFieldDefaultPrefix("teacher.")
:表示在表单中,请求参数名前必须要带一个teacher.
前缀。
2.2 Controller类中方法
@RequestMapping("/all")
@Controller
public class AllDataController {
@RequestMapping("/allPred")
public String saveBook(@ModelAttribute("t") Teacher teacher, @ModelAttribute("s") Student student){
System.out.println("老师:"+teacher);
System.out.println("学生:"+student);
return "success";
}
}
@ModelAttribute
注解中的值必须和AllPreHandler类中@InitBinder
注解中的值一样!
index.jsp页面中:
<h1>测试POJO类型的数据绑定</h1>
<form action="/all/allPred" method="post">
教师名称:<input type="text" name="teacher.name" ><br/>
教师年龄:<input type="text" name="teacher.age" ><br/>
教师性别:<input type="text" name="teacherSex" ><br/>
<input type="submit" value=" 保存 ">
</form>
表单中的name和age属性必须带有teacher.
前缀,该前缀就是来自于AllPreHandler类的binder.setFieldDefaultPrefix("teacher.")
测试结果:
成功!
二 异常处理
系统中异常包括两类:预期异常和运行时异常 RuntimeException
,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。系统的 dao、service、controller 出现都通过 throws Exception 向上抛出,最后由 SpringMVC 前端控制器交由异常处理器进行异常处理,如下图:
SpringMVC处理异常有三种方式:
- 使用Spring MVC提供的简单异常处理器
SimpleMappingExceptionResolver
; - 实现Spring的异常处理接口
HandlerExceptionResolver
自定义自己的异常处理器; - 使用
@ExceptionHandler
注解实现异常处理;
1. 入门案例(用@ExceptionHandler)
1.1创建Controller类
在Controller中分为了两个方法。方法1是正常的URL访问逻辑,但是内部有10 / i
运算,当用户传入i=0
时就会引发异常。
方法2就是对异常的判断。标注了@ExceptionHandler
注解,标明该方法是用于处理异常的。当发生异常后会将页面跳转到指定错误页面。在此例中handleException()
方法就是专门处理ArithmeticException
异常的!
@Controller
@RequestMapping("/error")
public class ErrorController {
@RequestMapping("/testOne")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i) {
System.out.println("10/" + i + "=" + (10 / i));
//如果没有异常就跳转到success页面
return "success";
}
@ExceptionHandler(value={java.lang.ArithmeticException.class})
public ModelAndView handleException(Exception ex){
String error="Controller类中的ArithmeticException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
//出现异常就跳转到error页面!
return modelAndView;
}
}
@ExceptionHandler
注解:属性为value
,它的值是某个异常类的class,被标注的方法就表示专门处理这个异常,如果发生了其他异常则不会被处理。我们可以自定义异常将value
值设为自定义异常的class。
1.2 jsp页面
index.jsp:
<a href="/error/testOne?i=0">testExceptionHandlerExceptionResolver</a>
其中i=0是专门用于测试的错误数据!
success.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>成功页面!</title>
</head>
<body>
操作成功!
</body>
</html>
my_error.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>发生异常!</title>
</head>
<body>
${error}
</body>
</html>
测试结果:
2. 入门案例的深入分析
我们知道ArithmeticException
继承于RuntimeException
,而RuntimeException
又继承于Exception
,如果一个同一个controller类中同时定义了这三个异常,会是哪个方法运行呢?
@Controller
@RequestMapping("/error")
public class ErrorController {
@RequestMapping("/testOne")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i) {
System.out.println("10/" + i + "=" + (10 / i));
return "success";
}
@ExceptionHandler(value={java.lang.ArithmeticException.class})
public ModelAndView handleException(Exception ex){
String error="Controller类中的ArithmeticException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
@ExceptionHandler(value={java.lang.RuntimeException.class})
public ModelAndView handleException2(Exception ex){
String error="Controller类中的RuntimeException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
@ExceptionHandler(value={java.lang.Exception.class})
public ModelAndView handleException3(Exception ex){
String error="Controller类中的Exception异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
}
测试结果:
可以看见还是ArithmeticException
异常对应的方法执行了,这就是精确匹配原则!
3.全局异常处理(@ControllerAdvice 注解的使用)
如果每个Controller类方法中都要定义异常,代码会变得臃肿。是否能够抽取处理创建一个类来专门来处理相同异常呢?
3.1 异常类代码
在异常类上标注@ControllerAdvice
注解,标明该类为全局异常处理类。在该类的每个方法上标注@ExceptionHandler
注解,标明该方法会处理发生的相关异常
@ControllerAdvice
public class HandleException {
@ExceptionHandler(value={java.lang.ArithmeticException.class})
public ModelAndView allException(Exception ex){
String error="AllHandleException类中的ArithmeticException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
}
}
在此例中,@ExceptionHandler(value={java.lang.ArithmeticException.class})
标明在任何一个Controller方法中发生了ArithmeticException
异常都会被该方法处理。
页面代码不变,进入测试:
测试成功!
3.2 全局异常处理细节分析
当Controller类中也有处理ArithmeticException异常的方法时,哪个方法会先执行呢?
@Controller
@RequestMapping("/error")
public class ErrorController {
@RequestMapping("/testOne")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i) {
System.out.println("10/" + i + "=" + (10 / i));
return "success";
}
@ExceptionHandler(value={java.lang.ArithmeticException.class})
public ModelAndView handleException(Exception ex){
String error="Controller类中的ArithmeticException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
}
@ControllerAdvice
public class AllHandleException {
@ExceptionHandler(value={java.lang.ArithmeticException.class})
public ModelAndView allException(Exception ex){
String error="AllHandleException类中的ArithmeticException异常出现啦"+ex.toString();
System.out.println(error);
ModelAndView modelAndView=new ModelAndView("my_error");
modelAndView.addObject("error",error);
return modelAndView;
}
}
jsp页面不变,进入测试:
可以看见是Controller类中的异常处理方法执行了!这就是就近原则!
三 视图和视图解析器
1 视图
在我们的入门案例中就是使用的InternalResourceView就是一个视图,它是专门用来处理JSP资源的,视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。SpringMVC提供了很多种视图类用于解析不同的资源,包括:AbstractPdfView(用于解析PDF资源)、AbstractExcelView(用于解析EXCEL资源)等等。不管是何种视图类都直接或间接的实现了接口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";
//重要方法:返回一个字符串,标明给用户什么类型的文件响应,可以是HTML,json,PDF等
@Nullable
default String getContentType() {
return null;
}
//用于渲染视图的方法!
void render(@Nullable Map<String, ?> var1, HttpServletRequest var2, HttpServletResponse var3) throws Exception;
}
2 视图解析器
在入门案例中我们进行了如下配置:
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!—3.3.1前缀(目录) -->
<property name="prefix" value="/WEB-INF/pages/"/>
<!—3.3.2后缀 -->
<property name="suffix" value=".jsp"/>
</bean>
InternalResourceViewResolver就是一个视图解析器。视图解析器的作用主要是负责示例化视图对象,针对不同的视图对象,我们使用不同的视图解析器来完成实例化工作,不管是什么视图解析器都都是直接实现或者间接实现了ViewResolver接口!
public interface ViewResolver {
//就是根据名称解析出视图View对象并返回!
@Nullable
View resolveViewName(String var1, Locale var2) throws Exception;
}
除此之外所有的视图解析器还应该实现Order接口并设置Order属性,通过该属性来设置优先级,order值越低,优先级越高!
3. 自定义视图和视图解析器
在大部分情况下,我们是不需要自定义视图和解析器的。但是在某些情况下,SpringMVC需要展示我们的特使视图格式的时候就需要自定义了,自定义的视图需要实现View接口(或者继承了它的抽象接口),自定义的视图解析器需要实现ViewResolver接口(或者继承了它的抽象接口)。
3.1 MyView
public class MyView implements View {
@Override
public String getContentType() {
return "text.html";
}
@Override
public void render(Map<String, ?> model, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
System.out.println("Controller方法中model.addAttribute()方法保存的数据"+model);
httpServletResponse.setContentType("test.html;charset=UTF-8");
//自定义视图对象来设置自定义渲染
httpServletResponse.getWriter().write("<h1>please waiting!请等待!</h1>");
}
}
3.2 MyViewResolver
public class MyViewResolver implements ViewResolver, Ordered {
//设置优先级的属性order
private Integer order;
//视图解析器该方法返回我们自定义视图
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return new MyView();
}
@Override
public int getOrder() {
return order;
}
public void setOrder(Integer order) {
this.order = order;
}
}
3.3 XML配置文件
<bean class="com.itachi.pojo.MyViewResolver">
<property name="order" value="100"></property>
</bean>
测试结果: