全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-18 6 分钟 ✍️ juanwangdev

Spring MVC 文件下载实现

文件下载是Web应用的常见功能,Spring MVC提供了多种下载实现方式。

基本文件下载

Java
@RestController
@RequestMapping("/api/download")
public class DownloadController {

    @GetMapping("/{fileName}")
    public ResponseEntity<Resource> download(@PathVariable String fileName) {
        File file = new File("/upload/" + fileName);

        if (!file.exists()) {
            throw new FileNotFoundException("文件不存在");
        }

        Resource resource = new FileSystemResource(file);
        String contentType = "application/octet-stream";

        return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + fileName + "\"")
            .body(resource);
    }
}

中文文件名处理

Java
@GetMapping("/chinese/{fileName}")
public ResponseEntity<Resource> downloadChinese(
        @PathVariable String fileName,
        HttpServletRequest request) throws Exception {

    File file = new File("/upload/" + fileName);

    if (!file.exists()) {
        return ResponseEntity.notFound().build();
    }

    Resource resource = new FileSystemResource(file);

    // 处理中文文件名编码
    String encodedFileName = encodeFileName(fileName, request);

    return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + encodedFileName + "\"")
        .body(resource);
}

private String encodeFileName(String fileName, HttpServletRequest request) {
    String userAgent = request.getHeader("User-Agent");

    try {
        if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
            // IE浏览器
            return URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
        } else {
            // Chrome、Firefox等
            return new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
        }
    } catch (Exception e) {
        return fileName;
    }
}

流式下载

Java
@GetMapping("/stream/{fileName}")
public void downloadStream(@PathVariable String fileName,
                          HttpServletResponse response) throws IOException {

    File file = new File("/upload/" + fileName);

    if (!file.exists()) {
        response.setStatus(404);
        response.getWriter().write("文件不存在");
        return;
    }

    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
        "attachment; filename=\"" + fileName + "\"");
    response.setContentLengthLong(file.length());

    // 流式传输
    try (InputStream inputStream = new FileInputStream(file);
         OutputStream outputStream = response.getOutputStream()) {

        byte[] buffer = new byte[4096];
        int bytesRead;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }

        outputStream.flush();
    }
}

大文件分片下载

Java
@GetMapping("/chunk/{fileName}")
public ResponseEntity<Resource> downloadChunk(
        @PathVariable String fileName,
        @RequestHeader(value = "Range", required = false) String range) {

    File file = new File("/upload/" + fileName);

    if (!file.exists()) {
        return ResponseEntity.notFound().build();
    }

    long fileLength = file.length();
    Resource resource = new FileSystemResource(file);

    if (range == null) {
        // 无Range头,返回完整文件
        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + fileName + "\"")
            .header(HttpHeaders.ACCEPT_RANGES, "bytes")
            .body(resource);
    }

    // 解析Range头,如 "bytes=0-1023"
    String[] ranges = range.substring("bytes=".length()).split("-");
    long start = Long.parseLong(ranges[0]);
    long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileLength - 1;

    long contentLength = end - start + 1;

    return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + fileName + "\"")
        .header(HttpHeaders.ACCEPT_RANGES, "bytes")
        .header(HttpHeaders.CONTENT_RANGE,
            "bytes " + start + "-" + end + "/" + fileLength)
        .contentLength(contentLength)
        .body(new PartialResource(resource, start, end));
}

内联显示(浏览器预览)

Java
@GetMapping("/inline/{fileName}")
public ResponseEntity<Resource> downloadInline(@PathVariable String fileName) {
    File file = new File("/upload/" + fileName);

    if (!file.exists()) {
        return ResponseEntity.notFound().build();
    }

    Resource resource = new FileSystemResource(file);
    String contentType = getContentType(fileName);

    return ResponseEntity.ok()
        .contentType(MediaType.parseMediaType(contentType))
        .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + fileName + "\"")
        .body(resource);
}

private String getContentType(String fileName) {
    String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();

    switch (extension) {
        case ".pdf": return "application/pdf";
        case ".jpg": return "image/jpeg";
        case ".png": return "image/png";
        case ".txt": return "text/plain";
        case ".html": return "text/html";
        default: return "application/octet-stream";
    }
}

批量文件下载

Java
@GetMapping("/batch")
public void downloadBatch(@RequestParam List<String> fileNames,
                         HttpServletResponse response) throws IOException {

    response.setContentType("application/zip");
    response.setHeader("Content-Disposition", "attachment; filename=\"files.zip\"");

    try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
        for (String fileName : fileNames) {
            File file = new File("/upload/" + fileName);

            if (file.exists()) {
                zipOut.putNextEntry(new ZipEntry(fileName));

                try (InputStream inputStream = new FileInputStream(file)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;

                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        zipOut.write(buffer, 0, bytesRead);
                    }
                }

                zipOut.closeEntry();
            }
        }
    }
}

下载统计

Java
@RestController
@RequestMapping("/api/download")
public class DownloadController {

    @Autowired
    private DownloadService downloadService;

    @GetMapping("/{id}")
    public ResponseEntity<Resource> downloadWithStats(@PathVariable Long id) {
        DownloadRecord record = downloadService.findById(id);

        if (record == null) {
            return ResponseEntity.notFound().build();
        }

        File file = new File(record.getFilePath());

        // 记录下载次数
        downloadService.incrementDownloadCount(id);

        Resource resource = new FileSystemResource(file);

        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=\"" + record.getOriginalName() + "\"")
            .body(resource);
    }
}

下载限速

Java
@GetMapping("/limited/{fileName}")
public void downloadLimited(@PathVariable String fileName,
                           HttpServletResponse response) throws IOException {

    File file = new File("/upload/" + fileName);

    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
        "attachment; filename=\"" + fileName + "\"");

    long maxSpeed = 1024 * 100;    // 100KB/s

    try (InputStream inputStream = new FileInputStream(file);
         OutputStream outputStream = response.getOutputStream()) {

        byte[] buffer = new byte[1024];
        int bytesRead;
        long totalRead = 0;
        long startTime = System.currentTimeMillis();

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            totalRead += bytesRead;

            // 限速控制
            long elapsedTime = System.currentTimeMillis() - startTime;
            long expectedTime = totalRead * 1000 / maxSpeed;

            if (elapsedTime < expectedTime) {
                Thread.sleep(expectedTime - elapsedTime);
            }
        }

        outputStream.flush();
    }
}

ClassPath资源下载

Java
@GetMapping("/classpath/{fileName}")
public ResponseEntity<Resource> downloadFromClassPath(@PathVariable String fileName) {
    Resource resource = new ClassPathResource("templates/" + fileName);

    if (!resource.exists()) {
        return ResponseEntity.notFound().build();
    }

    return ResponseEntity.ok()
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .header(HttpHeaders.CONTENT_DISPOSITION,
            "attachment; filename=\"" + fileName + "\"")
        .body(resource);
}

响应头设置对比

Header说明
Content-Typeapplication/octet-stream通用二进制类型
Content-Dispositionattachment下载到本地
Content-Dispositioninline浏览器预览
Accept-Rangesbytes支持分片下载
Content-Rangebytes start-end/total分片下载范围

要点总结

  • ResponseEntity返回文件资源
  • Content-Disposition: attachment触发下载
  • Content-Disposition: inline浏览器预览
  • 中文文件名需特殊编码处理
  • Range请求头支持大文件分片下载
  • 批量下载可打包为ZIP文件

📝 发现内容有误?点击此处直接编辑

← 上一篇 Spring MVC 文件上传原理与配置
下一篇 → Spring MVC 文件大小与类型限制
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库