[翻译]jxls入门指南

dark
LiinNs

今天碰到需求,要对埋点数据做统计,想利用Excel提供的强大函数库,所以激灵把数据导出成Excel文档。又苦于Apache POI太过于繁琐,谷歌搜寻到这款基于模板的工具。 原文文档见此:JXLS Getting Started Guide

以下属于搬砖翻译,不喜请轻喷。

假设我们有一个employee对象的集合想导出为Excel。

我们的Employee类可能长这样

public class Employee {
    private String name;
    private Date birthDate;
    private BigDecimal payment;
    private BigDecimal bonus;
    // ... constructors
    // ... getters/setters
}

为了利用jxls导出为Excel,我们需要作如下操作

  1. 添加相关的jar包至项目中
  2. 用指定的标记写一份Excel的模板文件
  3. 通过Jxls的API处理准备好的Excel模板,向其填充数据

如下,我们针对这三步做详细说明

添加相关的jar包至项目中

如果是使用Maven作为项目的管理工具,那么最简单的方式就是在配置文件中如下指定项目的依赖

<dependency>
    <groupId>org.jxls</groupId>
    <artifactId>jxls</artifactId>
    <version>2.4.6</version>
</dependency>

或者你可以从官方站点下载Jxsl的发布包,然后将其应用于自己的项目。

除了添加jxsl的核心依赖,我们还需添加一份Jxls transformer engine实现的依赖,用于执行所有从Java对象数据转换成Excel的底层操作。

就像在Main Concepts里Transformers这一节所描述的一样,Jxls的核心模块不去依赖任何一款具体的Java-Excel工具,而仅仅是通过一套预先定义好的接口去操作Excel。目前Jxls提供两套这个预先定义的接口的实现,分别是基于知名的Apache POIJava Excel API工具。

<dependency>
    <groupId>org.jxls</groupId>
    <artifactId>jxls-poi</artifactId>
    <version>1.0.15</version>
</dependency>
<dependency>
    <groupId>org.jxls</groupId>
    <artifactId>jxls-jexcel</artifactId>
    <version>1.0.7</version>
</dependency>

用指定的标签写一份Excel的模板文件

这里要求的模板文件其实是一份用特定标记描述Jxls应该如何输出数据的Excel文档。

Jxls提供了内建的标记处理器(markup processor),可以通过其解析模板文件以及提前操作指令。

根据要求亦可自定义一份标记处理器。通过自定义的标记书写一份Excel模板,并解析为合法的Jxls指令集(Jxls Commands structure)。

如下一起去了解Jxls内建的标记处理器如何完成工作。

默认情况下,Jxls支持通过Apache JEXL表达式从Excel模板文件内访问Java对象的属性和方法。在Jxls的提供的上下文中,透过某个指定的key必须要能访问到这个Java对象。如:为了能输出员工的姓名,我们可以将${employee.name}写于Excel的单元格中。通常情况下,我们将jexl表达式置于${}之间。我们还需确保在Jxls的上下文中能通过employeekey访问Employee对象。

表示属性的符号是可配置的,你或许想使用形如[[employee.name]]的表达式来表示。更多关于JEXL的用法,请查看Expression Language

然后最终的模板文件可能如下图,或点击此处下载

Excel模板

如图,第4行 所示,我们使用的就是上文讲解过的JEXL表达式来访问employee对象的属性。

A1单元格包含了一个jx:area(lastCell="D4")的批注,其中形如 Author: 的黑体字可以不用理会,为系统自动添加,也可能为 Administrator:,它指定了模板的根区域为 A1:D4

然后A4单元格批注中是一个Jxsl Each-Command jx:each(items="employees" var="employee" lastCell="D4")。这个Each-Command会遍历存于Jxls上下文中employees这个键下的集合,并将每一个集合项单次存入上下文内中由var关键词定义的employee键下。lastCell属性定义的是Each-Command的单个body区域,为A4:D4,往后它会被克隆并使用上下文中新Employee对象来循环处理数据。

