使用 EasyPOI 优雅导出Excel模板数据(含图片)

公众号程序猿DD

共 4954字,需浏览 10分钟

 ·

2020-09-24 10:08

点击上方蓝色“程序猿DD”,选择“设为星标”

回复“资源”获取独家整理的学习资料!

作者 | 星悬月

来源 | blog.csdn.net/u012441819/article/details/96828044

EasyPOI功能如同名字Easy,主打的功能就是容易,让一个没接触过POI的人员可以方便的写出Excel导出,Excel模板导出,Excel导入,Word模板导出。通过简单的注解和模板语言(熟悉的表达式语法),完成以前复杂的写法。

本文主要通过简单的分析让读者知道Excel模板该如何编写,EasyPOI要如何使用才能导出满足自己需要的Excel数据,从而简化编码。同时本文还会对一些不常见的功能如图片导出功能进行说明,让读者少踩坑。

版本及依赖说明

EasyPOI4.0.0及以后的版本依赖于Apache POI的4.0.0及以后版本。所以在maven的配置中,两者的版本号一定要匹配。

需要注意的是,Apache POI的4.0.0相对之前的版本有很大的变更,如果之前代码中Excel操作部分依赖于旧的版本,那么不建议使用4.0.0及之后的版本。当然,如果之前代码不涉及或很少涉及WorkBook的创建细节,使用新版也没有问题。

笔者需要改写的项目基于JEECG 3.7版本,依赖的是3.9版本的Apache POI,而JEECG维护的jeasypoi版本最高只有2.2.0,而该版本并不支持模板导出图片功能。说到这里又要吐槽以下JEECG团队,既然自己不打算维护jeasypoi,那项目中直接使用官方的EasyPOI不就好了,2.2.0版本的jeasypoi给开发者挖了多少坑啊!

为了和旧版本兼容,又想使用EasyPOI带来的图片导出功能,所以笔者最终采用的EasyPOI版本是3.3.0,对应的Apache POI依赖是3.15。

Maven配置如下所示:

<properties>
    <poi.version>3.15poi.version>
    <easypoi.version>3.3.0easypoi.version>
properties>

<dependencies>
    <dependency>
        <groupId>org.apache.poigroupId>
        <artifactId>poiartifactId>
        <version>${poi.version}version>
    dependency>
    <dependency>
        <groupId>org.apache.poigroupId>
        <artifactId>poi-ooxmlartifactId>
        <version>${poi.version}version>
    dependency>
    <dependency>
        <groupId>org.apache.poigroupId>
        <artifactId>poi-ooxml-schemasartifactId>
        <version>${poi.version}version>
    dependency>
    <dependency>
        <groupId>org.apache.poigroupId>
        <artifactId>poi-scratchpadartifactId>
        <version>${poi.version}version>
    dependency>
    <dependency>
        <groupId>cn.afterturngroupId>
        <artifactId>easypoi-webartifactId>
        <version>${easypoi.version}version>
    dependency>
dependencies>

Excel模板的设计

我们使用EasyPOI的模板导出功能就是不想通过编码的方式来设计Excel报表的样式,所以工作的第一步就是设计Excel模板,分清楚哪些部分是固定的,哪些是需要循环填充的。EasyPOI有自己的表达式语言,每种表达式的详细介绍请参考后文的参考链接。

一个简单的Excel报表模板

一些简单的模板就不在这里详细解释了,只放一下效果图和模板配置内容。等读者明白了复杂的模板如何制作并如何填值的时候,简单的很快就能明白了。

先看看报表效果图:

再看看实际的模板:

看了上述两张图,是不是已经感受到模板导出功能的强大了呢?

一个复杂的Excel报表模板

下面要介绍的这个模板比较复杂,不像是常见的那种一行是一条记录的情况,所以将详细介绍该模板的配置,顺带对EasyPOI的部分表达式进行简单介绍。

还是先看效果图:

再看看模板:

这两张图一对比,是不是有种知识改变命运的感觉?

复杂模板设计剖析

从货品信息的模板图及效果图中我们发现,整个模板实际上分为上下两部分。其中上部分为不变的抬头信息,下部分为循环插入的货品明细信息。所以我们关注的重点是下半部分的语法。

下半部分的第一列图中没有显示完整,实际上是{{!fe: list t.id。

注意,这里 没有 }}符号!根据EasyPOI的官方文档,{{}}代表的是表达式,根据表达式取里边的值。仔细看图可以发现,表达式的闭合符号{{}}出现在图中的右下角。也就是说,从第一列{{开始至右下角}}结束,这中间的所有内容都是表达式的一部分。

