0%

上传文件

Java Web 文件上传:从表单到服务器的完整实现

文件上传是 Web 应用的常见功能(如头像上传、文档提交等)。由于 HTTP 表单默认编码格式不支持二进制数据传输,需使用 multipart/form-data 格式处理文件上传。本文将详细介绍文件上传的原理、基于 commons-fileupload 工具的实现方式及最佳实践。

文件上传的核心原理

表单编码格式

默认情况下,表单使用 application/x-www-form-urlencoded 编码,会将数据转换为键值对字符串(如 name=zhangsan&age=20),不适合传输二进制文件(如图片、视频)。

文件上传需指定表单编码为 multipart/form-data,该格式将表单拆分为多个 “部分(part)”,每个部分对应一个表单项(文本或文件),并通过分隔符区分,支持二进制数据传输。

1
2
3
4
5
6
7
8
<!-- 文件上传表单 -->
<form method="post" enctype="multipart/form-data" action="/upload">
<!-- 文本字段 -->
<input type="text" name="username" />
<!-- 文件字段 -->
<input type="file" name="avatar" />
<button type="submit">上传</button>
</form>
  • method="post":文件上传必须使用 POST 方法(GET 有长度限制);
  • enctype="multipart/form-data":指定二进制编码格式;
  • type="file":文件选择框,用于选择本地文件。

上传数据格式

multipart/form-data 格式的请求体示例:

1
2
3
4
5
6
7
8
9
10
------WebKitFormBoundaryabc123
Content-Disposition: form-data; name="username"

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

[二进制图片数据]
------WebKitFormBoundaryabc123--
  • 分隔符(如 ----WebKitFormBoundaryabc123)用于区分不同表单项;
  • 每个部分包含描述信息(如字段名、文件名、MIME 类型)和数据内容;
  • 结尾以 -- 标识分隔符结束。

文件上传的实现:基于 commons-fileupload

手动解析 multipart/form-data 格式复杂且易出错,推荐使用 Apache 的 commons-fileupload 工具(依赖 commons-io),简化文件上传处理。

引入依赖

pom.xml 中添加依赖(Maven 项目):

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

核心类与方法

  • ServletFileUpload:负责解析 multipart/form-data 格式的请求;
  • FileItemFactory:创建 FileItem 对象(封装表单项数据),常用实现 DiskFileItemFactory
  • FileItem:表示一个表单项,提供判断是否为文件、获取文件名、写入文件等方法。

实现文件上传 Servlet

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
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");

// 1. 判断请求是否为 multipart/form-data 格式
if (!ServletFileUpload.isMultipartContent(request)) {
response.getWriter().write("请使用 multipart/form-data 格式上传文件");
return;
}

try {
// 2. 创建 FileItemFactory(磁盘文件项工厂,用于处理临时文件)
FileItemFactory factory = new DiskFileItemFactory();
// 可选:设置临时文件存储目录(默认使用系统临时目录)
((DiskFileItemFactory) factory).setRepository(new File("/tmp"));

// 3. 创建文件上传处理器
ServletFileUpload upload = new ServletFileUpload(factory);
// 可选:设置上传文件的最大大小(单位:字节,此处为 10MB)
upload.setFileSizeMax(10 * 1024 * 1024);
// 可选:设置请求总大小(包括所有文件和文本字段,此处为 50MB)
upload.setSizeMax(50 * 1024 * 1024);
// 可选:设置编码(解决文件名中文乱码)
upload.setHeaderEncoding("UTF-8");

// 4. 解析请求,获取所有表单项(FileItem 列表)
List<FileItem> items = upload.parseRequest(request);

// 5. 遍历表单项,分别处理文本和文件
for (FileItem item : items) {
if (item.isFormField()) {
// 处理文本字段
String fieldName = item.getFieldName(); // 表单字段名(如 "username")
String value = item.getString("UTF-8"); // 字段值(解决中文乱码)
System.out.println("文本字段:" + fieldName + " = " + value);
} else {
// 处理文件字段
String fieldName = item.getFieldName(); // 表单字段名(如 "avatar")
String fileName = item.getName(); // 原始文件名(如 "photo.jpg")
String contentType = item.getContentType(); // 文件 MIME 类型(如 "image/jpeg")
long fileSize = item.getSize(); // 文件大小(单位:字节)

System.out.println("文件字段:" + fieldName
+ ",文件名:" + fileName
+ ",类型:" + contentType
+ ",大小:" + fileSize + "B");

// 6. 保存文件到服务器
// 目标目录(确保目录存在,否则会抛异常)
String uploadDir = getServletContext().getRealPath("/upload");
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs(); // 创建目录(包括父目录)
}

// 避免文件名冲突(如添加时间戳)
String uniqueFileName = System.currentTimeMillis() + "_" + fileName;
File destFile = new File(dir, uniqueFileName);

// 写入文件(自动处理临时文件)
item.write(destFile);
response.getWriter().write("文件上传成功:" + uniqueFileName);
}
}
} catch (FileUploadBase.FileSizeLimitExceededException e) {
response.getWriter().write("单个文件大小不能超过 10MB");
} catch (FileUploadBase.SizeLimitExceededException e) {
response.getWriter().write("总上传大小不能超过 50MB");
} catch (Exception e) {
e.printStackTrace();
response.getWriter().write("文件上传失败:" + e.getMessage());
}
}
}

