news 2026/4/28 7:18:22

easy-excel fill+模板的情况下 如何合并单元格

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
easy-excel fill+模板的情况下 如何合并单元格

文章目录

  • 前言
  • 一、思路
  • 二、使用步骤
    • 1.模板
    • 2.service方法
    • 3.策略
    • 4.效果
  • 总结

前言

easy-excel 导出excel时,遇到需要保留模板内的格式和表头等,在使用模板+fill模式填充数据的情况下,单元格合并比较麻烦 在easy-excel版本比较老(2.x),升级牵扯到poi升级又涉及到poi-tl等组件也要升级的情况下,使用本文方法可实现单元格合并,如果有更好的方案欢迎交流 AI辅助开发的情况下可以把本问题喂给ai提供思路

一、思路

先使用fill导出数据到内存,在内存中使用poi对需要合并的单元格进行操作

二、使用步骤

1.模板


第5行为动态数据行,前后表头表尾都是保留的,为了实现导出所有数据后,A5相同的值合并单元格,

// 两次填充,对固定单元格和动态行list行进行添值处理// 填充单个数据(元数据)excelWriter.fill(batchMetaMap,writeSheet);// 填充列表数据FillConfig fillConfig=FillConfig.builder().forceNewRow(Boolean.TRUE).build();excelWriter.fill(rowList,fillConfig,writeSheet);

2.service方法

代码如下(示例):