因为整个模板信息都是表达式的一部分,所以即使是普通字符串也需要专门标明。下面对表达式中的子表达式进行逐个说明。

!fe: 遍历数据不创建row。

官方文档中的这句话大家理解起来可能有点费解,什么叫不创建row?实际上,不创建row是相对于创建row而言的,创建row的表达式是fe:。

就像是数据库中每条记录对应着一个实体对象,创建row表示每行就是一个实体对象Entity,这个实体对象的属性用{{}}表达式包裹起来。

不创建row表示整个表达式中只有一个实体对象Object,只不过这个Object比较特别,它是由list中N个Entity拼接起来的。每一个Entity不仅仅是指模型本身,也包含了Excel的样式,比如占用了几个单元格,单元格的坐标、排布顺序等。

list 自定义的名称,表示表达式中的数据集合,由代码以list为键,从Map中获取值的集合。

list这个名字容易理解,就是一个占位符,可以随便取。EasyPOI解析到list就知道Map中存在着该键的值的集合,后边解析到数据就从该集合中取即可。


t 预定义值,表示集合中的任意对象。

从模板中我们大致能感觉到,list中每个对象叫做t,t.name就代表t的name属性,所以t这个名字就可以随便叫,反正它和list一样,作用是占位符。

但实际上这是一个大坑!如果你把t换成了其他值比如g,模板中其他地方写g.name g.code等等,最终是解析不到的!官方文档对这一点并没有强调,而是作者实际踩了坑之后才发现的!

]] 换行符 多行遍历导出。

对于这个符号的官方解释也是莫名其妙,什么叫换行符,多行遍历导出?实际上它的意思就是,当解析到表达式中含有这个符号,该行后边的内容就不解析了,管你后边有没有其他内容或者样式。

该符号一定要写在每行的最后一列,不然会出现每行列数不一样的情况,EasyPOI内部做赋值的时候就会报空指针异常了。

‘’ 单引号表示常量值 ‘’ 比如’1’ 那么输出的就是 1

官方文档中这里的介绍也有坑。''是表示常量值,但实际上Excel中只是这么些是不对的,因为Excel的单元格中遇到'后会认为后面都是字符串,所以得在单元格中写''库别:',这样显示出来的才是'库别:',而不是字符串库别:'。

经过上述分析,图中的模板实际上就类似以下内容:

{{!fe: list t.id ‘库别:’ t.bin 换行 ‘商品名称:’ t.name 换行 ‘商品编号:’ t.code t.barcode 换行 ‘生产日期:’ t.proDate 换行 ‘进货日期:’ t.recvDate}}

如果list中有多条记录,上述字符串就再循环拼接一些内容,最终都在一个{{}}表达式中。

至此,模板的设计已剖析完毕,读者可根据自己的需求结合官方文档自行设计模板。下面将对模板赋值进行介绍。

准备模板数据

从上节的描述中可知,只需要准备一个Map的对象即可,其中键为list,值为一个List数组,数组中元素类型为Map。代码如下:

Map total = new HashMap<>();
List> mapList = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
    Map map = new HashMap<>();
    map.put("id", i + "");
    map.put("bin""001 1000千克");
    map.put("name""商品" + i);
    map.put("code""goods" + i);
    map.put("proDate""2019-05-30");
    map.put("recvDate""2019-07-07");

    // 插入图片
    ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
    BufferedImage bufferImg = ImageIO.read(BarcodeUtil.generateToStream("001"));
    ImageIO.write(bufferImg, "jpg", byteArrayOut);
    ImageEntity imageEntity = new ImageEntity(byteArrayOut.toByteArray(), 2001000);
    map.put("barcode", imageEntity);

    mapList.add(map);
}
total.put("list", mapList);

图片数据导出

上述代码中需要特殊关注的是图片导出部分。EasyPOI导出图片有两种方式,一种是通过图片的Url,还有一种是获取图片的byte[],毕竟图片的本质就是byte[]。因为笔者的项目中图片不是存放在数据库之中,而是需要根据查询结果动态生成条码,所以通过byte[]导出图片。

ImageEntity是EasyPOI内置的一个JavaBean,用于设定图片的宽度和高度、导出方式、RowSpan和ColumnSpan等。调试EasyPOI的源码可知,当设置了RowSpan或者ColumnSpan之后,图片的高度设置就失效了,图片大小会自动填充图片所在的单元格。

图片导出的坑点在于导出图片的大小。假设我们将四个单元格合成为一个,希望导出的图片能填充合并之后的单元格,但是对不起,EasyPOI暂时做不到,它只会填充合并之前左上角的单元格,具体原因如下源码所示:

