文件上传

前端部分

首先我们写一个文件上传的表单页面:

1
2
3
4
5
<form method="post" enctype="multipart/form-data" action="Upload">
<input type="file" name="photo"> <br> <!-- 这里要注意一定要写 name 属性,否则数据不会提交到 Post 里面 -->
用户名<input type="text" name="username"><br>
<input type="submit" name="submit">
</form>

当我们使用表单进行传输数据的时候,需要设定属性 enctype="multipart/form-data" 这是因为对于文件来说,不能像 XXX=XXX&XXX=XXX 这样的 Post 方式传送信息,所以需要使用 multipart 多块传送,将每一组信息都放到一个数据块里面提交。

这个表单有着血泪史,一定要记得在提交数据的时候一定要些 name 属性,不然组不成 name=value 的格式,就没有办法用 Post 传输数据。然后点击这个表单上传文件并提交之后,我们来看看发出来的 HTTP 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
POST http://localhost:8080/BookStore/Upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------16694935940386624681704157384
Content-Length: 36544
Origin: http://localhost:8080
Connection: keep-alive
Referer: http://localhost:8080/BookStore/test.jsp
Cookie: JSESSIONID=36C32D4D34BDD9D1138C898DA0C86440; Idea-b21bbe90=38338ff0-a6c6-49f8-b92a-980001c9c1a6
Upgrade-Insecure-Requests: 1

-----------------------------16694935940386624681704157384
Content-Disposition: form-data; name="photo"; filename=".......jpg"
Content-Type: image/jpeg

.PNG
.
....IHDR.......m......E.... .IDATx^.....E..;"....@...C.....
....h,...
.&."".]Y@...Q.(.`...rD...a.v.%x....,.%..d.r.D.C..=.._;....y.{f...T................./.d. ...@...... ...@...&!.{=.....@...... ...@ #.8.
-----------------------------16694935940386624681704157384
Content-Disposition: form-data; name="username"

Xorex
-----------------------------16694935940386624681704157384
Content-Disposition: form-data; name="submit"

............
-----------------------------16694935940386624681704157384--

然后我们来分析一波,下面这个头表示用表单设置出来的,不同的是多了一个 boundary 属性,是浏览器自己生成的随机数分割线,用来分隔 POST 传送的不同的数据块的。

1
Content-Type: multipart/form-data; boundary=---------------------------16694935940386624681704157384

然后,这个是图片数据块的开头,标识数据名 name="photo" 和文件名 filename=".......jpg" (这里因为编码原因无法显示),然后下面的 Content-Type: image/jpeg 作为请求头组成之一标识这个数据块的数据类型为图片,且是 jpeg 格式。

1
2
3
-----------------------------16694935940386624681704157384
Content-Disposition: form-data; name="photo"; filename=".......jpg"
Content-Type: image/jpeg

设置好了前端的文件上传的数据发送,就要设置好后端的数据接收。

后端部分

这里需要导入两个 Maven 依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>

这里的处理思路主要是先判断是否是 Multipart 类型的数据,如果是则进行处理。然后用 ServletFileUpload 的实例去解析 request 请求里面的数据块,它会将所有的数据块都解析成一个 FileItem 实例,然后封装成一个 List 之后传出来。我们遍历所有的数据块 FileItem ,然后选取不是来自于 Filed 的普通文字标段字段(也就是文件啦),去把这个文件里面的所有内容写入一个 File 里面即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class UploadService extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if(ServletFileUpload.isMultipartContent(req)) {
FileItemFactory fileItemFactory=new DiskFileItemFactory(); // 创建工厂实例
ServletFileUpload servletFileUpload=new ServletFileUpload(fileItemFactory); //传入工厂创建文件上传处理的实例
try {
List<FileItem> list = servletFileUpload.parseRequest(req); //解析请求里面的数据块
for(FileItem i:list) {
if(!i.isFormField()) { //如果不是文本字段,就一定是文件了
i.write(new File("E:/"+i.getName())); //将文件内容原封不动写入 File 中
}
}
} catch (Exception e) {
e.printStackTrace();
}

} else {
resp.setCharacterEncoding("UTF-8");
resp.setHeader("Content-Type","text/html; charset=UTF-8");
PrintWriter writer = resp.getWriter();
writer.println("<h1>上传失败哦</h1>");
resp.setHeader("Refresh", "5; url=/BookStore/test.jsp");
}
}
}

文件下载

Servlet 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DownloadService extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String downloadName="waifu.jpg"; //设置需要传给客户端的文件名
ServletContext context=getServletContext(); //获取 ServletContext 实例
String MimeType=context.getMimeType("/static/img/"+downloadName); //获取文件的 MIME 值,用于告诉客户端文件类型
resp.setContentType(MimeType); //将 MIME 添加到响应头
resp.setHeader("Content-Disposition", "attachment; fileName="+downloadName); //添加响应头,告诉浏览器要下载附件
InputStream inputStream=context.getResourceAsStream("/static/img/"+downloadName); //获取文件的流资源
OutputStream outputStream=resp.getOutputStream(); //获取输出流
IOUtils.copy(inputStream, outputStream); //将文件流资源拷贝到输出流中
}
}

乱码处理

因为 HTTP 协议在设计的时候就没有考虑到中文的问题,所以就有可能会出现中文文件名乱码。对于访问的客户端我们可以分为火狐浏览器和非火狐浏览器,对于非火狐浏览器,我们只需要对涉及到中文的地方进行 URL 编码即可,浏览器会自动对 URL 编码进行 URL 解码和 UTF-8 解码获取中文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DownloadService extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String downloadName="Waifu.jpg";
ServletContext context=getServletContext();
String MimeType=context.getMimeType("/static/img/"+downloadName);
resp.setContentType(MimeType);
resp.setHeader("Content-Disposition", "attachment; fileName=内存.jpg"+URLEncoder.encode("内存.jpg", "UTF-8")); //对文件名进行 UTF-8 方式的 URL 编码
InputStream inputStream=context.getResourceAsStream("/static/img/"+downloadName);
OutputStream outputStream=resp.getOutputStream();
IOUtils.copy(inputStream, outputStream);
}
}

对于火狐浏览器,就需要对文件名进行更特殊的编码了,格式为:=?charset?B?XXXX?=。其中 =??= 本别标识开始和结束,charset 表示字符的编码方式,B 表示使用 Base64 编码(因为火狐默认用的 Base64 解码后面的东西),XXXX 表示我们用 Base64 加密的 UTF-8 汉字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DownloadService extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String returnName;
if(req.getHeader("User-Agent").contains("FireFox")) {
returnName="=?utf-8?B?"+String.valueOf(Base64.getEncoder().encode("内存.jpg".getBytes(StandardCharsets.UTF_8)))+"?=";
System.out.println(returnName);
} else {
returnName=URLEncoder.encode("内存.jpg", "UTF-8");
}
String downloadName="Waifu.jpg";
ServletContext context=getServletContext();
String MimeType=context.getMimeType("/static/img/"+downloadName);
resp.setContentType(MimeType);
resp.setHeader("Content-Disposition", "attachment; fileName="+returnName);
InputStream inputStream=context.getResourceAsStream("/static/img/"+downloadName);
OutputStream outputStream=resp.getOutputStream();
IOUtils.copy(inputStream, outputStream);
}
}