【EasyExcel实践】导出多个sheet到多个excel文件,并压缩到一个zip文件

前言

工作中遇到一个需求,一次导出多个Excel 文件,并且每个excel中可能存在1到多个sheet页。
好在没有那种单元格合并的要求。

总体的思路是,设计两个实体,一个表示表格,一个表示sheet 数据。并且表格包含一个list 类型的sheet对象。

然后再使用ZipOutputStreamExcelWriterBuilderEasyExcel#writerSheet(...) 等类和方法去组装表格,最终进行压缩。

项目整体使用 java 8 和 阿里的easyexcel工具包。

正文

一、项目依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.11</version>

            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

二、封装表格实体和Sheet实体

2.1 表格实体

/**
 * excel数据
 */
@Data
public static class ExcelData {
	// sheet的列表
    private final List<ExcelShellData<?>> shellDataList = new ArrayList<>();
    // 表格文件名
    private String filename;

    public void addShellData(ExcelShellData<?> excelShellData) {
        this.shellDataList.add(excelShellData);
    }
}

2.2 Sheet实体

/**
 * sheet数据
 */
@Data
@AllArgsConstructor
public static class ExcelShellData<T> {
	// 数据
    private List<T> list;
    // sheet名
    private String sheetName;
    // 数据实体类型
    private Class<T> clazz;
}

三、核心实现

3.1 核心实现之导出为输出流