//BaseExportService.java
ClientAnchor anchor;
if (type.equals(ExcelType.HSSF)) {
    anchor = new HSSFClientAnchor(0000, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1),
            cell.getRow().getRowNum() + 1);
else {
    anchor = new XSSFClientAnchor(0000, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1),
            cell.getRow().getRowNum() + 1);
}

可以看到,在创建图片插入位置的时候已经指定了图片的跨度为1行1列,即左上角的单元格。如果之前又设置了RowSpan或者ColumnSpan,那么图片高度的设置也会失效,最终导致导出的图片非常小。


个人认为ImageEntity提供的RowSpan或者ColumnSpan的set方法并没有什么用,因为我们动态创建的合并单元格并不能被赋值。所以,导出图片的最好方式就是直接指定它的高度,因为宽度会自动填充单元格,模板中单元格的宽度要合适。

//ExcelExportOfTemplateUtil.java
if (img.getRowspan()>1 || img.getColspan() > 1){
    img.setHeight(0);
    PoiMergeCellUtil.addMergedRegion(cell.getSheet(),cell.getRowIndex(),
            cell.getRowIndex() + img.getRowspan() - 1, cell.getColumnIndex(), cell.getColumnIndex() + img.getColspan() -1);
}

将数据导出至模板

以上准备工作全部完成后就可以将模板和数据进行组装了,或者说是渲染,代码如下所示:

public static void exportByTemplate(String templateName, Map data, OutputStream fileOut) {
    TemplateExportParams params = new TemplateExportParams("export/template/" + templateName, true);
    try {
        Workbook workbook = ExcelExportUtil.exportExcel(params, data);
        workbook.write(fileOut);
    } catch (Exception e) {
        LogUtil.error("", e);
    }
}

总结

网上针对EasyPOI的介绍多限于最基本的行插入功能,但实际上Excel模板的需求可能各式各样。本文只是抛砖引玉,对EasyPOI中的部分概念做了详细介绍,希望帮助大家少踩坑。

如果想详细了解EasyPOI的各种功能,参考链接中的文档说明及测试项目源码就是最好的学习资料。希望大家都能得心应手地使用EasyPOI,大大提升开发效率!

参考链接

EasyPOI官方文档

  • https://opensource.afterturn.cn/doc/easypoi.html

EasyPOI测试项目

  • https://gitee.com/lemur/easypoi-test

一些坑

近日有网友求助我解决EasyPOI的复杂模板配置问题,通过解决该网友的问题发现了EasyPOI中的几个坑点,补充说明几个问题。

在复杂模板设计剖析一节中已经描述了EasyPOI支持的复杂的模板该如何配置。该模板的配置是绝对正确的,但是有3个点没有说清楚,大家在照葫芦画瓢时容易出错:

  1. {{!fe: list需要在一个单独的列中。EasyPOI源码中是根据该单元格的行、列跨度来决定list中的每个元素需要多少行的。比如上述图片中,该单元格的跨度是5行1列,也就是说,以后list中的每个元素都会占用5行。如果觉得该列不符合自定义模板的风格,可以把该列的列宽设置为0,但一定需要有{{!fe: list。
  2. 在对象的起始和结束符号{{}}之间不能有任何空的单元格!代码中在解析到该单元格为空时会直接抛异常,如果就希望该单元格为空,得显示写入空字符串:’’’。
  3. 换行符]]必须占用每行的最后一个单元格!比如说第一行有10个单元格,第二行只用了前5个,那么不能直接在第5个结束直接写换行符]],而是需要把6-10个单元格合并,然后写入]]。参考上述图片中生产日期所在行的最后一列。这么设置的原因是EasyPOI要求每行的单元格数目完全一致,因为源码中判断了每个单元格的列跨度,如果提前使用了]]换行符,那么该列的数目就和其他行不同,那么赋值的时候就乱掉了,会出现索引异常。


往期推荐

赠书:一本书带你吃透Nginx应用与运维

超全的 Linux Shell 文本处理工具集锦,快收藏

今年2月的微盟“删库”主角被判 6 年有期徒刑

MySQL中的InnoDB是怎么解决幻读的?

Redis为什么变慢了?常见延迟问题定位与分析

为什么 GROUP BY 之后不能直接引用原表中的列?

Java开发中Websocket的技术选型参考


我们在星球聊了很多深度话题,你不来看看?

我的星球是否适合你?

点击阅读原文看看我们都聊过啥?

浏览 89
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报