JAVA-poi导出excel到http响应流

导出结果为excel是相对常见的业务需求,大部分情况下只需要导出简单的格式即可,所以有许多可以采用的方案。有些方案还是很容易实现的。

一、可用的解决方案

目前可以有几类解决方案:

  1. 字处理企业提供的解决方案 -- 目前这个还没有看到,大概这些企业不屑于与民争利或者因为某些理由没有直接参与。例如微软,金山等都没有提供。如果有,就会压死一批三方产商。不过估计原厂看不上,或者是故意让利
  2. 三方产商提供的符合规范的解决方案
    1. APACHE poi,这是最著名的解决方案,已经存在20来年了。最早版本Version 0.1 (2001-08-28)。
    2. DOCX4J,也可以用,不过用得相对少。https://www.docx4java.org/ 。有提供收费的,企业级别的解决方案
    3. easyExcel,这是阿里的,本质是对POI的封装。让酷爱注解人士方便了不少。excel编程好像可以简化为对几个注解的背诵。
    4. 其它,暂时没有收集
  3. 各个公司自己的简易解决方案
    1. 输出csv的,这个是取巧的
    2. 输出xml的,这个也是取巧
    3. 其它,暂无收集

 

poi+注解

如果想简单一点处理,那么可以考虑easyExcel。

如果赶项目,而且客户对于excel格式没有什么要求,也是可以考虑的,毕竟这也节省了时间,某些项目中,这是项目经理所偏爱的,因为可能节约很多时间。

但是这种注解方式局限性也比较大,只能用于POJO/bean,格式无法定制,写一个查询就需要一个POJO。

此外,如果用于导出几万行数据,那么很可能会让某些人受不了,有点偏慢了,这种慢,在目前jvm版本下,没有什么特别好的方法。因为利用这种方式,意味着以下几个多余的步骤要做至少几万次:

a.数据映射为pojo,每一行的每个列都需要反射重复的判断等,隐式转换。

在某些业务场景下,这可能是一个不大不小的问题。此外跑得慢,消耗的能源自然也多一些。

 

当然最大的问题是定制excel样式比较麻烦。如果一定要坚持用这个实现也可以,不过最后工作就变为一部分人写POJO+注解,一个核心去实现注解。

最后从目前的jvm版本来说,注解+反射总是会慢了一些(不过现在大家也不是太关注这个)。

 poi(无注解)

非注解方式,足够灵活,可以避免写POJO和注解。通过定制一套工具,可以非常容易地实现一个简单的综合查询报表系统,并且可以实现比较简单的导出,

配合上模板,有时候还是不错的。

我个人倾向于直接使用poi,自己项目和团队中编写一些公用的工具。其它的方案暂时对我没有吸引力。

二、poi直接导出结果到http响应流

这里不讨论极限编程的事情,主要考虑导出一些不算太大的excel,例如10万行之类的。如果更大,一般不这么做了。

由于项目的需要,生成的excel无需缓存到服务器本地,而是可以直接输出到http响应流。

以下是例子代码,仅仅实例如何输出一个非常简单的excel导出。

环境:windows11,jdk1.8,springboot 2.6.7,poi-5.2.2,jquery 3.6.0,edge

2.1后端

 pom.xml

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.2</version>
</dependency>

 

 

核心代码(部分代码出处不可考):

import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
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.apache.poi.xssf.usermodel.XSSFWorkbook;

import tools.model.ExportExcelParam;

/**
 * excel导出工具
 * 
 * @author lzfto
 * @since
 */

public class ExcelTool {

    /**
     * 导出excel到http输出流
     * 
     * @param param
     */
    public static void exportExcelToHttpResponse(ExportExcelParam param) {
        outExcel(param.getFileName(), param.getColList(), param.getHeaderTitle(), param.getDataList(),
                param.getResponse(), param.getUserAgent());
    }

    /**
     * 导出文件到 http响应流
     * @param fileName      必须xls,xlsx之一为后缀
     * @param colList       key列表,非空-listMap中,map的key列表,多个以逗号分割。
     * @param headerTitle   表头-非空。多个以逗号分割
     * @param dataList      ListMap-非空。map中的key必须和colList对应
     * @param response      Http响应
     * @param userAgent     客户端信息(暂时不支持移动端)
     * @apiNote 如果dataList的行数大于5000条,会采用XSSFWorkbook,避免内存溢出
     * 此外,如果实在太大,那么请不要采用这个方法,而应该采用缓存+断点须传
     */
    private static void outExcel(String fileName, String colList, String headerTitle,
            List<Map<String, Object>> dataList, HttpServletResponse response, String userAgent) {

        // 第一步,创建一个workbook,对应一个Excel文件
        Workbook wb = null;
        if(dataList.size()<5000) {
            if (fileName.endsWith(".xls")) {
                wb = new HSSFWorkbook();
            } else if (fileName.endsWith(".xlsx")) {
                wb = new XSSFWorkbook();
            }    
        }
        else {
            wb=new SXSSFWorkbook();
        }
        

        // 第二步,在workbook中添加一个sheet,对应Excel文件中的sheet
        Sheet sheet = wb.createSheet("sheet1");
        // 第三步,在sheet中添加表头第0行,注意老版本poi对Excel的行数列数有限制short
        Row row = sheet.createRow((int) 0);
        // 第四步,创建单元格,并设置值表头 设置表头居中
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER); // 创建一个居中格式
        style.setFillForegroundColor(HSSFColor.HSSFColorPredefined.GREEN.getIndex());
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        // 为表头生成一个字体
        Font font = wb.createFont();
        font.setColor(HSSFColor.HSSFColorPredefined.BLACK.getIndex());
        font.setFontHeightInPoints((short) 12);
        font.setBold(true);
        // 把字体应用到当前的样式
        style.setFont(font);


