# EasyExcel-Export-Demo **Repository Path**: Cau1i/easy-excel-export-demo ## Basic Information - **Project Name**: EasyExcel-Export-Demo - **Description**: 基于EasyExcel复杂的Excel导出Demo: 1.涉及动态表头、合并单元格等; 2.采用自定义合并策略进行复杂的单元格合并; 3.使用EasyExcel的模板填充方式,使用已有模板导出Excel模板流,再用该Excel模板流作为新模板再次进行导出,以此完成复杂的导出需求。 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 29 - **Forks**: 18 - **Created**: 2022-12-02 - **Last Updated**: 2025-08-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, Excel, easyExcel ## README # [EasyExcel-Export-Demo](https://blog.csdn.net/ManGooo0/article/details/128094925) ## 🎈 1 参考文档 >[填充Excel | Easy Excel官方文档](https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill) > >[EasyExcel-合并单元格 | 博客园-默念x](https://www.cnblogs.com/monianxd/p/16359369.html) --- ## 🔍2 个人需求 ### 2.1 数据需求 - **第一部分** - **粉色部分:**表头固定,直接使用模板; - **红色部分:**正常数据内容,和一般的Excel导出大同小异; - **第二部分** - **绿色部分:**上面的表头是动态的,并且这些字段没有落表; - **橙色部分:**左边的实测值、总权重得分、总得分相当于固定的表头; - **黄色部分:**这部分是正常数据,但是橙色部分也需要被当作成数据。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d9b7f86229844cba95ee84baa8ce5e46.png) ### 2.2 单元格合并需求 - **紫色部分:**表头部分,类别相同的进行**横向**和**纵向**合并; - **绿色部分:**根据相同类别,将表格数据进行**横向**合并; - **橙色部分:**相同两行实测值包括实测值所属的数据进行**纵向**合并; - **红色部分:**因为橙色部分加上绿色部分对应“实测值、实测值、总权重得分、总得分”一共四行,所以红色部分是相同的四行产品数据,需要进行**纵向**合并。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2240c985f9594288b110be6bd194e740.png) **合并后的样子:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/f673ba4e45c348b692e7ffa9dd5d83da.png) ## 💡 3 解决方案 ### 3.1 数据处理 使用**EasyExcel**利用**[模板填充](https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill)**的方式,以一个**单元格**为最小单位,把数据**全部查出来**,然后将数据处理成**一行一行**的形式进行填充,碰到相同的数据,就进行合并单元格。 ### 3.2 数据字段没落表 有一部分表头数据的字段没有落表,在实际数据库中都属于**一个字段**,例如下图:光学、电学、声学实际上都属于`category`,而不是`optics`、`electricity`、`acoustics`。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/9954582cd08948a98c39b1b93db530a2.png) **可以使用`map`的进行对数据进行处理和存储**,处理后的样子: ![在这里插入图片描述](https://img-blog.csdnimg.cn/f729124edfc841b79343e4b2b40ebc8d.png) ### 3.3 动态表头 一般都是固定表头,然后填充数据,相当于一维的。因为表头是**动态**的,所以第二部分数据相当于二维的,需要将**表头**和**表格**数据分别进行填充。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/61fdd628ac024f1397bd58441496beb7.png) EasyExcel的**填充**方式是通过**模板**进行填充导出的,那我们可以导出两次,第一次用`/resources/template`下的模板文件将Excel导出成流,接着以第一次导出的Excel流,作为第二次导出的模板,最后再导出需要的Excel表格。 **模板:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/436b612d70454513b62d39eaf1bce73d.png) **第一次导出:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/02b2895329c24c10a5ed50a3781bbd57.png) **第二次导出:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/2dcc87ff9bf94297b626544e4fc0d142.png) **以这种方法,不仅仅是导出两次,还可以导出多次,以此处理更加复杂的表格。** ### 3.4 合并单元格 参考官方的文章[合并单元格](https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write#%E5%90%88%E5%B9%B6%E5%8D%95%E5%85%83%E6%A0%BC)和文章[EasyExcel-合并单元格-默念x](https://www.cnblogs.com/monianxd/p/16359369.html),然后根据需要自定义合并策略`Strategy`并且继承于`AbstractMergeStrategy`,计算出需要合并单元格数量的列表,然后利用`CellRangeAddress`进行单元格合并。 ``` CellRangeAddress cellRangeAddress = new CellRangeAddress(起始行,结尾行,起始列,结尾列); sheet.addMergedRegionUnsafe(cellRangeAddress); ``` 使用合并策略的方式: ``` ExcelWriter screenTemplateExcelWriter = EasyExcel .write(templateOut) // 导出最终临时文件 .withTemplate(templateFileName) // 使用的模板 .registerWriteHandler(new XXXStrategy(需要的参数)) // 自定义单元格合并策略 .build(); ``` --- ## 🚀4 第一次导出(部分代码) ### 4.1 Excel 填充模板 1. 总体样貌,模板名称为`screenTemplate.xlsx`,工作表名称为`sheet0`。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/5d28b50440874c7b9f2a8766df8da45c.png) 2. 拉长单元格,查看具体变量。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/87088eff91de47aaa34922cfe90e8098.png) ### 4.2 ScreenServiceImpl 业务实现层 ```java @Service public class ScreenServiceImpl implements ScreenService { @Override public void export(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { // HttpServletResponse消息头参数设置 String filename = "exportFile.xlsx"; httpServletResponse.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); httpServletResponse.setContentType("application/octet-stream;charset=UTF-8"); httpServletResponse.addHeader("Pragma", "no-cache"); httpServletResponse.addHeader("Cache-Control", "no-cache"); // 通过ClassPathResource获取/resources/template下的模板文件 ClassPathResource classPathResource = new ClassPathResource("template/screenTemplate.xlsx"); // 这里用try-with-resource try ( // 获取模板文件 InputStream screenParamTemplateFileName = classPathResource.getInputStream(); OutputStream screenOut = httpServletResponse.getOutputStream(); BufferedOutputStream screenBos = new BufferedOutputStream(screenOut); ) { // --------------------------------基本配置-------------------------------- // 设置内容的策略 WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); // 设置内容水平居中对齐 contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 设置内容垂直居中对齐 contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 自动换行 contentWriteCellStyle.setWrapped(true); // 设置字体样式和大小 WriteFont contentWriteFont = new WriteFont(); contentWriteFont.setFontHeightInPoints((short) 12); contentWriteFont.setFontName("微软雅黑"); contentWriteCellStyle.setWriteFont(contentWriteFont); // 配置横向填充 FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build(); // sheet名称 WriteSheet writeSheet = EasyExcel.writerSheet("sheet0").build(); // --------------------------------基本配置-------------------------------- // ---------------------模拟获取第一部分的表格数据、表头参数--------------------- List screenGatherDTOList = new ArrayList<>(); // 构造5个产品数据 for (int i = 1; i <= 5; i++) { // 每份数据乘以4,为了合并单元格做准备 for (int j = 0; j < 4; j++) { ScreenGatherDTO screenGatherDTO = new ScreenGatherDTO(); screenGatherDTO.setScreenSize(String.valueOf(i * 10)); screenGatherDTO.setSupplier("厂商" + i); screenGatherDTO.setPartMode("型号" + i); screenGatherDTO.setResolution("1080P"); screenGatherDTO.setRefreshRate("60Hz"); screenGatherDTO.setPanel("IPS"); screenGatherDTOList.add(screenGatherDTO); } } if (CollectionUtils.isNotEmpty(screenGatherDTOList)) { for (int i = 0; i < screenGatherDTOList.size(); i++) { // 在屏规格末尾加上表格模板参数 screenGatherDTOList.get(i).setValueTemplateParam("{screenValueTemplateParam" + i + ".value}"); } } // 填充第一个表头的单元格 ScreenValueExcelDTO screenValueExcelDTO = new ScreenValueExcelDTO(); List screenValueExcelDTOList = new ArrayList<>(); screenValueExcelDTO.setValue("产品测试"); screenValueExcelDTOList.add(screenValueExcelDTO); // 在屏规格末尾加上表头模板参数 List screenTableExcelDTOList = new ArrayList<>(); for (int i = 0; i < 4; i++) { ScreenValueExcelDTO screenTableExcelDTO = new ScreenValueExcelDTO(); switch (i) { case 0: screenTableExcelDTO.setValue("{screenTableExcelDTOList.modelName}"); break; case 1: screenTableExcelDTO.setValue("{screenTableExcelDTOList.testItemCategory}"); break; case 2: screenTableExcelDTO.setValue("{screenTableExcelDTOList.testItemName}"); break; case 3: screenTableExcelDTO.setValue("{screenTableExcelDTOList.subTestItemName}"); break; default: break; } screenTableExcelDTOList.add(screenTableExcelDTO); } // ---------------------模拟获取第一部分的表格数据、表头参数--------------------- // --------------------------------第一次导出-------------------------------- ExcelWriter screenTemplateExcelWriter = EasyExcel .write(screenBos) // 导出临时文件,使用的是BufferedOutputStream .withTemplate(screenParamTemplateFileName) // 使用的模板 .registerWriteHandler(new ScreenValueMergeStrategy(screenGatherDTOList, 1, 6, 5)) // 自定义单元格合并策略 .registerWriteHandler(new HorizontalCellStyleStrategy(null, contentWriteCellStyle)) // 只配置内容策略,头部为null .build(); // 填充屏规格表格数据 screenTemplateExcelWriter.fill(new FillWrapper("screenGatherDTOList", screenGatherDTOList), writeSheet); // 填充第一个表头的单元格 screenTemplateExcelWriter.fill(new FillWrapper("screenValueExcelDTOList", screenValueExcelDTOList), writeSheet); // 填充表头模板参数 screenTemplateExcelWriter.fill(new FillWrapper("screenTableExcelDTOList", screenTableExcelDTOList), writeSheet); screenTemplateExcelWriter.finish(); // --------------------------------第一次导出-------------------------------- } catch (IOException e) { throw new RuntimeException(e); } } } ``` ### 4.3 ScreenValueMergeStrategy 自定义合并单元格策略 ```java public class ScreenValueMergeStrategy extends AbstractMergeStrategy { /** * 分组,每几行合并一次 */ private List exportFieldGroupCountList; /** * 合并的目标开始列索引 */ private Integer targetBeginColIndex; /** * 合并的目标结束列索引 */ private Integer targetEndColIndex; /** * 需要开始合并单元格的首行索引 */ private Integer firstRowIndex; public ScreenValueMergeStrategy() { } /** * @param exportDataList 待合并目标行的值 * @param targetBeginColIndex 合并的目标开始列索引 * @param targetEndColIndex 合并的目标结束列索引 * @param firstRowIndex 需要开始合并单元格的首行索引 */ public ScreenValueMergeStrategy(List exportDataList, Integer targetBeginColIndex, Integer targetEndColIndex, Integer firstRowIndex) { this.exportFieldGroupCountList = getGroupCountList(exportDataList); this.targetBeginColIndex = targetBeginColIndex; this.targetEndColIndex = targetEndColIndex; this.firstRowIndex = firstRowIndex; } @Override protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { if (cell.getRowIndex() == this.firstRowIndex && cell.getColumnIndex() >= targetBeginColIndex - 1 && cell.getColumnIndex() <= targetEndColIndex - 1) { int rowCount = this.firstRowIndex; for (Integer count : exportFieldGroupCountList) { if (count == 1) { rowCount += count; continue; } // 合并单元格 CellRangeAddress cellRangeAddress; for (int i = 0; i < targetEndColIndex - targetBeginColIndex + 1; i++) { cellRangeAddress = new CellRangeAddress(rowCount - 1, rowCount + count - 2, i, i); sheet.addMergedRegionUnsafe(cellRangeAddress); } rowCount += count; } } } /** * 该方法将目标列根据值是否相同连续可合并,存储可合并的行数 * * @param exportDataList * @return */ private List getGroupCountList(List exportDataList) { if (CollectionUtils.isEmpty(exportDataList)) { return new ArrayList<>(); } List groupCountList = new ArrayList<>(); int count = 1; for (int i = 1; i < exportDataList.size(); i++) { boolean equals = exportDataList.get(i).getPartMode().equals(exportDataList.get(i - 1).getPartMode()); if (equals) { count++; } else { groupCountList.add(count); count = 1; } } // 处理完最后一条后 groupCountList.add(count); return groupCountList; } } ``` ### 4.4 拉长单元格并查看导出效果 1. 未合并的效果。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/531fd513d44e4ba08edb82f4fed3a487.png) 2. 合并后的效果。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/23bdffcbdef04005a1624edaaf64795e.png) --- ## 5 第二次导出(完整代码):以第一次导出的excel流,作为第二次导出的模板 ### 5.1 至 5.5 直接查看仓库代码或者[博客](https://blog.csdn.net/ManGooo0/article/details/128094925) ### 5.6 项目目录结构 ![在这里插入图片描述](https://img-blog.csdnimg.cn/a894d91496174e81858ab62fc4951189.png) --- ## 📋6 最终导出效果 ![在这里插入图片描述](https://img-blog.csdnimg.cn/c2f56bbb458c4d40a349f67203d1b802.png) ## 📕7 原文链接 [基于EasyExcel模板填充方式进行二次导出(动态表头、合并单元格问题处理)](https://blog.csdn.net/ManGooo0/article/details/128094925)