目录
- 核心原理:HTTP 协议如何传输文件
- 准备工作:HTML 表单
- 使用 Servlet 3.0+ 内置 API (推荐)
- 代码示例
- 配置说明 (
web.xml) - 优点与缺点
- 使用 Apache Commons FileUpload (传统方式)
- 添加依赖
- 代码示例
- 优点与缺点
- 最佳实践与注意事项
- 文件存储位置
- 安全性
- 性能
- 中文文件名乱码问题
- 完整示例代码
核心原理:HTTP 协议如何传输文件
标准的 HTTP 请求中,表单数据默认是 application/x-www-form-urlencoded 格式,它只能传输文本,当表单包含 <input type="file"> 时,必须使用 multipart/form-data 编码类型。

这种类型的请求会将表单的各个部分(包括文本字段和文件内容)分割成多个“块”(Part),每个块之间用一个特殊的边界字符串(Boundary)分隔,服务器端需要一个特殊的解析器来读取这个混合流,识别出每个块的类型(是文本还是文件),并分别提取出文件名、文件内容等数据。
关键点:
<form>标签必须设置enctype="multipart/form-data"。<form>标签必须使用POST方法,因为文件数据通常很大。
准备工作:HTML 表单
我们需要一个在前端页面上传文件的表单,这是一个简单的 upload.html 示例。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">文件上传</title>
</head>
<body>
<h1>选择文件上传</h1>
<form action="upload" method="post" enctype="multipart/form-data">
<!--
name="file" 这个 name 属性非常重要,后端代码会通过它来获取文件
multiple 属性允许一次选择多个文件
-->
<input type="file" name="file" multiple>
<input type="submit" value="上传">
</form>
</body>
</html>
关键点:

action="upload":指向服务器端处理上传请求的 Servlet 或 URL。method="post":必须使用 POST。enctype="multipart/form-data":必须设置,这是文件上传的标志。
方案一:使用 Servlet 3.0+ 内置 API (推荐)
从 Servlet 3.0 开始,Java EE 规范内置了对文件上传的支持,使用起来非常简单,无需额外引入第三方库。
代码示例
创建一个 UploadServlet.java 来处理请求。
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
@WebServlet("/upload")
@MultipartConfig // <-- 关键注解,声明该Servlet可以处理multipart请求
public class UploadServlet extends HttpServlet {
// 定义上传文件保存的目录(相对于 webapp 目录)
private static final String UPLOAD_DIR = "uploads";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 获取上传文件的目录
// getServletContext().getRealPath() 可以获取 webapp 目录下的绝对路径
String applicationPath = getServletContext().getRealPath("");
String uploadFilePath = applicationPath + File.separator + UPLOAD_DIR;
// 如果上传目录不存在,则创建
File uploadFileDir = new File(uploadFilePath);
if (!uploadFileDir.exists()) {
uploadFileDir.mkdirs();
}
// 2. 遍历所有上传的 Part
Collection<Part> parts = request.getParts();
if (parts.size() > 0) {
for (Part part : parts) {
// 3. 获取文件名
// 注意:对于非文件字段(如文本输入),part.getSubmittedFileName() 返回 null
String fileName = getSubmittedFileName(part);
if (fileName != null && !fileName.isEmpty()) {
// 4. 写入文件到服务器
// part.write() 方法将文件内容写入到指定路径
part.write(uploadFilePath + File.separator + fileName);
System.out.println("文件 " + fileName + " 上传成功!");
}
}
}
// 5. 向客户端返回成功信息
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h3>文件上传成功!</h3>");
out.println("<a href='upload.html'>返回上传页面</a>");
out.println("</body></html>");
}
/**
* 辅助方法:从 Part 对象中获取提交的文件名。
* 这个方法可以兼容不同浏览器提交的文件名格式。
*/
private String getSubmittedFileName(Part part) {
String contentDisp = part.getHeader("content-disposition");
String[] items = contentDisp.split(";");
for (String s : items) {
if (s.trim().startsWith("filename")) {
// 处理可能的中文文件名乱码问题
String fileName = s.substring(s.indexOf("=") + 2, s.length() - 1);
return fileName;
}
}
return "";
}
}
配置说明 (web.xml)
使用 @WebServlet 和 @MultipartConfig 注解后,通常不再需要在 web.xml 中配置 Servlet,但如果想进行更详细的配置(如设置上传文件的最大尺寸),可以在 web.xml 中配置 multipart-config。
<!-- web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>com.yourpackage.UploadServlet</servlet-class>
<multipart-config>
<!-- 设置单个文件的最大尺寸 (e.g., 5MB) -->
<max-file-size>5242880</max-file-size>
<!-- 设置整个请求的最大尺寸 (e.g., 10MB) -->
<max-request-size>10485760</max-request-size>
<!-- 设置临时文件存放目录,如果未设置,则使用系统默认的临时目录 -->
<file-temp-directory>/path/to/temp/dir</file-temp-directory>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/upload</url-pattern>
</servlet-mapping>
</web-app>
优点与缺点
- 优点:
- 无需额外依赖:是 Java EE 标准的一部分,开箱即用。
- 代码简洁:API 设计直观,
request.getParts()和part.write()非常简单。 - 官方支持:由 Java 社区维护,稳定可靠。
- 缺点:
- 灵活性稍差:对于复杂的业务逻辑(如获取文件类型、动态生成文件名等),可能需要写更多辅助代码。
方案二:使用 Apache Commons FileUpload (传统方式)
如果你在使用旧版本的 Servlet(低于 3.0),或者更喜欢使用功能更强大的库,Apache Commons FileUpload 是一个非常经典的选择。

