我会从最基础的传统 Servlet 方式讲起,然后介绍目前主流的 Spring Boot 方式,并附上完整的代码示例。

核心概念与原理
文件上传原理
文件上传的本质是 HTTP 协议中的 multipart/form-data 请求。
- 普通表单 (
application/x-www-form-urlencoded): 数据以key=value的形式拼接在 URL 后面,只能上传文本,不能上传文件。 - 文件上传表单 (
multipart/form-data): 它会将表单的数据拆分成多个部分(Part),每个部分可以是文本字段,也可以是文件内容,浏览器会将这些部分打包成一个 HTTP 请求发送给服务器。
服务器端需要专门的解析器来解析这种复杂的 multipart 请求,从中提取出文件和其他字段。
文件下载原理
文件下载的本质是 服务器将文件内容通过 HTTP 响应发送给浏览器。
- Content-Type (响应头): 告诉浏览器响应体的数据类型,对于下载,我们通常不使用浏览器能直接解析的类型(如
text/html),而是使用application/octet-stream(二进制流),这样浏览器就不会尝试渲染它,而是会触发下载。 - Content-Disposition (响应头): 这是关键,它告诉浏览器如何处理响应体,我们使用
attachment,并配合filename="..."来指定下载时保存的文件名。Content-Disposition: attachment; filename="myFile.txt"
传统 Servlet 方式实现
这种方式是 Java Web 的基础,理解它有助于你了解底层原理。

准备工作:引入依赖
如果你使用 Maven,需要在 pom.xml 中添加 commons-fileupload 和 commons-io 依赖。commons-io 提供了很多实用的 I/O 工具类。
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- Apache Commons FileUpload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
<!-- Apache Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
文件上传
a) 前端页面 (upload.html)
关键点:<form> 的 enctype 必须设为 multipart/form-data,<input> 的 type 必须是 file。
<!DOCTYPE html>
<html>
<head>文件上传</title>
</head>
<body>
<h1>文件上传</h1>
<form action="upload" method="post" enctype="multipart/form-data">
选择文件: <input type="file" name="file" />
<input type="submit" value="上传" />
</form>
</body>
</html>
b) 后端 Servlet (UploadServlet.java)

