背景
项目需要实现一个下载接口,将数据根据条件分类,满足同一个条件的数据写入一个excel里,最终将所有excel打包成一个压缩包。
实现思路
- 打开一个ZipOutputStream
- 新建Workbook
- 将一组数据写入Workbook
- 将Workbook写入ZipOutputStream
- 重复步骤2至步骤5直至写完所有数据
- 关闭ZipOutputStream
注意
步骤4中将Workbook中的数据写入ZipOutputStream时不能直接调用Workbook的write()方法,因为write()方法最终会关闭它写入的输出流。
原因分析
当我们只往输出流里写一个Workbook时,用write()方法很方便,因为写完之后输出流再也不会用到了。但是当我们用反复调用这个方法往输出流里写多个Workbook的时候,你会惊喜的发现,欸,异常了!Stream closed!!!写多少个表就报多少次异常!(当然准确地说是n-1个,因为写第一个的时候流还是开着的)
java.io.IOException: Stream closed
at java.util.zip.ZipOutputStream.ensureOpen(ZipOutputStream.java:97) ~[na:1.8.0_60]
at java.util.zip.ZipOutputStream.putNextEntry(ZipOutputStream.java:190) ~[na:1.8.0_60]
从输出的异常信息来看,是在执行ZipOutputStream.ensureOpen()的时候出错了,我们可以进源码看一下触发这个异常的条件。
private boolean closed = false;
private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
}
可以看到ZipOutputStream有一个标记"closed",表示流是否被关闭,默认是false。而ensureOpen()只有在closed为true时才会抛出异常。那么谁能修改这个标记呢?只有ZipOutputStream.close()。
public void close() throws IOException {
if (!closed) {
super.close();
closed = true;
}
}
由于我们并没有手动调用close()方法,且closed已经被修改,那么就说明这个方法在别的地方被调用了。我们来看一下Workbook的write()方法,关注传入的输出流是怎么被关闭的。
public void write(OutputStream stream) throws IOException {
Iterator i$ = this._xFromSxHash.values().iterator();
while(i$.hasNext()) {
SXSSFSheet sheet = (SXSSFSheet)i$.next();
sheet.flushRows();
}
File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx");
try {
FileOutputStream os = new FileOutputStream(tmplFile);
try {
this._wb.write(os);
} finally {
os.close();
}
//可以看到write()写数据的时候调用了injectData()
this.injectData(tmplFile, stream);
} finally {
if (!tmplFile.delete()) {
throw new IOException("Could not delete temporary file after processing:" + tmplFile);
}
}
}
private void injectData(File zipfile, OutputStream out) throws IOException {
ZipFile zip = new ZipFile(zipfile);
try {
//创建了out的包装流zos
ZipOutputStream zos = new ZipOutputStream(out);
InputStream is;
try {
for(Enumeration en = zip.entries(); en.hasMoreElements(); is.close()) {
ZipEntry ze = (ZipEntry)en.nextElement();
zos.putNextEntry(new ZipEntry(ze.getName()));
is = zip.getInputStream(ze);
XSSFSheet xSheet = this.getSheetFromZipEntryName(ze.getName());
if (xSheet != null) {
SXSSFSheet sxSheet = this.getSXSSFSheet(xSheet);
InputStream xis = sxSheet.getWorksheetXMLInputStream();
try {
copyStreamAndInjectWorksheet(is, zos, xis);
} finally {
xis.close();
}
} else {
copyStream(is, zos);
}
}
} finally {
//这里关闭了包装流zos
zos.close();
}
} finally {
zip.close();
}
}
其实关闭包装流的时候节点流就被关闭了,具体原因可以参考:Java IO包装流如何关闭? - 嗨,你的益达~~~ - 博客园 (cnblogs.com)
概括一下就是Java中流的创建使用的是装饰者模式,执行装饰类的方法时最终会调用被装饰类的对应方法。
正确写法
那么如何才能实现往一个ZipOutputStream中写入多个excel呢?
我们可以思考一下,既然问题是Workbook的write()方法最终关闭了输出流引起的,那么是不是可以模仿write()方法的思路,只要最后去掉关闭流的操作就可以了!
由于源码中的实现思路比较复杂,感兴趣的可以自己研究。我在这里给出一种简便的写法,核心就是将workbook数据写入临时文件,最后再将临时文件数据写入ZipOutputStream。这样write()关闭的就是临时文件的输出流,ZipOutputStream一直保持开启状态。当然最后记得自己把它关了,或者用try-with-resource。
为了方便大家看效果,我直接写了个controller,大家可以自己写个web项目测试一下。这里贴一张自测效果图。文章来源:https://www.toymoban.com/news/detail-775707.html
文章来源地址https://www.toymoban.com/news/detail-775707.html
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 多个excel压缩为一个压缩包
*/
@Slf4j
@RestController
@RequestMapping("/zip")
public class WorkbookCompress {
private final static int BUFFER_READ_SIZE = 10000;
@GetMapping("/excel")
public void compressExcels(HttpServletResponse response) {
List<SXSSFWorkbook> workbooks = getWorkbooks();
try(ServletOutputStream servletOutputStream = response.getOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(servletOutputStream)) {
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename= test.zip");
response.setHeader(HttpHeaders.CONTENT_TYPE, "application/zip");
SXSSFWorkbook wk1 = workbooks.get(0);
Workbook wk2 = workbooks.get(1);
//写第一个文件
zipOutputStream.putNextEntry(new ZipEntry(getName(wk1)+".xlsx"));
File tempFile = new File(getName(wk1));
FileOutputStream fos = new FileOutputStream(tempFile);
wk1.write(fos);
wk1.close();
FileInputStream fis = new FileInputStream(tempFile);
StreamUtils.copy(fis, zipOutputStream);
fis.close();
tempFile.delete();
zipOutputStream.closeEntry();
//写第二个文件
zipOutputStream.putNextEntry(new ZipEntry(getName(wk2)+".xlsx"));
tempFile = new File(getName(wk2));
fos = new FileOutputStream(tempFile);
wk2.write(fos);
wk2.close();
fis = new FileInputStream(tempFile);
StreamUtils.copy(fis, zipOutputStream);
fis.close();
tempFile.delete();
zipOutputStream.closeEntry();
} catch (Exception e) {
log.warn("error occurred, ", e);
}
}
public static List<SXSSFWorkbook> getWorkbooks() {
/*create workbooks*/
String[] keys = {"id", "name"};
//create data
List<Map<String, Object>> logs = new ArrayList<>();
Map<String, Object> log1 = new HashMap<>(2);
log1.put("id", 111);
log1.put("name", "aaa");
Map<String, Object> log2 = new HashMap<>(2);
log2.put("id", 222);
log2.put("name", "bbb");
Map<String, Object> log3 = new HashMap<>(2);
log3.put("id", 333);
log3.put("name", "aaa");
Map<String, Object> log4 = new HashMap<>(2);
log4.put("id", 444);
log4.put("name", "bbb");
logs.add(log1);
logs.add(log2);
logs.add(log3);
logs.add(log4);
//split
Map<String, List<Map<String, Object>>> results = logs.stream()
.collect(Collectors.groupingBy(log -> log.get("name").toString()));
List<Map<String, Object>> result = results.get("aaa");
//write to workbooks
SXSSFWorkbook wk1 = (SXSSFWorkbook)createWorkBook(results.get("aaa"), keys, keys);
SXSSFWorkbook wk2 = (SXSSFWorkbook)createWorkBook(results.get("bbb"), keys, keys);
ArrayList<SXSSFWorkbook> workbooks = new ArrayList<>();
workbooks.add(wk1);
workbooks.add(wk2);
return workbooks;
}
public static String getName(Workbook wk) {
Sheet sheet = wk.getSheetAt(0);
if (sheet != null) {
Row row = sheet.getRow(1);
Cell cell = row.getCell(1);
String value = cell.getStringCellValue();
return value;
}
return "default";
}
/**
* 创建excel文档
* @param list 数据
* @param keys list中map的key数组集合
* @param columnNames excel的列名
* */
public static Workbook createWorkBook(List<Map<String, Object>> list, String[] keys, String[] columnNames) {
// 创建excel工作簿
// HSSFWorkbook每个sheet最大数据量为65536, 而且内存占用高, 这里改为SXSSFWorkbook, edited By jianghan on V4.2
Workbook wb = new SXSSFWorkbook(BUFFER_READ_SIZE);
// 创建第一个sheet
Sheet sheet = wb.createSheet("sheet1");
// 设置列宽, 第一个参数表示要为第几列设, 第二个参数表示列的宽度。
for (int i=0; i<keys.length; i++) {
sheet.setColumnWidth(i, (int) 35.7 * 150);
}
// 创建第一行
Row row = sheet.createRow(0);
// 写入列名
for (int i=0; i < columnNames.length; i++) {
Cell cell = row.createCell(i);
cell.setCellValue(columnNames[i]);
}
// 写入各行各列的值
if (!CollectionUtils.isEmpty(list)) {
for (int i = 0; i < list.size(); i++) {
Row row1 = sheet.createRow(i+1);
for (int j=0; j< keys.length; j++) {
Cell cell = row1.createCell(j);
cell.setCellValue(list.get(i).get(keys[j]) == null ? " " : list.get(i).get(keys[j]).toString());
}
}
}
return wb;
}
}
到了这里,关于[JAVA]使用ZipOutputStream压缩多个excel的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!