絮语
好久不见伙计们,小子这厢有礼了~
因为最近事情太多,工作太忙,没有时间码字了,不过小子倒是积累了不少好的小小的东西(大佬们勿喷),后续找时间逐一码出来,哈哈
背景
使用Jeecg开源的3.6.2版本作为基础框架,Jeecg框架自带使用自己研发的AutoPoi 1.4.8来作为Excel导出的依赖工具。此框架也AutoPoi官方文档(传送门)说明支持使用Map自定义模板语法导出的功能:
也支持使用模板语法导出图片的功能:
因为项目中需要对复杂的报表进行导出,想着遵循原有框架依赖所以使用Jeecg自带的AutoPoi 1.4.8,按照官方文档所描述的如果需要导出图片,但没想到引发了一系列问题...
发现问题
注意:以下代码为部分业务代码,不涉及敏感信息,伙计们可以根据官方文档的例子对比查看!
大概步骤
就是:
- (1)定义模板文件;
- (2)获取模板绝对路径;
- (3)使用
TemplateExportParams建立模板导出参数对象; - (4)配置map具体通用模板数据;
- (5)使用
ImageEntity建立图片实体对象; - (6)将
ImageEntity图片实体对象也添加到map中; - (7)通过
ExcelExportUtil.exportExcel传入模板导出参数和配置的map具体模板数据,获取WorkBook对象; - (8)通过流响应前端;
rectification_template.xlsx的模板定义:
具体代码
根据官方文档Map模板导出的使用(业务代码):
// 注意:ImageEntity来自于AutoPoi 1.4.8
import org.jeecgframework.poi.entity.ImageEntity;
import org.jeecgframework.poi.excel.ExcelExportUtil;
import org.jeecgframework.poi.excel.entity.TemplateExportParams;
...
/**
* 导出excel
*/
@RequestMapping(value = "/exportXls/{id}")
public void exportXls(@PathVariable("id") String id, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
String domain = "http://localhost:9999/";
String requestURI = request.getRequestURI();
System.out.println("请求的 URI: " + requestURI);
if(StrUtil.isEmpty(id)) {
throw new JeecgBootException("导出失败,参数指定单id不能为空!");
}
InspurRectificationTable rectificationTable = inspurRectificationTableService.getInfo(id.toString());
if(ObjectUtil.isEmpty(rectificationTable)) {
throw new JeecgBootException("导出失败,未查询到id="+ id +"的指令单!");
}
if(!"passed".equals(rectificationTable.getApprovalStatus())) {
throw new JeecgBootException("导出失败,id="+ id +"的指令单未完成审批!");
}
// 标题
StringBuilder titleBuilder = new StringBuilder("整改指令单").append("-").append(id.toString());
//获取模板文件路径
File classpath = new File(ResourceUtils.getURL("classpath:").getPath());
File templateFilePath = new File(classpath, "/template/rectification_template.xlsx"); //注意要将模板文件放入在启动类所在模块的resources下,因为上面获取的classpath:的绝对路径
// 获取模板
TemplateExportParams params = new TemplateExportParams(templateFilePath.getAbsolutePath(), true); // 导入模板
// 参数数据集合
Map<String, Object> map = new HashMap<>();
map.put("instructCode", rectificationTable.getInstructCode());
map.put("instructName", rectificationTable.getInstructName());
map.put("instructTime", DateUtil.format(rectificationTable.getInstructTime(), "yyyy-MM-dd"));
map.put("inspectedByName", rectificationTable.getInspectedByName());
map.put("rectificationDeptName", rectificationTable.getRectificationDeptName());
map.put("recipientName", rectificationTable.getRecipientName());
map.put("instructAddressName", rectificationTable.getInstructAddressName());
map.put("limitedTerm", DateUtil.format(rectificationTable.getLimitedTerm(), "yyyy-MM-dd"));
map.put("existingProblems", rectificationTable.getExistingProblems());
map.put("rectificationRequirements", rectificationTable.getRectificationRequirements());
map.put("completionDate", DateUtil.format(rectificationTable.getCompletionDate(), "yyyy-MM-dd"));
// 图片实体
ImageEntity deptLeaderStampPathImage = new ImageEntity();
deptLeaderStampPathImage.setHeight(100);
deptLeaderStampPathImage.setWidth(100);
deptLeaderStampPathImage.setUrl(domain + rectificationTable.getDeptLeaderStampPath());
ImageEntity equipmentDeptStampPathImage = new ImageEntity();
equipmentDeptStampPathImage.setHeight(100);
equipmentDeptStampPathImage.setWidth(100);
equipmentDeptStampPathImage.setUrl(domain + rectificationTable.getEquipmentDeptStampPath());
ImageEntity responsiblePersonSignPathImage = new ImageEntity();
responsiblePersonSignPathImage.setHeight(100);
responsiblePersonSignPathImage.setWidth(251);
responsiblePersonSignPathImage.setUrl(domain + rectificationTable.getResponsiblePersonSignPath());
ImageEntity responsibleDeptPersonSignPathImage = new ImageEntity();
responsibleDeptPersonSignPathImage.setHeight(100);
responsibleDeptPersonSignPathImage.setWidth(251);
responsibleDeptPersonSignPathImage.setUrl(domain + rectificationTable.getResponsibleDeptPersonSignPath());
ImageEntity rectificationDeptStampPathImage = new ImageEntity();
rectificationDeptStampPathImage.setHeight(100);
rectificationDeptStampPathImage.setWidth(100);
rectificationDeptStampPathImage.setUrl(domain + rectificationTable.getResponsibleDeptPersonSignPath());
map.put("deptLeaderStampPath", deptLeaderStampPathImage);
map.put("equipmentDeptStampPath", equipmentDeptStampPathImage);
map.put("rectificationSituation", rectificationTable.getRectificationSituation());
map.put("responsiblePersonSignPath", responsiblePersonSignPathImage);
map.put("rectificationDeptStampPath", rectificationDeptStampPathImage);
map.put("responsibleDeptPersonSignPath", responsibleDeptPersonSignPathImage);
// 获取Workboo对象
Workbook book = ExcelExportUtil.exportExcel(params, map);
// 输出流响应
OutputStream outputStream = null;
try {
String fileName = titleBuilder.toString();
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes("gb2312"),"ISO-8859-1"));
outputStream = response.getOutputStream();
book.write(outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
// 模板视图(不推荐)
// ModelAndView mv = new ModelAndView(new JeecgTemplateExcelView());
// mv.addObject(TemplateExcelConstants.FILE_NAME, "测试指令单");
// // 添加表格参数
// mv.addObject(TemplateExcelConstants.PARAMS, params);
// // 添加模板参数
// mv.addObject(TemplateExcelConstants.MAP_DATA, map);
// mv.addObject(NormalExcelConstants.FILE_NAME, titleBuilder.toString()+".xlsx"); // 文件名称,但是前端会重新命名
// mv.addObject(NormalExcelConstants.PARAMS, new ExportParams(titleBuilder.toString(), "sheet1")); // 标题,sheet名称
// mv.addObject(NormalExcelConstants.DATA_LIST, new ArrayList<>()); // list数据
// return mv;
}
最初问题
导出发现,基础数据没有问题,但是凡是图片实体的直接打印结果是将ImageEntity给toString()了
问题分析过程
P1 配置和语法问题
一度以为是因为对AutoPoi的语法和配置不熟悉,所以一开始就疯狂查找关于AutoPoi图片模板导出的资料,发现业务模块中有poi-ooxml的冲突
POI OOXML是
Apache POI项目中的一个子项目,它旨在提供对Microsoft Office Open XML(OOXML)格式(例如docx,xlsx和pptx)的读写支持。 它允许Java开发人员在他们的应用程序中读取和写入Microsoft Office格式的文件,同时仍然保持格式的完整性。 通过使用POI OOXML,开发人员可以创建和修改Microsoft Office文件,从而为他们的用户提供更好的体验和更多的功能。有兴趣的可以研究下,因不属于此文章讨论范畴,简单介绍忽略,望伙计们海涵!
我这里经过研究讨论,发现此依赖没用,也不知道是谁搞得~(这里草地上的泥坑中一群马跑过去了...),是直接去掉了,也可以使用exclusion排除具体的依赖包
UReport2是一款高性能的架构在Spring之上纯Java报表引擎,通过迭代单元格可以实现任意复杂的中国式报表。
在UReport2中,提供了全新的基于网页的报表设计器,可以在Chrome、Firefox、Edge等各种主流浏览器运行(IE浏览器除外),打开浏览器即可完成各种复杂报表的设计制作。 UReport2是第一款基于Apache-2.0协议开源的中式报表引擎。有兴趣的可以研究下,因不属于此文章讨论范畴,简单介绍忽略,望伙计们海涵!
去掉解决完依赖冲突后发现还是不行,使用的语法经过和官网反复比对和查询了大量的资料发现自身没问题,果断换思路。
P2 不设图片路径而设置图片二进制数据
发现ImageEntity有个setData(byte[] data)的方法
作为大聪明的我已经将图片路径直接手写绝对路径的本地图片做测试了,但还是不行,所以我想是不是可以直接传入图片二进制数据来实现Excel导出图片效果,我真是太聪明了...
最后...
好吧...还是不行~
P3 抛弃AutoPoi使用EasyPoi
本身想着使用Jeecg的自带生态来做,果然打败我的还是我~
代码与<具体代码>一样,换一下包就行(为什么这么丝滑,我之前直接用Poi比较多,但也用过EasyPoi,没有具体深究,但是我隐隐约约的感到AutoPoi和EasyPoi肯定存在着某种联系~)
伙计们,这里省略了EasyPoi的依赖引入,请海涵!
注意,EasyPoi的版本是4.4.0,比较新,但是这也是引发后面情节的主要原因,请看我娓娓道来~
// 注意:ImageEntity来自于EasyPoi 4.4.0
import cn.afterturn.easypoi.entity.ImageEntity;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.TemplateExportParams;
...
//后面代码参考上面的一样
导出,又有新问题出现了,发现导出虽然不会直接把ImageEntity给toString了,但是图片却是空白。为什么?
局部图片(其他地方也一样空白):
查阅了大量资料,没找到具体原因。
何必呢~ 何必要逼我出手呢?于是各种buff(气死了)叠满的我蓄力开始~
问题解决
P1 EasyPoi 4.4.0模板ImageEntity导出图片空白问题
追溯源码
EasyPoi 4.4.0源码
看我今天不干死你!伙计们,点进去,一起干他!
ExcelExportUtil.exportExcel
parseTemplate(wb.getSheetAt(i), map, params.isColForEach());
setValueForCellByMap(row.getCell(i), map)
找到处理ImageEntity的分支, 具体处理方法:createImageCell(cell, (double)img.getHeight(), img.getRowspan(), img.getColspan(), img.getUrl(), img.getData());
createImageCell(cell, (double)img.getHeight(), img.getRowspan(), img.getColspan(), img.getUrl(), img.getData())
具体代码:
public void createImageCell(Cell cell, double height, int rowspan, int colspan, String imagePath, byte[] data) throws Exception {
if (height > (double)cell.getRow().getHeight()) {
cell.getRow().setHeight((short)((int)height));
}
Object anchor;
if (this.type.equals(ExcelType.HSSF)) {
anchor = new HSSFClientAnchor(10, 10, 1010, 245, (short)cell.getColumnIndex(), cell.getRow().getRowNum(), (short)(cell.getColumnIndex() + colspan - 1), cell.getRow().getRowNum() + rowspan - 1);
} else {
anchor = new XSSFClientAnchor(10, 10, 1010, 245, (short)cell.getColumnIndex(), cell.getRow().getRowNum(), (short)(cell.getColumnIndex() + colspan - 1), cell.getRow().getRowNum() + rowspan - 1);
}
if (StringUtils.isNotEmpty(imagePath)) {
data = ImageCache.getImage(imagePath);
}
if (data != null) {
PoiExcelGraphDataUtil.getDrawingPatriarch(cell.getSheet()).createPicture((ClientAnchor)anchor, cell.getSheet().getWorkbook().addPicture(data, this.getImageType(data)));
}
}
了解源码后发现
HSSFClientAnchor和XSSFClientAnchor用于创建一个新的端锚,并设置锚的左下和右下坐标,用于图片插入,画线等操作。参数相同:
HSSFClientAnchor(int dx1, int dy1, int dx2, int dy2, short col1, int row1, short col2, int row2) XSSFClientAnchor(int dx1, int dy1, int dx2, int dy2, short col1, int row1, short col2, int row2)
dx1:图片/锚点左边相对excel格的位置(x偏移) 范围值为:0~1023;即输100 偏移的位置大概是相对于整个单元格的宽度的100除以1023大概是10分之一
dy1:图片/锚点上方相对excel格的位置(y偏移) 范围值为:0~256 原理同上。
dx2:图片/锚点/锚点右边相对excel格的位置(x偏移) 范围值为:0~1023; 原理同上。
dy2:图片/锚点下方相对excel格的位置(y偏移) 范围值为:0~256 原理同上。
col1和row1:图片/锚点左上角的位置,以excel单元格为参考,比喻这两个值为(1,1),那么图片/锚点左上角的位置就是excel表(1,1)单元格的左上角的点(A,1)右下角的点。
col2和row2:图片/锚点右下角的位置,以excel单元格为参考,比喻这两个值为(2,2),那么图片/锚点右下角的位置就是excel表(2,2)单元格的右下角的点(B,2)右下角的点。
可见,创建锚点的右下坐标的结束单元格的列索引和行索引都是:当前的索引+默认的合并数值-1
那默认的是多少呢?
可见默认的colspan和rowspan都为1,那么空白的原因就找到了~
问题原因
EasyPoi 4.4.0源码中如果不手动设置ImageEntity的colspan和rowspan的默认值,默认值都为1,则被减去1后则参数col1、row1和col2、row2的值是一样的,意味着锚点的左上角位置和右下角位置是一样的,可以理解为图片宽高都为0。
解决
P1 使用时手动设置colspan和rowspan
// 注意:ImageEntity来自于EasyPoi 4.4.0
import cn.afterturn.easypoi.entity.ImageEntity;
...
ImageEntity imageEntity = new ImageEntity();
imageEntity.setHeight(100);
imageEntity.setWidth(100);
imageEntity.setColspan(2);
imageEntity.setRowspan(2);
imageEntity.setUrl("url");
...
P2 降低EasyPoi版本
因为是源码层面的问题,所以我第一时间考虑是不是版本的问题,EasyPoi 4.4.0版本是不是太高了,然后我查询一些资料后,发现EasyPoi 4.2.0版本源码中rowspan和colspan不会减1。
EasyPoi 4.2.0源码
createImageCell(Cell cell, double height, int rowspan, int colspan, String imagePath, byte[] data)
public void createImageCell(Cell cell, double height, int rowspan, int colspan, String imagePath, byte[] data) throws Exception {
if (height > (double)cell.getRow().getHeight()) {
cell.getRow().setHeight((short)((int)height));
}
Object anchor;
if (this.type.equals(ExcelType.HSSF)) {
anchor = new HSSFClientAnchor(0, 0, 0, 0, (short)cell.getColumnIndex(), cell.getRow().getRowNum(), (short)(cell.getColumnIndex() + colspan), cell.getRow().getRowNum() + rowspan);
} else {
anchor = new XSSFClientAnchor(0, 0, 0, 0, (short)cell.getColumnIndex(), cell.getRow().getRowNum(), (short)(cell.getColumnIndex() + colspan), cell.getRow().getRowNum() + rowspan);
}
if (StringUtils.isNotEmpty(imagePath)) {
data = ImageCache.getImage(imagePath);
}
if (data != null) {
PoiExcelGraphDataUtil.getDrawingPatriarch(cell.getSheet()).createPicture((ClientAnchor)anchor, cell.getSheet().getWorkbook().addPicture(data, this.getImageType(data)));
}
}
没有具体查询具体是什么版本EasyPoi改的这块源码内容,大概是4.2.0之后改的,有兴趣的朋友可以查询一下,补充在评论区,小子我感激不尽~
最终图片导出效果
到此EasyPoi4.4.0模版导出图片ImageEntity空白问题完美解决,撒花~
P2 AutoPoi 1.4.8模板ImageEntity导出图片toString问题
上面也说了,一开始我就感觉AutoPoi和ImageEntity很像,所以也可以使用追溯源码的方式查找原因。
追溯源码
AutoPoi 1.4.8源码
伙计们,点进去,继续干他啊~
ExcelExportUtil.exportExcel
parseTemplate(wb.getSheetAt(i), map, params.isColForEach())
setValueForCellByMap(row.getCell(i), map)
注意,setValueForCellByMap方法之前,和EasyPoi几乎一模一样~
但在setValueForCellByMap中没有找到关于ImageEntity的分支判断!也就是说ImageEntity根本没用,就是个摆设(又看见草地上的泥坑中一群马跑过去了...)
至于为什么被toString()了,估计就是走默认的字符形式的处理方式了~
private void setValueForCellByMap(Cell cell, Map<String, Object> map) throws Exception {
CellType cellType = cell.getCellTypeEnum();
if (cellType == CellType.STRING || cellType == CellType.NUMERIC) {
cell.setCellType(CellType.STRING);
String oldString = cell.getStringCellValue();
if (oldString != null && oldString.indexOf("{{") != -1 && !oldString.contains("fe:")) {
String params = null;
boolean isNumber = false;
if (this.isNumber(oldString)) {
isNumber = true;
oldString = oldString.replace("n:", "");
}
while(oldString.indexOf("{{") != -1) {
params = oldString.substring(oldString.indexOf("{{") + 2, oldString.indexOf("}}"));
oldString = oldString.replace("{{" + params + "}}", PoiElUtil.eval(params, map).toString());
}
if (isNumber && StringUtils.isNotBlank(oldString)) {
cell.setCellValue(Double.parseDouble(oldString));
cell.setCellType(CellType.NUMERIC);
} else {
cell.setCellValue(oldString);
}
}
if (oldString != null && oldString.contains("fe:")) {
this.addListDataToExcel(cell, map, oldString.trim());
}
}
}
问题原因
AutoPoi 1.4.8在源码层面没有关于ImageEntity的逻辑处理,就让他自生自灭了,这也可能是使用setData(byte[] data)直接设置图片的二进制数据没有效果的原因。
解决
无解~
希望后续版本会优化此问题~
结语
项目最终使用EasyPoi 4.2.0,但也没有彻底舍弃AutoPoi 1.4.8,而是采用共存的方式,因为Jeecg中的其他基础模块导入和导出几乎都基于它,不过后续再开发导入导出功能可能逐步替换为EasyPoi 4.2.0。
处事不决,源码解决~
祝福伙计们!
假如生活欺骗了你
不要悲伤,不要心急
忧郁的日子里须要镇静
相信吧,快乐的日子将会来临
快乐的日子将会来临
心儿永远向往着未来
现在却常是忧郁
一切都是瞬息
一切都将会过去
而那过去了的,就会成为亲切的怀恋
一键三连,又是码代码的一天~
