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;
相关推荐
weixin_7042660510 分钟前
Spring 注解驱动开发与 Spring Boot 核心知识点梳理
java·spring boot·spring
无籽西瓜a23 分钟前
【西瓜带你学设计模式 | 第五期 - 建造者模式】建造者模式 —— 产品构建实现、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·建造者模式
小江的记录本1 小时前
【Spring注解】Spring生态常见注解——面试高频考点总结
java·spring boot·后端·spring·面试·架构·mvc
计算机学姐1 小时前
基于SpringBoot的奶茶店点餐系统【协同过滤推荐算法+数据可视化统计】
java·vue.js·spring boot·mysql·信息可视化·tomcat·推荐算法
程序员cxuan1 小时前
来了来了,Claude Code 全架构解析 !!!
人工智能·后端·claude
艾莉丝努力练剑1 小时前
【Linux信号】Linux进程信号(下):可重入函数、Volatile关键字、SIGCHLD信号
linux·运维·服务器·c++·人工智能·后端·学习
常利兵2 小时前
Spring Boot 实现网络限速:让流量“收放自如”
网络·spring boot·后端
掘金者阿豪2 小时前
Claude Code“泄漏源码”曝光:Anthropic 最强终端 AI,原来早就不是聊天工具了
后端
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第七期 - 适配器模式】适配器模式 —— 类适配器与对象适配器实现、优缺点与适用场景
java·后端·设计模式·软件工程·适配器模式
前端付豪2 小时前
实现消息级操作栏
前端·人工智能·后端