在这个例子中,我们假设使用XlsCommentAreaBuilder这个类来构造模板文件中Jxls的处理区域,通过这个类我们可以将Jxls命令写在单元格的批注中。若非如此,你也可以将Jxls命令写在Java代码中。

通过Jxls的API处理准备好的Excel模板,向其填充数据

如下展示怎么使用Jxls API处理我们的Excel 模板

...
    logger.info("Running Object Collection demo");
    List<Employee> employees = generateSampleEmployeeData();
    try(InputStream is = ObjectCollectionDemo.class.getResourceAsStream("object_collection_template.xls")) {
        try (OutputStream os = new FileOutputStream("target/object_collection_output.xls")) {
            Context context = new Context();
            context.putVar("employees", employees);
            JxlsHelper.getInstance().processTemplate(is, os, context);
        }
    }
...

在这个代码示例中,我们将从classpath读取模板文件object_collection_template.xls。然后将处理结果输出于target/object_collection_output.xls

所有主要操作仅需一行代码
JxlsHelper.getInstance().processTemplate(is, os, context);

除非另做声明,JxlsHelper会默认将数据覆盖在Excel模板内写有标记的sheet中。
或者像下面这样,选择将数据生成于另一个sheet
JxlsHelper.getInstance().processTemplateAtCell(is, os, context, "Result!A1");
这样一来,操作区域就修改成了sheetResultA1单元格。

最终的结果如下图,或从此处下载
结果图

常见异常梳理

错误栈信息如下

...
2018-08-16 14:22:31.055 ERROR 43068 --- [nio-8080-exec-1] org.jxls.util.TransformerFactory         : Method createTransformer of org.jxls.transform.poi.PoiTransformer class thrown an Exception

java.lang.reflect.InvocationTargetException: null
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_144]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_144]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_144]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_144]
	at org.jxls.util.TransformerFactory.createTransformer(TransformerFactory.java:35) ~[jxls-2.4.6.jar:na]
	at org.jxls.util.JxlsHelper.createTransformer(JxlsHelper.java:381) [jxls-2.4.6.jar:na]
	at org.jxls.util.JxlsHelper.processTemplateAtCell(JxlsHelper.java:253) [jxls-2.4.6.jar:na]
...
...
2018-08-16 14:22:31.075 ERROR 43068 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: Cannot load XLS transformer. Please make sure a Transformer implementation is in classpath] with root cause

java.lang.IllegalStateException: Cannot load XLS transformer. Please make sure a Transformer implementation is in classpath
	at org.jxls.util.JxlsHelper.createTransformer(JxlsHelper.java:383) ~[jxls-2.4.6.jar:na]
	at org.jxls.util.JxlsHelper.processTemplateAtCell(JxlsHelper.java:253) ~[jxls-2.4.6.jar:na]
...

以上错误栈信息实为ObjectCollectionDemo.class.getResourceAsStream("object_collection_template.xls")未能成功获取到文件流。 由此我们应该去了解下相关API,如ObjectClass.class.getClassLoader().getResourceAsStream("name")ObjectClass.class.getResourceAsStream("name"),以及FileSystem,绝对路径,相对路径相关知识。

此处,假设项目结构如下

\- project
   \- src
      +- main
      |  +- java
      |  |  \- com.jxls.app
      |  |     +- object_1_template.xls
      |  |     \- ObjectCollectionDemo.java
      |  +- resources
      |     +- templates
      |     |  \- object_3_template.xls
      |     \- object_2_template.xls
      +- test
      \- pom.xml

主要处理逻辑写于ObjectCollectionDemo.java内

  1. ObjectCollectionDemo.class.getResourceAsStream("object_1_template.xls")可以访问object_1_template.xls,因为模板1和当前类同包,可以通过相对路径去访问,如果模板一位于com.jxls包下,是当前包的父包,则需通过..\object_1_template.xls去访问
  2. ObjectCollectionDemo.class.getResourceAsStream("\object_2_template.xls")可以访问object_2_template.xls,因为项目编译后模板2位于classpath根路径,则\表示根路径,下面类似,templates位于根路径下
  3. ObjectCollectionDemo.class.getResourceAsStream("\templates\object_3_template.xls")可以访问object_3_template.xls