插播一条关于easypoi的,因为这两天正好在用。
java项目中可能会碰到生成凭证、对账单之类的需求,用easypoi的模板导出功能应该是一个不错的选择,因为easypoi简单易用轻量级,学习成本低、容易上手。
但是在使用过程中碰到如下问题(easypoi4.4.0):
- 使用foreach后,模板中设置的公式在生成的excel中不见了
- 模板中的合并单元格问题
- 不能支持excel中同一行放置两个结果集
foreach公式问题
先描述一下问题。
设置一个模板,非常简单,使用$fe设置foreach输出结果集,然后对某一输出做合计:
保存模板为template1.xlsx,然后写一个简单的测试程序。为了方便调试并解决问题,测试程序的项目不是在引入easypoi包的独立项目中做的,而是直接从官网下载easypoi 4.4.0的源码,在源码项目下新建一个测试class做的。
测试代码比较简单,所以就不贴出完整的代码了。数据准备好之后绑定模板、调用exportExcel生成excel文件、保存生成的excel文件。
...
TemplateExportParams templateExportParams=new TemplateExportParams("F:/template1.xlsx");
Map map=new HashMap();
map.put("userList",userList);
map.put("userList2",userList2);
map.put("userList3",userList3);
Workbook wk = ExcelExportUtil.exportExcel(templateExportParams,map);
FileOutputStream fo=new FileOutputStream("f:/test1.xlsx");
//wk.setForceFormulaRecalculation(true);
wk.write(fo);
wk.close();
然后打开生成的excel文件后,发现模板中设置的公式不见了:
反复测试后发现是模板的excel版本的问题,如果用excel的早期版本xls就没有问题,但是如果模板是xlsx,就有问题。
但是发现问题的原因已经是解决问题之后了。所以如果你也碰到了类似问题,可以选择使用xls格式的模板,当然也可以尝试找找原因,看看是否能彻底解决问题。
公式丢失的原因
所以我们就一定要发扬钻牛角尖的程序员精神探究一番公式消失的原因。
找到了ExcelExportOfTemplateUtil的addListDataToExcel方法,是专门处理foreach的(foreach指的就是模板中的fe、$fe等占位符,easypoi需要将此类占位符对应的数据集的每一行数据都循环写入到excel文件中)。
根据输入的map数据集完成对模板excel文件的shift(也就是在目标excel文件中插入行,以便对数据集中的数据进行循环写入)后,调用了PoiExcelTempUtil.reset对插入行之后的、顺序向后shift的原模板内容进行了resest。
//修复不论后面有没有数据,都应该执行的是插入操作
if (isShift && datas.size() > 1 && datas.size() * rowspan > 1 && cell.getRowIndex() + rowspan <= cell.getRow().getSheet().getLastRowNum()) {
int lastRowNum = cell.getRow().getSheet().getLastRowNum();
int shiftRows = lastRowNum - cell.getRowIndex() - rowspan;
cell.getRow().getSheet().shiftRows(cell.getRowIndex() + rowspan, lastRowNum, (datas.size() - 1) * rowspan, true, true);
mergedRegionHelper.shiftRows(cell.getSheet(), cell.getRowIndex() + rowspan, (datas.size() - 1) * rowspan, shiftRows);
templateSumHandler.shiftRows(cell.getRowIndex() + rowspan, (datas.size() - 1) * rowspan);
PoiExcelTempUtil.reset(cell.getSheet(), cell.getRowIndex() + rowspan + (datas.size() - 1) * rowspan, cell.getRow().getSheet().getLastRowNum());
}
不太了解reset的用途,试着注释掉这行代码,貌似对我的这个小测试也没有什么影响,而且公式消失不见的问题确实也解决了。
但是没搞清楚他具体的作用也不敢贸然注释不调用这个方法。所以,就简单看了一下这个方法。
不知道什么原因,如果是FORMULA的话就没做什么处理。所以就加了处理,为了展示明显一点,上图中注释掉的两句话就是新加的代码。
然后不管是对于xls的模板,还是xlsx的模板,都没有问题了。
模板中合并单元格的问题
这个问题具有一定的偶然性,不太好重现。
有些情况下,模板稍微复杂一点、输出数据的行数碰巧的话,会抛异常:模板错误。
所以我们有必要分析一下原因。
同样,还是看ExcelExportOfTemplateUtil的addListDataToExcel方法。
shift数据后,调用了mergedRegionHelper的shiftRow方法:
mergedRegionHelper
到这里我们就需要简单分析一下mergedRegionHelper类。
有一句注释:合并单元格帮助类。
就是用来处理合并单元格的,针对我们的案例(根据模板导出),分析了一下发现mergedRegionHelper其实主要就是用来处理模板插入数据之后的合并单元格的。
他的主要思想如下:
- ExcelExportOfTemplateUtil#parseTemplate解析模板的时候初始化mergedRegionHelper,mergedRegionHelper会读取模板当前sheet的所有合并单元格,缓存到属性mergedCache中。
- mergedCache的key值为sheet中有合并单元格的:row_col,value为合并行数、列数组成的数组。
- 当模板中针对foreach插入行(shift)之后,调用mergedRegionHelper的shiftRow方法对缓存在mergedCache中的数据做处理。
- 然后将数据集中的数据填写到新插入的行中,写入每一个cell之前首先判断单元格如果是合并单元格的话,则对当前正在处理的cell做合并处理。
- 判断是否是合并单元格的依据:原来模板中对应的cell是否为合并单元格,就是通过mergedRegionHelper的isMergedRegion方法、其实也就是检查mergedCache中是是否存在当前cell的缓存数据进行判断的。
不知道上述是否描述清楚了,以我们上面这个测试案例再简单解释一下。
比如我们模板中第4行、第3列第4列为合并单元格,mergedCache中记录大概是[4_3,[1,2]],意思是第4行第3列是合并单元格,合并了1行、2列(为了容易理解就不考虑起始列从0开始这种情况了)。
假设数据集是5条数据,那么需要在Excel中第2行下面插入4行(有1条数据写入到了原来模板的第2行了,所以不需要插入行)。
插入之后原来的第4行就变成第8行了,所以mergedCache缓存的数据就不对了。
mergedRegionHelper的shiftRow方法就是希望在插入行之后把这个错误给调整过来。
但是上图的这个调整算法有问题,比如第4行变成第8行的时候,假设模板的第8行本来就有合并单元格,就会存在原来的第8行的数据可能会被覆盖的风险,代码虽然对这种情况做了处理,但是确实没处理好,依然会有问题。
设想的解决方案之一是:如果缓存mergedCache中的keys能按照行数排倒序的话,从Excel表最下面的行开始由下至上逐步处理mergedCache中的数据的话,问题应该就能彻底解决。
但是其实这种缓存数据在缓存之后的使用过程中最好就别动了,可以想想是否还有其他办法解决shift之后带来的变动。
所以想到的第二个解决方案是:shift数据的时候不对mergedCache数据做重新处理,只需要让mergedRegionHelper记录下来该sheet当前共插入(shift)多少行,在需要使用mergedCache缓存数据的地方考虑这个插入行数就可以了。
采用第二个解决方案需要特别小心改造所造成的影响,需要仔细考察不止是mergedRegionHelper、还有mergedRegionHelper的调用方。
比如下面我们要说到的同一行输出两个结果集的场景,就不适合采用第二种解决方案,需要考虑用第一种解决方案。
mergedRegionHelper相对来说比较简单,但是其调用方可能相对复杂一点,主要包括:
- ExcelToHtmlService
- ExcelExportOfTemplateUtil
我的项目中暂时只用到了ExcelExportOfTemplateUtil,所以我们暂时只分析ExcelExportOfTemplateUtil。
合并单元格问题解决#mergedRegionHelper的改造
首先需要增加成员变量,初始化为0:
private int rowsShifted=0;
方法shiftrow,修改为简单粗暴的只累加插入行数:
方法getRowAndColSpan:
方法isMergedRegion:
方法isNeedCreate:
修改完毕。
合并单元格问题解决#ExcelExportOfTemplateUtil改造
其实按理说我们对mergedRegionHelper的改造方式不应该造成对调用方的影响,但是测试发现确实还是有影响的。
造成影响的主要在setForeachRowCellValue方法:
//合并对应单元格
boolean isNeedMerge = (params.getRowspan() != 1 || params.getColspan() != 1)
&& !mergedRegionHelper.isMergedRegion(row.getRowNum() + 1, ci);
这里应该是判断新插入的行的每一个cell是否是合并单元格,如果是的话就进行合并。判断的依据是模板中设置了foreach的行对应的cell(已经处理在params中了)是否是合并单元格。但是源码中除了判断params之外还必须是 !mergedRegionHelper.isMergedRegion(参数是当前cell),直观理解应该是:模板中对应的cell是合并单元格,同时当前cell不在mergedRegionHelper的缓存中。
不太理解加!mergedRegionHelper.isMergedRegion的原因,推测可能的逻辑是要判断必须是新插入的行、而不是模板中原来的行,才执行单元格的合并。
我们对mergedRegionHelper做了改造之后,这个地方的判断就必须要调整一下了:
图中红色框柱的部分注释掉就可以了。
重新设置了各种情况下的(当然也很有限了......)几个带有合并单元格的模板,包括继续测试修改之前有问题、报错的项目中的模板,都可以正常工作了。
当然,easypoi是要应对各种不同场景的,上述方案暂时可能只能应对我项目中碰到的、针对我的模板出现的问题,情况复杂一点的话上述方案可能仍然有问题。
支持excel中同一行放置两个结果集
这个问题我不知道其他人是否遇到过,比如我想用下面的模板导出:
在第一行的左侧3列为结果集userList结果集的数据,右侧3列为结果集userList2的数据。
比如userList设置2条数据,userList2设置3条数据。
想要实现这个目标还是有点难度的,同样需要修改ExcelExportOfTemplateUtil的addListDataToExcel方法,相对比较复杂一点:
这种情况下如果模板比较复杂的话可能还是会有问题,需要具体问题具体分析、解决。
以上。