-
基础篇:使用 Servlet 和 Apache Commons FileUpload 实现简单上传
(图片来源网络,侵删)- 原理介绍
- 环境搭建
- 前端代码
- 后端代码
- 测试与问题
-
进阶篇:使用 Spring Boot 实现上传
- 为什么选择 Spring Boot
- 项目搭建与配置
- 控制器代码
- 高级配置(文件大小、类型限制)
-
生产环境最佳实践
- 文件存储:为什么不能存在服务器本地?(使用云存储OSS)
- 文件处理:视频封面、转码(使用 FFmpeg)
- 安全性:防止恶意文件上传
- 用户体验:上传进度条
基础篇:使用 Servlet 和 Apache Commons FileUpload
这是最传统、最核心的 Java Web 上传方式,理解它有助于你掌握上传的本质。
原理介绍
标准的 HTTP 请求在传输文件时,会使用 multipart/form-data 内容类型,浏览器会将表单中的每个字段(包括文件)拆分成多个部分(Part)进行发送,Java Servlet 本身不直接处理这种复杂的请求格式,因此我们需要借助第三方库,Apache Commons FileUpload,它能够解析这个请求,帮我们轻松地获取到上传的文件。

环境搭建
-
创建项目:使用任何你喜欢的 IDE(如 IntelliJ IDEA, Eclipse)创建一个 Dynamic Web Project。
-
添加依赖:你需要两个核心库:
commons-fileupload:用于解析multipart请求。commons-io:提供一些实用的 I/O 工具类,简化代码。
如果你使用 Maven,在
pom.xml中添加以下依赖:<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>
前端代码 (upload.html)
创建一个简单的 HTML 页面,用于选择和上传文件,注意 form 的两个关键属性:

