spring boot大文件与多文件下载

一、简单大文件下载:

java 复制代码
/**
 * 下载大文件
 * @param path 路径
 * @param fileName 文件名
 * @return
 * @throws IOException
 */
public static ResponseEntity<InputStreamResource> downloadFile(String path, String fileName) throws IOException {
    Path filePath = Paths.get(path);
    long size = Files.size(filePath);
    InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION, STR."attachment; filename=\{URLEncoder.encode(fileName, StandardCharsets.UTF_8)}");
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
    headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");

    return ResponseEntity.ok()
            .headers(headers)
            .contentLength(size)
            .body(resource);
}

二、多文件下载:

java 复制代码
/**
 * 起线程压缩文件,返回流
 * @param pathList 路径集合
 * @param fileName 文件名
 * @return
 */
public static ResponseEntity<InputStreamResource> zipByThread(List<String> pathList, String fileName) throws IOException {
    PipedInputStream pipedInputStream = new PipedInputStream();
    PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream);
    // 启动新线程以写入管道输出流
    Thread.ofVirtual().start(() -> {
        try (ZipOutputStream zos = new ZipOutputStream(pipedOutputStream)) {
            for (String path : pathList) {
                Path filePath = Paths.get(path);
                if (Files.exists(filePath)) {
                    if (Files.isDirectory(filePath)) {
                        zipDirectory(filePath, filePath.getFileName().toString(), zos);
                    } else {
                        zipFile(filePath, zos);
                    }
                }
            }
        } catch (IOException e) {
            log.error("文件压缩失败:{}", e.getMessage());
            throw new BusinessException("文件压缩失败");
        } finally {
            try {
                pipedOutputStream.close();
            } catch (IOException _) {}
        }
    });
    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_DISPOSITION, STR."attachment; filename=\{URLEncoder.encode(fileName, StandardCharsets.UTF_8)}");
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
    headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(-1));

    return ResponseEntity.ok().headers(headers).body(new InputStreamResource(pipedInputStream));
}

/**
 * 压缩文件夹
 * @param folder
 * @param parentFolder
 * @param zos
 * @throws IOException
 */
private static void zipDirectory(Path folder, String parentFolder, ZipOutputStream zos) throws IOException {
    try (Stream<Path> paths = Files.walk(folder)) {
        paths.filter(Files::isRegularFile).forEach(path -> {
            String zipEntryName = Paths.get(parentFolder).resolve(folder.relativize(path)).toString().replace("\\", "/");
            try {
                zos.putNextEntry(new ZipEntry(zipEntryName));
                Files.copy(path, zos);
                zos.closeEntry();
            } catch (IOException e) {
                throw new BusinessException("压缩文件夹失败");
            }
        });
    }
}

/**
 * 压缩文件
 * @param file
 * @param zos
 * @throws IOException
 */
private static void zipFile(Path file, ZipOutputStream zos) throws IOException {
    zos.putNextEntry(new ZipEntry(file.getFileName().toString()));
    Files.copy(file, zos);
    zos.closeEntry();
}

三、 Reactive WebFlux 实现非阻塞流式传输(推荐)

1.引入依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2.单文件下载
java 复制代码
/**
 * 响应式流式下载文件,支持断点续传
 * @param folderPath 文件路径
 * @param fileName 文件名
 * @return 响应实体
 */
@GetMapping("/download/one")
public Mono<ResponseEntity<Resource>> download(@RequestParam String folderPath,
                                               @RequestParam String fileName) throws IOException {
    // 获取文件
    Path basePath = Paths.get(folderPath).toAbsolutePath().normalize();
    Path filePath = basePath.resolve(fileName).normalize();

    // 校验文件路径,防止访问到其他目录
    if (!filePath.startsWith(basePath)) {
        throw new SecurityException("文件路径不在允许的目录内");
    }

    // 读取文件
    Resource resource = new PathResource(filePath);
    if (!resource.exists()) {
        return Mono.just(ResponseEntity.notFound().build());
    }

    // 获取文件大小
    long contentLength = resource.contentLength();
    // 获取文件类型
    MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
    // 响应文件
    return Mono.just(ResponseEntity.ok()
           .contentType(mediaType)
           .contentLength(contentLength)
           .header(HttpHeaders.ACCEPT_RANGES, "bytes")
           .body(resource));
}
3.多文件下载
java 复制代码
/**
 * 响应式流式下载多个文件,不支持断点续传,因为需要压缩多个文件,长度未知
 * @param folderPath 文件路径
 * @param fileNames 文件名列表
 * @return 响应实体
 */
