[JAVA]使用ZipOutputStream压缩多个excel

背景

项目需要实现一个下载接口,将数据根据条件分类,满足同一个条件的数据写入一个excel里,最终将所有excel打包成一个压缩包。

实现思路

  1. 打开一个ZipOutputStream 
  2. 新建Workbook
  3. 将一组数据写入Workbook
  4. 将Workbook写入ZipOutputStream 
  5. 重复步骤2至步骤5直至写完所有数据
  6. 关闭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项目测试一下。这里贴一张自测效果图。

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;
    }
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用Java可以将多个Excel文档存入一个文档并进行压缩的方法有多种。其中一种简单的实现方式如下: 首先,需要导入Apache POI的相关依赖库,POI提供了Java操作Office文档的功能。 1. 创建一个新的Excel文档(Workbook)对象,使用POI的Workbook类。可以使用HSSFWorkbook类(用于处理.xls格式)或XSSFWorkbook类(用于处理.xlsx格式)根据需要选择。如:HSSFWorkbook workbook = new HSSFWorkbook(); 2. 通过创建一个新的Sheet对象,使用workbook对象的createSheet方法。如:Sheet sheet = workbook.createSheet("Sheet1"); 3. 编写代码读取每个要存储的Excel文档,可以使用POI的Workbook类打开每个Excel文档。如:Workbook inputWorkbook = WorkbookFactory.create(new File("input.xlsx")); 4. 通过创建新的Row和Cell对象,并使用setCellValue方法将要存储的数据写入这些Cell中。如:Cell cell = row.createCell(0); cell.setCellValue(value); 5. 将读取的Excel文档的内容复制到新创建的Excel文档中。可以使用POI库的Sheet类的copyRows方法或copySheet方法进行复制。 6. 当需要将多个Excel文档压缩成一个文件时,可以使用JavaZipOutputStream类来实现。首先创建一个ZipOutputStream对象,然后将新创建的Excel文档以及其他要压缩的文件写入该输出流。如:ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream("output.zip")); 7. 将新创建的Excel文档输出到ZipOutputStream对象中。如:zipOut.putNextEntry(new ZipEntry("output.xlsx")); workbook.write(zipOut); zipOut.closeEntry(); 8. 最后,关闭所有打开的文件和流。 通过以上步骤,我们可以将多个Excel文档存入一个文档中,并将其压缩成一个zip文件。这样可以方便地存储和传输多个Excel文档。需要注意的是,代码实现可能会有所不同,因为具体的实现方式可能依赖于使用的库或工具。以上仅为一种可能的实现方式的简要介绍。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值