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;
相关推荐
老李不敲代码9 分钟前
榕壹云无人共享系统:基于SpringBoot+MySQL+UniApp的物联网共享解决方案
spring boot·物联网·mysql·微信小程序·uni-app·软件需求
慕容静漪4 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ4 小时前
Golang|锁相关
开发语言·后端·golang
烛阴5 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
良许Linux5 小时前
请问做嵌入式开发C语言应该学到什么水平?
后端
Pitayafruit5 小时前
SpringBoot整合Flowable【08】- 前后端如何交互
spring boot·后端·workflow
小丁爱养花6 小时前
驾驭 Linux 云: JavaWeb 项目安全部署
java·linux·运维·服务器·spring boot·后端·spring
uhakadotcom6 小时前
Amazon GameLift 入门指南:六大核心组件详解与实用示例
后端·面试·github
小杨4046 小时前
springboot框架项目实践应用十九(nacos配置中心)
spring boot·后端·spring cloud