@GetMapping("/download/multiple")
public Mono<ResponseEntity<InputStreamResource>> downloadMultipleFile(@RequestParam String folderPath,
                                                                      @RequestParam List<String> fileNames) throws IOException {
    // 验证文件路径
    Path basePath = Paths.get(folderPath).toAbsolutePath().normalize();
    List<Path> filePaths = new ArrayList<>(fileNames.size());
    for (String fileName : fileNames) {
        Path filePath = basePath.resolve(fileName).normalize();
        if (!filePath.startsWith(basePath)) {
            throw new SecurityException("文件路径不在允许的目录内");
        }
        filePaths.add(filePath);
    }

    // 创建管道流
    PipedOutputStream pos = new PipedOutputStream();
    PipedInputStream pis = new PipedInputStream(pos, 1024 * 32);

    // 在单独的线程中执行压缩
    Thread.ofVirtual().start(() -> {
        try (ZipOutputStream zos = new ZipOutputStream(pos)) {
            // 设置压缩级别
            zos.setLevel(Deflater.BEST_SPEED);
            for (Path path : filePaths) {
                if (!Files.exists(path)) {
                    continue;
                }
                if (Files.isDirectory(path)) {
                    zipDirectory(path, path.getFileName().toString(), zos);
                } else {
                    zipFile(path, zos);
                }
            }
            zos.finish();
        } catch (IOException e) {
            // 处理错误
        } finally {
            try {
                pos.close();
            } catch (IOException e) {
                // 忽略关闭异常
            }
        }
    });

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    headers.setContentDispositionFormData("attachment", "download.zip");
    headers.setContentLength(-1);

    return Mono.just(ResponseEntity.ok()
            .headers(headers)
            .body(new InputStreamResource(pis)));
}

/**
 * 压缩文件夹
 * @param folder 文件夹路径
 * @param parentFolder 父文件夹名
 * @param zos 压缩输出流
 * @throws IOException 异常
 */
private static void zipDirectory(Path folder, String parentFolder, ZipOutputStream zos) throws IOException {
    try (Stream<Path> paths = Files.walk(folder)) {
        paths.filter(Files::isRegularFile).forEach(path -> {
            String zipEntryName = Paths.get(parentFolder).resolve(folder.relativize(path)).toString().replace("\\", "/");
            try {
                zos.putNextEntry(new ZipEntry(zipEntryName));
                Files.copy(path, zos);
                zos.closeEntry();
            } catch (IOException e) {
                throw new RuntimeException("压缩文件夹失败");
            }
        });
    }
}

/**
 * 压缩文件
 * @param file 文件路径
 * @param zos 压缩输出流
 * @throws IOException 异常
 */
private static void zipFile(Path file, ZipOutputStream zos) throws IOException {
    zos.putNextEntry(new ZipEntry(file.getFileName().toString()));
    Files.copy(file, zos);
    zos.closeEntry();
}

注意

如果使用了nginx反代,下载超时需要在nginx配置

java 复制代码
proxy_buffering off;
相关推荐
gnawkhhkwang几秒前
Flask + YARA-Python*实现文件扫描功能
后端·python·flask
whhhhhhhhhw14 分钟前
Go语言常量
开发语言·后端·golang
LaoZhangAI22 分钟前
ChatGPT 5发布日期揭秘:2025年8月上线,多模态推理能力全面升级
前端·后端
平凡运维之路26 分钟前
生成ip可授信证书文件
后端
汪子熙28 分钟前
什么是计算机软件测试领域的 false positive?
后端
二闹28 分钟前
面试必杀技:如何把“秒杀系统”讲得明明白白?
后端·面试
_杨瀚博31 分钟前
Maven 构建知识库
java·后端
.又是新的一天.1 小时前
SpringBoot+SpringMVC常用注解
java·spring boot·后端
三木水1 小时前
Spring-rabbit使用实战六
java·后端·spring·消息队列·java-rabbitmq
夕颜1111 小时前
Claude AI 编程初体验
后端