文件上传的本质是客户端(通常是浏览器)通过 HTTP POST 请求,将文件数据作为请求体的一部分发送给服务器,Servlet 提供了 HttpServletRequest 来处理请求,但直接解析原始的请求流来获取文件数据非常繁琐且容易出错。
我们通常会使用成熟的第三方库来简化这个过程,最常用和推荐的库是 Apache Commons FileUpload。
下面我将分为几个部分来详细说明:
- 核心原理
- 准备工作:添加依赖
- 前端 HTML 表单
- 后端 Servlet 实现
- 高级配置与最佳实践
- 常见问题与解决方案
核心原理
multipart/form-data编码:标准的表单提交使用application/x-www-form-urlencoded编码,这种编码方式不适合传输二进制文件,文件上传必须使用multipart/form-data编码,它会将表单中的每个字段(包括文本和文件)分割成独立的部分(part),并用一个特殊的边界字符串隔开。- Servlet API 的限制:标准的
HttpServletRequestAPI 在getInputStream()或getReader()时,如果请求体是multipart/form-data格式,它会尝试一次性读取整个请求体到内存中,这对于大文件来说是灾难性的,并且解析逻辑非常复杂。 - 第三方库的作用:像 Apache Commons FileUpload 这样的库,专门用于解析
multipart/form-data请求,它能够:- 高效地读取请求流,避免内存溢出。
- 将请求体分解成一个个
FileItem对象,每个对象代表一个表单字段(文本或文件)。 - 提供简单易用的 API 来处理这些
FileItem。
准备工作:添加依赖
你需要在你的项目中添加 commons-fileupload 和它的依赖 commons-io。
如果你使用 Maven,在 pom.xml 中添加以下依赖:
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!-- Apache Commons FileUpload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload2-jakarta</artifactId>
<version>2.0.0-M1</version>
</dependency>
<!-- Apache Commons IO (FileUpload 的依赖) -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
注意:这里使用的是
commons-fileupload2-jakarta版本,它兼容 Jakarta EE 9+(Tomcat 10+),如果你使用的是旧版的 Java EE(Tomcat 9 及以下),请使用commons-fileupload的 1.x 版本。
前端 HTML 表单
前端表单是文件上传的入口,有几个关键点:
method="post":必须使用POST方法。enctype="multipart/form-data":这是最关键的一步,告诉浏览器要以多部分形式编码数据。<input type="file">:创建文件选择控件。
示例 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 属性用于后端识别这个字段 -->
<label for="file">选择文件:</label>
<input type="file" id="file" name="myFile" required>
<br><br>
<label for="description">文件描述:</label>
<input type="text" id="description" name="description">
<br><br>
<button type="submit">上传</button>
</form>
</body>
</html>
name="myFile":这个name值在后端 Servlet 中用来识别文件字段。name="description":这是一个普通的文本字段,也会被上传。
后端 Servlet 实现
这是整个流程的核心,我们将创建一个 UploadServlet 来处理上传请求。
UploadServlet.java:
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet("/upload") // 将 Servlet 映射到 /upload 路径
public class UploadServlet extends HttpServlet {
// 上传文件存储目录
private static final String UPLOAD_DIRECTORY = "uploads";
// 配置上传参数
private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3; // 3MB
private static final int MAX_FILE_SIZE = 1024 * 1024 * 40; // 40MB
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 50; // 50MB
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 检查是否为 multipart 内容
if (!JakartaServletFileUpload.isMultipartContent(request)) {
// 如果不是,则停止
PrintWriter writer = response.getWriter();
writer.println("错误: 表单必须包含 enctype=multipart/form-data");
writer.flush();
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存临界值 - 超过后将产生临时文件存储于临时目录
factory.setSizeThreshold(MEMORY_THRESHOLD);
// 设置临时存储目录
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
JakartaServletFileUpload upload = new JakartaServletFileUpload(factory);
// 设置最大文件上传值
upload.setFileSizeMax(MAX_FILE_SIZE);
// 设置最大请求值 (包含文件和表单数据)
upload.setSizeMax(MAX_REQUEST_SIZE);
// 构建临时上传目录
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求内容, 提取文件数据
List<DiskFileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
// 迭代表单数据
for (DiskFileItem item : formItems) {
// 处理不在表单中的字段
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String filePath = uploadPath + File.separator + fileName;
File storeFile = new File(filePath);
// 在控制台输出上传文件的信息
System.out.println("文件上传路径: " + filePath);
// 保存文件到硬盘
item.write(storeFile);
} else {
// 处理普通表单字段
String fieldName = item.getFieldName();
String fieldValue = item.getString("UTF-8"); // 指定编码,防止中文乱码
System.out.println("字段名: " + fieldName + ", 值: " + fieldValue);
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
// 上传成功后,重定向到成功页面
request.setAttribute("message", "文件上传成功!");
getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
}
}
代码解析:
- 检查
Content-Type:JakartaServletFileUpload.isMultipartContent(request)用于确保请求确实是multipart/form-data类型。 DiskFileItemFactory:这是一个工厂类,负责配置如何处理上传的文件。setSizeThreshold:设置内存阈值,如果文件大小小于这个值,它会直接保存在内存中,如果大于,则会写入到临时目录(由setRepository指定)。setRepository:设置临时文件存储的目录,通常是系统默认的临时目录。
JakartaServletFileUpload:这是核心的解析器。setFileSizeMax:限制单个文件的最大大小。setSizeMax:限制整个请求(包括所有文件和字段)的最大大小。
upload.parseRequest(request):这是最关键的一步,它解析HttpServletRequest并返回一个List<FileItem>列表,每个FileItem对应表单中的一个字段。- 遍历
FileItem:item.isFormField():判断这个FileItem是不是一个普通的表单字段(文本)。- 如果是文件:
item.getName():获取原始文件名。item.write(storeFile):将文件内容写入到指定的File对象中,这是保存文件最简单的方式。
- 如果是普通字段:
item.getFieldName():获取字段名(如description)。item.getString("UTF-8"):获取字段值,并指定编码,防止中文乱码。
- 创建上传目录:在
getServletContext().getRealPath("")获取的 Web 应用根目录下创建一个uploads文件夹来存放上传的文件。 - 响应:上传成功后,通常使用
forward或redirect跳转到另一个页面,而不是直接在 Servlet 中输出 HTML。
高级配置与最佳实践
1 处理中文乱码
- 文件名乱码:在某些操作系统(如 Windows)和浏览器中,文件名可能包含中文。
item.getName()返回的文件名可能是乱码,可以使用以下方法处理:// 处理文件名乱码 String fileName = new File(new String(item.getName().getBytes("ISO-8859-1"), "UTF-8")).getName(); - 普通字段乱码:如代码所示,使用
item.getString("UTF-8")即可。
2 使用 Servlet 3.0+ 的内置 API
从 Servlet 3.0 开始,规范引入了内置的文件上传 API,不再需要第三方库,使用方式如下:
// 在 Servlet 的 doPost 方法中
Part filePart = request.getPart("myFile"); // "myFile" 是 input 的 name 属性
String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 安全地获取文件名
InputStream fileContent = filePart.getInputStream();
// 保存文件
String uploadPath = getServletContext().getRealPath("") + File.separator + "uploads";
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdir();
File file = new File(uploadPath + File.separator + fileName);
try (InputStream input = fileContent;
OutputStream output = new FileOutputStream(file)) {
input.transferTo(output);
}
优缺点:
- 优点:无需第三方库,API 更现代化。
- 缺点:
- 不同 Servlet 容器(Tomcat, Jetty 等)的实现细节可能略有不同。
- 功能相对简单,不如 Commons FileUpload 灵活(内存和临时文件的控制不如前者精细)。
对于新项目,如果不需要非常精细的控制,可以使用内置 API,但 Commons FileUpload 依然是业界标准,功能更强大、更稳定。
3 安全性考虑
- 文件名安全:绝对不要直接使用用户提供的文件名,恶意用户可能上传如
../../../etc/passwd这样的文件名,试图进行路径穿越攻击,应使用上面提到的Paths.get(...).getFileName()方法或类似逻辑来提取一个安全的文件名。 - 文件类型验证:不要仅依赖文件扩展名,文件扩展名可以被轻易伪造,应该检查文件的“魔法数字”(Magic Number),即文件头部的几个字节,来判断真实的文件类型。
- 病毒扫描:对于生产环境,上传的文件应经过杀毒软件的扫描。
- 存储位置:不要将上传的文件放在 Web 服务器的根目录下,否则可能被直接访问,应该放在 Web 根目录之外,或者通过一个专门的 Servlet 来提供文件下载服务,并在此过程中进行权限验证。
常见问题与解决方案
-
The temporary upload location is not valid- 原因:
DiskFileItemFactory使用的临时目录无效或没有写入权限。 - 解决:确保应用程序有权限在系统临时目录(如 Windows 的
C:\Users\XXX\AppData\Local\Temp)中创建和写入文件,可以在代码中打印出System.getProperty("java.io.tmpdir")来确认路径。
- 原因:
-
Uploaded file size exceeds错误- 原因:上传的文件大小超过了在
JakartaServletFileUpload中设置的MAX_FILE_SIZE或MAX_REQUEST_SIZE。 - 解决:调整这些阈值,或者在前端使用 JavaScript 库(如 jQuery File Upload)进行预检查。
- 原因:上传的文件大小超过了在
-
FileNotFoundException或Permission Denied- 原因:应用程序没有权限在指定的上传目录(
uploads)中创建文件。 - 解决:确保
uploads目录存在,并且运行 Tomcat 的用户(如tomcat)对该目录有读写权限。
- 原因:应用程序没有权限在指定的上传目录(
希望这份详细的指南能帮助你成功实现 Java Servlet 的文件上传功能!
