EXCEL文件导出

123 阅读8分钟

EXCEL文件

操作Excel主要WorkBook接口,相关对象有Workbook、Sheet、Row、Cell

WorkBook

有三种实现方式

  • HSSFWorkbook:一般用于操作Excel2003以前(包括2003)的版本,扩展名是.xls。
  • XSSFWorkbook:一般用于操作Excel2007及以上的版本,扩展名是.xlsx。
  • SXSSFWorkbook(POI 3.8+版本):一般用于大数据量的导出。比如数据量超过5000条即可考虑这种工作表

第一种:HSSFWorkbook

针对EXCEL 2003版本,扩展名为.xls

缺点: 最多只能导出 65535行、256列,也就是导出的数据函数超过这个数据就会报错;

优点: 一般不会报内存溢出。(因为数据量还不到7w所以内存一般都够用,首先你得明确知道这种方式是将数据先读取到内存中,然后再操作)

第二种:XSSFWorkbook

其对应的是EXCEL2007+ ,扩展名为.xlsx

优点: 这种形式的出现是为了突破HSSFWorkbook的65535行局限,是为了针对Excel2007版本的1048576行,16384列,最多可以导出104w条数据;

缺点: 伴随的问题来了,虽然导出数据行数增加了好多倍,但是随之而来的内存溢出问题也成了噩梦。因为你所创建的book,Sheet,row,cell等在写入到Excel之前,都是存放在内存中的(还有Excel的一些样式格式等等),可想而知,内存不溢出就有点不科学了!!!

第三种: SXSSFWorkbook

POI3.8之后的版本才有的,它可以操作Excel2007以后的所有版本Excel,扩展名是.xlsx

优点: 这种方式不会一般不会出现内存溢出(它使用了硬盘来换取内存空间,也就是当内存中数据达到一定程度这些数据会被持久化到硬盘中存储起来,而内存中存的都是最新的数据),并且支持大型Excel文件的创建(存储百万条数据绰绰有余)。

可设置最大内存条数,比如设置最大内存量为5000行, new SXSSFWookbook(5000),当行数达到 5000 时,把内存持久化写到文件中,以此逐步写入,避免OOM。这样就完美解决了大数据下导出的问题

缺点:

  • 既然一部分数据持久化到了硬盘中,且不能被查看和访问那么就会导致,在同一时间点我们只能访问一定数量的数据,也就是内存中存储的数据;
  • sheet.clone()方法将不再支持,还是因为持久化的原因;
  • 不再支持对公式的求值,还是因为持久化的原因,在硬盘中的数据没法读取到内存中进行计算;
  • 在使用模板方式下载数据的时候,不能改动表头,还是因为持久化的问题,写到了硬盘里就不能改变了;

代码编写

原生workbook