@Override publicvoidexportYearPlan(Long masterId,HttpServletResponse response,OpsPlanMaster opsPlanMaster){try{// 1. 查询该主表下的所有年计划子表记录List<OpsPlanYear>planList=this.listByMasterId(masterId);if(opsPlanMaster==null||planList==null||planList.isEmpty()){return;}// 2. 设置响应头response.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");String fileName=URLEncoder.encode("年度检查巡视计划","UTF-8");response.setHeader("Content-disposition","attachment;filename="+fileName+".xlsx");// 3. 加载模板文件String templatePath="template/excel/OpsPlanYearFillTemplate.xlsx";try(InputStream templateStream=ResourceLoaderUtil.getResourceAsStream(templatePath)){if(templateStream==null){throw newRuntimeException("模板文件不存在:"+templatePath);}// 4. 准备动态行数据List<Map<String,Object>>rowList=new ArrayList<>();for(OpsPlanYear plan:planList){Map<String,Object>row=new HashMap<>();row.put("categoryName",plan.getCategoryName());row.put("subCategoryName",plan.getSubCategoryName());row.put("month01",plan.getMonth01()!=null?plan.getMonth01():"");row.put("month02",plan.getMonth02()!=null?plan.getMonth02():"");row.put("month03",plan.getMonth03()!=null?plan.getMonth03():"");row.put("month04",plan.getMonth04()!=null?plan.getMonth04():"");row.put("month05",plan.getMonth05()!=null?plan.getMonth05():"");row.put("month06",plan.getMonth06()!=null?plan.getMonth06():"");row.put("month07",plan.getMonth07()!=null?plan.getMonth07():"");row.put("month08",plan.getMonth08()!=null?plan.getMonth08():"");row.put("month09",plan.getMonth09()!=null?plan.getMonth09():"");row.put("month10",plan.getMonth10()!=null?plan.getMonth10():"");row.put("month11",plan.getMonth11()!=null?plan.getMonth11():"");row.put("month12",plan.getMonth12()!=null?plan.getMonth12():"");row.put("ownerName",plan.getOwnerName()!=null?plan.getOwnerName():"");row.put("remark",plan.getRemark()!=null?plan.getRemark():"");rowList.add(row);}// 填充批次元数据:年月信息、部门、审核人、制表人等Map<String,Object>batchMetaMap=new HashMap<>();batchMetaMap.put("orgName",opsPlanMaster.getOrgName());// 部门名称batchMetaMap.put("planDate",opsPlanMaster.getPlanDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));// 标题中的年月batchMetaMap.put("approvedBy",opsPlanMaster.getApprovedBy());// 审核人batchMetaMap.put("reviewedBy",opsPlanMaster.getReviewedBy());batchMetaMap.put("preparedBy",opsPlanMaster.getPreparedBy());// 使用自定义合并策略:合并A列(大类名称),从第5行(索引4)开始FillMergeStrategy mergeStrategy=newFillMergeStrategy(4,0);// 先将Excel写入内存ByteArrayOutputStream byteArrayOutputStream=newByteArrayOutputStream();ExcelWriter excelWriter=EasyExcel.write(byteArrayOutputStream).withTemplate(templateStream).build();WriteSheet writeSheet=EasyExcel.writerSheet(0).build();// 填充单个数据(元数据)excelWriter.fill(batchMetaMap,writeSheet);// 填充列表数据FillConfig fillConfig=FillConfig.builder().forceNewRow(Boolean.TRUE).build();excelWriter.fill(rowList,fillConfig,writeSheet);excelWriter.finish();// 用POI打开内存中的Excel,执行合并try(Workbook workbook=WorkbookFactory.create(newByteArrayInputStream(byteArrayOutputStream.toByteArray()))){org.apache.poi.ss.usermodel.Sheet sheet=workbook.getSheetAt(0);mergeStrategy.doMergeWithPoi(sheet);// 将合并后的Excel写入响应workbook.write(response.getOutputStream());}catch(Exception e){throw newRuntimeException("合并单元格失败:"+e.getMessage(),e);}}}catch(IOException e){throw newRuntimeException("导出失败:"+e.getMessage(),e);}}

3.策略

代码如下(示例):

package com.bjhz.microservice.assets.server.full.listener;import com.alibaba.excel.ExcelWriter;import com.alibaba.excel.write.metadata.WriteSheet;import lombok.extern.slf4j.Slf4j;import org.apache.poi.ss.usermodel.Cell;import org.apache.poi.ss.usermodel.DateUtil;import org.apache.poi.ss.usermodel.Row;import org.apache.poi.ss.usermodel.Sheet;import org.apache.poi.ss.util.CellRangeAddress;/** * EasyExcel Fill模式动态单元格合并策略 * * <p>功能说明:</p> * <ul> * <li>支持多列独立合并,每列根据单元格内容自动判断是否合并</li> * <li>仅当相邻行的单元格内容完全相同时才执行合并</li> * <li>在ExcelWriter.finish()之前手动调用doMerge执行合并</li> * </ul> * * <p>使用示例:</p> * <pre> * // 创建合并策略 * FillMergeStrategy strategy = new FillMergeStrategy(4, 0); * * // 在所有fill操作完成后,finish之前调用 * strategy.doMerge(excelWriter, writeSheet); * * // 合并A列和B列(索引0和1),从第5行开始 * FillMergeStrategy strategy = new FillMergeStrategy(4, 0, 1); * </pre> * * @author * @date 2026-04-26 */@Slf4j public class FillMergeStrategy{/** 需要合并的列索引数组(从0开始),A列 = 0 */private finalint[]mergeColumnIndexes;/** 数据开始的行索引(从0开始计数),例如第5行传入4 */private finalintstartRowIndex;/** * 构造函数 * * @param startRowIndex 数据开始的行索引(从0开始),例如第5行传入4 * @param columnIndexes 需要合并的列索引数组,可变参数 */publicFillMergeStrategy(intstartRowIndex,int...columnIndexes){this.startRowIndex=startRowIndex;this.mergeColumnIndexes=columnIndexes;}/** * 执行单元格合并 * * <p>调用时机:</p> * <ul> * <li>在所有fill操作完成后</li> * <li>在excelWriter.finish()之前</li> * </ul> * * @param excelWriter EasyExcel写入器 * @param writeSheet 写入的Sheet */publicvoiddoMerge(ExcelWriter excelWriter,WriteSheet writeSheet){Sheet sheet=excelWriter.writeContext().writeSheetHolder().getSheet();log.info("开始执行单元格合并,startRowIndex={}, mergeColumnIndexes={}",startRowIndex,mergeColumnIndexes);log.info("Sheet总行数: {}",sheet.getLastRowNum());// 遍历每一列需要合并的列for(intcolumnIndex:mergeColumnIndexes){mergeColumnByContent(sheet,columnIndex);}log.info("单元格合并完成");}/** * 使用POI Sheet执行单元格合并 * * <p>调用时机:</p> * <ul> * <li>ExcelWriter.finish()之后</li> * <li>使用POI打开Excel后</li> * </ul> * * @param sheet POI的Sheet对象 */publicvoiddoMergeWithPoi(Sheet sheet){log.info("开始执行单元格合并,startRowIndex={}, mergeColumnIndexes={}",startRowIndex,mergeColumnIndexes);log.info("Sheet总行数: {}",sheet.getLastRowNum());// 遍历每一列需要合并的列for(intcolumnIndex:mergeColumnIndexes){mergeColumnByContent(sheet,columnIndex);}log.info("单元格合并完成");}/** * 对指定列执行内容相同的单元格合并 * * @param sheet Excel Sheet对象 * @param columnIndex 列索引(从0开始) */privatevoidmergeColumnByContent(Sheet sheet,intcolumnIndex){intlastRowNum=sheet.getLastRowNum();if(lastRowNum<startRowIndex){log.info("数据行数不足,第{}列无需合并",columnIndex);return;}String lastValue=null;intmergeStartRow=-1;intmergeCount=0;// 从指定行开始遍历for(introwIndex=startRowIndex;rowIndex<=lastRowNum;rowIndex++){Row row=sheet.getRow(rowIndex);if(row==null){continue;}Cell cell=row.getCell(columnIndex);String currentValue=getCellValue(cell);// 当前值与上一行值相同,记录合并起始位置if(currentValue!=null&&currentValue.equals(lastValue)){if(mergeStartRow==-1){mergeStartRow=rowIndex-1;}}else{// 当前值与上一行值不同,执行上一段合并if(mergeStartRow!=-1){intmergeEndRow=rowIndex-1;log.info("合并第{}列,从第{}行到第{}行,值='{}'",columnIndex,mergeStartRow,mergeEndRow,lastValue);CellRangeAddress region=newCellRangeAddress(mergeStartRow,mergeEndRow,columnIndex,columnIndex);try{sheet.addMergedRegion(region);clearMergedCells(sheet,region);mergeCount++;}catch(Exception e){log.warn("合并失败: {}",e.getMessage());}mergeStartRow=-1;}}lastValue=currentValue;}// 处理最后一段合并if(mergeStartRow!=-1&&lastRowNum>mergeStartRow){log.info("合并第{}列,从第{}行到第{}行(最后一行),值='{}'",columnIndex,mergeStartRow,lastRowNum,lastValue);CellRangeAddress region=newCellRangeAddress(mergeStartRow,lastRowNum,columnIndex,columnIndex);try{sheet.addMergedRegion(region);clearMergedCells(sheet,region);mergeCount++;}catch(Exception e){log.warn("合并失败: {}",e.getMessage());}}log.info("第{}列合并完成,共合并{}个区域",columnIndex,mergeCount);}/** * 清空合并区域中除第一个单元格外的其他单元格的值 * * @param sheet Excel Sheet对象 * @param region 合并区域 */privatevoidclearMergedCells(Sheet sheet,CellRangeAddress region){for(introwIndex=region.getFirstRow()+1;rowIndex<=region.getLastRow();rowIndex++){Row row=sheet.getRow(rowIndex);if(row!=null){Cell cell=row.getCell(region.getFirstColumn());if(cell!=null){// POI 3.16兼容:设置单元格类型为空来清空值cell.setCellType(Cell.CELL_TYPE_BLANK);}}}}/** * 安全获取单元格的字符串值 * * @param cell Excel单元格对象 * @return 单元格的字符串值,如果单元格为null则返回null */private StringgetCellValue(Cell cell){if(cell==null){returnnull;}intcellType=cell.getCellType();if(cellType==Cell.CELL_TYPE_STRING){returncell.getStringCellValue();}elseif(cellType==Cell.CELL_TYPE_NUMERIC){if(DateUtil.isCellDateFormatted(cell)){returnString.valueOf(cell.getDateCellValue());}else{returnString.valueOf(cell.getNumericCellValue());}}elseif(cellType==Cell.CELL_TYPE_BOOLEAN){returnString.valueOf(cell.getBooleanCellValue());}elseif(cellType==Cell.CELL_TYPE_FORMULA){returncell.getCellFormula();}returnnull;}}

示例代码,思路是这样,代码随便看看

clearMergedCells必须要有,否则虽然看着单元格合并了,但是下面的单元格每个还有自己的值,不像在excel里直接执行合并,只保留上面的值,这样的话在导入的情况下,会导致你按空处理合并单元格的第二行会莫名奇妙读到值,具体解释看最后

4.效果

导出效果(示例):


总结

先使用fill导出数据到内存,在内存中使用poi对需要合并的单元格进行操作

踩坑问题:
导出策略中,如果只合并单元格,不进行置空处理,这样导出的excel,合并的单元格并不像我们在excel中合并单元格一样只保留左上角的值,下面单元格也是有值的,如果是导出的时候当成空置判断就会有问题

在 Excel 中,合并单元格后,只有左上角的单元格保留值,其他被合并的单元格应该被清空。但 POI 的 addMergedRegion() 只是设置合并区域,不会自动清除其他单元格的值。
解决方案:
需要修改 FillMergeStrategy ,在执行合并后,清空被合并区域中除了第一个单元格之外的其他单元格的值:
代码已经修复

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/28 7:13:29

YOLO12检测结果后处理:NMS阈值调整与多框融合策略

YOLO12检测结果后处理&#xff1a;NMS阈值调整与多框融合策略 1. 引言&#xff1a;为什么检测框需要“精修”&#xff1f; 当你用YOLO12跑完一张图片&#xff0c;看到屏幕上密密麻麻的检测框时&#xff0c;是不是觉得大功告成了&#xff1f;先别急&#xff0c;这其实只是完成…

作者头像 李华
网站建设 2026/4/28 7:12:37

代码随想录算法训练营第三十九天|LeetCode 198 打家劫舍、LeetCode 213 打家劫舍 ||、LeetCode 337 打家劫舍 |||

参考文章均来自代码随想录 LeetCode 198 打家劫舍 参考文章链接 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯…

作者头像 李华
网站建设 2026/4/28 7:09:33

AI模型安全评估实战:多维度构建与行业解决方案

1. 项目概述 AI模型安全评估这个领域最近两年突然火了起来&#xff0c;但真正能说清楚该怎么做的团队其实不多。去年我们团队接手了一个金融行业的AI安全评估项目&#xff0c;客户要求我们对他们的信贷审批模型做全面"体检"&#xff0c;那次经历让我深刻认识到&#…

作者头像 李华
网站建设 2026/4/28 7:07:59

全球化出行回暖,为什么要升级护照识别能力

跨境旅游、商务出行、留学交流持续复苏&#xff0c;涉外证件办理与核验量显著增长。对酒店、旅行社、航空公司、银行、会展中心等机构来说&#xff0c;护照信息处理能力&#xff0c;直接关系到服务效率、客户体验与合规风险。过去靠人工应付小流量尚可&#xff0c;如今高峰期日…

作者头像 李华
网站建设 2026/4/28 7:02:22

470-510MHz频段无线通信系统设计与CC1100E+CC1190方案优化

1. 470-510MHz频段无线通信系统设计挑战在工业自动化和物联网应用中&#xff0c;470-510MHz频段因其良好的传播特性成为热门选择。这个频段属于中国短距离设备(SRD)管制范围&#xff0c;最大允许输出功率为17dBm&#xff08;50mW&#xff09;。实际部署中&#xff0c;工程师常面…

作者头像 李华