关键配置与优化

1. 限制上传大小

  • setFileSizeMax(long):限制单个文件大小(如 10 * 1024 * 1024 表示 10MB);
  • setSizeMax(long):限制请求总大小(所有文件 + 文本字段)。
    超过限制时,会抛出 FileSizeLimitExceededExceptionSizeLimitExceededException,需捕获并处理。

2. 处理中文乱码

  • 文本字段:通过 item.getString("UTF-8") 指定编码;
  • 文件名:ServletFileUpload 需设置 setHeaderEncoding("UTF-8"),避免文件名中文乱码。

3. 避免文件名冲突

同一文件名上传会覆盖已有文件,解决方案:

  • 拼接时间戳(如 System.currentTimeMillis() + "_" + fileName);
  • 生成 UUID(如 UUID.randomUUID() + "_" + fileName);
  • 按用户 / 日期分目录存储(如 /upload/202310/zhangsan/)。

4. 临时文件处理

DiskFileItemFactory 会将超过阈值的文件先保存到临时目录,上传完成后自动删除。可通过以下方式配置:

1
2
3
4
// 设置内存中缓存文件的阈值(默认 10KB),超过则写入临时文件
((DiskFileItemFactory) factory).setSizeThreshold(1024 * 10); // 10KB
// 设置临时文件目录
((DiskFileItemFactory) factory).setRepository(new File("/tmp/upload-temp"));

5. 安全校验

  • 文件类型校验:通过 item.getContentType() 或文件名后缀判断是否为允许的类型(如图片仅允许 image/jpegimage/png);
  • 文件内容校验:部分文件可能伪装后缀,需读取文件头部 magic number 验证(如 JPG 头部为 FF D8 FF);
  • 权限控制:限制上传目录的写入权限,避免上传恶意脚本(如 .jsp 文件执行)。

Servlet 3.0+ 原生文件上传(无依赖)

Servlet 3.0 及以上提供了原生文件上传 API,无需依赖第三方库,通过 @MultipartConfig 注解开启支持:

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
@WebServlet("/upload-native")
@MultipartConfig(
fileSizeThreshold = 1024 * 10, // 10KB 以上写入临时文件
maxFileSize = 10 * 1024 * 1024, // 单个文件最大 10MB
maxRequestSize = 50 * 1024 * 1024 // 总请求最大 50MB
)
public class NativeUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");

// 获取文本字段
String username = request.getParameter("username");
System.out.println("用户名:" + username);

// 获取文件字段
Part part = request.getPart("avatar");
// 获取文件名(需从 Content-Disposition 中解析)
String fileName = part.getSubmittedFileName();
// 获取文件大小
long fileSize = part.getSize();

// 保存文件
String uploadDir = getServletContext().getRealPath("/upload");
File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs();

String uniqueFileName = UUID.randomUUID() + "_" + fileName;
part.write(new File(dir, uniqueFileName).getPath());

response.getWriter().write("原生 API 上传成功:" + uniqueFileName);
}
}
  • @MultipartConfig:标记 Servlet 支持文件上传,可配置大小限制;
  • request.getPart("name"):获取文件字段;
  • part.write(path):保存文件到指定路径

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10