添加依赖
如果你使用 Maven,在 pom.xml 中添加以下依赖:
<dependencies>
<!-- Commons FileUpload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- Commons IO (FileUpload 依赖它) -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
代码示例
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/upload-legacy")
public class LegacyUploadServlet extends HttpServlet {
private static final String UPLOAD_DIR = "uploads";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 检查请求是否为 multipart
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
if (!isMultipart) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求不是 multipart/form-data 类型");
return;
}
// 2. 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存中存储文件内容的阈值,超过此大小将写入临时文件
factory.setSizeThreshold(1024 * 1024); // 1MB
// 设置临时文件存储目录
String tempPath = getServletContext().getRealPath("") + File.separator + "temp";
File tempDir = new File(tempPath);
if (!tempDir.exists()) tempDir.mkdirs();
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置单个文件的最大尺寸
upload.setFileSizeMax(1024 * 1024 * 5); // 5MB
// 设置整个请求的最大尺寸
upload.setSizeMax(1024 * 1024 * 10); // 10MB
// 3. 解析请求,获取 FileItem 列表
try {
List<FileItem> items = upload.parseRequest(request);
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdirs();
for (FileItem item : items) {
// 4. 判断是普通表单字段还是文件
if (item.isFormField()) {
// 普通字段,例如文本输入
String fieldName = item.getFieldName();
String fieldValue = item.getString("UTF-8"); // 指定编码防止乱码
System.out.println("字段: " + fieldName + ", 值: " + fieldValue);
} else {
// 文件字段
String fileName = new File(item.getName()).getName();
String filePath = uploadPath + File.separator + fileName;
// 将文件写入到指定路径
item.write(new File(filePath));
System.out.println("文件 " + fileName + " 上传成功!");
}
}
// 5. 返回成功信息
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h3>文件上传成功!(Legacy方式)</h3>");
out.println("</body></html>");
} catch (Exception e) {
throw new ServletException("文件解析或上传失败", e);
}
}
}
优点与缺点
- 优点:
- 功能强大:提供了对上传过程的细粒度控制,如内存阈值、临时目录、进度监听等。
- 兼容性好:适用于所有 Servlet 版本。
- 处理普通字段方便:能自动区分文件和普通字段,并提供了
getString()方法方便获取文本内容。
- 缺点:
- 需要引入外部依赖:增加了项目的复杂性和体积。
- 代码量稍多:需要创建
FileItemFactory和ServletFileUpload对象,配置步骤比内置 API 繁琐。
最佳实践与注意事项
文件存储位置
- 不要存放在
WEB-INF之外:直接存放在webapp目录下的文件(如uploads)可以通过 URL 直接访问,存在安全隐患,建议将上传的文件存放在WEB-INF目录下,这样它们就不能被直接通过浏览器访问了。// 推荐:存放在 WEB-INF 下 String uploadPath = getServletContext().getRealPath("") + File.separator + "WEB-INF" + File.separator + UPLOAD_DIR; - 使用绝对路径:始终使用
getServletContext().getRealPath()来获取服务器的绝对路径,而不是使用相对路径,因为后者在不同部署环境下可能出错。
安全性
- 文件名过滤:恶意用户可能上传包含 的文件名,试图将文件写入到服务器上的任意位置(路径遍历攻击),在保存文件前,务必对文件名进行清理和验证。
// 简单的文件名清理示例 String fileName = new File(item.getName()).getName(); // 防止路径遍历攻击 fileName = fileName.replaceAll("\\.\\./", ""); // 也可以限制文件名只包含字母、数字、下划线和点 fileName = fileName.replaceAll("[^a-zA-Z0-9._-]", ""); - 文件类型验证:不要仅依赖文件扩展名,应该读取文件的“魔数”(Magic Number)或文件头来判断真实的文件类型,防止用户将
.exe文件伪装成.jpg文件上传。 - 病毒扫描:对于生产环境,上传的文件应经过杀毒软件的扫描。
性能
- 限制文件大小:始终在
web.xml或代码中设置max-file-size和max-request-size,防止用户上传超大文件耗尽服务器磁盘空间或导致内存溢出。 - 使用临时文件:对于大文件,像 Commons FileUpload 那样使用临时文件机制,可以避免在内存中占用过多空间。
中文文件名乱码问题
- Servlet 3.0+:
part.getSubmittedFileName()方法通常能正确处理文件名,如果仍有问题,可以尝试获取Content-Disposition头并手动解析,指定字符集。 - Commons FileUpload:在调用
item.getString()获取普通字段内容时,可以指定编码,如item.getString("UTF-8"),但对于文件名,item.getName()返回的通常是原始字符串,需要确保客户端(HTML<meta charset="UTF-8">)和服务器端都使用 UTF-8 编码。
完整示例代码
这里提供一个完整的、包含安全性的 Servlet 3.0+ 示例。
UploadServlet.java
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.UUID;
@WebServlet("/safe-upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1 MB
maxFileSize = 1024 * 1024 * 5, // 5 MB
maxRequestSize = 1024 * 1024 * 10 // 10 MB
)
public class SafeUploadServlet extends HttpServlet {
private static final String UPLOAD_DIR = "WEB-INF/uploads";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 确保上传目录存在
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
// 2. 获取上传的 Part
Part filePart = request.getPart("file"); // 通过 name="file" 获取
if (filePart == null || filePart.getSize() == 0) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "没有选择文件");
return;
}
// 3. 安全地处理文件名
String fileName = getSubmittedFileName(filePart);
if (fileName == null || fileName.isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件名无效");
return;
}
// 防止路径遍历攻击
fileName = new File(fileName).getName();
// 使用 UUID 生成新的文件名,防止文件名冲突和恶意文件名
String fileExtension = fileName.substring(fileName.lastIndexOf("."));
String newFileName = UUID.randomUUID().toString() + fileExtension;
// 4. 构建安全的文件路径
String filePath = uploadPath + File.separator + newFileName;
// 5. 写入文件
filePart.write(filePath);
System.out.println("文件 " + fileName + " 已安全保存为 " + newFileName + " 在 " + filePath);
// 6. 返回成功信息
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h3>文件上传成功!</h3>");
out.println("<p>原始文件名: " + fileName + "</p>");
out.println("<p>新文件名: " + newFileName + "</p>");
out.println("<a href='upload.html'>返回上传页面</a>");
out.println("</body></html>");
}
private String getSubmittedFileName(Part part) {
String contentDisp = part.getHeader("content-disposition");
String[] items = contentDisp.split(";");
for (String s : items) {
if (s.trim().startsWith("filename")) {
String fileName = s.substring(s.indexOf("=") + 2, s.length() - 1);
return fileName;
}
}
return "";
}
}
对于新的项目,强烈推荐使用 Servlet 3.0+ 的内置 API,因为它更简单、更标准,只有在维护旧项目或需要非常精细的控制时,才考虑使用 Apache Commons FileUpload,无论选择哪种方案,安全性、性能和健壮性都是开发时必须考虑的重点。
