一、簡單大文件下載:
/*** 下載大文件* @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;