1. 概述
文件下载是 Web 应用中最常见的功能之一。
本文将✅手把手演示如何通过 Java Servlet 实现一个简单的文件下载功能。
我们下载的文件来源于 Web 应用的资源目录(webapp resources),整个过程不依赖任何第三方框架,原生 Servlet 即可搞定。
这类功能看似简单,但实际开发中很容易踩坑,比如 MIME 类型写错导致浏览器直接打开文件而不是下载,或者流未正确关闭引发内存泄漏。本文帮你避坑。
2. Maven 依赖
如果你使用的是 Jakarta EE(如 Tomcat 10+),Servlet API 已内置,✅无需额外引入依赖。
但如果你还在用 Java EE 环境(比如 Tomcat 9 及以下),则需要手动添加 javax.servlet-api
:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
📌 最新版本可参考 Maven Central。
⚠️ 注意:<scope>provided</scope>
是必须的,因为运行时容器(如 Tomcat)已经提供了该依赖,打包时不应包含进去,否则可能引发冲突。
3. Servlet 实现
先看代码,再逐段拆解:
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
private final int ARBITARY_SIZE = 1048;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.setHeader("Content-disposition", "attachment; filename=sample.txt");
try(InputStream in = req.getServletContext().getResourceAsStream("/WEB-INF/sample.txt");
OutputStream out = resp.getOutputStream()) {
byte[] buffer = new byte[ARBITARY_SIZE];
int numBytesRead;
while ((numBytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, numBytesRead);
}
}
}
}
3.1. 接口映射(Endpoint Mapping)
@WebServlet("/download")
✅ 使用 @WebServlet
注解将 DownloadServlet
映射到 /download
这个接口路径。
替代方案是使用传统的 web.xml
配置:
<servlet>
<servlet-name>DownloadServlet</servlet-name>
<servlet-class>com.example.DownloadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadServlet</servlet-name>
<url-pattern>/download</url-pattern>
</servlet-mapping>
现代开发推荐注解方式,简单粗暴,少写配置。
3.2. 设置响应 Content-Type
resp.setContentType("text/plain");
这行代码设置 HTTP 响应头中的 Content-Type
,也叫 媒体类型(Media Type),以前叫 MIME Type。
常见取值包括:
text/plain
→ 纯文本application/pdf
→ PDF 文件image/jpeg
→ JPEG 图片application/octet-stream
→ 通用二进制流(适合未知类型)
📌 官方注册表由 IANA 维护:IANA Media Types
本例是 .txt
文件,所以用 text/plain
。❌别乱用 application/octet-stream
,虽然能下载,但丢失了类型语义。
3.3. 设置 Content-Disposition
resp.setHeader("Content-disposition", "attachment; filename=sample.txt");
这个头是控制浏览器行为的关键:
inline
→ 在浏览器中直接打开(如预览 PDF)attachment
→ 弹出下载对话框或直接下载
📌 默认是 inline
,所以必须显式设置为 attachment
才能触发下载。
filename
参数指定下载时的默认文件名。浏览器会优先使用这个值,但最终是否显示对话框,取决于用户设置和浏览器类型(Chrome、Firefox 行为可能不同)。
⚠️ 文件名含中文时需做 URL 编码或使用 filename*
扩展语法,否则可能乱码。本文略过,后续可单独展开。
3.4. 文件读取与输出流写入
核心逻辑:
try(InputStream in = req.getServletContext().getResourceAsStream("/WEB-INF/sample.txt");
OutputStream out = resp.getOutputStream()) {
byte[] buffer = new byte[ARBITARY_SIZE];
int numBytesRead;
while ((numBytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, numBytesRead);
}
}
关键点:
- ✅ 使用
ServletContext.getResourceAsStream()
读取 Web 应用内的资源,路径以/
开头表示 Web Root。 - ✅
/WEB-INF/
目录是安全的,用户无法直接通过 URL 访问,适合存放下载文件。 - ✅
getOutputStream()
获取响应输出流,用于写入文件数据。 - 缓冲区大小
ARBITARY_SIZE
设为 1048 字节,属于经验值。太小 → 循环次数多,性能差;太大 → 内存占用高。一般 1KB~8KB 之间平衡即可。
数据从输入流 → 缓冲区 → 输出流,逐块传输,避免一次性加载大文件到内存。
3.5. 流的关闭与刷新
try (InputStream in = ...; OutputStream out = ...) {
// 自动关闭
}
✅ 使用 try-with-resources 语法,JVM 会在块结束时自动调用 close()
,释放文件句柄和网络连接。
📌 OutputStream
在 close 时会自动 flush,无需手动调用 flush()
。
❌ 错误写法:手动 try-catch-close,不仅啰嗦还容易漏写。
3.6. 测试下载功能
部署应用后,访问:
http://localhost:8080/your-app/download
浏览器应弹出下载对话框,或直接下载 sample.txt
文件。
📌 确保 /WEB-INF/sample.txt
文件存在,内容可任意,例如:
Hello, this is a sample download file.
Created by dev@mycompany.com
4. 总结
通过本文,你应该掌握了:
✅ 原生 Servlet 实现文件下载的核心四步:
- 设置
Content-Type
- 设置
Content-Disposition: attachment
- 使用
getResourceAsStream
读取资源 - 通过
OutputStream
分块写入响应
⚠️ 常见坑点回顾:
- 忘记设
Content-Disposition
→ 浏览器直接显示内容 - 路径写错或资源不存在 → 返回空白或 500
- 流未用 try-with-resources → 潜在内存泄漏
- 大文件未分块 → OOM 风险
所有示例代码已托管至 GitHub:
👉 https://github.com/eugenp/tutorials/tree/master/web-modules/javax-servlets
实际项目中,若需支持断点续传、限速、大文件异步下载等,建议结合 Nginx 或使用 Spring Web 的 ResourceHttpRequestHandler
,但底层原理不变。先掌握原生实现,才能更好理解高层封装。