前言:
最近接到一个需求,要导出巡查日志报表,导出word使用的是easyPoi模板。模板中要实现遍历循环嵌套,在一个单元格内展示多张图片,先展示下最终效果:
使用的模板文件:
在网上找了好多资料,踩了好多坑,但都不是我想要的效果。最后查看了easyPoi的源码,找到赋值的地方,重写了此方法,实现了该需求。下面使用代码介绍如下。
一.项目中用的easyPoi版本依赖
<properties>
<easypoi.version>4.3.0</easypoi.version>
</properties>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>${easypoi.version}</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>${easypoi.version}</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>${easypoi.version}</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>${easypoi.version}</version>
</dependency>
二.easyPoi 支持的模板标签语法
下面列举下EasyPoi支持的指令以及作用,最主要的就是各种fe的用法
-
空格分割
-
三目运算 {{test ? obj:obj2}}
-
n: 表示 这个cell是数值类型 {{n:}}
-
le: 代表长度{{le:()}} 在if/else 运用{{le:() > 8 ? obj1 : obj2}}
-
fd: 格式化时间 {{fd:(obj;yyyy-MM-dd)}}
-
fn: 格式化数字 {{fn:(obj;###.00)}}
-
fe: 遍历数据,创建row
-
!fe: 遍历数据不创建row
-
$fe: 下移插入,把当前行,下面的行全部下移.size()行,然后插入
-
#fe: 横向遍历
-
v_fe: 横向遍历值
-
!if: 删除当前列 {{!if:(test)}}
-
单引号表示常量值 ‘’ 比如’1’ 那么输出的就是 1
-
&NULL& 空格
-
&INDEX& 表示循环中的序号,自动添加
-
]] 换行符 多行遍历导出
-
sum:统计数据
-
cal: 基础的+-X% 计算
-
dict: 字典
-
i18n: 国际化
三.踩坑及重写jar包指定方法方式
在网上找了下easyPoi文档看文档上是支持多层循环的,但不知道为啥示例没有。
也支持横向遍历,所以我就改了下模板,试试能不能实现,结果不能,不知道语法是不是错了。
以上方式都不能实现我想要的效果,就想着看下源码,找到了往单元格赋值图片的方法代码。代码路径为:cn.afterturn.easypoi.word.parse.excel.ExcelMapParse。
看了下实现对象类型是ImageEntity 而我的对象里字段类型是List<ImageEntity>,因为对象类型不匹配,所以图片没赋值上。那就有思路了,直接重写这个方法就行了。
重写步骤:
-
找到你所要重写的方法的所在类,查看其中的路径;
-
在我们的 src 目录下新建一个同包名同类名的类;
-
将 jar 包中的重写方法所在类的所有代码复制到我们新建的同包名同类名的类中;
-
在我们新建的同包名同类名的类中修改对应的方法中的代码,注意要保持方法中的参数不要发生改变,也不要删除原类中已有的方法,但是可以新增一些方法。
原理:编译输出的时候会优先使用我们 src 下面的类,而不是优先使用 Jar 包里面的类,这样就达到了覆盖 jar 包方法的目的
。
四.代码实现:
1.按重写步骤先创建指定类ExcelMapParse
2.重写parseNextRowAndAddRow方法
public static void parseNextRowAndAddRow(XWPFTable table, int index, List<Object> list) throws Exception {
XWPFTableRow currentRow = table.getRow(index);
List<ExcelForEachParams> paramsList = parseCurrentRowGetParamsEntity(currentRow);
String listname = ((ExcelForEachParams) paramsList.get(0)).getName();
boolean isCreate = !listname.contains("!fe:");
listname = listname.replace("!fe:", "").replace("$fe:", "").replace("fe:", "").replace("{{", "");
String[] keys = listname.replaceAll("\s{1,}", " ").trim().split(" ");
((ExcelForEachParams) paramsList.get(0)).setName(keys[1]);
List<XWPFTableCell> tempCellList = new ArrayList();
tempCellList.addAll(table.getRow(index).getTableCells());
Map<String, Object> tempMap = Maps.newHashMap();
LOGGER.debug("start for each data list :{}", list.size());
Iterator var11 = list.iterator();
while (var11.hasNext()) {
Object obj = var11.next();
currentRow = isCreate ? table.insertNewTableRow(index++) : table.getRow(index++);
tempMap.put("t", obj);
Object val;
int cellIndex;
for (cellIndex = 0; cellIndex < currentRow.getTableCells().size(); ++cellIndex) {
val = PoiElUtil.eval(((ExcelForEachParams) paramsList.get(cellIndex)).getName(), tempMap);
clearParagraphText(((XWPFTableCell) currentRow.getTableCells().get(cellIndex)).getParagraphs());
//todo 重写方法 注意未测试其他场景,主要是支持嵌套循环图片
if (val instanceof ImageEntity) {
addAnImage((ImageEntity) val, (XWPFTableCell) tempCellList.get(cellIndex));
} else if (val instanceof List) {
int finalCellIndex = cellIndex;
((List >) val).forEach((image) -> {
if (image instanceof ImageEntity) {
addAnImage((ImageEntity) image, (XWPFTableCell) tempCellList.get(finalCellIndex));
}
});
} else {
PoiWordStyleUtil.copyCellAndSetValue((XWPFTableCell) tempCellList.get(cellIndex), (XWPFTableCell) currentRow.getTableCells().get(cellIndex), val.toString());
}
}
for (; cellIndex < paramsList.size(); ++cellIndex) {
val = PoiElUtil.eval(((ExcelForEachParams) paramsList.get(cellIndex)).getName(), tempMap);
XWPFTableCell cell = currentRow.createCell();
if (((ExcelForEachParams) paramsList.get(cellIndex)).getColspan() > 1) {
cell.getCTTc().addNewTcPr().addNewGridSpan().setVal(new BigInteger(((ExcelForEachParams) paramsList.get(cellIndex)).getColspan() + ""));
}
//todo 重写方法 注意未测试其他场景
if (val instanceof ImageEntity) {
addAnImage((ImageEntity) val, cell);
} else if (val instanceof List) {
((List >) val).forEach((image) -> {
if (image instanceof ImageEntity) {
addAnImage((ImageEntity) image, cell);
}
});
} else {
PoiWordStyleUtil.copyCellAndSetValue((XWPFTableCell) tempCellList.get(cellIndex), cell, val.toString());
}
}
}
table.removeRow(index);
}
先判断对象是否是集合类型,集合里面的对象是否是ImageEntity类型,是的话就把图片对象插入到单元格里面。
注意:这里可以看你业务逻辑具体实现,代码不用非得一致!
3.导出工具类
public class ExportWordUtil {
/**
* 导出word通用版
*
* @param params
* @param word
* @param response
*/
public void exportWordCurrencyByImage(Map<String, Object> params, String word, String fileName, HttpServletResponse response) {
String baseLocation = "/doctemplate/";
try {
InputStream is = this.getClass().getResourceAsStream(baseLocation + word);
XWPFDocument doc = new MyXWPFDocument(is);
WordExportUtil.exportWord07(doc, params);
downloadWord(true, fileName, doc, response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Word导出
*
* @param fileName Excel导出
* @param response HttpServletResponse对象
*/
public static void downloadWord(boolean flag, String fileName, XWPFDocument xwpfDocument, HttpServletResponse response) {
try {
if (StringUtils.isEmpty(fileName)) {
throw new RuntimeException("导出文件名不能为空");
}
String encodeFileName = URLEncoder.encode(fileName, "UTF-8");
// response.setHeader("content-Type", "application/vnd.ms-excel; charset=utf-8");
response.setHeader("content-Type", "application/msword; charset=utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" + encodeFileName);
response.setHeader("FileName", encodeFileName);
response.setHeader("flag", String.valueOf(flag));
response.setHeader("Access-Control-Expose-Headers", "FileName");
xwpfDocument.write(response.getOutputStream());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
4.HttpImgUtils 工具类
因为访问图片地址比较耗时,图片一多可能会导致断开连接,所以此处改成多线程请求获取图片字节流,最后合并结果再返回!
4j
public class HttpImgUtils {
/**
* 异步线程池
*/
public static final ExecutorService EXECUTOR = new ThreadPoolExecutor(5, 10,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(2000), new ThreadFactoryBuilder()
.setNameFormat("asyncTaskThreadPool-pool-%d").build(), new ThreadPoolExecutor.AbortPolicy());
/**
* 获取网络图片转成字节流
*
* @param strUrl 完整图片地址
* @return 图片资源数组
*/
public static byte[] getNetImgByUrl(String strUrl) {
try {
URL url = new URL(strUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(2 * 1000);
// 通过输入流获取图片数据
InputStream inStream = conn.getInputStream();
// 得到图片的二进制数据
return readInputStream(inStream);
} catch (Exception e) {
log.error("获取图片失败[{}]", strUrl, e);
}
return null;
}
/**
* 从输入流中获取字节流数据
*
* @param inStream 输入流
* @return 图片流
*/
private static byte[] readInputStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
// 设置每次读取缓存区大小
byte[] buffer = new byte[1024 * 10];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
inStream.close();
return outStream.toByteArray();
}
/**
* 将图片转成Word中可渲染的实体
*
* @param fileUrl 字符串
* @param width
* @param height
* @return
*/
public static ImageEntity initImageSingleData(String fileUrl, Integer width, Integer height) {
ImageEntity result = new ImageEntity();
if (StringUtils.isNotBlank(fileUrl)) {
byte[] imageData = HttpImgUtils.getNetImgByUrl(fileUrl);
result.setWidth(width);
result.setHeight(height);
result.setData(imageData);
result.setType(ImageEntity.Data);
} else {
result.setWidth(width);
result.setHeight(height);
result.setData(null);
result.setType(ImageEntity.Data);
}
return result;
}
/**
* 将图片转成Word中可渲染的实体多线程操作
*
* @param fileUrl 字符串,多个用逗号隔开
* @param width
* @param height
* @return
*/
public static List<ImageEntity> initImageDataBatch(String fileUrl, Integer width, Integer height) {
List<ImageEntity> result = new ArrayList<>();
if (StringUtils.isNotBlank(fileUrl)) {
if (fileUrl.indexOf(",") > 0) {
String[] fileUrlArr = fileUrl.split(",");
CountDownLatch countDownLatch = new CountDownLatch(fileUrlArr.length);
System.out.println("开始" + System.currentTimeMillis());
for (String s : fileUrlArr) {
Runnable runnable = () -> {
try {
byte[] imageData = HttpImgUtils.getNetImgByUrl(s);
ImageEntity item = new ImageEntity();
item.setWidth(width);
item.setHeight(height);
item.setUrl(s);
item.setData(imageData);
item.setType(ImageEntity.Data);
result.add(item);
System.out.println("当前线程name : " + Thread.currentThread().getName());
} finally {
countDownLatch.countDown();
}
};
EXECUTOR.execute(runnable);
}
countDownLatch.await(); // 主线程阻塞,等计数器==0,唤醒主线程往下执行。
System.out.println("结束" + System.currentTimeMillis());
} else {
byte[] imageData = HttpImgUtils.getNetImgByUrl(fileUrl);
ImageEntity item = new ImageEntity();
item.setWidth(width);
item.setHeight(height);
item.setUrl(fileUrl);
item.setData(imageData);
item.setType(ImageEntity.Data);
result.add(item);
}
} else {
ImageEntity item = new ImageEntity();
item.setWidth(width);
item.setHeight(height);
item.setData(null);
item.setType(ImageEntity.Data);
result.add(item);
}
return result;
}
}
5.业务代码
这里只展示一部分业务逻辑,uploadEventImageStrs集合里是外网能访问的图片地址。
6.ImageEntity实体类
true) (chain =
(JsonInclude.Include.NON_NULL)
public class StatisticsVO implements Serializable {
private static final long serialVersionUID = -6666621715005005787L;
private List<ImageEntity> image;
}
总结
本文介绍了EasyPoi 模板导出word文档方法步骤,并给出了工具类代码,拿来即用。其中word文档中遍历嵌套循环图片的需求,最终是靠重写easyPoi的方法实现的。其中优化点是多线程获取图片字节流,大大减少了接口响应时间,提高了效率。有什么问题欢迎私信我一起交流。
发表评论