POI动态导出多层表头的EXCEL文件

  • 表格表头导出
    • 单行表头
    • 多行表头

以前接触过一个很古老的导出Excel,实现的逻辑是先声明一个导出的Excel模板,模板里报表的表头名称和顺序是固定的,这样执行导出时,系统是先读取手动创建的Excel模板文件,然后就往里面set查询结果值,最近因需要,写了一个可以根据表头信息自动导出Excel报表文件的方法,思路是把表头信息和表格数据进行分开2次写入,接下来分开讲解下思路,并贴上部分代码供大家参考。

首先,因为这种导出函数是可复用的,根据也无需要会导出不同的excel文件,里面数据不同,所以我们不能先声明Excel模板,只能通过POI里的Workbook类动态新建空白的excel模板,至于workbook具体的实现类,就要看你想导出03 还是07版excel了。

表格表头导出

单行表头

单行表头导出直接把获取的表头信息按照顺序输出就可以了,这里就不做特殊说明了。

多行表头

重点来了,多行表头动态导出难点是表头的行合并和列合并,这个能通过递归分析出来,分析出表头的行合并和列合并信息后就可以根据这些信息推导出每个表头信息在excel表格中所在的坐标以及单元格合并样式,剩下的就是根据这些信息进行写入workbook了。
下面是我的代码实现,只截取了部分仅供参考:
1.声明excel表格节点类 和 表头标题类

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;import java.io.Serializable;/*** 导出表头节点信息* @author wr*/
@Setter
@Getter
@ToString
public class HeaderNodeVo implements Serializable {private static final long serialVersionUID = 6243427059921159767L;/*** 行号*/private Integer rowStart;/*** 行号*/private Integer rowEnd;/*** 列号*/private Integer colStart;/*** 列号*/private Integer colEnd;/*** 层级*/private Integer level;/*** 标题名称*/private String name;/*** 对应的字段别名*/private String prop;/*** 节点ID*/private String id;/*** 节点父ID*/private String parentId;/*** 子节点数量*/private Integer sonNum;
}public class TargetTitleVo {/*** 指标ID列表*/private List<String> targetIds;/*** 标题ID*/private String titleId;/*** 标题*/private String value;/*** 对应数据集的字段名称* 最后一级标题以外为空*/private String prop;/*** 排序*/private Integer sort;/*** 子标题*/private List<TargetTitleVo> children;/*** 上级titleId*/private String parentId;/*** 该标题下叶子总数*/private int nodeCount;
}

2.获取表头字段名信息(这里要根据你们本地工程动态调整)

List<TargetTitleVo> titleVos = reportDataDao.getTitles();

3.初始化标题信息并组装HeaderNodeVo 类基本信息

