本文目录导读:

Servlet实现文件上传与下载:从原理到实战的完整指南
目录导读
- 文件上传与下载基础原理
- Servlet处理文件上传的三大组件(Part、MultipartConfig、Apache Commons FileUpload)
- 文件上传实战代码解析(单文件/多文件)
- 文件下载的两种实现方式(直接流输出与断点续传)
- 安全防护与性能优化
- 常见问题解答(Q&A)
文件上传与下载基础原理
文件上传本质上是通过HTTP协议的POST请求,将客户端二进制数据(文件内容)以multipart/form-data格式发送到服务器端,Servlet容器解析请求体,将文件内容提取为InputStream流,文件下载则相反,服务器读取本地文件(或数据库中的BLOB),通过设置响应头(Content-Disposition: attachment)将二进制流写回客户端。
关键点:文件传输的核心是处理输入/输出流(I/O Stream),以及合理的内存管理。
Servlet处理文件上传的核心组件
在Servlet 3.0+规范中,官方引入了@MultipartConfig注解和Part接口,极大简化了文件上传实现,若使用旧版或特殊需求,可选择Apache Commons FileUpload库。
| 组件 | 适用版本 | 特点 |
|---|---|---|
| @MultipartConfig + Part | Servlet 3.0+ | 原生支持,无需额外JAR |
| Commons FileUpload | 所有版本 | 功能丰富,支持内存/磁盘阈值 |
核心思路:通过request.getPart()或request.getParts()获取上传文件对象,然后调用write()方法保存到服务器目录。
文件上传实战代码解析
1 使用Servlet原生API实现单文件上传
@WebServlet("/upload")
@MultipartConfig(
location = "/tmp", // 临时目录
fileSizeThreshold = 1024 * 1024, // 1MB内存阈值
maxFileSize = 1024 * 1024 * 10, // 10MB单文件大小
maxRequestSize = 1024 * 1024 * 50 // 50MB总请求大小
)
public class FileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 获取上传文件Part
Part filePart = req.getPart("file"); // 表单name属性
String fileName = filePart.getSubmittedFileName();
// 2. 防止路径穿越攻击
fileName = new File(fileName).getName();
// 3. 存储到上传目录
String uploadPath = getServletContext().getRealPath("/uploads");
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdirs();
filePart.write(uploadPath + File.separator + fileName);
resp.getWriter().write("文件上传成功:" + fileName);
}
}
关键点:@MultipartConfig必须配置在Servlet上;getSubmittedFileName()在部分容器中可能返回完整路径,需提取文件名。
2 多文件上传处理
HTML表单需设置multiple属性:<input type="file" name="files" multiple>
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Collection<Part> parts = req.getParts();
for (Part part : parts) {
if (part.getContentType() != null) { // 仅处理文件域
String fileName = part.getSubmittedFileName();
part.write(uploadPath + File.separator + fileName);
}
}
}
核心逻辑:遍历getParts(),通过getContentType()或getSize()判断是否为文件域(普通表单域ContentType为null)。
文件下载的两种实现方式
1 基础下载(标准流输出)
@WebServlet("/download")
public class FileDownloadServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String fileName = req.getParameter("file");
String filePath = getServletContext().getRealPath("/uploads") + File.separator + fileName;
File downloadFile = new File(filePath);
if (!downloadFile.exists()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 设置响应头
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition",
"attachment; filename=\"" + URLEncoder.encode(fileName, "UTF-8") + "\"");
resp.setContentLengthLong(downloadFile.length());
// 流复制
try (InputStream is = new FileInputStream(downloadFile);
OutputStream os = resp.getOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
}
}
关键配置:
Content-Disposition: attachment强制浏览器下载URLEncoder.encode解决中文文件名乱码- 使用
try-with-resources自动关闭流
2 支持断点续传的高级下载
String rangeHeader = req.getHeader("Range");
if (rangeHeader != null) {
long rangeStart = Long.parseLong(rangeHeader.replace("bytes=", "").split("-")[0]);
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader("Content-Range", "bytes " + rangeStart + "-" + (fileSize-1) + "/" + fileSize);
// 从指定位置开始读取
}
适用场景:大文件下载、视频流播放。
安全防护与性能优化
1 安全防护要点
- 文件类型校验:检查
Content-Type和文件魔数(Magic Number),而非仅依赖扩展名 - 路径穿越防护:使用
new File(fileName).getName()过滤目录符号 - 大小限制:通过
@MultipartConfig的maxFileSize限制 - 存储隔离:文件存储路径不要放在Web根目录下,避免直接访问
2 性能优化建议
- 内存阈值设置:
fileSizeThreshold设置为1-4MB,小文件直接在内存处理 - 缓冲区大小:流复制时使用8KB以上缓冲区
- 异步处理:大文件上传可采用
AsyncContext - 避免重复读取:上传时如需获取文件内容,先将Part保存到临时文件再读取
常见问题解答(Q&A)
Q1: 上传文件时出现"Request exceeds the configured size limit"错误怎么办?
A: 增大@MultipartConfig中的maxRequestSize值,或调整容器配置(如Tomcat的maxSwallowSize),若上传文件超过50MB,建议采用分片上传方案。
Q2: 下载文件时中文名变成乱码如何解决?
A: 使用URLEncoder.encode(fileName, "UTF-8")编码文件名,并确保Content-Disposition头中包含filename*=UTF-8''格式:
resp.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName);
Q3: Servlet原生API和Commons FileUpload哪个更好?
A: 如果使用Servlet 3.0+容器,优先选择原生API(无需额外依赖、代码简洁),Commons FileUpload更适合需要底层控制(如自定义缓冲区、多Part处理策略)的场景。
Q4: 上传的文件如何防止被恶意执行?
A: 设置存储目录的权限为只读(不可执行);文件上传后重命名为UUID格式;将文件存储在与Web应用分离的目录(如/data/uploads),通过专用下载Servlet提供访问。
Q5: 如何实现文件上传的进度条?
A: Servlet原生API不支持进度跟踪,可通过以下方式实现:
- 前端使用XMLHttpRequest的
upload.onprogress事件 - 后端使用WebSocket或Session存储进度状态
- 使用Spring的
MultipartFile与CommonsMultipartResolver的监听器
通过以上六个维度的解析,您可以掌握Servlet处理文件上传与下载的完整技术栈,核心在于理解HTTP协议的多部分数据封装、输入输出流的生命周期管理,以及在实现中始终考虑安全性和性能边界,实际生产环境建议结合Spring MVC的MultipartFile封装,可进一步简化代码逻辑。