WebFlux实现文件打包压缩下载
1. 前言
基于传统的SpringMVC框架下,实现一个这样的功能并不难,但是在响应式编程中,使用webflux实现时却大不一样,需要做到无阻塞,符合响应式规范。来一起看看吧
2. 从入口开始,前端部分
我们假设一种常见的场景,用户需要一个批量导出的功能,所以会勾选表单中的多个,然后拿到对应的id,传给后端,这里我传递的直接是文件名
/**
* 批量导出
*/
function batchExport(){
// 获取表单
const form = document.getElementById("fileForm");
// 获取所有选中的复选框
const checkedBoxes = [];
const checkboxes = form.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
checkedBoxes.push($(checkbox).attr("id"));
}
});
//异步请求
fetch(contextPath + "share/batchDownloadFile", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileNames: checkedBoxes,
})
})
.then(response => {
const disposition = response.headers.get('Content-Disposition');
const filename = disposition ? disposition.split('filename=')[1].replaceAll("\"","") : 'compressedFiles.zip';
return response.blob().then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
});
})
.catch(error => {console.error('Error:', error);});
}
因为发送的是POST请求,所以拿到后端返回的是直接的字节数组,需要对返回的数据再进行一次转换,提供一个虚拟的a标签触发浏览器的下载。
const disposition = response.headers.get('Content-Disposition');
const filename = disposition ? disposition.split('filename=')[1].replaceAll("\"","") : 'compressedFiles.zip';
return response.blob().then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
});
接着我们看后段部分
3. 数据处理,后端部分
直接使用Mono<String> requestBody
接收前端的请求参数,这样的操作需要主要的是,不要直接像传统MVC框架一样,在方法第一行就直接
log.info("batchDownloadFile received payload:{}", requestBody);
这样的操作是会有问题的,因为requestBody
并不是实际的请求参数,而是Mono对象。
完整代码如下
/**
* 批量下载文件
*
* @param requestBody 请求正文
* @return {@link Mono}<{@link ResponseEntity}<{@link Flux}<{@link DataBuffer}>>>
*/
@PostMapping(value = "/batchDownloadFile", produces = "application/zip")
public Mono<ResponseEntity<Flux<DataBuffer>>> batchDownloadFile(@RequestBody Mono<String> requestBody) {
return requestBody.doOnNext(it -> log.info("batchDownloadFile received payload:{}", it))
.map(JSON::parseObject)
.doOnError(it -> {
it.printStackTrace();
log.error("batchDownloadFile failed");
})
.map(it -> {
//获取入参中所有的文件名,进行遍历,并增加到一个zip文件中
final Stream<String> fileNames = Optional.ofNullable(it.getJSONArray("fileNames"))
.orElse(new JSONArray())
.stream()
.map(fileName -> exportPath + File.separator + fileName);
//压缩多个文件并转为字节数组
return zipFileToByteArray(fileNames);
})
.flatMap(data -> {
//上一个map传到这的已经是字节数组。我们直接对字节数组进行返回就行
//设置响应headers
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentDispositionFormData("Content-Disposition", "download.zip");
final ResponseEntity<Flux<DataBuffer>> body = ResponseEntity.ok()
.headers(httpHeaders)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
//最后ResponseEntity中的body,则是由new DefaultDataBufferFactory().wrap包裹响应出去
.body(Flux.just(data).map(b -> new DefaultDataBufferFactory().wrap(b)));
return Mono.just(body);
});
}
/**
* 压缩多个文件并转为字节数组
*
* @param filepathList 文件路径列表
* @return {@link byte[]}
*/
@SneakyThrows
private static byte[] zipFileToByteArray(Stream<String> filepathList) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
filepathList.forEach(file -> addToZipFile(file, zipOutputStream));
zipOutputStream.close();
return byteArrayOutputStream.toByteArray();
}
/**
* 压缩某一个文件到返回的zip文件中
*
* @param fileName 文件名
* @param zos 佐斯
*/
@SneakyThrows
private static void addToZipFile(String fileName, ZipOutputStream zos){
File file = new File(fileName);
try (FileInputStream fis = new FileInputStream(file);){
//这里的fileName可以控制生成的压缩包当中的文件的嵌套层级关系
ZipEntry zipEntry = new ZipEntry(FilenameUtils.getName(fileName));
zos.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zos.write(bytes, 0, length);
}
zos.closeEntry();
}
}
3.1 响应的对象为什么是Mono<ResponseEntity<Flux<DataBuffer>>>
可能会有人有疑问,有必要嵌套这么多层吗?
其实如果只有ResponseEntity<Flux<DataBuffer>>
也是可以的,但是如果你这么写那么就和传统的MVC写法一样了,我们都知道在响应式编程中,只有subscribes
的时候才会真正出发整个流的执行,然后生成元素,所以我们需要返回的是Mono<ResponseEntity>
而不是ResponseEntity<T>
接着ResponseEntity<Flux<DataBuffer>>
,有必要再包裹一层吗?
使用ResponseEntity<Flux<DataBuffer>>
来包裹Flux<DataBuffer>
。这是因为在处理流式数据时,通常使用Flux
来表示一系列的数据块。这种方式可以异步地将数据块逐个发送到客户端,而不是等待整个响应的数据都准备好后再发送。这种异步处理可以提高性能并降低内存消耗。