/***  初始化标题信息* @param titleVos  标题vo集合* @param list   表头层级集合* @param level 表头开始层级号* @param nodes 表头节点集合*/private void initTitleInfo(List<TargetTitleVo> titleVos, List<Integer> list, Integer level, List<HeaderNodeVo> nodes){if(!CollectionUtils.isEmpty(titleVos)){//nofor(TargetTitleVo vo : titleVos){list.add(level);//初始化节点HeaderNodeVo node = this.initNode(vo, level);nodes.add(node);if(hasChild(vo)){//含有子项initTitleInfo(vo.getChildren(),list,level+1,nodes);}}}}/*** 判断标题是否有子项* @param vo    维度标题vo* @return  boolean*/private boolean hasChild(TargetTitleVo vo){boolean result = false;if(vo!=null){List<TargetTitleVo> children = vo.getChildren();if(children!=null &&children.size() >0){result = true;}}return result;}/*** 初始化节点* @param vo    指标vo* @param level 表头层数* @return  表头节点*/private HeaderNodeVo initNode(TargetTitleVo vo, Integer level){HeaderNodeVo result = new HeaderNodeVo();//节点idresult.setId(vo.getTitleId());//节点名称result.setName(vo.getValue());//父节点idresult.setParentId(vo.getParentId());//层级result.setLevel(level);//含有子项int sonNum = 0;//需要考虑多层表头,另处理if (hasChild(vo)){sonNum = vo.getChildren().size();}result.setSonNum(sonNum);String prop = vo.getProp();if(prop!=null&&!"".equals(prop)){result.setProp(prop);}return result;}

4.获取表头最大行数

/*** 获取表头行数* @param params    表头行数集合* @return  表头行数*/private Integer getMaxLevel(List<Integer> params){Integer result = 0;for(Integer param :params){if(param>result){result = param;}}return result;}

5.分析所有的表头节点,并将节点装配成Map<id,node>

/*** 装配 节点Id-节点 键值对* @param nodes 表头节点集合* @return  节点Id-节点 键值对*/private Map<String,HeaderNodeVo> assembleIdMap(List<HeaderNodeVo> nodes){Map<String,HeaderNodeVo> map = new HashMap<>(nodes.size());if(!CollectionUtils.isEmpty(nodes)){for(HeaderNodeVo node:nodes){if(node.getId()!=null){map.put(node.getId(),node);}}}return map;}

6.分析所有的表头节点,并保存成Map<父id,Set<子节点>>

/*** 装配 父节点ID-节点 键值对* @param nodes 表头节点集合* @return 父节点ID-节点 键值对*/private Map<String,List<HeaderNodeVo>> assemblePidMap(List<HeaderNodeVo> nodes){Map<String,List<HeaderNodeVo>> result = new HashMap<>(nodes.size());String parentId;List<HeaderNodeVo> ids;for(HeaderNodeVo node:nodes){parentId = node.getParentId();if(parentId != null){ids = result.get(parentId);if(ids==null){ids = new ArrayList<>();ids.add(node);result.put(parentId,ids);}else{ids.add(node);}}}return result;}

7.递归遍历分析所有的头节点,分析出每一个头节点含有的最下层节点个数

/*** 设置表头节点含有的子孙项数量* @param nodes 表头节点vo* @param pMap  父节点ID-节点 键值对*/private void setNodeSonNum(List<HeaderNodeVo> nodes,Map<String,List<HeaderNodeVo>> pMap){if(!CollectionUtils.isEmpty(nodes)){//not nullInteger sonNum;for(HeaderNodeVo node:nodes){if(node!=null&&node.getSonNum()>0){//含有子项需要遍历子项含有的孙项sonNum = recNode(node,pMap);node.setSonNum(sonNum);}//没有子项,退出}}}/*** 遍历表头节点信息* @param node  表头节点vo* @param pMap  父节点ID-节点 键值对* @return  含有的子孙项*/private Integer recNode(HeaderNodeVo node, Map<String,List<HeaderNodeVo>> pMap){int result = 0;if(node.getSonNum()==0){return 1;}else{if(pMap.get(node.getId())!=null && pMap.get(node.getId()).size()>0){for(HeaderNodeVo sNode:pMap.get(node.getId())){//子项result = result + recNode(sNode,pMap);}}else{return 0;}}return result;}

8.遍历所有的表头节点,将节点按照层级分开(与下一个步骤是同一个函数处理)

 /*** 初始化节点合并信息* @param nodes 表头节点集合* @param level 表头总行数* @param idMap 节点id-节点键值对*/private void initPositionData(List<HeaderNodeVo> nodes, Integer level, Map<String,HeaderNodeVo> idMap){//解析所有节点,定义位置信息//key 为层级,从1开始, List里node是有顺序的Map<Integer,List<HeaderNodeVo>> levelMap = new HashMap<>(level);Integer selLevel;List<HeaderNodeVo> levelList;if(!CollectionUtils.isEmpty(nodes)){for(HeaderNodeVo node:nodes){selLevel = node.getLevel();levelList = levelMap.get(selLevel);if(levelList==null){//nulllevelList = new ArrayList<>();levelList.add(node);levelMap.put(selLevel,levelList);}else{//not nulllevelList.add(node);}}}}

9.按照分好的层级和含有的所有子项分析出在excel中的坐标与合并单元格个数(与上一个步骤是同一个函数处理)

 //按照层级来遍历节点,组装合并节点信息List<HeaderNodeVo> selNodes;HeaderNodeVo selNode;HeaderNodeVo pNode;Integer rowStart = null;int rowEnd;Integer colStart = null;int colEnd;Integer sonNum;int lastColStart;Integer pColStart;for(int i=1;i<=level;i++){//一层所有的节点selNodes = levelMap.get(i);if(i==1){for(int j=0;j<selNodes.size();j++){//第一层第一个节点从 行2,列0开始if(j==0){rowStart = 2;colStart = 0;}selNode = selNodes.get(j);//含有的子项sonNum = selNode.getSonNum();if(sonNum>0){colEnd = colStart + sonNum - 1;rowEnd = 2;}else{colEnd = colStart;if(level==1){rowEnd = 2;}else{rowEnd = 2+level-1;}}setPosition(selNode,rowStart,rowEnd,colStart,colEnd);//再初始化下个节点的开始行列rowStart = 2;colStart = colEnd + 1;}}else{//非第一列lastColStart = 0;for (HeaderNodeVo node : selNodes) {//从节点的父节点往下一列开始selNode = node;pNode = idMap.get(selNode.getParentId());rowStart = pNode.getRowStart() + 1;pColStart = pNode.getColStart();if (lastColStart >= pColStart) {colStart = lastColStart;} else {colStart = pColStart;}//含有的子项sonNum = selNode.getSonNum();if (sonNum > 0) {colEnd = colStart + sonNum - 1;rowEnd = 2 + i - 1;} else {colEnd = colStart;rowEnd = 2 + level - 1;}setPosition(selNode, rowStart, rowEnd, colStart, colEnd);//再初始化下个节点的开始行列lastColStart = colEnd + 1;}}}/*** 设置节点合并单元格信息* @param node  表头节点Vo* @param rowStart  开始行号* @param rowEnd    结束行号* @param colStart  开始列号* @param colEnd    结束列号*/private void setPosition(HeaderNodeVo node, Integer rowStart, Integer rowEnd, Integer colStart, Integer colEnd){if(node !=null){node.setRowStart(rowStart);node.setRowEnd(rowEnd);node.setColStart(colStart);node.setColEnd(colEnd);}}

10最终组合在一起

/***  新增导出功能** @param vo 查询信息* @return 报表数据* @throws Exception 异常处理*/@Overridepublic Workbook exportReport(ReportDetailVo vo) throws Exception {long start = System.currentTimeMillis();log.info("[exportReport] - start.");Workbook wk;// 校验AssertUtil.voNotNull(vo);//将导出分成3类ReportType reportType = ReportType.getType(vo.getReportType());ReportDataVo reportDataVo;if(ReportType.TABLE.equals(reportType)){//报表//结果数据reportDataVo = reportQueryService.getReportData(vo);//报表名称String reportName = vo.getReportName();//表头字段名List<TargetTitleVo> titleVos = reportDataVo.getTitles();String[] titles = this.getTitles(titleVos);//表头总行数Integer rowCount;//表中数据List<Map<String, String>> list = reportDataVo.getPageResult().getList();//=============================================start=========================================List<Integer> levelList = new ArrayList<>();//处理完的节点信息List<HeaderNodeVo> nodes = new ArrayList<>();//获取最大levelinitTitleInfo(titleVos, levelList, 1, nodes);//总行数rowCount = getMaxLevel(levelList);Map<String,HeaderNodeVo> idMap = assembleIdMap(nodes);//遍历节点获取所有父节点含有的下一级节点数量   key:parentId   value:下一级子节点个数Map<String,List<HeaderNodeVo>> pMap = assemblePidMap(nodes);//遍历节点获取所有节点含有的子项,从下往上统计每个节点含有的子项(到最底层),所有的节点含有所有子孙项个数已统计setNodeSonNum(nodes,pMap);//level 层级数//nodes 所有的节点数//idMap id-节点//sMap  子id-父节点//准备定义合并坐标initPositionData(nodes, rowCount, idMap);Map<String,String> proTypes = this.getProType(titleVos);//=============================================end=========================================//对查询结果进行解析导出到Excelif(rowCount==1){//单行报表导出List<String> pros = this.getPros(titleVos);wk = ExportBetterUtil.exportReport(reportName,titles,pros,list,proTypes);}else{//多行表头导出//重新组装pros属性信息,注意顺序信息List<String> pros = this.getLevelPros(nodes);wk = ExportBetterUtil.exportReportByNode(reportName,nodes,pros,list,rowCount,proTypes);}//准备输出流}else if(ReportType.XX.equals(reportType)){return null;}else{return null;}log.info("[exportReport_] - end. vo: {}, TickCount:{} ms", vo, (System.currentTimeMillis() - start));return wk;}

结语:
第一次写博客,思路有点混乱望大家见谅,以上是怎么分析导出excel多行表头的思路,希望对大家有点帮助,最后有个题外话如果大家要导出和读取的excel文件很大,数据量很多,workbook实现类可以用SXSSFWorkbook 去做读写,阿里巴巴的easyExcel也可以,可以防止OOM,最后给大家拜个早年。

补充:
1. hasChild(vo),setPosition() 2个函数已添加。
2. ExportBetterUtil.java 导出工具类放在百度网盘了(C站的下载链接还在审核),可以手动下载,该文件可能与上面的代码版本不一致,你们可以自己手动修改下,

链接:https://pan.baidu.com/s/1KwKd9RDGQkcbJpEfjYVtNA
提取码:ukfj

POI动态导出多层表头的EXCEL文件相关推荐

  1. 通过Java批量导出带有图片的Excel文件数据

    批量导出带有图片的Excel文件 一.思路解析 二.关键源码 三.总结 Java通过POI或者一些常见的Excel工具类能够轻易导出后台的结构化数据,但是最近面临一个新需求,需要将对应记录数据和图片网 ...

  2. 使用easy excel导出复杂表头的excel

    使用easy-excel导出复杂表头的excel 今天想写一个双层表头的excel导出,一开始使用的是poi来画发现太麻烦, 于是就想到了使用easy-excel的模板填充来实现,将导出写成了一个简单 ...

  3. EasyExcel动态导出多级表头

    EasyExcel动态导出多级表头 工具类 /*** 特殊表头导出方法* @param response* @param fileName* @param sheetName* @param list ...

  4. php mysql生成excel文件,PHP导出MySQL数据到Excel文件简单示例

    这篇文章主要为大家详细介绍了PHP导出MySQL数据到Excel文件简单示例,具有一定的参考价值,可以用来参考一下. 对phpPHP导出MySQL数据到Excel文件简单示例感兴趣的小伙伴,下面一起跟 ...

  5. anki 插入表格_Anki之导出卡牌组到Excel文件

    最近有导出Anki卡牌组到Excel文件的需要,查到了这个教程,Anki插件--导出卡牌到Excel文件-LearnHacks,但结果并不令人满意,所导出的Excel文件只有一列,所选中的卡片字段全部 ...

  6. POI3.8解决导出大数据量excel文件时内存溢出的问题

    POI3.8解决导出大数据量excel文件时内存溢出的问题 参考文章: (1)POI3.8解决导出大数据量excel文件时内存溢出的问题 (2)https://www.cnblogs.com/feng ...

  7. php按列导出excel2010,excel2010官方下载 免费完整版 PHP导出MySQL数据到Excel文件fputcsv...

    这里的方法是利用fputcsv写CSV文件的方法,直接向浏览器输出Excel文件. 复制代码 代码如下: // 输出Excel文件头,可把user.csv换成你要的文件名 header('Conten ...

  8. POI动态导出Excel,后台返回文件流,前端responseType格式下载

    针对各个表的数据导出,实现的代码往往相似,出于这个目的,开启自己代码简略之旅.本文是针对.xls的excel文件. 1.思路描述 ①.确定各个模板的.xls文件格式 ②.定义模板的存在的参数,如第一行 ...

  9. 使用EasyExcel做自定义表头的excel文件导出

    如题所示 项目中需要做表格导出功能,且表头为复杂的动态表头,决定采用EasyExcel来进行操作 demo使用到的依赖 <dependency><groupId>com.ali ...

最新文章

  1. linux进程 网络占用率,linux CPU SI软中断比较占用率比较大(网络解决方案)
  2. java菱形有几种状态_java程序,打出一个菱形,有什么规律吗
  3. 兜兜转转一个圈,到底What is all you need?
  4. 二阶振荡环节的谐振频率_晶体振荡器和谐振器的区别 555压控振荡器电路图
  5. VI3的VLAN配置:VST、EST和VGT标记
  6. 【复杂系统迁移 .NET Core平台系列】之迁移项目工程
  7. 如何在React Native和Firebase中设置Google登录
  8. 爬取亚马逊评论_如何利用插件抓取亚马逊评论和关键词?
  9. 高斯滤波器是低通还是高通_经典模拟滤波器仍值得研究吗?
  10. python5000行代码项目_5000行python代码+可视化60W数据,告诉你知乎用户不为人知的事...
  11. C#对象序列化与反序列化zz
  12. php地图,php调用google地图
  13. Quartz时间配置(周期任务类)
  14. 机器学习之MCMC算法(转载)
  15. java restsharp_RestSharp使用总结
  16. 数据中心SAN网络综合布线方案分析
  17. 介绍一个很好的英语学习软件——单词风暴
  18. 硬盘盒刷固件使其不休眠
  19. 计算机同步时间解析错误,Windows时间同步时出错该怎么解决?
  20. 免费分享:4个不为人知的手机APP,1个资源丰富的网站

热门文章

  1. 20145326蔡馨熤《信息安全系统设计基础》期末总结
  2. 洛谷P2619 [国家集训队]Tree I 题解
  3. 按照C++语言程序结构组成数字电路进行计算的计算机
  4. Tecplot操作记录
  5. tecplot合速度
  6. 编译openssl1.1.1f for android
  7. Nginx高级优化(2): shell脚本日志切割,连接超时,进程数,网页压缩,防盗链,FPM 参数优化!!
  8. Unambiguous Text Localization, Retrieval,and Recognition for Cluttered Scenes
  9. 判断设备是否是 iphone5
  10. win7 装MacOS虚拟机做iOS开发