分sheet页写数据

 int memorySize = 100;//内存中只创建100个对象,写临时文件,当超过100条,就将内存中不用的对象释放
 int sheetRowMax = 200000;// 单sheet最大行数,用于分页
 private void exportExcel(ExcelData data, OutputStream out) {
    List<Map<String, Object>> dataList = data.getDataList();
    //为防止数据量过大导致OOM,POI3.8以上支持大数据导出
    SXSSFWorkbook workbook = new SXSSFWorkbook(memorySize);
    SXSSFSheet sheet = null;
    int sheetSize = 1;
    if (dataList.size() > sheetRowMax) {
        if (dataList.size() % sheetRowMax == 0) {
            sheetSize = dataList.size() / sheetRowMax;
        } else {
            sheetSize = dataList.size() / sheetRowMax + 1;
        }
    }
    // 设置样式
    CellStyle headStyle = getHeadStyle(workbook);
    // 设置数据
    for (int i = 0; i < sheetSize; i++) {
        int startInx = sheetRowMax * i;
        int endInx = sheetRowMax * (i + 1) - 1;
        //获取当页数据
        if (sheetRowMax * (i + 1) - 1 > dataList.size()) {
            endInx = dataList.size();
        }
        sheet = workbook.createSheet();
        // 设置宽度
        setSheetWidth(sheet, data.getHeadList());
        // 创建列头
        createHeadRow(sheet, headStyle, 0, data.getHeadList());
        for (int j = 1; j < endInx - startInx + 1; j++) {
            SXSSFRow row = sheet.createRow(j);
            Map<String, Object> map = dataList.get(j + startInx - 1);
            AtomicInteger m = new AtomicInteger();
            map.forEach((key, val) -> {
                row.createCell(m.get()).setCellValue(val.toString());
                m.getAndIncrement();
            });
        }
    }
    try {
        workbook.write(out);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/**
 * 创建列头
 *
 * @param sheet
 * @param cellStyle
 * @param index
 * @param headList
 */
private void createHeadRow(Sheet sheet, CellStyle cellStyle, int index, List<Head> headList) {
    Row row = sheet.createRow(index);
    row.setHeight((short) 300);
    for (int i = 0; i < headList.size(); i++) {
        Cell cell = row.createCell(i);
        cell.setCellStyle(cellStyle);
        XSSFRichTextString text = new XSSFRichTextString(headList.get(i).getName());
        cell.setCellValue(text);
    }
}

/**
 * 设置列宽
 *
 * @param sheet
 * @param headList
 */
private void setSheetWidth(Sheet sheet, List headList) {
    for (int i = 0; i < headList.size(); i++) {
        sheet.setColumnWidth(i, 4000);
    }
}

/**
 * 设置标题样式
 *
 * @param workbook
 * @return
 */
private CellStyle getHeadStyle(SXSSFWorkbook workbook) {
    // 生成一个样式(标题行)
    CellStyle headStyle = workbook.createCellStyle();
    // 设置这些样式
    headStyle.setFillForegroundColor(Short.valueOf("22").shortValue());
    headStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
    headStyle.setBorderBottom(BorderStyle.THIN);
    headStyle.setBorderLeft(BorderStyle.THIN);
    headStyle.setBorderRight(BorderStyle.THIN);
    headStyle.setBorderTop(BorderStyle.THIN);
    headStyle.setAlignment(HorizontalAlignment.CENTER);
    // 生成一个字
    Font headFont = workbook.createFont();
    headFont.setFontHeightInPoints((short) 12);
    headFont.setFontName("Microsoft YaHei UI Light");
    headFont.setBold(true);
    // 把字体应用到当前的样式
    headStyle.setFont(headFont);
    return headStyle;
}


@Data
public static class ExcelData {
    String title;
    Map<String, String> condition;
    List<Head> headList;
    List<Map<String, Object>> dataList;
    List<?> list;
}

@Data
class Head {
    String name;
    String value;

    public Head(String name, String value) {
        this.name = name;
        this.value = value;
    }
}

第三方包

第三方包包括hutool和阿里的easyExcel,本文只考虑hutool

链接参考:

hutool.cn/docs/#/poi/…

private void exportExcelWriter(ExcelData data, OutputStream out) {
    ExcelWriter writer = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
    // 只展示别名标题
    writer.setOnlyAlias(Boolean.TRUE);
    // 增加标题行内容
    writer.addHeaderAlias("orderNo", "单号").setColumnWidth(0, 35);
    writer.addHeaderAlias("revenueExpenseTypeDesc", "收支类型").setColumnWidth(1, 20);
    writer.addHeaderAlias("feeTypeDesc", "费用类型").setColumnWidth(2, 20);
    writer.addHeaderAlias("oppositeAcctName", "对方信息").setColumnWidth(3, 35);
    writer.addHeaderAlias("tradeAmount", "交易金额/元").setColumnWidth(4, 20);
    writer.addHeaderAlias("acctBalance", "余额/元").setColumnWidth(5, 20);
    writer.addHeaderAlias("outChannel", "出款通道").setColumnWidth(6, 35);
    writer.addHeaderAlias("tradeTime", "交易时间").setColumnWidth(7, 35);
    writer.addHeaderAlias("tradeFlowNo", "交易流水号").setColumnWidth(8, 35);
    writer.addHeaderAlias("useParty", "使用方").setColumnWidth(9, 35);
    // 数据为空,但需要强制写出标题行时,写出标题
    if (CollectionUtil.isEmpty(data.getList())) {
        writer.writeHeadRow(writer.getHeaderAlias().values());
    } else {
        writer.write(data.getList(), true);
    }
    //一次性写出内容,强制输出标题
    writer.write(rows, true);
    //关闭writer,释放内存
    writer.close();
}

性能比较

网上参考

(1)HSSFWorkbook

优点:过程中写入缓存,不操作磁盘,最后一次性写入磁盘,速度快。

缺点:最多只能处理65536行,否则会抛出异常。

(2)XSSFWorkbook

缺点:写数据时速度非常慢,非常耗内存,也会发生内存溢出,如100万条。

优点:可以写较大的数据量,如20万条。

(3)SXSSFWorkbook

过程中产生临时文件,需要清理临时文件。

默认由100条记录被保存在内存中,如果超过这数量,则最前面的数据被写入临时件。

如果想自定义内存中数据的数量,可以使用new SXSSFWorkbook(数量)

真实记录

项目中实验导出9999条数据耗时(调用hutool工具包下方法)

  SXSSFWorkbook:excel生成条数9999,耗时93980毫秒

  XSSFWorkbook:excel生成条数9999,耗时163440毫秒

  HSSFWorkbook:excel生成条数9999,耗时83201毫秒

项目中导出1万条耗时:95080ms(调用hutool工具包下方法)

单独main方法导出1万条耗时:3195ms(调用hutool工具包下方法)

注意点

原生workbook可提高其导出性能,不过提高效果较低,多线程并发写sheet可提高较高性能

导出方式

导出方式分为流和文件上传OSS服务器

重点为流必须关闭

流导出

writer.flush(out, true); 这样数据才会输入到文件中

public void excelExport(HttpServletResponse response, List<?> list, Integer exportType) {
    ExcelWriter writer = null;
    ServletOutputStream out = null;
    //response为HttpServletResponse对象
    response.reset();
    response.setContentType("application/vnd.ms-excel;charset=utf-8");
    //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码
    response.setHeader("Content-Disposition", "attachment;filename=test.xlsx");
    response.setHeader("code", ResultCode.SUCCESS.getCode());
    try {
        // 填充excelWriter 设置标题行及内容
        writer = fillExcelWriter(list, exportType);
        out = response.getOutputStream();
        writer.flush(out, true);
    } catch (Exception e) {
        log.error("exception{}", e);
        setFailedMsg(response);
    } finally {
        // 关闭writer,释放内存
        if (writer != null) {
            writer.close();
        }
        IoUtil.close(out);
    }
}

private ExcelWriter fillExcelWriter(Collection<?> data, Integer exportType) {
    // BigExcelWriter性能比ExcelUtil.getWriter(true)快
    ExcelWriter writer = ExcelUtil.getBigWriter();
    // 只展示别名标题
    writer.setOnlyAlias(Boolean.TRUE);
    // 增加标题行内容
    excelFactory.getExcelHeaderAlias(exportType).accept(writer);
    // 数据为空,但需要强制写出标题行时,写出标题
    if (CollectionUtil.isEmpty(data)) {
        writer.writeHeadRow(writer.getHeaderAlias().values());
    } else {
        writer.write(data, true);
    }
    return writer;
}

文件上传oss服务器

文件上传oss服务器时,excel导出文件大小9999,耗时2639毫秒

public String uploadFile(List<?> list, Integer exportType, String fileName) {
    ExcelWriter writer = null;
    //创建一个流,等待写入excel文件内容
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    InputStream inputstream = null;
    String singleFileName = fileName + FILE_EXTENSION_XLSX;
    try {
        // 填充excelWriter 设置标题行及内容
        writer = fillExcelWriter(list, exportType);
        writer.flush(outputStream, true);
        byte[] buffer = outputStream.toByteArray();
        inputstream = new ByteArrayInputStream(buffer);
        // 文件上传到OSS服务器
        String url = ossClientUtil.uploadFile2OSS(inputstream, singleFileName);
        // 返回文件url
        if ("".equals(url)) {
            return UPLOAD_FILE_FAILED;
        }
        return url;
    } catch (Exception e) {
        log.error("exception{}", e);
        return UPLOAD_FILE_FAILED;
    } finally {
        // 关闭writer,释放内存
        if (writer != null) {
            writer.close();
        }
        IoUtil.close(inputstream);
        IoUtil.close(outputStream);
    }
}

public String uploadFile2OSS(InputStream instream, String fileName) {
    String ret = "";
    try {
        // 创建上传Object的Metadata
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(instream.available());
        objectMetadata.setCacheControl("no-cache");
        objectMetadata.setHeader("Pragma", "no-cache");
        objectMetadata.setContentType(getcontentType(fileName.substring(fileName.lastIndexOf("."))));
        objectMetadata.setContentDisposition("inline;filename=" + fileName);
        String filePath = fileName;
        // 上传文件
        ossClient.putObject(bucketName, filePath, instream, objectMetadata);
        ret = HTTPS + bucketName + "." + endpoint + "/" + filePath;
    } catch (IOException e) {
        log.error(e.getMessage(), e);
    } finally {
        try {
            if (instream != null) {
                instream.close();
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }
    return ret;
}

文件打包

在zip流进行putNextEntry之后必须进行zipOutputStream.flush();zipOutputStream.closeEntry();

不然会导致压缩包中最后一个文件没数据

public String uploadFileZip(List<?> list, Integer exportType, String fileName) {
    InputStream zipInputStream = null;
    ByteArrayOutputStream zipByteArrayOutputStream = new ByteArrayOutputStream();
    ZipOutputStream zipOutputStream = null;
    String zipFileName = fileName + FILE_EXTENSION_ZIP;
    try {
        //创建压缩文件
        zipOutputStream = new ZipOutputStream(zipByteArrayOutputStream);
        // 切分list,生成多个文件,打包zip
        List<? extends List<?>> partitions = Lists.partition(list, FILE_ROW_MAX);
        for (int i = 0; i < partitions.size(); i++) {
            List<?> partitionList = partitions.get(i);
            String singleFileName = fileName + "_" + i + FILE_EXTENSION_XLSX;
            ExcelWriter writer = null;
            //创建一个流,等待写入excel文件内容
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            try {
                // 填充excelWriter 设置标题行及内容
                writer = fillExcelWriter(partitionList, exportType);
                writer.flush(outputStream, true);
                byte[] buffer = outputStream.toByteArray();
                zipOutputStream.putNextEntry(new ZipEntry(singleFileName));
                zipOutputStream.write(buffer);
            } catch (IOException e) {
                log.error("exception{}", e);
                return UPLOAD_FILE_FAILED;
            } finally {
                zipOutputStream.flush();
                zipOutputStream.closeEntry();
                // 关闭writer,释放内存
                if (writer != null) {
                    writer.close();
                }
                IoUtil.close(outputStream);
            }
        }
        byte[] zipBuffer = zipByteArrayOutputStream.toByteArray();
        zipInputStream = new ByteArrayInputStream(zipBuffer);
        String url = ossClientUtil.uploadFile2OSS(zipInputStream, zipFileName);
        zipOutputStream.finish();
        // 返回文件url
        if ("".equals(url)) {
            return UPLOAD_FILE_FAILED;
        }
        return url;
    } catch (Exception e) {
        log.error("exception{}", e);
        return UPLOAD_FILE_FAILED;
    } finally {
        IoUtil.close(zipInputStream);
        IoUtil.close(zipByteArrayOutputStream);
        if (zipOutputStream != null) {
            try {
                zipOutputStream.close();
            } catch (IOException e) {
                log.error("exception{}", e);
            }
        }
    }
}

其他

buffer读取

第一种

InputStream inputStream = null;
//创建一个Buffer字符串
byte[] buffer = new byte[1024];
//每次读取的字符串长度,如果为-1,代表全部读取完毕
int len;
//使用一个输入流从buffer里把数据读取出来
while ((len = inputStream.read(buffer)) != -1) {
    //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
    outStream.write(buffer, 0, len);
}
//关闭输入流
inputStream.close();
//把outstream里的数据写入内存
return outStream.toByteArray();

第二种

//使用一个输入流从buffer里把数据读取出来
while ((len = inputStream.read(buffer)) != -1) {
    //用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
    outStream.write(buffer);
}

使用第二种会使文件多占用一部分空间,除非输入流大小正好是缓冲区buffer大小整数倍

csv文件生成

csv文件生成比excel文件生成效率快很多,不过需生成本地文件,会有编码问题;

项目中生成csv文件大小:10000,耗时:2007毫秒

//指定路径和编码
CsvWriter writer = CsvUtil.getWriter("e:/testWrite.csv", CharsetUtil.CHARSET_UTF_8);
//按行写出
writer.write(
    new String[] {"a1", "b1", "c1"}, 
    new String[] {"a2", "b2", "c2"}, 
    new String[] {"a3", "b3", "c3"}
);