一、简单大文件下载:
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;