一、简单大文件下载:
/*** 下载大文件* @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);
}
二、多文件下载:
/*** 起线程压缩文件,返回流* @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.引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
2.单文件下载
/*** 响应式流式下载文件,支持断点续传* @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.多文件下载
/*** 响应式流式下载多个文件,不支持断点续传,因为需要压缩多个文件,长度未知* @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配置
proxy_buffering off;