本文是 “文件上传怎么做?” 教程的下篇, 主要讲解服务器端的实现.

基础理论和客户端实现方法看这里: 文件上传怎么做 (上)

3. 服务器端实现

如前所述, 服务器端可以使用不同的技术来实现, 只要和前端接口对上就行, 下面我们分不同的技术来举例说明.

3.1 使用 Servlet 实现

(1) 下载第 3 方工具包

这里, 我们会使用到 Apache 基金会的子项目提供的工具包: commons-fileupload , 而它又依赖于 commons-io

所以, 我们得先把这两个工具包准备好. 如果懒得找, 这里提供两个链接:

commons-fileupload-1.4.jar , commons-io-2.6.jar

(2) 将第 3 方工具包添加到项目 Class Path

将下载到的两个 jar 文件拷贝到项目的 WEB-INF/lib 文件夹下 ( 如果没有 lib 文件夹, 自己新建即可 )

右键单击 lib 图标 → Add as Library … , 名称随意, 点 “OK”.

(3) 新建 UploadServlet.java

src / servlets 下新建一个名为 UploadServlet 的 Java 类文件 ( 自己新建 servlets 包 )

完成上述步骤后, 项目结构大概应该是这样子滴:

项目结构

若上述第 (2) 步操作有误, 那么 commons-fileupload-1.4.jar, commons-io-2.6.jar 前面不会有 “小三角”, 也就是说, 你没有成功把它们添加到 Class Path.

若使用 Eclipse, 亦需要把第 3 方库添加到 Class Path, 操作方法差不多.

(4) 编写 UploadServlet.java 的代码

Code 3.1: UploadServlet.java

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package servlets;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;
import java.util.UUID;

@WebServlet(urlPatterns = "/upload")
public class UploadServlet extends HttpServlet {

// 2MB, 内存中数据超过此阀值时使用临时文件缓冲数据
final private static int MEMORY_THRESHOLD = 1024 * 1024 * 2;
// 10MB, 最大文件上传值
final private static int MAX_FILE_SIZE = 1024 * 1024 * 10;
// 12MB, 最大请求值 (包含文件和表单数据)
final private static int MAX_REQUEST_SIZE = 1024 * 1024 * 12;
// 上传文件的保存路径
final private static String FILE_PATH = "portrait";

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) {

// 若前端设置的 Content-Type 不是 multipart/form-data 则抛出异常
if (!ServletFileUpload.isMultipartContent(req)) {
throw new RuntimeException("Content-Type != 'multipart/form-data'");

// 创建 DiskFileItemFactory
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存临界值 - 超过后将产生临时文件并存储于临时目录中
factory.setSizeThreshold(MEMORY_THRESHOLD);
// 设置临时存储目录为系统临时文件夹
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));

ServletFileUpload upload = new ServletFileUpload(factory);
// 设置最大文件上传值
upload.setFileSizeMax(MAX_FILE_SIZE);
// 设置最大请求值 (包含文件和表单数据)
upload.setSizeMax(MAX_REQUEST_SIZE);
// 中文处理, 防止取到的客户端文件名乱码
upload.setHeaderEncoding("UTF-8");

// 上传的文件的存储路径, 相对于当前项目根目录
String uploadPath = req.getServletContext().getRealPath("/") + FILE_PATH;

// 若文件夹目录不存在则创建
File uploadFolder = new File(uploadPath);
if (!uploadFolder.exists()) {
// 若创建文件夹失败, 抛出异常
if (!uploadFolder.mkdirs()) {
throw new RuntimeException("Make folder error: " + uploadPath);
}
}

try {
// 解析请求的内容提取表单数据
List<FileItem> formItems = upload.parseRequest(req);
if (formItems != null && formItems.size() > 0) {
// 循环处理表单数据
for (FileItem item : formItems) {
if (item.isFormField()) { // 普通表单字段
System.out.println(item.getFieldName() + "=" + item.getString());
} else { // 文件数据流
if (item.getSize() == 0) continue; // 若上传文件为0字节文件, 忽略

// 客户端文件名
String clientFileName = item.getName();

// 服务器端文件名
String serverFileName = UUID.randomUUID().toString()
+ clientFileName.substring(clientFileName.lastIndexOf("."));

// 文件存储路径(含文件名)
String filePath = uploadPath + File.separator + serverFileName;

// 保存文件数据到磁盘
item.write(new File(filePath));

System.out.println("客户端表单字段名:" + item.getFieldName());
System.out.println("客户端文件名:" + clientFileName);
System.out.println("文件大小:" + item.getSize());
System.out.println("文件类别:" + item.getContentType());
System.out.println("服务器端文件名:" + serverFileName);
System.out.println("服务器端相对路径:" + FILE_PATH + File.separator + serverFileName);
System.out.println("服务器端物理路径:" + filePath);
}
}
}
resp.getWriter().write("Upload File SUCCESS.");
} catch (Exception e) {
throw new RuntimeException("Upload file ERROR.", e);
}
}
}

