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

📅 2026/7/15 16:11:57
POI动态导出多层表头的EXCEL文件
POI动态导出多层表头的EXCEL文件表格表头导出单行表头多行表头以前接触过一个很古老的导出Excel实现的逻辑是先声明一个导出的Excel模板模板里报表的表头名称和顺序是固定的这样执行导出时系统是先读取手动创建的Excel模板文件然后就往里面set查询结果值最近因需要写了一个可以根据表头信息自动导出Excel报表文件的方法思路是把表头信息和表格数据进行分开2次写入接下来分开讲解下思路并贴上部分代码供大家参考。首先因为这种导出函数是可复用的根据也无需要会导出不同的excel文件里面数据不同所以我们不能先声明Excel模板只能通过POI里的Workbook类动态新建空白的excel模板至于workbook具体的实现类就要看你想导出03 还是07版excel了。表格表头导出单行表头单行表头导出直接把获取的表头信息按照顺序输出就可以了这里就不做特殊说明了。多行表头重点来了多行表头动态导出难点是表头的行合并和列合并这个能通过递归分析出来分析出表头的行合并和列合并信息后就可以根据这些信息推导出每个表头信息在excel表格中所在的坐标以及单元格合并样式剩下的就是根据这些信息进行写入workbook了。下面是我的代码实现只截取了部分仅供参考1.声明excel表格节点类 和 表头标题类importlombok.Getter;importlombok.Setter;importlombok.ToString;importjava.io.Serializable;/** * 导出表头节点信息 * author wr */Setter Getter ToStringpublicclassHeaderNodeVoimplementsSerializable{privatestaticfinal long serialVersionUID6243427059921159767L;/** * 行号 */privateInteger rowStart;/** * 行号 */privateInteger rowEnd;/** * 列号 */privateInteger colStart;/** * 列号 */privateInteger colEnd;/** * 层级 */privateInteger level;/** * 标题名称 */privateString name;/** * 对应的字段别名 */privateString prop;/** * 节点ID */privateString id;/** * 节点父ID */privateString parentId;/** * 子节点数量 */privateInteger sonNum;}publicclassTargetTitleVo{/** * 指标ID列表 */privateListStringtargetIds;/** * 标题ID */privateString titleId;/** * 标题 */privateString value;/** * 对应数据集的字段名称 * 最后一级标题以外为空 */privateString prop;/** * 排序 */privateInteger sort;/** * 子标题 */privateListTargetTitleVochildren;/** * 上级titleId */privateString parentId;/** * 该标题下叶子总数 */privateint nodeCount;}2.获取表头字段名信息这里要根据你们本地工程动态调整ListTargetTitleVotitleVosreportDataDao.getTitles();3.初始化标题信息并组装HeaderNodeVo 类基本信息/** * 初始化标题信息 * param titleVos 标题vo集合 * param list 表头层级集合 * param level 表头开始层级号 * param nodes 表头节点集合 */privatevoidinitTitleInfo(ListTargetTitleVotitleVos,ListIntegerlist,Integer level,ListHeaderNodeVonodes){if(!CollectionUtils.isEmpty(titleVos)){//nofor(TargetTitleVo vo:titleVos){list.add(level);//初始化节点HeaderNodeVo nodethis.initNode(vo,level);nodes.add(node);if(hasChild(vo)){//含有子项initTitleInfo(vo.getChildren(),list,level1,nodes);}}}}/** * 判断标题是否有子项 * param vo 维度标题vo * return boolean */privatebooleanhasChild(TargetTitleVo vo){boolean resultfalse;if(vo!null){ListTargetTitleVochildrenvo.getChildren();if(children!nullchildren.size()0){resulttrue;}}returnresult;}/** * 初始化节点 * param vo 指标vo * param level 表头层数 * return 表头节点 */privateHeaderNodeVoinitNode(TargetTitleVo vo,Integer level){HeaderNodeVo resultnewHeaderNodeVo();//节点idresult.setId(vo.getTitleId());//节点名称result.setName(vo.getValue());//父节点idresult.setParentId(vo.getParentId());//层级result.setLevel(level);//含有子项int sonNum0;//需要考虑多层表头另处理if(hasChild(vo)){sonNumvo.getChildren().size();}result.setSonNum(sonNum);String propvo.getProp();if(prop!null!.equals(prop)){result.setProp(prop);}returnresult;}4.获取表头最大行数/** * 获取表头行数 * param params 表头行数集合 * return 表头行数 */privateIntegergetMaxLevel(ListIntegerparams){Integer result0;for(Integer param:params){if(paramresult){resultparam;}}returnresult;}5.分析所有的表头节点并将节点装配成Mapid,node/** * 装配 节点Id-节点 键值对 * param nodes 表头节点集合 * return 节点Id-节点 键值对 */privateMapString,HeaderNodeVoassembleIdMap(ListHeaderNodeVonodes){MapString,HeaderNodeVomapnewHashMap(nodes.size());if(!CollectionUtils.isEmpty(nodes)){for(HeaderNodeVo node:nodes){if(node.getId()!null){map.put(node.getId(),node);}}}returnmap;}6.分析所有的表头节点并保存成Map父idSet子节点/** * 装配 父节点ID-节点 键值对 * param nodes 表头节点集合 * return 父节点ID-节点 键值对 */privateMapString,ListHeaderNodeVoassemblePidMap(ListHeaderNodeVonodes){MapString,ListHeaderNodeVoresultnewHashMap(nodes.size());String parentId;ListHeaderNodeVoids;for(HeaderNodeVo node:nodes){parentIdnode.getParentId();if(parentId!null){idsresult.get(parentId);if(idsnull){idsnewArrayList();ids.add(node);result.put(parentId,ids);}else{ids.add(node);}}}returnresult;}7.递归遍历分析所有的头节点分析出每一个头节点含有的最下层节点个数/** * 设置表头节点含有的子孙项数量 * param nodes 表头节点vo * param pMap 父节点ID-节点 键值对 */privatevoidsetNodeSonNum(ListHeaderNodeVonodes,MapString,ListHeaderNodeVopMap){if(!CollectionUtils.isEmpty(nodes)){//not nullInteger sonNum;for(HeaderNodeVo node:nodes){if(node!nullnode.getSonNum()0){//含有子项需要遍历子项含有的孙项sonNumrecNode(node,pMap);node.setSonNum(sonNum);}//没有子项退出}}}/** * 遍历表头节点信息 * param node 表头节点vo * param pMap 父节点ID-节点 键值对 * return 含有的子孙项 */privateIntegerrecNode(HeaderNodeVo node,MapString,ListHeaderNodeVopMap){int result0;if(node.getSonNum()0){return1;}else{if(pMap.get(node.getId())!nullpMap.get(node.getId()).size()0){for(HeaderNodeVo sNode:pMap.get(node.getId())){//子项resultresultrecNode(sNode,pMap);}}else{return0;}}returnresult;}8.遍历所有的表头节点将节点按照层级分开与下一个步骤是同一个函数处理/** * 初始化节点合并信息 * param nodes 表头节点集合 * param level 表头总行数 * param idMap 节点id-节点键值对 */privatevoidinitPositionData(ListHeaderNodeVonodes,Integer level,MapString,HeaderNodeVoidMap){//解析所有节点定义位置信息//key 为层级从1开始 List里node是有顺序的MapInteger,ListHeaderNodeVolevelMapnewHashMap(level);Integer selLevel;ListHeaderNodeVolevelList;if(!CollectionUtils.isEmpty(nodes)){for(HeaderNodeVo node:nodes){selLevelnode.getLevel();levelListlevelMap.get(selLevel);if(levelListnull){//nulllevelListnewArrayList();levelList.add(node);levelMap.put(selLevel,levelList);}else{//not nulllevelList.add(node);}}}}9.按照分好的层级和含有的所有子项分析出在excel中的坐标与合并单元格个数与上一个步骤是同一个函数处理//按照层级来遍历节点组装合并节点信息ListHeaderNodeVoselNodes;HeaderNodeVo selNode;HeaderNodeVo pNode;Integer rowStartnull;int rowEnd;Integer colStartnull;int colEnd;Integer sonNum;int lastColStart;Integer pColStart;for(int i1;ilevel;i){//一层所有的节点selNodeslevelMap.get(i);if(i1){for(int j0;jselNodes.size();j){//第一层第一个节点从 行2,列0开始if(j0){rowStart2;colStart0;}selNodeselNodes.get(j);//含有的子项sonNumselNode.getSonNum();if(sonNum0){colEndcolStartsonNum-1;rowEnd2;}else{colEndcolStart;if(level1){rowEnd2;}else{rowEnd2level-1;}}setPosition(selNode,rowStart,rowEnd,colStart,colEnd);//再初始化下个节点的开始行列rowStart2;colStartcolEnd1;}}else{//非第一列lastColStart0;for(HeaderNodeVo node:selNodes){//从节点的父节点往下一列开始selNodenode;pNodeidMap.get(selNode.getParentId());rowStartpNode.getRowStart()1;pColStartpNode.getColStart();if(lastColStartpColStart){colStartlastColStart;}else{colStartpColStart;}//含有的子项sonNumselNode.getSonNum();if(sonNum0){colEndcolStartsonNum-1;rowEnd2i-1;}else{colEndcolStart;rowEnd2level-1;}setPosition(selNode,rowStart,rowEnd,colStart,colEnd);//再初始化下个节点的开始行列lastColStartcolEnd1;}}}/** * 设置节点合并单元格信息 * param node 表头节点Vo * param rowStart 开始行号 * param rowEnd 结束行号 * param colStart 开始列号 * param colEnd 结束列号 */privatevoidsetPosition(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 异常处理 */OverridepublicWorkbookexportReport(ReportDetailVo vo)throws Exception{long startSystem.currentTimeMillis();log.info([exportReport] - start.);Workbook wk;// 校验AssertUtil.voNotNull(vo);//将导出分成3类ReportType reportTypeReportType.getType(vo.getReportType());ReportDataVo reportDataVo;if(ReportType.TABLE.equals(reportType)){//报表//结果数据reportDataVoreportQueryService.getReportData(vo);//报表名称String reportNamevo.getReportName();//表头字段名ListTargetTitleVotitleVosreportDataVo.getTitles();String[]titlesthis.getTitles(titleVos);//表头总行数Integer rowCount;//表中数据ListMapString,StringlistreportDataVo.getPageResult().getList();//startListIntegerlevelListnewArrayList();//处理完的节点信息ListHeaderNodeVonodesnewArrayList();//获取最大levelinitTitleInfo(titleVos,levelList,1,nodes);//总行数rowCountgetMaxLevel(levelList);MapString,HeaderNodeVoidMapassembleIdMap(nodes);//遍历节点获取所有父节点含有的下一级节点数量 key:parentId value:下一级子节点个数MapString,ListHeaderNodeVopMapassemblePidMap(nodes);//遍历节点获取所有节点含有的子项,从下往上统计每个节点含有的子项到最底层,所有的节点含有所有子孙项个数已统计setNodeSonNum(nodes,pMap);//level 层级数//nodes 所有的节点数//idMap id-节点//sMap 子id-父节点//准备定义合并坐标initPositionData(nodes,rowCount,idMap);MapString,StringproTypesthis.getProType(titleVos);//end//对查询结果进行解析导出到Excelif(rowCount1){//单行报表导出ListStringprosthis.getPros(titleVos);wkExportBetterUtil.exportReport(reportName,titles,pros,list,proTypes);}else{//多行表头导出//重新组装pros属性信息注意顺序信息ListStringprosthis.getLevelPros(nodes);wkExportBetterUtil.exportReportByNode(reportName,nodes,pros,list,rowCount,proTypes);}//准备输出流}elseif(ReportType.XX.equals(reportType)){returnnull;}else{returnnull;}log.info([exportReport_] - end. vo: {}, TickCount{} ms,vo,(System.currentTimeMillis()-start));returnwk;}结语第一次写博客思路有点混乱望大家见谅以上是怎么分析导出excel多行表头的思路希望对大家有点帮助最后有个题外话如果大家要导出和读取的excel文件很大数据量很多workbook实现类可以用SXSSFWorkbook 去做读写阿里巴巴的easyExcel也可以可以防止OOM最后给大家拜个早年。补充1. hasChild(vo)setPosition() 2个函数已添加。2. ExportBetterUtil.java 导出工具类放在百度网盘了C站的下载链接还在审核可以手动下载该文件可能与上面的代码版本不一致你们可以自己手动修改下链接https://pan.baidu.com/s/1KwKd9RDGQkcbJpEfjYVtNA提取码ukfj