import org.apache.commons.fileupload.FileItem;
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")
public class UploadServlet extends HttpServlet {
// 上传文件存储目录
private static final String UPLOAD_DIRECTORY = "upload";
// 配置上传参数
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
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查是否为多媒体上传
if (!ServletFileUpload.isMultipartContent(request)) {
// 如果不是则停止
PrintWriter writer = response.getWriter();
writer.println("Error: 表单必须包含 enctype=multipart/form-data");
writer.flush();
return;
}
// 配置上传参数
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);
// 构建上传文件的路径
String uploadPath = getServletContext().getRealPath("/") + File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求的内容提取文件数据
@SuppressWarnings("unchecked")
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
for (FileItem 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);
request.setAttribute("message", "文件 " + fileName + " 上传成功!");
}
}
}
} catch (Exception ex) {
request.setAttribute("message", "错误信息: " + ex.getMessage());
}
// 跳转到 message.jsp 页面
getServletContext().getRequestDispatcher("/message.jsp").forward(request, response);
}
}
c) 提示页面 (message.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>上传结果</title>
</head>
<body>
<h2>${message}</h2>
<a href="upload.html">返回上传页面</a>
</body>
</html>
文件下载
a) 后端 Servlet (DownloadServlet.java)
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.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
private static final String UPLOAD_DIRECTORY = "upload";
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取要下载的文件名
String fileName = request.getParameter("fileName");
if (fileName == null || fileName.isEmpty()) {
request.setAttribute("message", "文件名不能为空!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
// 构建文件在服务器上的完整路径
String appPath = getServletContext().getRealPath("/");
String filePath = appPath + File.separator + UPLOAD_DIRECTORY + File.separator + fileName;
File downloadFile = new File(filePath);
if (!downloadFile.exists()) {
request.setAttribute("message", "文件不存在: " + fileName);
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
// 设置响应头
response.setContentType("application/octet-stream");
// 处理中文文件名乱码问题
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
// 将文件写入响应流
try (FileInputStream inStream = new FileInputStream(downloadFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inStream.read(buffer)) != -1) {
response.getOutputStream().write(buffer, 0, bytesRead);
}
}
}
}
b) 下载链接 (message.jsp 或其他页面)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>文件下载</title>
</head>
<body>
<h2>文件列表</h2>
<ul>
<!-- 假设服务器上传目录下有 a.txt 和 b.jpg -->
<li><a href="download?fileName=a.txt">下载 a.txt</a></li>
<li><a href="download?fileName=b.jpg">下载 b.jpg</a></li>
</ul>
</body>
</html>
Spring Boot 方式实现 (推荐)
Spring Boot 通过 MultipartFile 接口极大地简化了文件上传和下载的操作,我们不再需要手动解析 multipart 请求。
准备工作:引入依赖
Spring Boot Web starter 已经包含了处理文件上传所需的核心依赖。
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok (可选,简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
配置上传限制
在 application.properties 或 application.yml 中配置文件上传的相关参数。
# 单个文件大小限制 spring.servlet.multipart.max-file-size=10MB # 总请求大小限制 spring.servlet.multipart.max-request-size=10MB # 临时文件存储位置 (可选) # spring.servlet.multipart.location=/tmp
文件上传
a) 前端页面
与 Servlet 方式完全相同。
b) 后端 Controller (FileUploadController.java)
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileUploadController {
// 定义一个上传目录,放在项目根目录下的 upload 文件夹
private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/upload/";
@GetMapping("/")
public String index() {
return "upload"; // 返回 upload.html 视图
}
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
redirectAttributes.addFlashAttribute("message", "请选择一个文件上传");
return "redirect:uploadStatus";
}
try {
// 确保上传目录存在
Path uploadPath = Paths.get(UPLOADED_FOLDER);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 构建目标文件路径
Path destination = uploadPath.resolve(originalFilename);
// 将文件保存到目标路径
Files.copy(file.getInputStream(), destination);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + originalFilename + "' to '" + destination + "'");
} catch (IOException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("message", "上传失败: " + e.getMessage());
}
return "redirect:/uploadStatus";
}
@GetMapping("/uploadStatus")
public String uploadStatus() {
return "uploadStatus"; // 返回 uploadStatus.html 视图
}
}
c) 提示页面 (uploadStatus.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>Upload Status</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.message { padding: 10px; background-color: #f0f0f0; border-radius: 5px; }
</style>
</head>
<body>
<h1>Upload Status</h1>
<div th:if="${message}" class="message" th:text="${message}"></div>
<p><a href="/">Go to upload page</a></p>
</body>
</html>
文件下载
a) 后端 Controller (FileDownloadController.java)
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileDownloadController {
private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/upload/";
@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
// 构建文件路径
Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
// 检查文件是否存在且可读
if (!resource.exists() || !resource.isReadable()) {
return ResponseEntity.notFound().build();
}
// 设置响应头
String contentType = "application/octet-stream";
String headerValue = "attachment; filename=\"" + resource.getFilename() + "\"";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, headerValue)
.body(resource);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
}
b) 下载链接
可以在 uploadStatus.html 中添加一个文件列表,并提供下载链接。
<!-- 在 uploadStatus.html 中添加 -->
<h2>已上传文件</h2>
<ul>
<!-- 这里需要后端提供文件列表,或者前端扫描 upload 目录 -->
<!-- 为了演示,我们硬编码几个链接 -->
<li><a th:href="@{/download/a.txt}">下载 a.txt</a></li>
<li><a th:href="@{/download/b.jpg}">下载 b.jpg</a></li>
</ul>
最佳实践与注意事项
-
安全性
- 文件名安全: 不要直接使用用户提供的文件名,它可能包含路径(如
../../etc/passwd)或恶意字符,应该提取纯文件名,并进行过滤或重命名。 - 文件类型校验: 不要仅凭文件后缀判断类型,可以通过检查文件内容("Magic Number")来验证,防止上传伪装成图片的可执行文件。
- 病毒扫描: 对上传的文件进行病毒扫描。
- 存储位置: 不要将上传的文件放在 Web 根目录下,防止被直接访问,应该放在一个非公开的目录,并通过程序提供下载。
- 文件名安全: 不要直接使用用户提供的文件名,它可能包含路径(如
-
大文件处理
- 临时文件: 对于大文件,
commons-fileupload和 Spring Boot 都会使用临时文件存储,直到请求处理完毕,确保服务器有足够的磁盘空间。 - 进度显示: 高级的上传功能需要显示上传进度,这通常需要前端(如
XMLHttpRequest或fetchAPI)和后端(如Servlet 3.1+的异步特性或 WebSocket)的配合。
- 临时文件: 对于大文件,
-
用户体验
- 异步上传: 使用 AJAX 技术实现无刷新上传,避免页面卡顿。
- 错误处理: 提供清晰的错误提示,告诉用户上传失败的原因(如文件过大、类型不符等)。
-
性能
- 存储策略: 对于生产环境,文件不应存储在应用服务器的本地磁盘上,应该使用专门的对象存储服务,如 Amazon S3, 阿里云 OSS, MinIO 等,这提高了可靠性、可扩展性,并降低了应用服务器的负载。
总结对比
| 特性 | 传统 Servlet 方式 | Spring Boot 方式 |
|---|---|---|
| 依赖 | 需要手动添加 commons-fileupload 等 |
内置在 spring-boot-starter-web 中 |
| 上传 | 手动解析 multipart 请求,获取 FileItem |
通过 MultipartFile 接口,操作非常简单 |
| 下载 | 手动设置响应头,读取文件流写入 response |
通过 Resource 对象,结合 ResponseEntity 轻松实现 |
| 配置 | 在代码中硬编码或通过 web.xml 配置 |
在 application.properties 中简单配置 |
| 代码量 | 较多,代码冗长 | 极少,非常简洁 |
| 推荐度 | 适合学习原理,或在不使用框架的旧项目中 | 强烈推荐,是现代 Java Web 开发的标准 |
对于新的 Java Web 项目,毫无疑问应该选择 Spring Boot,它能让你更专注于业务逻辑,而不是繁琐的底层实现。