        Cell cell = null;
        String[] headerTitleArr = headerTitle.split(",");
        for (int i = 0; i < headerTitleArr.length; i++) {
            cell = row.createCell((short) i);
            cell.setCellValue(headerTitleArr[i]);
            cell.setCellStyle(style);
        }

        // 第五步,写入实体数据 实际应用中这些数据从数据库得到
        // 如果有需要,可以为内容设置一个字体,此处略
        String[] columnList = StringUtils.split(colList, ',');
        for (int i = 0; i < dataList.size(); i++) {
            row = sheet.createRow((int) i + 1);
            Map<String, Object> dataMap = dataList.get(i);
            for (int j = 0; j < columnList.length; j++) {
                cell = row.createCell((short) j);
                String value = "";
                if (dataMap.get(columnList[j]) != null) {
                    value = dataMap.get(columnList[j]).toString();
                }
                cell.setCellValue(value);
            }
        }

        for (int i = 0; i < headerTitleArr.length; i++) {
            sheet.setColumnWidth(i, headerTitleArr[i].getBytes().length * 2 * 256);
        }

        // 第六步,将excel内存信息写入到http响应流
        try {
            String outputFileName = fileName;
            if (userAgent.toUpperCase().indexOf("MSIE") > 0) {
                outputFileName = URLEncoder.encode(fileName, "UTF-8");
            } else if (userAgent.toUpperCase().indexOf("IPHONE") > 0) {
                outputFileName = new String(fileName.getBytes(), "ISO-8859-1");
            } else {
                outputFileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
            }
            response.setContentType("application/octet-stream");
            response.setHeader("Content-disposition", "attachment; filename=\"" + outputFileName + "\"");
            wb.write(response.getOutputStream());            
            response.getOutputStream().flush();

        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

}

 

这里需要注意的是:内存溢出问题,这是使用SXSSFWorkbook 完成的,但这个东西目前限制比较多。

关于SXSSFWorkbook ,可以参阅:https://poi.apache.org/components/spreadsheet/

这里摘取一些资料:

Since 3.8-beta3, POI provides a low-memory footprint SXSSF API built on top of XSSF.

SXSSF is an API-compatible streaming extension of XSSF to be used when very large spreadsheets have to be produced, and heap space is limited.
SXSSF achieves its low memory footprint by limiting access to the rows that are within a sliding window, while XSSF gives access to all rows in the document.
Older rows that are no longer in the window become inaccessible, as they are written to the disk. In auto-flush mode the size of the access window can be specified, to hold a certain number of rows in memory.
When that value is reached, the creation of an additional row causes the row with the lowest index to to be removed from the access window and written to disk.
Or, the window size can be set to grow dynamically; it can be trimmed periodically by an explicit call to flushRows(int keepRows) as needed. Due to the streaming nature of the implementation, there are the following limitations when compared to XSSF: .Only a limited number of rows are accessible at a point in time. .Sheet.clone() is not supported. .Formula evaluation is not supported See more details at SXSSF How-To

原文下面有个图,略。

 

这个东西怎么用,建议好好阅读 https://poi.apache.org/components/spreadsheet/how-to.html#sxssf 和官方api文档。 更多介绍略(咱暂时也不用)。

 

2.2前端

原生js方案

/**
 * 导出所有满足条件的内容为excel格式
 */
function exportClick() {
  let _param = getParamValue();
  var url = '/log/srv/export';
  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);    // 也可以使用POST方式,根据接口
  xhr.setRequestHeader('content-type', 'application/json');
  xhr.responseType = "blob";  // 返回类型blob
  // 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
  xhr.onload = function () {
    // 请求完成
    if (this.status === 200) {
      // 返回200
      var blob = this.response;
      var reader = new FileReader();
      reader.readAsDataURL(blob); 
      reader.onload = function (e) {
        // 转换完成,创建一个a标签用于下载
        var a = document.createElement('a');
        a.download = '服务日志.xlsx';
        a.href = e.target.result;
        $("body").append(a); 
        a.click();
        $(a).remove();
      };
    }
  };
  // 发送ajax请求
  xhr.send(JSON.stringify(_param));
}

 

这是非常简单的代码,没有特别的优化,以及异常处理等等。

jquery方案

也可以用jquery处理,例如:

function downAsExcel() {
  let _param = getParamValue();
  $.ajax({
    url: '/log/srv/export',
    type: 'POST',
    dataType: 'blob',
    contentType: "application/json",
    async: true,
    data: JSON.stringify(_param),
    success: function (rs, status, xhr) {
      var blob = rs;
      var reader = new FileReader();
      reader.readAsDataURL(blob);
      reader.onload = function (e) {
        // 转换完成,创建一个a标签用于下载
        var a = document.createElement('a');
        a.download = '服务日志.xlsx';
        a.href = e.target.result;
        $("body").append(a);
        a.click();
        $(a).remove();
      };
    },
    error: function (rs) {
      showMessgeBox(_MSG_ERR, '网络错误,汇总失败', 1);
    }
  });
}

 

三、小结

如果厌倦注解,并且想灵活一些,建议直接使用POI来处理EXCEL的导出。

POI的功能还是很强大的,虽然极限情况和复杂情况处理的不够好(毕竟不是原厂),但是大部分情况下,已经足够用了。

如果不是为了导出,那么完全可以使用厂商提供的api来处理文档,例如微软和金山都有提供类似的开放api,实现得很完美。

 

posted @ 2022-06-27 22:57  正在战斗中  阅读(1795)  评论(0编辑  收藏  举报