SpringMVC实现文件的上传和下载 相对于JavaWeb阶段我们使用过servlet实现文件上传和下载操作;而SpringMVC实现了对上传操作的直接支持,提供了multipart
解析器。MultipartFile
提供了一些对文件操作的方法,使得文件上传变得更简单。无论上传还是下载都是进行二进制流的转换,下面我们以案例的形式了解一下如何使用SpringMVC实现文件的上传操作。
<!--more-->
文件上传
准备
了解
- 文件上传我们首先要考虑的就是把文件上传到哪里?是上传到工程目录下,还是上传到本地磁盘中?
- 因为上传的文件一般都是二进制文件,所以我们需要通过某种方式对表单提交进行编码。通过将
enctype
设置为multipart/form-data
,每个输入域都将作为POST请求的不同部分进行提交(默认提交的表单中数据存储格式是名字-值
,显然是不适合类似文件上传那种二进制数据的)。 - Spring提供了对
multipart
数据的解析器CommonsMultipartResolver
(MultipartResolver
接口的实现类),但是这个解析器是基于Apache Commons FileUpload
技术的,所以需要commons-filrUpload.jar
支持。
配置
除了我们之前使用的Spring以及SpringMVC的先关jar依赖包,还需要导入以下jar文件:
commons-fileupload.jar
commons-io.jar
实例
首先我们看一下项目结构:
注意:这里我使用的配置是:IDEA + tomcat + Maven,对于maven项目我们要清楚项目编译后的文件都放在target
目录下。
- 修改我们的
save.jsp
<%@ page isELIgnored="false" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path;
%>
<html>
<head>
<title>Title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h2>表单</h2>
<form action="<%=basePath%>/user/save" method="post" enctype="multipart/form-data">
username: <input type="text" name="username"/><br/>
password: <input type="password" name="password"/><br/>
profile image:<input type="file" name="image"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
解释: 注意在上面<%%>
中的Java代码是获取本项目的相对路径,相当于<%=pageContext.request.ContextPath%>
。接下来我们将表单<form>
标签中设置属性enctype="multipart/form-data"
。
-
Controller
层
//保存用户
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(@RequestParam String username, @RequestParam String password, @RequestParam(value="image",required = false)MultipartFile image, HttpServletRequest request, Model model) {
//获取文件在服务器上的储存位置
String path = request.getSession().getServletContext().getRealPath("resources/upload");
File filePath = new File(path);
System.out.println("文件保存路径:" + path);
if (!filePath.exists() && !filePath.isDirectory()) {
System.out.println("目录不存在,创建目录:" + filePath);
filePath.mkdir();
}
//获取原始文件名称
String originalFileName = image.getOriginalFilename();
System.out.println("原始文件名称:" + originalFileName);
//获取文件类型,以最后一个`.`作为标识
String type = originalFileName.substring(originalFileName.lastIndexOf(".") + 1);
System.out.println("文件类型:" + type);
//设置文件新名字
String fileName = System.currentTimeMillis() + "." + type;
System.out.println("文件新名称:" + fileName);
//在指定路径创建一个文件
File targetFile = new File(path, fileName);
//将文件保存到服务器指定位置
try {
image.transferTo(targetFile);
model.addAttribute("message", "保存数据成功");
userService.save(username,password,"resources/upload/" + fileName);
return "view/success";
} catch (IOException e) {
System.out.println("保存文件错误...");
e.printStackTrace();
}
return "view/error";
}
Controller层就比较复杂了,因为对于本案例而言文件上传操作的主要代码都在Controller层,而Service和Dao层仅是执行保存操作。我们首先了解一下文件操作的API方法:
- request.getSession().getServletContext().getRealPath(String s):获取本项目下指定目录的绝对路径。
- MultipartFile.getOriginalFilename():获取被
MultipartFile
绑定的上传参数的原始名称- System.currentTimeMillis():获取自1970年1月1日0时起到现在时间的毫秒数。
- MultipartFile.transferTo(): 在指定的磁盘路径下生成一个新的文件。
- jsp中的form表单将数据提交到这个映射方法,那么我们就要接受jsp中传递的参数,要注意的就是绑定
image
参数是MultipartFile
数据类型。 - 下面就要定义一个上传文件的保存目录,本例中使用
request.getSession().getServletContext().getRealPath()
获取到的是本项目的绝对路径,而getRealPath("resources/upload")
是针对此项目的相对路径,如此时的文件保存路径其实是:/Users/ty/Documents/Java/Learn/ssm/target/TyCoding/resources/upload
。 - 如果指定路径下的文件夹存在,就将上传的文件写入进这个文件夹中,如果此文件夹不存在,就调用
mkdir()
方法创建此文件夹。 - 上述一切都准备就绪,最后会调用Service层的
save()
方法将表单数据保存到数据库中。(**注意:**在调用的save()
方法中我们会看到实际写入进数据库中的上传文件的路径其实仅是相对此项目的相对路径(即相对于webapp
)。
- 最后我们要在Spring配置文件中注册这个multipart解析器:
<!-- 配置支持文件上传 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:maxUploadSize="500000"/>
maxUploadSize
是设置上传文件的大小,可以更改。
下面我们看一下实际效果:
点击提交按钮,访问后台映射方法:
可以看到已经获取到表中的数据,并准备调用Service层保存数据,那么我们看一下最后的Dao层将数据存入进数据库的方法:
public class UserDaoImp extends JdbcDaoSupport implements UserDao {
//保存数据
public void save(String username, String password, String image) {
//使用Spring提供的JDBC模板可以直接执行SQL语句
this.getJdbcTemplate().update("insert into user(id,username,password,image) values(?,?,?,?)",null,username,password,image);
}
}
最后数据保存成功,跳转到成功页面:
看一下此文件在工程中的位置:
看一下储存到数据库中数据:
- 最后我们看一下通过SQL语句:
select * from user
,将查询到的数据回显到页面上:list.jsp
<table border="1">
<tr>
<td>id</td>
<td>username</td>
<td>password</td>
<td>avatar</td>
</tr>
<c:forEach items="${userList}" var="user">
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.password}</td>
<td><img src="<%=basePath%>/${user.image}" width="100" height="80"></td>
</tr>
</c:forEach>
</table>
答疑:
- 我们设置的
getRealPath("resources/upload")
不该是相对于工程的路径吗?(也就是在webapp
下)。为什么上传的文件会保存到target
目录下? 答:我们要明白Tomcat下运行的项目实际是编译后的class文件,即此maven项目中的target
文件夹中的数据。那么我们设置的文件上传路径其实是在此编译后的文件夹中的相对路径(TyCoding
是我设置的artifact id
) - 为什么我们保存到数据中的路径是项目的相对路径? 答:要知道在Tomcat下运行项目,我们访问的图片路径是需要在
localhost:8080
下的路径,也就是当前项目的相对路径,所以我们如果保存一个带盘符的绝对路径是肯定不能访问到的。
文件下载
准备
了解
文件上传我们这里设计的思路是:在页面设置一个可点击的连接(比如<a>
),点击即可下载,而我们需要在其href
属性中拼接要下载的文件的名称,然后通过这个请求路径,Controller层的映射方法接收到你要下载的文件名称,然后根据指定的下载文件的路径查询到这个文件的名称,然后将文件转换成二进制流,然后让客户端读取这个二进制流写入到本机中从而实现下载。
-
ResponseEntity<byte[]>
: SpringMVC提供的用于实现响应头、文件数据(以字节储存)、状态封装都一起返回给浏览器实现文件的下载。 -
header.setContentDispositionFormData()
: 告诉浏览器要以指定的数据类型打开这个文件 -
FileUtils.readFileToByteArray()
: 使用FileUtils工具类强制将指定文件数据转换成byte字节流的形式。
实例
- 首先我们更改上面的
save.jsp
页面,新增一个下载的连接:
<h2>文件下载</h2>
<a href="<%=basePath%>/user/download?fileName=图片.jpg">点击我下载图片</a>
如上所示,我们在请求路径中拼接了fileName
参数值是图片.jpg
那么后台接收到这个参数值,就会查询指定位置的文件。
- 然后看一下Controller的请求
//文件下载
@RequestMapping("/download")
public ResponseEntity<byte[]> download(HttpServletRequest request, @RequestParam(value="fileName",required = false) String fileName) throws Exception {
try{
//下载路径
String path = request.getServletContext().getRealPath("/resources/upload/");
File file = new File(path + File.separator + fileName);
HttpHeaders headers = new HttpHeaders();
//解决文件名中文乱码问题
String downloadFileName = new String(fileName.getBytes("UTF-8"),"iso-8859-1");
//告诉浏览器以"attachment"方式打开文件
headers.setContentDispositionFormData("attachment",downloadFileName);
//设置请求头的媒体格式类型为 application/octet-stream(二进制流数据)
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),headers,HttpStatus.CREATED);
} catch(Exception e){
e.printStackTrace();
System.out.println("文件下载出错...");
return null;
}
}
首先要指定映射方法的返回数据类型是ResponseEntity<byte[]>
,首先要规定一个下载文件的目录路径,需要下载文件时,就会中这个路径中查询需要下载的文件,如果找不到指定文件,那么就进catch里面。 File.separator
用来分隔同一个路径字符串中的目录,相当于/
。找到了指定路径下的文件后,需要解决中文乱码问题,然后告诉浏览器要以attachment
的方式打开这个文件。 最后这只ContentType
媒体格式类型,最后使用FileUtils
的readFileToByteArray
将文件数据转换成二进制字节流,连同设置好的响应数据格式一同返回给浏览器,实现文件的下载。
最后看一下实际的效果: