1. 文件上传与下载的定义
- 数据上传是指客户端向服务器上传数据,客户端向服务器发送的所有请求都属于数据上传。文件上传是数据上传的一种特例,指客户端向服务器上传文件。即将保存在客户端的文件上传至服务器中的一个副本,保存到服务器中。
- 文件下载指的是文件从服务器下载到浏览器后,浏览器不直接解析,而是以附件的形式保存到客户端。
2. 以表单的方式实现文件上传
上传表单的要求:
- method属性值必须是post,不能是get (因为post对请求上传的文件大小原理上没有要求)
- 要为表单指定enctype属性值为mutipart/form-data (enctype即encoding type,编码类型)
<form action="/myWeb/one" method="post" enctype="multipart/form-data">
姓名:<input type="text" name="name"><br/>
年龄:<input type="text" name="age"><br/>
照片:<input type="file" name="photo"><br/>
<input type="submit" value="注册">
</form>
服务端需要做的事情:
- 判断是否是multipart请求
- 创建一个FileItem工厂
- 创建文件上传核心组件
- 解析请求,获取到所有的item
- 遍历获取到的item,如果只是普通的表单项则直接获取即可,若是文件表单项,则先获取文件上传的原始名称,获取输入流,同时指定要存储路径,然后创建目标文件用于保存上传
- 根据目标文件创建文件输出流,将输入流中的数据写入到输出流中
public class OneServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if (!ServletFileUpload.isMultipartContent(request)){ // 1. 判断是否为multipart请求
throw new RuntimeException("当前请求不支持文件上传");
}
try {
// 创建一个FileItem工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置使用临时文件的边界值,大于该值,上传文件会先保存在临时文件中,否则,上传文件将直接写入到内存中
// 单位:字节
factory.setSizeThreshold(1024*1024*1); // 设置边界值为1M
// 更改临时文件存放位置
String tempPath = this.getServletContext().getRealPath("/temp");
File temp = new File(tempPath);
factory.setRepository(temp);
// 创建文件上传核心组件
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置每一个item的头部字符编码,其可以防止文件名中文乱码问题
upload.setHeaderEncoding("utf-8");
// 设置单个文件上传的最大边界值为2M
upload.setFileSizeMax(1024 * 1024 * 2);
// 设置一次上传所有文件的最大边界值为5M
upload.setSizeMax(1024 * 1024 * 5);
// 解析请求,获取到所有的item
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items){
if (item.isFormField()){ // 若item为普通表单项
String fieldName = item.getFieldName(); // 获取表单名称
String fieldValue = item.getString("utf-8"); // 获取表单项的值,并指定编码格式
System.out.println(fieldName + " = " + fieldValue);
}else{ // 若item为文件表单项
String fileName = item.getName(); // 获取文件上传的原始名称
fileName = System.currentTimeMillis() + fileName; // 防止文件名重复
InputStream is = item.getInputStream(); // 获取输入流,其中有上传文件的内容
String path = this.getServletContext().getRealPath("/images"); // 获取文件保存在服务器的路径
/**
* 避免所有资源放同一文件夹,因为在linux系统中一个文件夹中的文件数量是有限制的
*/
// 获取当前系统时间
Calendar now = Calendar.getInstance();
// 获取年月日
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH) + 1;
int day = now.get(Calendar.DAY_OF_MONTH);
path = path + "/" + year + "/" + month + "/" + day;
File dirFile = new File(path);
if (!dirFile.exists()){ // 如果该目录不存在则创建该目录
dirFile.mkdirs();
}
// System.out.println(path);
File descFile = new File(path, fileName); // 创建目标文件,将来用于保存上传
OutputStream os = new FileOutputStream(descFile); // 创建文件输出流
// 将输入流中的数据写入到输出流中
int len = -1;
byte [] buf = new byte[1024];
while ((len = is.read(buf)) != -1){
os.write(buf,0, len);
}
// 关闭流
os.close();
is.close();
item.delete(); // 删除临时文件
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
}
部分代码分析:
- 设置在什么情况下上传的文件会暂时保存在内存,什么情况下会保存在临时文件中。(最好指定一下,因为临时文件是存放在磁盘中,可以为服务器减少点压力)
// 设置使用临时文件的边界值,大于该值,上传文件会先保存在临时文件中,否则,上传文件将直接写入到内存中
// 单位:字节
factory.setSizeThreshold(1024*1024*1); // 设置边界值为1M
- 更改临时文件的存放位置,易于管理和掌控。
// 更改临时文件存放位置
String tempPath = this.getServletContext().getRealPath("/temp");
File temp = new File(tempPath);
factory.setRepository(temp);
- 删除临时文件(没有下面这行代码的话,它在上传完成之后也不会自动删除,占用服务器磁盘空间)
item.delete(); // 删除临时文件
- 设置item头部字符编码,防止文件名中文乱码问题。
upload.setHeaderEncoding("utf-8");
- 获取普通表单项的值时指定编码,防止中文乱码
String fieldValue = item.getString("utf-8");
- 设置上传文件的路径
/**
* 避免所有资源放同一文件夹,因为在linux系统中一个文件夹中的文件数量是有限制的
*/
// 获取当前系统时间
Calendar now = Calendar.getInstance();
// 获取年月日
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH) + 1;
int day = now.get(Calendar.DAY_OF_MONTH);
path = path + "/" + year + "/" + month + "/" + day;
File dirFile = new File(path);
if (!dirFile.exists()){ // 如果该目录不存在则创建该目录
dirFile.mkdirs();
}
这段代码就很有讲究了,它会根据不同天去创建文件夹,然后存放该天上传的所有文件。
7. 设置存放到服务器的文件名(即目标文件名)fileName = System.currentTimeMillis() + fileName;
为了防止用户上传两次1.png,导致前一次上传的1.png被覆盖的情况,这里对目标文件名在原文件名的基础上加上了当前毫秒数,避免因文件名重复而出现覆盖的情况。
需要用到的jar包
commons-fileupload-xxxx.jar
-
commons-io-xxx.jar
第二个的版本需要根据第一个的版本作选择,具体怎么选看文档即可。
3. 以超链接的方式实现文件下载
简单形式:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/images/1.png">1.png</a>
</body>
</html>
直接让超链接指向该文件即可。但是这样会有一个缺点就是,对于浏览器能解析的资源浏览器会直接解析显示,然后需要用户进一步点击另存为保存下载,对于浏览器解析不了的,才会以附件的形式出现。即资源文件的出现形式由浏览器解析能力决定,不由服务器决定。
于是我们进行改进,首先让超链接指向一个servlet
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/two">1.png</a>
</body>
</html>
然后在该servlet中进行返回资源的操作
public class TwoServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String fileName = "陌言.png";
// 打散:按当前字符编码进行打散
byte[] bytes = fileName.getBytes("utf-8");
// 组装:按目标字符编码进行组装
fileName = new String(bytes, "ISO8859-1");
// 修改响应的头部属性content-disposition值为attachment
response.setHeader("content-disposition", "attachment;filename=" + fileName);
// 获取连接服务器端资源文件的输入流
InputStream is = this.getServletContext().getResourceAsStream("/images/1.png");
// 获取输出流
ServletOutputStream os = response.getOutputStream();
// 将输入流中数据写入到输出流
int len = -1;
byte [] buf = new byte[1024];
while ((len = is.read(buf)) != -1){
os.write(buf, 0, len);
}
// 关闭流
os.close();
is.close();
}
}