凌峰创科服务平台

java web 从服务器下载文件

核心原理

无论使用何种技术框架(Servlet, Spring Boot 等),从服务器下载文件的底层原理都是一致的:

java web 从服务器下载文件-图1
(图片来源网络,侵删)
  1. 设置响应头:通过 HTTP 响应头告诉浏览器,这次请求不是一个普通的 HTML 页面或 JSON 数据,而是一个需要下载的文件。
    • Content-Type: 文件的 MIME 类型(application/octet-stream 表示通用二进制流,application/pdf 表示 PDF 文件),浏览器会根据这个类型决定如何处理。
    • Content-Disposition: 这是最关键的头,它告诉浏览器这是一个附件,并建议一个文件名,格式通常是 attachment; filename="你的文件名.pdf"
    • Content-Length: 文件的大小(可选,但推荐),这能让浏览器显示下载进度条。
  2. 读取文件:在服务器端,通过文件流(FileInputStream)将要下载的文件读取到内存中。
  3. 写入响应流:将内存中的文件数据通过 ServletOutputStream 写入到 HTTP 响应体中。
  4. 刷新并关闭流:确保所有数据都被发送到客户端,然后关闭所有打开的流,释放资源。

实现方式

我们将介绍两种最主流的实现方式:

  1. 传统 Servlet 方式:理解底层原理的最佳方式,适用于任何 Java Web 容器(如 Tomcat, Jetty)。
  2. Spring Boot 方式:更现代、更简洁,利用框架封装好的工具类,代码量更少。

传统 Servlet 实现

这种方式直接操作 HttpServletResponse,能让你清晰地看到每一个步骤。

步骤:

  1. 创建一个 Servlet:处理下载请求。
  2. 获取文件路径:从请求参数或路径中获取要下载的文件名或 ID。
  3. 设置响应头:如上所述,设置 Content-Type, Content-Disposition 等。
  4. 读写文件流:使用 try-with-resources 语句自动关闭流,防止资源泄漏。

示例代码:

假设你的文件存放在 webapp/downloads/ 目录下。

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.*;
import java.net.URLEncoder;
@WebServlet("/downloadServlet")
public class FileDownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 获取要下载的文件名
        String fileName = request.getParameter("fileName");
        if (fileName == null || fileName.isEmpty()) {
            response.getWriter().write("Error: File name is required.");
            return;
        }
        // 2. 设置文件在服务器上的真实路径
        // getServletContext().getRealPath("/downloads/") 获取 webapp 下的 downloads 目录的绝对路径
        String filePath = getServletContext().getRealPath("/downloads/") + File.separator + fileName;
        File downloadFile = new File(filePath);
        // 3. 检查文件是否存在
        if (!downloadFile.exists()) {
            response.getWriter().write("Error: File not found.");
            return;
        }
        // 4. 设置响应头
        // 设置 Content-Type,这里使用通用二进制流,浏览器会提示下载
        response.setContentType("application/octet-stream");
        // 设置 Content-Disposition,告诉浏览器这是一个附件,并设置文件名
        // 注意:文件名可能包含中文或空格,需要进行 URL 编码
        String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
        // 设置 Content-Length,可选,但推荐
        response.setContentLengthLong(downloadFile.length());
        // 5. 读取文件并写入响应输出流
        // 使用 try-with-resources 确保 stream 自动关闭
        try (InputStream inStream = new FileInputStream(downloadFile);
             ServletOutputStream outStream = response.getOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inStream.read(buffer)) != -1) {
                outStream.write(buffer, 0, bytesRead);
            }
        } catch (IOException e) {
            // 处理 IO 异常,例如客户端取消下载
            System.err.println("Download failed for file: " + fileName);
            e.printStackTrace();
        }
    }
}

前端调用示例 (HTML):

java web 从服务器下载文件-图2
(图片来源网络,侵删)
<!DOCTYPE html>
<html>
<head>文件下载</title>
</head>
<body>
    <h1>文件下载列表</h1>
    <p><a href="downloadServlet?fileName=report.pdf">下载 report.pdf</a></p>
    <p><a href="downloadServlet?fileName=数据报表.xlsx">下载 数据报表.xlsx</a></p>
</body>
</html>

Spring Boot 实现

Spring Boot 极大地简化了 Web 开发,文件下载也不例外,我们可以直接返回 ResponseEntity,让 Spring 框架帮我们处理大部分底层工作。

步骤:

  1. 创建一个 Controller:处理下载请求。
  2. 获取文件路径:从请求参数或路径中获取文件名。
  3. 构建 Resource 对象:Spring 的 Resource 接口是对资源的抽象,可以是文件系统中的文件、类路径下的文件等。
  4. 返回 ResponseEntity:使用 ResponseEntity.ok()Resource 构建 HTTP 响应,并方便地设置头信息。

示例代码:

假设你的文件存放在 src/main/resources/downloads/ 目录下(在打包后,这个目录会位于 classpath 的根目录)。

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.RequestParam;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileDownloadController {
    // 文件在服务器上的存储目录(相对于 classpath 根目录)
    private static final String DOWNLOAD_DIR = "downloads/";
    @GetMapping("/download")
    public ResponseEntity<Resource> downloadFile(@RequestParam("fileName") String fileName) {
        try {
            // 1. 构建文件的 Path 对象
            Path filePath = Paths.get(DOWNLOAD_DIR).resolve(fileName).normalize();
            // 2. 将 Path 转换为 Spring 的 Resource 对象
            Resource resource = new UrlResource(filePath.toUri());
            // 3. 检查文件是否存在且可读
            if (!resource.exists() || !resource.isReadable()) {
                // 如果文件不存在,返回 404 Not Found
                return ResponseEntity.notFound().build();
            }
            // 4. 确定文件的 Content-Type
            String contentType = "application/octet-stream";
            // 可以根据文件扩展名设置更精确的 Content-Type
            // String contentType = probeContentType(filePath);
            // 5. 构建并返回 ResponseEntity
            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);
        } catch (IOException e) {
            // 处理异常,返回 500 Internal Server Error
            return ResponseEntity.internalServerError().build();
        }
    }
    // 可选:根据文件扩展名探测 Content-Type
    // private String probeContentType(Path filePath) throws IOException {
    //     return Files.probeContentType(filePath);
    // }
}

前端调用示例 (HTML):

和传统 Servlet 方式完全一样,因为请求的 URL 和参数格式是兼容的。

java web 从服务器下载文件-图3
(图片来源网络,侵删)
<!DOCTYPE html>
<html>
<head>文件下载 (Spring Boot)</title>
</head>
<body>
    <h1>文件下载列表</h1>
    <p><a href="/download?fileName=report.pdf">下载 report.pdf</a></p>
    <p><a href="/download?fileName=数据报表.xlsx">下载 数据报表.xlsx</a></p>
</body>
</html>

重要注意事项和最佳实践

  1. 中文文件名编码问题

    • 问题:如果文件名包含中文或特殊字符(如空格),直接放在 filename 属性中可能会导致浏览器解析错误,文件名变成乱码。
    • 解决方案:对文件名进行 URL 编码,如示例中的 URLEncoder.encode(fileName, "UTF-8"),对于不同浏览器,有时可能需要更复杂的处理(如 filename* 语法的 RFC 5987 编码),但 URLEncoder 在大多数现代浏览器中已经足够。
  2. 异常处理

    • 文件不存在:应返回一个明确的错误信息或 HTTP 404 状态码,而不是让服务器抛出 500 错误。
分享:
扫描分享到社交APP
上一篇
下一篇