这一步主要组装表格数据,以及生成sheet。最终将数据放到输出流outputStream中 。


    private static void exportZipStream(List<ExcelData> excelDataList, OutputStream outputStream) {
        try {
            // 开始存入
            try (ZipOutputStream zipOut = new ZipOutputStream(outputStream)) {
                try {
                    for (ExcelData excelData : excelDataList) {
                        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                        com.alibaba.excel.ExcelWriter excelWriter = null;
                        try {
                            ExcelWriterBuilder builder = EasyExcel.write(byteArrayOutputStream).autoCloseStream(false)
                                    // 自动适配
                                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());

                            excelWriter = builder.build();
                            zipOut.putNextEntry(new ZipEntry(excelData.getFilename()));
                            // 开始写入excel
                            for (ExcelShellData<?> shellData : excelData.getShellDataList()) {
                                WriteSheet writeSheet = EasyExcel.writerSheet(shellData.getSheetName()).head(shellData.getClazz()).build();
                                excelWriter.write(shellData.getList(), writeSheet);
                            }

                        } catch (Exception e) {
                            throw new RuntimeException("导出Excel异常", e);
                        } finally {
                            if (excelWriter != null) {
                                excelWriter.finish();
                            }
                        }
                        byteArrayOutputStream.writeTo(zipOut);
                        zipOut.closeEntry();
                    }
                } catch (Exception e) {
                    throw new RuntimeException("导出Excel异常", e);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常", e);
        }
    }

3.2 web导出

可以调用本方法,直接在Controller中调用之后,当访问对应url,会直接下载到浏览器。

    /**
     * 导出多个sheet到多个excel文件,并压缩到一个zip文件
     */
    public static void exportZip(String zipFilename, List<ExcelData> excelDataList, HttpServletResponse response) {
        try {
            // 这里URLEncoder.encode可以防止中文乱码
            zipFilename = URLEncoder.encode(zipFilename, "utf-8");
            // 指定文件名
            response.setHeader("Content-disposition", "attachment;filename=" + zipFilename);
            response.setContentType("application/x-msdownload");
            response.setCharacterEncoding("utf-8");
            exportZipStream(excelDataList, response.getOutputStream());
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常", e);
        }
    }

3.3 导出为字节数组

当我们需要导出为字节数组时,可以调用本方法。之后随你怎么加工。

    /**
     * 导出多个sheet到多个excel文件,并压缩到一个zip文件。最终得到一个字节数组。
     */
    public static byte[] exportZip(List<ExcelData> excelDataList) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        exportZipStream(excelDataList, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }

四、调试

4.1 构建调试用的实体类

指定简单的几个字段作为导出数据。

    @Data
    @AllArgsConstructor
    public static class DemoData {
        @ExcelProperty("字符串标题")
        private String string;
        @ExcelProperty("日期标题")
        private Date date;
        @ExcelProperty("数字标题")
        private Double doubleData;
    }

4.2 控制器调用

组装假数据,进行导出。

@Controller
@RequestMapping("/excel")
public class ExcelDemoController {
	@GetMapping("/exportTransportDetail")
    public String exportTransportDetail(HttpServletResponse response) throws IOException {

        // 压缩包文件名
        String fileName = "结算单运单明细-" + System.currentTimeMillis() + ".zip";
        List<ExcelData> excelDataList = new ArrayList<>();
        // 第一个Excel
        ExcelData excelData1 = new ExcelData();
        excelData1.setFilename("结算单运单明细-1-" + System.currentTimeMillis() + ".xlsx");

        List<DemoData> demoData1 = new ArrayList<>();
        demoData1.add(new DemoData("excel-sheet1", new Date(), 123112.321));
        demoData1.add(new DemoData("excel-sheet1", new Date(), 34.3));

        List<DemoData> demoData2 = new ArrayList<>();
        demoData2.add(new DemoData("excel-sheet2", new Date(), 123112.321));
        demoData2.add(new DemoData("excel-sheet2", new Date(), 34.3));
        ExcelShellData<DemoData> shellData1 = new ExcelShellData<>(demoData1, "sheet1", DemoData.class);
        ExcelShellData<DemoData> shellData2 = new ExcelShellData<>(demoData2, "sheet2", DemoData.class);
        excelData1.addShellData(shellData1);
        excelData1.addShellData(shellData2);

        // 第2个Excel
        ExcelData excelData2 = new ExcelData();
        excelData2.setFilename("结算单运单明细-2-" + System.currentTimeMillis() +".xlsx");

        List<DemoData> demoData21 = new ArrayList<>();
        demoData21.add(new DemoData("excel-sheet21", new Date(), 123112.321));
        demoData21.add(new DemoData("excel-sheet22", new Date(), 34.3));

        List<DemoData> demoData22 = new ArrayList<>();
        demoData22.add(new DemoData("excel-sheet21", new Date(), 123112.321));
        demoData22.add(new DemoData("excel-sheet22", new Date(), 34.3));
        ExcelShellData<DemoData> shellData21 = new ExcelShellData<>(demoData21, "sheet1", DemoData.class);
        ExcelShellData<DemoData> shellData22 = new ExcelShellData<>(demoData22, "sheet2", DemoData.class);
        excelData2.addShellData(shellData21);
        excelData2.addShellData(shellData22);

        excelDataList.add(excelData1);
        excelDataList.add(excelData2);

        // 写法1///
        // exportZip(fileName, excelDataList , response);

        // 写法2///
        byte[] bytes = exportZip(excelDataList);
        response.setHeader("Content-disposition", "attachment;filename=" + fileName);
        response.setContentType("application/x-msdownload");
        response.setCharacterEncoding("utf-8");
        response.getOutputStream().write(bytes);
        response.getOutputStream().flush();

        return "succ";
    }
}

4.3 测试结果

可以看到压缩包解压后的效果:
在这里插入图片描述
其中一个文件内容如下:
sheet1:
在这里插入图片描述

sheet2:
在这里插入图片描述

五、注册大数转换器,长度大于15时,转换为字符串

5.1 实现转换器


    /**
     * Excel 数值长度大于maxLength的数值转换为字符串
     */
    public static class ExcelBigNumberConvert implements Converter<Long> {

        private final int maxLength;

        public ExcelBigNumberConvert() {
            this(15);
        }

        public ExcelBigNumberConvert(Integer maxLength) {
           this.maxLength = maxLength;
        }

        @Override
        public Class<Long> supportJavaTypeKey() {
            return Long.class;
        }

        @Override
        public CellDataTypeEnum supportExcelTypeKey() {
            return CellDataTypeEnum.STRING;
        }

        @Override
        public Long convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
            Object data = cellData.getData();
            if (data == null) {
                return null;
            }
            String s = String.valueOf(data);
            if (s.matches("^\\d+$")) {
                return Long.parseLong(s);
            }
            return null;
        }

        @Override
        public CellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
            if (object != null) {
                String str = object.toString();
                if (str.length() > maxLength) {
                    return new CellData<>(str);
                }
            }
            return null;
        }
    }

5.2 使用转换器

在构建建造器时,增加注册转换器即可。
在这里插入图片描述