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 实现文件下载的核心四步:

  1. 设置 Content-Type
  2. 设置 Content-Disposition: attachment
  3. 使用 getResourceAsStream 读取资源
  4. 通过 OutputStream 分块写入响应

⚠️ 常见坑点回顾:

  • 忘记设 Content-Disposition → 浏览器直接显示内容
  • 路径写错或资源不存在 → 返回空白或 500
  • 流未用 try-with-resources → 潜在内存泄漏
  • 大文件未分块 → OOM 风险

所有示例代码已托管至 GitHub:

👉 https://github.com/eugenp/tutorials/tree/master/web-modules/javax-servlets

实际项目中,若需支持断点续传、限速、大文件异步下载等,建议结合 Nginx 或使用 Spring Web 的 ResourceHttpRequestHandler,但底层原理不变。先掌握原生实现,才能更好理解高层封装。


原始标题:Example of Downloading File in a Servlet