哎哟~ 妈呀~ 这么长! 别慌, 我们慢慢来…

代码中有大量注释, 同学们细品. 下面只重点提几个点:

(1) 第 30 ~ 47 行:

对使用到的第 3 方工具进行初始化和必要的配置.

其中, 第 37 行解释一下: 当文件上传到服务器端时会将文件数据暂存于内存中, 但如果上传的文件比较大, 可能会上传很长时间, 这样一直将已上传的文件数据放在内存中并不妥, 因此设置了一个临时存储位置, 当读取到的文件数据超过了我们设定的阀值就写入磁盘.

(2) 第 49 ~ 59 行:

第 50 行拼接了一个服务器端文件的保存路径. 这里我们拟将上传的文件保存在项目根目录的子文件夹 ( portrait ) 中. 这样做只是为了便于后面演示如何在前端展示上传成功的文件. 实际的项目中, 不建议将客户上传的文件直接放在前端可以直接访问到的位置, 这样会可能会成为一个坏人攻击的入口. 更好的做法是把客户上传的文件放在诸如 WEB-INF 的下级目录中, 这样前端无法直接访问, 你可以在需要把文件从服务器端传给前端时, 进行鉴权后, 将文件数据从服务器端磁盘读取后输出给前端. ( 呵呵, 看不懂, 是吧~ 暂时别管它, 长大后你就明白了… )

第 53 ~ 59 行, 判断用来存储文件的文件夹是否存在, 若不存在则创建它.

若有大量文件会上传到服务器端, 建议按月/日, 或其它的方式把文件归类存放到不同的文件夹. 而不是像本例一样堆在一个文件夹下.

(3) 第 61 ~ 98 行:

重点来啦 !

这一大段即是处理上传的文件及其它表单字段数据的核心部分, 使用一个 try … catch … 的结构包住, 意思是如果中间出问题了, 那就抛出异常.

第 63 行, 使用第 3 方工具解析出 HTTP 数据包中的内容. 注意, 解析出的东西是一个 List , 也就是说其中可能包含了多个表单字段的值, 以及多个文件的数据 ( 支持多文件上传 ). 所以, 在第 64 行判定 List 不为空之后, 第 66 行开始的 for 循环开始对每一个表单字段/文件进行依次处理.

刚才说了, 数据包中可能有表单字段值, 也可能包含了文件数据, 所以, 第 67 行的 if … else … 语句进行了分情况处理. 第 67 ~ 69 行处理普通的表单字段, 这很简单, 表单字段嘛, 就一个参数名 和 一个值而已. 看最终的控制台输出就明白什么意思了.

无论前端填写的是什么类型 ( String / int / float / bool / … ) 的值, 来到服务器端就都成了 String . 若有必要, 须自己写代码转换, 如: Integer age = Integer.parseInt( strAage );

第 76 ~ 77 行, 使用 UUID + 客户端文件扩展名 的方式拼接出一个新的文件名, 用来命名上传到服务器端的文件.

所谓 UUID 是36个字符组成的一个字符串, 理论上来说, 不会重复. 这里我们使用 Java 自带的工具来生成.

因为我们无法预见客户上传的文件是否会重名, 也就是说, 如果一个客户或多个客户上传了相同名称的文件, 这样把文件保存到服务器端的同一个文件夹下就会导致 要么相互覆盖, 要么保存不成功. 所以, 我们这里使用 UUID 作为文件名, 而扩展名部分取客户端文件的扩展名.

拼接出来大概是这样子: 2f111142-09e5-42f2-851b-dcb369de082d.jpeg

第 80 行拼接出了文件的完整路径 ( 包含存储路径和文件名 ), 然后在 83 行写入磁盘.

第 85 ~ 91行看文字就知道是什么意思了 ( 不明白的话, 看后台的截图 ).

注意: 在实际的项目中, 85 ~ 91 输出的这些信息可能需要存储到数据库中, 以便未来客户需要下载 ( 或向前端呈现 ) 的时候你知道文件放在哪里, 原本的文件名叫什么…

最后, 第 95 行给前端一个反馈.

以下是测试时服务器端控制台的输出:

控制台输出