enctype="multipart/form-data":必须设置,表示表单包含文件。method="POST":上传必须使用 POST 方法。
<!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">
<label for="videoFile">选择视频文件:</label>
<input type="file" id="videoFile" name="videoFile" accept="video/*" required>
<button type="submit">上传</button>
</form>
</body>
</html>
后端代码 (UploadServlet.java)
创建一个 Servlet 来处理上传请求。
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_DIR = "uploads";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 检查是否是 multipart 请求
if (!ServletFileUpload.isMultipartContent(request)) {
// 如果不是,则停止处理
PrintWriter writer = response.getWriter();
writer.println("Error: 表单必须包含 enctype=multipart/form-data");
writer.flush();
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置内存临界值 - 超过后将产生临时文件存储于临时目录
factory.setSizeThreshold(1024 * 1024 * 3); // 3MB
// 设置临时存储目录
File tempDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
// 设置最大文件大小 (10MB)
upload.setSizeMax(1024 * 1024 * 10);
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求内容,提取文件
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);
}
}
}
// 上传成功,重定向到成功页面
getServletContext().getRequestDispatcher("/success.html").forward(request, response);
} catch (Exception ex) {
throw new ServletException("上传文件失败", ex);
}
}
}
测试与问题
- 部署:将项目部署到 Tomcat 等服务器上。
- 访问:在浏览器中访问
http://localhost:8080/你的项目名/upload.html。 - 上传:选择一个视频文件并点击上传。
- 检查:上传成功后,你会在项目的
webapp目录下看到一个名为uploads的文件夹,里面有你上传的视频文件。
常见问题:
java.io.tmpdir空间不足:如果上传的文件很大,可能会先存放在临时目录,确保服务器的临时目录有足够空间。- 文件名乱码:
FileItem.getName()在不同浏览器下可能返回不同的编码(如ISO-8859-1),如果文件名是中文,需要转换编码:new String(item.getName().getBytes("ISO-8859-1"), "UTF-8")。 - 文件大小限制:如果文件超过
upload.setSizeMax()设置的大小,会抛出SizeLimitExceededException。
进阶篇:使用 Spring Boot 实现上传
Spring Boot 大大简化了文件上传的开发,通过自动配置,我们只需要几行代码就能完成。
为什么选择 Spring Boot?
- 简化配置:无需手动配置
Servlet和FileUpload组件。 - 嵌入式服务器:无需额外安装 Tomcat。
- 强大的生态系统:与 Spring MVC 无缝集成,易于扩展。
项目搭建与配置
-
使用 Spring Initializr 创建一个新项目。
- Project: Maven Project
- Language: Java
- Spring Boot: 选择一个稳定版本 (e.g., 3.x.x)
- Project Metadata: Group, Artifact, Name 等
- Dependencies: 添加 Spring Web
-
配置文件 (
application.properties)在
src/main/resources/application.properties中添加配置,限制上传文件的大小。# 单个文件最大大小 (10MB) spring.servlet.multipart.max-file-size=10MB # 总请求最大大小 (包含多个文件) (11MB) spring.servlet.multipart.max-request-size=11MB # 临时文件存储路径 (可选,Spring Boot会自动处理) # spring.servlet.multipart.location=/tmp
控制器代码 (FileUploadController.java)
创建一个控制器来处理文件上传请求。
import org.springframework.stereotype.Controller;
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 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 {
// 定义上传文件的目录
private static final String UPLOADED_FOLDER = "uploads/";
@PostMapping("/upload")
@ResponseBody
public String handleFileUpload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "请选择一个文件上传";
}
try {
// 创建上传目录
Path uploadPath = Paths.get(UPLOADED_FOLDER);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
// 构建目标文件路径
Path destinationFile = uploadPath.resolve(originalFilename);
// 将文件保存到目标路径
Files.copy(file.getInputStream(), destinationFile);
return "文件上传成功: " + destinationFile.toString();
} catch (IOException e) {
e.printStackTrace();
return "文件上传失败: " + e.getMessage();
}
}
}
高级配置(文件类型限制)
在实际应用中,你可能只允许特定类型的文件上传,可以通过 MultipartFile 的 getContentType() 或文件后缀名来检查。
@PostMapping("/upload-video")
@ResponseBody
public String handleVideoUpload(@RequestParam("file") MultipartFile file) {
// 检查文件是否为空
if (file.isEmpty()) {
return "请选择一个文件上传";
}
// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("video/")) {
return "只允许上传视频文件!";
}
// ... (后续的保存逻辑)
return "视频文件上传成功!";
}
生产环境最佳实践
上面的基础和进阶教程实现了基本功能,但在生产环境中远远不够。
文件存储:为什么不能存在服务器本地?
将文件直接存在服务器本地(webapp/uploads)是极其危险的,原因如下:
- 数据丢失:如果服务器宕机、重装系统或 Docker 容器被删除,所有上传的文件都会丢失。
- 扩展性差:当需要增加服务器进行负载均衡时,文件无法在多台服务器之间共享。
- 安全性低:文件暴露在 Web 根目录下,可能被直接通过 URL 访问,存在安全隐患。
- 成本高:服务器磁盘空间有限且昂贵。
解决方案:使用云存储服务
主流云服务商都提供了对象存储服务,这是处理文件的最佳选择。
- 阿里云 OSS
- 腾讯云 COS
- Amazon S3
- MinIO (自建 S3 兼容的私有云存储)
如何集成(以阿里云 OSS 为例)
-
添加依赖:
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency> -
配置信息:在
application.properties中配置你的 OSS 信息。# 阿里云 OSS aliyun.oss.accessKeyId=你的AccessKeyId aliyun.oss.accessKeySecret=你的AccessKeySecret aliyun.oss.bucketName=你的BucketName aliyun.oss.endpoint=你的Endpoint地址
-
编写上传服务:
@Service public class OssService { @Value("${aliyun.oss.accessKeyId}") private String accessKeyId; @Value("${aliyun.oss.accessKeySecret}") private String accessKeySecret; @Value("${aliyun.oss.bucketName}") private String bucketName; @Value("${aliyun.oss.endpoint}") private String endpoint; public String uploadFile(MultipartFile file) throws IOException { // 创建OSSClient实例 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); String originalFilename = file.getOriginalFilename(); // 上传文件流 InputStream inputStream = file.getInputStream(); ossClient.putObject(bucketName, originalFilename, inputStream); // 关闭OSSClient ossClient.shutdown(); // 返回文件的URL return "https://" + bucketName + "." + endpoint + "/" + originalFilename; } } -
在 Controller 中调用服务:
@PostMapping("/upload-to-oss") @ResponseBody public String uploadToOss(@RequestParam("file") MultipartFile file) { try { String fileUrl = ossService.uploadFile(file); return "文件上传成功,访问地址: " + fileUrl; } catch (IOException e) { return "上传失败: " + e.getMessage(); } }
文件处理:视频封面、转码
上传后的视频通常不能直接播放,需要处理。
-
视频封面:使用 FFmpeg 截取视频的第一帧作为封面。
- 在服务器上安装 FFmpeg。
- 通过 Java 调用 FFmpeg 的命令行工具来执行截图。
// 示例:使用 ProcessBuilder 调用 FFmpeg String videoPath = "path/to/your/video.mp4"; String coverPath = "path/to/your/cover.jpg"; ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", videoPath, "-ss", "00:00:01", "-vframes", "1", coverPath); Process process = pb.start(); process.waitFor(); -
视频转码:将视频转码为更通用的格式(如 MP4/H.264)和不同的分辨率(如 720p, 1080p),以适应不同设备和网络条件,同样使用 FFmpeg 实现。
安全性:防止恶意文件上传
这是最关键的一环,绝不能忽视。
-
验证文件类型:
- 不要只信任
Content-Type:因为Content-Type可以被轻易伪造。 - 检查文件后缀名:白名单机制,只允许
.mp4,.mov,.avi等特定后缀。 - 检查文件“魔术数字”(Magic Number):读取文件的前几个字节,判断其真实的文件类型,MP4 文件的开头通常是
ftyp,这是最可靠的方法。
- 不要只信任
-
重命名文件:不要使用用户上传的原始文件名,可以生成一个唯一的、随机的文件名(如 UUID),防止文件名冲突和恶意脚本(如
../../../shell.php)。 -
设置文件权限:确保上传的文件不具有可执行权限。
-
病毒扫描:对于重要的业务,可以集成杀毒软件 API 对上传文件进行扫描。
用户体验:上传进度条
大文件上传需要很长时间,进度条能极大提升用户体验。
- 前端实现:使用 HTML5 的
XMLHttpRequest或 Fetch API 的onUploadProgress事件。 - 后端实现:后端不需要做特殊处理,但需要确保服务器配置(如
max-file-size)允许大文件上传,并且有足够的超时时间。
前端示例 (Fetch API):
<input type="file" id="myFile">
<button id="uploadBtn">上传</button>
<progress id="progressBar" value="0" max="100"></progress>
<script>
document.getElementById('uploadBtn').addEventListener('click', function() {
const file = document.getElementById('myFile').files[0];
const formData = new FormData();
formData.append('file', file);
fetch('/upload-to-oss', {
method: 'POST',
body: formData,
onUploadProgress: function(progressEvent) {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
document.getElementById('progressBar').value = percentCompleted;
}
});
});
</script>
| 方面 | 基础/学习 | 生产环境推荐 |
|---|---|---|
| 框架 | Servlet / Spring Boot | Spring Boot |
| 存储 | 服务器本地硬盘 | 云存储 (OSS/S3) |
| 处理 | 无 | FFmpeg (转码/封面) |
| 安全 | 简单后缀名检查 | 魔术数字 + 白名单 + 重命名 |
| 体验 | 无进度条 | 前端进度条 |
从学习角度看,从 Servlet 开始理解原理是很好的,但从实际项目出发,强烈推荐使用 Spring Boot + 云存储 + FFmpeg 的组合,这才是构建健壮、可扩展、安全的视频上传服务的标准做法。
