文件上传与下载
1. 文件上传
为了能上传文件,必须将表单的 method
设置为 POST
,并将 enctype
设置为 multipart/form-data
。
有两种实现文件上传的方式:
-
底层使用 Apache Commons FileUpload 包
-
底层使用 Servlet 3.1 内置的文件上传功能
无论是哪种方式,其使用方式都是一样的,将 file 类型的请求参数绑定到请求处理方法的特定类型的参数上:
-
CommonsMultipartFile(commons-fileupload)
-
MultipartFile(Servlet 3.1)
Web 3.0 的文件上传
普通的表单(form)元素无法直接上传文件,必须通过「特殊处理」。
对上传文件功能而言,有些特殊的地方:
- form 表单内,要添加控件
<input type="file" name="myfile">
- form 表单的提交方式必须是 post 方式
- form 表单的内容格式要定义成 multipart/form-data 格式
<form action="..." method="post" enctype="multipart/form-data">...
</form>
enctype="multipart/form-data"
表示表单元素中的 input 数据以二进制的方式发送到服务端。此时如果是普通的 input 数据,无法像之前一样从 request 中直接获得。
对于上传文件的的大小问题,惯例是:
- 足够小的文件,先接收到内存中,最后写入磁盘。
- 稍大的文件,写入磁盘临时文件,最后写入最终目的地。
- 大型文件,禁止上传。
在 Web 3.0 之前 使用 commons-fileupload 库是最常见的上传办法。当 Servlet 的设计者意识到文件上传的重要性后,在 Web 3.0 中它就成了一项内置的特性。
Web 3.0 中的文件上传主要围绕着 MultipartConfig 注解和 Part 接口。
@MultipartConfig 注解
属性 | 说明 |
---|---|
fileSizeThreshold 可选属性 | 超过该值大小的文件,在上传过程中,将被写入磁盘临时文件,而不是保存在内存中。 |
maxFileSize 可选属性 | 每个上传文件的大小上限。 |
maxRequestSize 可选属性 | 一次请求(可能包含多个上传)的大小上限。 |
@WebServlet(urlPatterns="/hello.do")
@MultipartConfig(maxFileSize = 5*1024*1024)
public class HelloServlet extends HttpServlet {...
}
Part 接口
在一个表单(Form)中,无论是否有文件上传控件,Servlet 3.0 都会将这些表单控件对应成代码中的一个 Part 对象。
通过 request 对象的 .getParts()
方法可以获得所有的这些 Part 对象。
Collection<Part> parts = request.getParts();
在一个或多个部分组成的请求中,每一个表单域(包括非文本域),都将被转换成一个 Part 。
普通文本域和文件上传域的区别在于,其 Part 对象的 .getContentType()
方法返回值的不同。对于普通文本域的 Part 对象而言,该方法返回 null 。
for (Part part : parts) {if (part.getContentType() == null) {System.out.println("普通文本域");}else {System.out.println("文件上传域");}
}
补充,如果是要获取普通文本域的值,其实直接使用正常 request.getParameter()
就行。
每一个 Part 分为「头」和「体」两部分。普通文本域只有头部,而文件上传域则有头有体。
普通文本域的头部形式为:
content-disposition:form-data; name="域名"
上传文本域的头部形式为:
content-type:内容类型
content-disposition:form-data; name="域名"; filename="文件名"
对我们而言,需要的是文本上传域中的 content-disposition 中的 filename 部分。
String header = part.getHeader("content-disposition");
// 内容为 form-data; name="域名"; filename="文件名"
通常会使用工具类,将 content-disposition 中的 filename 中的值截取出来。
private String getFileName(String header) {String[] arr = header.split("; ");String item = null;for (String cur : arr) {// System.out.println("debug: " + cur);if (cur.startsWith("filename=")) {item = cur;break;}}int start = item.indexOf('"')+1;int end = item.lastIndexOf('"');String filename = item.substring(start, end);// System.out.println(filename);return filename;
}
Part 对象直接提供了方法将上传文件的内容写入盘:
String savePath = request.getServletContext().getRealPath("/WEB-INF/uploadFile/");
String filePathName = savePath + File.separator + fileName; // 目标文件路径名
part.write(filePathName); // 把文件写到指定路径
Part 的其它常用方法
方法 | 说明 |
---|---|
getContentType() | 获得 Part 的内容类型。如果 Part 是普通文本,那么返回 null 。 该方法可以用于区别是普通文本域,还是文件上传域。 |
getHeader() | 该方法用于获取指定的标头的值。 对于上传文本域的 Part,该参数有 content-type 和 content-disposition 对于普通文本域,则只有 content-disposition 一种。 |
getName() | 无论是普通文本域 Part ,还是文件上传域 Part ,都是获得域名值。 |
write() | 将上传文件写入磁盘中。 |
delete() | 手动删除临时文件 |
getInputStream() | 以 InputStream 形式返回上传文件的内容。 |
利用 commons-fileupload 文件上传
利用 commons-fileupload 文件上传需要利用引入 commons-fileupload 包(它依赖于 commons-io 包)
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
作为 Servlet 内置上传功能之前的『准标准』,Servlet 在引入内置上传功能时借鉴了 commons-fileupload 的实现方式。因此,在了解 Servlet 内置上传功能之后,再回头看 commons-fileupload 文件上传时,你会发现它们的基本逻辑/大道理时一样的,只不过 commons-fileupload 的实现会罗嗦一些
在 Servlet 内置的上传功能中,从 request 中获得的是一个 Part[]
,其中的每一个 Part 对象对应着表单中的一个表单域(Form Field)。而 commons-fileupload 中我们从 request 中获得的是一个 List<FileItem>
,commons-fileupload 中使用 FileItem 来对应每一个表单域,起到和 Part 一样的作用。
commons-fileupload 的罗嗦体现在以下几个方面:
- commons-fileupload 不能对 Servlet 使用注解,因此相关的上传配置需要通过编码实现。
- commons-fileupload 不能使用
request.getParameter()
为了能够从 request 中获得 List<FileItem>
,你需要两个对象:
// 创建上传所需要的两个对象
DiskFileItemFactory factory = new DiskFileItemFactory(); // 磁盘文件对象
ServletFileUpload sfu = new ServletFileUpload(factory); // 文件上传对象
如果不做出设置,那么相关设置则采用默认值。
// 设置上传过程中所收到的数据是『存内存』还是『存磁盘』的阈值
factory.setSizeThreshold(100 * 1024);
// 设置磁盘临时文件的保存目录
factory.setRepository(new File("D:/upload"));// 设置解析文件上传中的文件名的编码格式,解决上传文件名中文乱码问题
sfu.setHeaderEncoding("utf-8");
// 限制单个文件的大小
sfu.setFileSizeMax(10*1024);
// 限制上传的总文件大小
sfu.setSizeMax(100*1024);
在创建文件上传对象(并作出相应设置)之后,我们可以通过它从 request 中获取我们所需要的 List<FileItem>
。
List<FileItem> list = sfu.parseRequest(request);
FileItem 自带了方法,可以判断当前的 FileItem 对应的是页面上的普通文本域,还是文件上传域:
for (FileItem item : list) {if (item.isFormField()) {System.out.println("普通文本域");}else { System.out.println("文件上传域");}
}
由于 commons-fileupload 中无法使用 request.getParameter()
,因此,为了获得普通文本域中的数据,需要使用 FileItem 自己的方法:
for (FileItem item : list) {if (item.isFormField()) {String fieldName = item.getFieldName(); // 例如:username / passwordString fieldValue = item.getString("UTF-8"); // 例如,tom / 123456System.out.println(fieldName + ": " + fieldValue);}else { System.out.println("文件上传域");}
}
由于 commons-fileupload 引用了 commons-io,所以,将上传的文件内容写入磁盘倒是十分简单:
for (FileItem item : list) {if (item.isFormField()) {...}else { System.out.println("文件上传域");// 创建输出文件String name = item.getName();// 获取上传文件的名字String outPath = "D:/upload/" + name;FileOutputStream output = new FileOutputStream(new File(outPath));// 获得上传文件字节流InputStream input = item.getInputStream(); // 使用 IOUtils 工具将输入流中的数据写入到输出流。IOUtils.copy(input, output);System.out.println("上传成功!保存的路径为:" + outPath);input.close(); // 关闭输入流output.close(); // 关闭输出流item.delete(); // 删除处理文件上传时生成的临时文件}
}
2. 文件下载
内容类型 | 文件扩展名 | 描述 |
---|---|---|
text/plain | txt | 文本文件(包括但不仅包括txt) |
application/msword | doc | Microsoft Word |
application/pdf | Adobe Acrobat | |
application/zip | zip | winzip |
audio/mpeg | mp3 | mp3 音频文件 |
image/gif | gif | COMPUSERVE GIF 图像 |
image/jpeg | jpeg jpg | JPEG 图像 |
image/png | png | PNG 图像 |
详细 MIME 参见 网址 。
相对于上传而言,下载文件较为简单,只需要完成两步:
- 设置响应的内容类型。
- 添加一个
content-disposition
到响应标头(addHeader()
方法),其值为:attachment; filename=文件名
- 通过 resp 对象获得输出流,将文件内容发送至客户端。
resp.setContentType("text/plain"); // step 1
resp.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode("D:/note.txt", "UTF-8")); // step 2InputStream is = new FileInputStream(new File("D:/note.txt"));
OutputStream out = resp.getOutputStream();byte[] buffer = new byte[1024];
int n = 0;
while ((n = is.read(buffer)) > 0) {out.write(buffer, 0, n); // step 3
}is.close();
out.close();
System.out.println("下载成功");