背景
最近有个需求,是需要根据后台系统给的筛选条件导出一个Zip文件,Zip文件最终包含的数据有以下两个方面
- 查询出指定得数据,并将数据根据某个字段进行分组并 分为多个Sheet写入excel, 写入最终的Zip
- 将查询出的数据的 oss的url下载下来压缩为ZIP文件,也一并写入Zip文件
1 需求分析
1.1 ZipOutputStream
既然牵扯到了ZIP文件的下载,那么也就毫无疑问要牵扯到一个类ZipOutputStream,我们来看下这个类的源码:
/**
* This class implements an output stream filter for writing files in the
* ZIP file format. Includes support for both compressed and uncompressed
* entries.
*
* @author David Connelly
*/
public
class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
public void putNextEntry(ZipEntry e) throws IOException{}
public synchronized void write(byte[] b, int off, int len){}
public void closeEntry() throws IOException { }
翻译过来大概就是:
此类实现了一个输出流过滤器,用于以 ZIP 文件格式写入文件。包括对压缩和未压缩条目的支持。
我摘取了其中三个API,
方法名 | 解释 |
---|---|
putNextEntry | 写入新的 ZIP 文件条目并将流定位到条目数据的开头 |
write | 将指定的字节数组写入ZIP条目中 |
closeEntry | 关闭当前的Entry |
1.2 EasyExcel多Sheet写入
使用EasyExcel将数据写入多Sheet,一般使用其Api,按照如下格式即可。
File fileExcel = new File(xlsxPath);
ExcelWriter excelWriter = EasyExcel.write(fileExcel, User.class).build();
// 对数据分组,然后写入每一个Sheet中
Map<String, List<User>> sheetListMap = requests.stream()
.collect(Collectors.groupingBy(User::getName));
int index = 0;
// 遍历写入sheet中
for (Map.Entry<String, List<User>> entry : sheetListMap.entrySet()) {
String key = entry.getKey();
List<User> value = entry.getValue();
WriteSheet writeSheet = EasyExcel.writerSheet(index++, key).build();
excelWriter.write(value, writeSheet);
}
excelWriter.finish();
1.3 Aliyun-oss的下载文件
一般我们拿到Ali-oss下载的文件路径后,通过如下操作即可拿到其OutputStream,然后进行读写操作即可。
// 首先获取OssClient客户端
OSSClient ossClient = getOSSClient();
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest("bucket名称", "文件地址", HttpMethod.GET);
// 设置过期时间。
request.setExpiration(expiration);
// 生成签名URL(HTTP GET请求)。
URL signedUrl = ossClient.generatePresignedUrl(request);
// 使用签名URL发送请求。
OSSObject ossObject = ossClient.getObject(signedUrl, new HashMap<>());
if (ossObject != null) {
InputStream inputStream = ossObject.getObjectContent();
// 后续操作不再演示
......
}
2 流程&编码
大致的流程图:
废话不多说,直接上代码
2.1 Controller层
@RequestMapping(value = "/user/exportUserZipFile", method = RequestMethod.GET)
public void getExportData(UserQueryDTO userQueryDTO,
HttpServletResponse response) {
try {
userService.exportUserData(sampleQueryDTO, response);
} catch (IOException e) {
e.printStackTrace();
}
}
2.2 Service层
public void exportUserData(UserQueryDTO userQueryDTO, HttpServletResponse response) throws IOException {
// 查询过程不在演示
......
List<User> userList = new ArrayList();
// 1 定义三个临时文件,分别代表最终生成的zip文件、需要写入zip中的文件1-excel、需要写入zip中的文件2-pdf.zip
// 获取当前系统的临时目录
String FilePath = System.getProperty("java.io.tmpdir") + File.separator;
// 最终生成的zip文件
String zipFileName = "export.zip";
String tempzipPath = FilePath + zipFileName;
//需要加到ZIP的文件1- request.xlsx
String xlsxPath = FilePath + "export.xlsx";
// 需要加到ZIP的文件2- pdf.zip
String jpgPath = "jpg.zip";
String jpgZipPath = FilePath + jpgPath;
// 2 任务1:excel的处理
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(tempzipPath));
CompletableFuture cf1 = CompletableFuture.runAsync(() -> {
FileInputStream fileInputStream = null;
try {
// 1 先处理Excel文件,将Excel数据写入文件,然后写入ZIP中
File fileExcel = new File(xlsxPath);
ExcelWriter excelWriter = EasyExcel.write(fileExcel, User.class).build();
doProcessExcelByMultiSheet(excelWriter, userList);
byte[] buffer = new byte[4096];
// 读取文件写入zip
fileInputStream = new FileInputStream(fileExcel);
zipOutputStream.putNextEntry(new ZipEntry(fileExcel.getName()));
int len;
// 读入需要下载的文件的内容,打包到zip文件
while ((len = fileInputStream.read(buffer)) > 0) {
zipOutputStream.write(buffer, 0, len);
}
} catch (Exception e) {
log.error("导出zip文件,写入excel 失败,{}", e);
} finally {
StreamUtils.closeStream(fileInputStream);
}
}, executor);
// 2 任务2:aliyun-oss文件 的处理
CompletableFuture cf2 = CompletableFuture.runAsync(()-> {
FileInputStream fileInputStream2 = null;
try {
// 2 处理所有oss图片,打包成一个zip
File filePdf = new File(jpgZipPath );
Map<String, String> userFileNameMap = userList.stream()
.collect(Collectors.toMap(CovidSampleDTO::getImageUrl,
getUserJpgFileName(),
(v1,v2)-> v1));
//2.1 从aliyun-oss读取数据然后写入pdfZipOps中
AliYunOssClientUtil.batchDownLoadOssFileAndSaveTempFile(userFileNameMap,jpgZipPath );
//将image的zip文件写入到最后的zip中
byte[] buffer2 = new byte[4096];
fileInputStream2 = new FileInputStream(filePdf);
zipOutputStream.putNextEntry(new ZipEntry(filePdf.getName()));
int len2;
// 2.2 读入需要下载的文件的内容,打包到zip文件
while ((len2 = fileInputStream2.read(buffer2)) > 0) {
zipOutputStream.write(buffer2, 0, len2);
}
} catch (Exception e){
log.error("导出zip文件,pdf.zip 失败");
} finally {
StreamUtils.closeStream(fileInputStream2);
}
}, executor);
CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(cf1, cf2);
voidCompletableFuture.whenComplete((x, throwable) -> {
try {
zipOutputStream.closeEntry();
zipOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (throwable != null) {
log.error("发生异常了");
return;
} else {
if (FileUtil.file(tempzipPath).exists()) {
try {
downLoadFile(response, zipFileName, tempzipPath, xlsxPath, pdfZipPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
voidCompletableFuture.join()
}
3 出现的问题
3.1 ZipOutputStream
使用这个API的时候,zipOutputStream.putNextEntry(new ZipEntry(“fileName”)),必须保证fileName是独一无二的,否则会报错:
public void putNextEntry(ZipEntry e) throws IOException {
......
// 这个地方
if (! names.add(e.name)) {
throw new ZipException("duplicate entry: " + e.name);
}
3.2 Aliyun下载的时候
这个地方需要注意的是,需要把OSS的url进行处理,把你的公共前缀给去掉,否则会报错在指定的bucket上找不到文件,就是因为前缀未处理的原因。
例如: https://xxx.xxx.cn/jpg/xxx/12312.jpg
需要将https://xxx.xxx.cn前缀给去掉,否则会报错
3.3 为什么考虑异步呢?
因为导出Excel和导出用户头像的ZIP文件是两个不相干的操作,没必要让他同步进行,将其异步,主线程阻塞等待两个线程都完成,然后下载即可。
感谢大家,如果有帮助到您欢迎点个红心,你们的支持是我最大的动力,有问题欢迎评论区指正或者留言~~~~