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");
if (!ServletFileUpload.isMultipartContent(request)) { response.getWriter().write("请使用 multipart/form-data 格式上传文件"); return; }
try { FileItemFactory factory = new DiskFileItemFactory(); ((DiskFileItemFactory) factory).setRepository(new File("/tmp"));
ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(10 * 1024 * 1024); upload.setSizeMax(50 * 1024 * 1024); upload.setHeaderEncoding("UTF-8");
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) { if (item.isFormField()) { String fieldName = item.getFieldName(); String value = item.getString("UTF-8"); System.out.println("文本字段:" + fieldName + " = " + value); } else { String fieldName = item.getFieldName(); String fileName = item.getName(); String contentType = item.getContentType(); long fileSize = item.getSize();
System.out.println("文件字段:" + fieldName + ",文件名:" + fileName + ",类型:" + contentType + ",大小:" + fileSize + "B");
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):限制请求总大小(所有文件 + 文本字段)。
超过限制时,会抛出 FileSizeLimitExceededException 或 SizeLimitExceededException,需捕获并处理。
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
| ((DiskFileItemFactory) factory).setSizeThreshold(1024 * 10);
((DiskFileItemFactory) factory).setRepository(new File("/tmp/upload-temp"));
|
5. 安全校验
- 文件类型校验:通过
item.getContentType() 或文件名后缀判断是否为允许的类型(如图片仅允许 image/jpeg、image/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"); 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):保存文件到指定路径
v1.3.10