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;
相关推荐
Q_Q19632884754 分钟前
python小说网站管理系统-小说阅读系统
开发语言·spring boot·python·django·flask·node.js·php
喵手6 分钟前
Spring Boot 中的事务管理是如何工作的?
数据库·spring boot·后端
玄武后端技术栈2 小时前
什么是延迟队列?RabbitMQ 如何实现延迟队列?
分布式·后端·rabbitmq
液态不合群3 小时前
rust程序静态编译的两种方法总结
开发语言·后端·rust
bingbingyihao3 小时前
SpringBoot教程(vuepress版)
java·spring boot·后端
一切皆有迹可循4 小时前
Spring Boot 基于 CAS 实现单点登录:原理、实践与优化全解析
java·spring boot·后端
Clf丶忆笙4 小时前
Spring Boot配置文件详解:从入门到精通
java·spring boot
上官箫羽4 小时前
Spring Boot 单元测试使用教程(仅供参考)
java·spring boot·单元测试
Kookoos4 小时前
从单体到微服务:基于 ABP vNext 模块化设计的演进之路
后端·微服务·云原生·架构·c#·.net
weixin_438335406 小时前
springboot使用阿里云OSS实现文件上传
spring boot·后端·阿里云