因为我测试使用的是 MacOS, 所以那个文件的物理存储路径看上去有点 “诡异”, 在 Windows 下, 它应该就是类似 C:\xxx\xxx\xxx.jpg 的样子

3.2 使用 Node.js 实现

本节我们使用 Node.js 来实现服务器端, 配合第 2 节的前端代码, 实现文件上传. ( 功能与 3.1 节相同 )

Node.js 环境的搭建与简单的 Web 应用程序开发参看: 使用 Node.js 开发 Web 应用程序

Node.js 本身是一个 JavaScript 的运行时环境 ( Runtime Environment ), 具体功能的实现往往需要借助第 3 方库.

本例我们使用大家常用的 Express 构建 Web 服务器端, 文件上传接口使用 multer 实现.

OK, 开始吧~

打开控制台窗口… 找一个你喜欢的位置, 执行如下命令:

1
2
3
4
$ mkdir upload_example
$ cd upload_example
$ npm init
$ npm install express multer --save

第 1 行, 创建文件夹 upload_example 作为项目文件夹

第 2 行, 进入 upload_example 文件夹

第 3 行, 初始化项目. 其间会问你一些问题, 直接暂时一路回车到底即可.

第 4 行, 安装 express 和 multer, 并将安装配置保存于 package.json

OK, 构建好了项目框架, 下面接着在 upload_example 文件夹中创建程序入口文件: index.js , 代码如下:

Code 3.2: index.js

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const express = require('express');
const multer = require('multer')
const app = express();

const port = 8080; // 监听8080端口
const path = 'portrait'; // 文件保存文件夹名

// multer文件存储配置
const storage = multer.diskStorage({
// 返回服务器端保存路径
destination: function (req, file, cb) {
cb(null, path);
},

// 返回服务器端文件名
filename: function (req, file, cb) {
// 使用 "当前时间 + 随机数 + 客户端文件扩展名" 构成服务器端文件名
const serverFileName = Date.now() + '-' + Math.round(Math.random() * 1E9)
+ file.originalname.substr(file.originalname.lastIndexOf('.'));

console.log('客户端表单字段名: ' + file.fieldname);
console.log('客户端文件名: ' + file.originalname);
console.log('文件类别: ' + file.mimetype);
console.log('服务器端文件名: ' + serverFileName);
console.log('服务器端相对路径: ' + path + '/' + serverFileName);

// 返回生成的服务器端文件名
cb(null, serverFileName);
}
});
const upload = multer({ storage: storage })

// 监听前端文件上传请求
// 注意upload.single('portrait')与前端file字段名(portrait)一致
app.post('/upload', upload.single('portrait'), function (req, res, next) {
// 上传成功向前端反馈{result: 'SUCCESS'}
res.send({result: 'SUCCESS'});
});

// 静态文件位于public文件夹
app.use(express.static('public'));

// 启动程序
app.listen(port, function () {
console.log('启动成功! 快试试吧~ http://localhost:' + port);
});

简单解释一下…

第 9 ~ 30 行, 对 multer 进行配置, 包含 2 个方面:

  • 第 11 ~ 13 行: 文件的存储位置. 这里统一将文件上传到 upload_example/portrait
  • 第 16 ~ 28 行: 文件在服务器端的名称. 这里我们使用 “当前时间 + 随机数” 的方式构成文件在服务器端的存储名称, 以避免同名文件冲突. 关注第 20 ~ 24 行在控制台输出的内容, 这些信息可能你需要存储到数据库中.

第 34 ~ 37 行, 监听前端文件上传请求, 注意第 34 行 app.post() 方法的第 1 个参数 ( /upload ) 应与前端代码中的配置一致, 而第 2 个参数 ( portrait ) 是前端文件字段名, 同样应与前端代码中的配置一致.

本例为单个文件上传的示例, 若需要支持多文件上传也并不复杂, 请参看 multer 文档.

最后, 我们来测试一下吧~

(1) 把 2.1 或 2.2 节的前端代码复制到 upload_example/public/

(2) 控制台窗口中, upload_example 文件夹下, 执行 node index.js 启动程序

(3) 打开浏览器, http://localhost:8080/, 快试试吧~

App , 微信小程序 … 自己测试吧!

4. 小结

本教程展示了 4 种客户端实现方式, 2 种服务器端实现方式.

希望你已经体会到了一点: 客户端与服务器端是可以完全”分离”的, 它们之间通过通信协议 ( 如: HTTP / HTTPS ) 联系在一起, 只要双方相互配合, “接口”对接上了就能正常工作. 今后若使用其它技术, 依葫芦画瓢即可.

现在, 建议再回头看下 1.2 节.