JXLS 库导出复杂 Excel
JXLS java 库导出复杂 excel。JXLS 简述、与 Apache POI 区别、使用姿势、核心特性(如 XLS 区域、单数据绑定、循环、条件判断、公式支持、多工作表、图像插入、合并单元格、单元格样式)、各功能使用示例等。
版本
- jdk:17
- spring boot:3.2.2
- jxls:2.14.0
- apache poi:5.2.2
目录
1 概述
1.1 什么是 JXLS
JXLS 是一个构建在 Apache POI 之上、基于模板的、高层抽象的、开源的 java 库,专门用于简化 excel 的生成。其核心思想是将 excel 的布局设计与数据填充逻辑分离。JXLS 官方文档。
1.2 JXLS 与 Apache POI 区别
特性 | Apache POI | JXLS |
---|---|---|
核心思想 | 底层、全面的 excel API,直接操作 Microsoft office | 基于模板,通过在模板的标注/批注中使用模板语法来进行数据渲染 |
优点 | 功能及其强大灵活,几乎可实现 excel 支持的所有操作 | 大幅简化 excel 生成,维护方便(修改模板即可) |
缺点 | 代码冗长、繁琐,尤其是操作复杂 excel 时;学习曲线相对陡峭; | 受限于模板引擎,灵活性略低;需要额外学习模板语法(但相对简单); |
主要用途 | 精细操作、读取解析、高级特性、动态结构、其它 office 格式 | 快速生成格式化的数据报表 |
2 使用姿势
2.1 引入依赖
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>2.14.0</version>
</dependency>
2.2 设计模板
创建 excel 模板文件(如 template.xlsx),设计内容格式、样式等。如下示例:
2.3 添加标记
在刚才创建的模板中添加标注(批注)。
- 在 A1 单元格 右键 -> 添加批注:jx:area(lastCell = “H4”),其中 lastCell=“H4” 表示当前 excel 表格覆盖区域为 A1:H4(并非表示实际数据覆盖区域,而是指模板内容覆盖区域)。
- 在 C2 单元格键入 ${name},name 为 名称 对应数据的 key。
- 在 G2 单元格键入 ${time},time 为 时间 对象数据的 key。
- 在 A4 单元格添加批注:jx:each(items=“employees” var=“item” lastCell=“D4”),其中 items 表示列表数据集所对应 key,var 表示循环内的当前元素,lastCell 表示当前列表的模板范围。
- 在 A4、B4、C4、D4 单元格依次键入 i t e m . i n d e x 、 {item.index}、 item.index、{item.nickname}、 i t e m . n a m e 、 {item.name}、 item.name、{item.region}。
最终模板将如下:
2.4 准备数据
在业务逻辑代码中准备数据,如下:
List<Employee> employees = new ArrayList<>(); // Employee 类较简单 此处略过
employees.add(Employee.builder().index(1).nickname("影流之主").name("劫").region("艾欧尼亚").build());
employees.add(Employee.builder().index(2).nickname("潮汐海灵").name("菲兹").region("比尔吉沃特").build());
employees.add(Employee.builder().index(3).nickname("九尾妖狐").name("阿狸").region("艾欧尼亚").build());
Context context = new Context(); // JXLS 上下文对象 使用该对象将数据与模板绑定
context.putVar("name", "吧啦吧啦");
context.putVar("time", "2025-05-21");
context.putVar("employees", employees);
2.5 生成 excel
String template = "/template/template.xlsx"; // 模板文件位于 resources/template/ 目录下
String excel = "C:\\Users\\Lenovo\\Desktop\\test.xlsx";
try (InputStream is = ExportTest.class.getResourceAsStream(template);
OutputStream os = Files.newOutputStream(Paths.get(excel))) {
JxlsHelper.getInstance()
.processTemplate(is, os, context); // 传入 输入流、输出流、上下文对象 生成 excel 文件
} catch (IOException e) {
throw new RuntimeException("导出Excel失败", e);
}
生成结果如下:
3 核心特性
JXLS 基于模板的核心特性包括 XLS 区域、单数据绑定、循环、条件判断、分组与汇总、公式支持、多工作表、图像插入、单元格样式、合并单元格等。
3.1 XLS 区域
XLS Area 是指 excel 模板中需要转换的矩形区域,即模板当前工作表中模板内容所占区域,实际上是指模板内容中以最长行和最高列为边的矩形区域。以 2 章节中模板为例,其区域模板语法为:
jx:area(lastCell = "H4")
其中 lastCell 即表示模板内容矩形区域,即 H 列与第 4 行所对应的单元格 H4。
3.2 单数据绑定
模板中独立数据的渲染,可直接在单元格中使用 ${name} 方式当作占位符,其中 name 表示当前单元格对应数据的 key,其支持对象的多层嵌套(即渲染时可从潜嵌套的对象中获取到属性值)。如 e m p l o y e e . r e g i o n 、 {employee.region}、 employee.region、{aaa.bbb.ccc.fieldName}。
3.3 循环
循环操作对应模板语法的命令为 each, 其用来遍历集合进行数据渲染,同时可进行过滤、分组、排序、枚举值替换等操作。其命令属性如下:
items:要遍历的数据集的变量名称。
var:循环内的元素变量名称。
lastCell:当前列表的模板范围,即当前列表模板区域的最后一个单元格。
select:其核心作用是过滤或筛选传入的集合,可通过集合中元素自身属性或上下文其它变量,动态决定哪些元素应该出现再最终的 excel 中。其值为返回一个布尔值的 JEXL 表达式。可在 JEXL 表达式中访问元素属性、JXLS Context 上下文中的其它变量、JEXL 支持的运算符和函数等。
假设已有类 Employee 拥有 index(序号)、nickname(称号)、name(名称)、region(所属地区)、attackPower(普通攻击力)属性和 isAssAssin() 方法(该方法返回布尔值),示例如下:
基于元素属性过滤(单一条件):
// 只渲染普通攻击力 > 50 的英雄 jx:each(items="employees" var="item" lastCell="D4" select="item.attackPower > 50")
基于元素属性过滤(组合条件):
// 只渲染所属地区为艾欧尼亚 且 普通攻击力 > 50 的英雄 jx:each(items="employees" var="item" lastCell="D4" select="item.region == 'Ionia' && item.attackPower > 50")
基于元素属性过滤(包含关系 in):
// 只渲染所属地区属于 艾欧尼亚 或 诺克萨斯 的英雄 jx:each(items="employees" var="item" lastCell="D4" select="item.region in ['Ionia', 'Noxus']")
基于元素方法过滤:
// 只渲染刺客类英雄 // Employee 类中的 isAssassin() 返回值表示其是否为刺客 // 则 JEXL 在解析表达式时会将 item.assassin 解析为调用 item.isAssassin() 方法 jx:each(items="employees" var="item" lastCell="D4" select="item.assassin")
结合 Context 上下文变量动态过滤:
// 只渲染所属地区为艾欧尼亚 且 普通攻击力 > 50 的英雄 // 其中变量 region 和 attackPower 为上下文对象 Context 中的动态值 Context context = new Context(); context.putVar("region", "Ionia"); context.putVar("attackPower", 50); context.putVar("employees", employees); jx:each(items="employees" var="item" lastCell="D4" select="item.region == region && item.attackPower > attackPower")
注意事项:
- 空指针安全:JEXL 表达式中可访问嵌套属性,但需注意空指针问题。如 item.aaa.bbb.fieldName,若 aaa 为 null 则会出现空指针异常。推荐使用安全操作符 ?. ,如 item.aaa?.bbb?.fieldName,若 aaa 为 null,则整个表达式为 null,null 在布尔值上下文中通常被视为 false。
- 性能问题:建议在 java 业务代码中对数据进行过滤。
- 与 groupBy 使用:select 过滤发生在 groupBy 分组之前,若有在分组后进行二次过滤的需求,则可结合 jx:if 指令在分组后进行二次过滤。
- 值比较:使用 == 或 != 比较值时,需确保双方类型一致,如都是 String 或 Integer。
- 转义特殊字符:对于 xml 相关的特殊字符(如 < > 等),需要进行转义。
groupBy:用于实现数据分组和分组汇总。可将数据按指定属性分组,并在分组前后添加自定义内容(如分组标题、小计等),同时可通过 groupOrder 指定组内排序方式(如 desc 或 asc)。示例如下:
模板:
A1 批注:jx:area(lastCell = "G5") A3 批注:jx:each(items="employees" var="item" lastCell="E5" groupBy="region" groupOrder="desc") A5 批注:jx:each(items="item.items" var="e" lastCell="E5")
数据:
List<Employee> employees = new ArrayList<>(); employees.add(Employee.builder().index(1).nickname("影流之主").name("劫").region("艾欧尼亚").attackPower(66).build()); employees.add(Employee.builder().index(2).nickname("潮汐海灵").name("菲兹").region("比尔吉沃特").attackPower(58).build()); employees.add(Employee.builder().index(3).nickname("九尾妖狐").name("阿狸").region("艾欧尼亚").attackPower(53).build()); Context context = new Context(); context.putVar("name", "吧啦吧啦"); context.putVar("time", "2025-05-21"); context.putVar("employees", employees);
结果:
orderBy:根据属性进行排序,且可根据多个属性排序,用逗号隔开。如 orderBy=“region desc, attackPower asc”。
3.4 条件判断
jx:if 命令,用来根据条件表达式显示/隐藏单元格。如:jx:if(condition=“e.attackPower > 60”, lastCell=“E4”, areas=[“A4:E4”,“A5:D5”]),其中 condition 表示条件表达式,areas 为区域数组,数组第一个元素指定条件为真时要显示的区域,数组第二个元素指定条件为假时要显示的区域。示例如下:
模板:
A1 批注:jx:area(lastCell = "G5") // 该条件表示 若 attackPower > 60 则不渲染 A4 批注:jx:each(items="employees" var="e" lastCell="E4") jx:if(condition="e.attackPower > 60", lastCell="E4", areas=["A4:E4","A5:D5"])
结果:
3.5 公式支持
JXLS 通过 evaluateFormulas 属性控制是否在模板中启用 excel 公式。其默认为 true,表示启用,但启用公式会消耗一定性能,故,若实际无公式使用需求,则可将其关闭。
JxlsHelper.getInstance()
.setEvaluateFormulas(false) // 关闭公式支持
.processTemplate(is, os, context);
同时支持参数化公式,即在公式中使用变量。当在公式中使用变量时,需要使用 $[ ] 符号将公式包裹,且公式那的变量需要通过 ${} 标记。如:
$[SUM(E4) * ${param}] // 其中 param 参数为 Context 上下文变量
3.6 多工作表
可通过 jx:each 命令和 multisheet 属性来动态创建多工作表的 excel。其中 multisheet 属性表示工作表列表,其元素中应包含每个工作表的名称和数据。示例如下:
模板:
A1 批注:jx:area(lastCell = "G4") jx:each(items="regions", var="region", multisheet="region.region", lastCell="G4") A4 批注:jx:each(items="region.employees" var="e" lastCell="E4")
数据:
List<Region> regions = new ArrayList<>(); List<Employee> employees = new ArrayList<>(); employees.add(Employee.builder().index(1).nickname("影流之主").name("劫").region("艾欧尼亚").attackPower(66).build()); employees.add(Employee.builder().index(2).nickname("刀锋舞者").name("艾瑞莉娅").region("艾欧尼亚").attackPower(65).build()); employees.add(Employee.builder().index(3).nickname("九尾妖狐").name("阿狸").region("艾欧尼亚").attackPower(53).build()); regions.add(Region.builder().region("艾欧尼亚").employees(employees).build()); List<Employee> employees1 = new ArrayList<>(); employees1.add(Employee.builder().index(1).nickname("潮汐海灵").name("菲兹").region("比尔吉沃特").attackPower(58).build()); employees1.add(Employee.builder().index(2).nickname("卡牌大师").name("崔斯特").region("比尔吉沃特").attackPower(10).build()); regions.add(Region.builder().region("比尔吉沃特").employees(employees1).build()); Context context = new Context(); context.putVar("regions", regions); context.putVar("time", "2025-05-21");
结果:
3.7 图像插入
可通过 jx:image 命令渲染图像。如 jx:image(lastCell=“E16” src=“dafei” imageType=“PNG”),其中 lastCell 表示图像大小(即图像所占单元格区域),src 表示图像文件的字节数组,imagedType 表示图像类型(支持通过 Context 上下文动态传入)。示例如下:
模板:
// 在图像起始位置单元格(此例中为 A1)插入批注 jx:image(lastCell="D8" src="dafei" imageType="PNG")
数据:
// 图像文件位于 resources/template/ 目录下 读取文件并将其转为 byte[] InputStream dafeiIs = ExportTest.class.getResourceAsStream("/template/xiaodafei.png"); byte[] dafeiBytes = IOUtils.toByteArray(dafeiIs); context.putVar("dafei", dafeiBytes);
结果:
3.8 合并单元格
可通过 jx:mergeCells 命令合并单元格。
jx:mergeCells(cols="3", rows="3", minCols="1", minRows="1", lastCell="D4")
cols: 要合并的列数
rows: 要合并的行数
minCols: 要合并的最小列数
minRows: 要合并的最小行数
lastCell: 要合并的单元格范围
注:动态变化值则可通过 Context 上下文传入
示例一:
模板:
A1 批注:jx:area(lastCell = "E3") A2 批注:jx:mergeCells(cols="1", rows="employeeNumber", minCols="1", minRows="1", lastCell="A3") B3 批注:jx:each(items="employees" var="item" lastCell="E3")
数据:
List<Employee> employees = new ArrayList<>(); employees.add(Employee.builder().index(1).nickname("影流之主").name("劫").region("艾欧尼亚").attackPower(66).build()); employees.add(Employee.builder().index(2).nickname("潮汐海灵").name("菲兹").region("比尔吉沃特").attackPower(58).build()); employees.add(Employee.builder().index(3).nickname("九尾妖狐").name("阿狸").region("艾欧尼亚").attackPower(53).build()); Context context = new Context(); context.putVar("employees", employees); context.putVar("employeeNumber", employees.size() + 1); // 加 1 指表头占一行
结果:
示例二:
模板:
A1 批注:jx:area(lastCell = "F5") A2 批注:jx:mergeCells(cols="1", rows="employeeNumber", minCols="1", minRows="2", lastCell="A5") B2 批注:jx:mergeCells(cols="1", rows="assassinNumber", minCols="1", minRows="1", lastCell="B3") C3 批注:jx:each(items="assassins" var="item" lastCell="F3") B4 批注:jx:mergeCells(cols="1", rows="warriorNumber", minCols="1", minRows="1", lastCell="B5") C5 批注:jx:each(items="warriors" var="item" lastCell="F5")
数据:
List<Employee> assassins = new ArrayList<>(); assassins.add(Employee.builder().index(1).nickname("影流之主").name("劫").region("艾欧尼亚").attackPower(66).build()); assassins.add(Employee.builder().index(2).nickname("潮汐海灵").name("菲兹").region("比尔吉沃特").attackPower(58).build()); assassins.add(Employee.builder().index(3).nickname("九尾妖狐").name("阿狸").region("艾欧尼亚").attackPower(53).build()); List<Employee> warriors = new ArrayList<>(); warriors.add(Employee.builder().index(1).nickname("刀锋舞者").name("艾瑞丽娅").region("艾欧尼亚").build()); warriors.add(Employee.builder().index(2).nickname("放逐之刃").name("锐雯").region("诺克萨斯").build()); Context context = new Context(); context.putVar("assassins", assassins); context.putVar("assassinNumber", assassins.size() + 1); context.putVar("warriors", warriors); context.putVar("warriorNumber", warriors.size() + 1); context.putVar("employeeNumber", assassins.size() + warriors.size() + 2);
结果:
3.9 单元格样式
JXLS 模板支持单元格样式,即模板中的样式最终会渲染到生成的 excel 文件中。
4 课后小知识
4.1 图像 svg 转 png
使用 batik 库将 Base64 格式的 svg 图像转成 png,若 Base64 是压缩后的数据,则需要解压缩。
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-codec</artifactId>
<version>1.19</version>
</dependency>
String svgBase64 = "";
byte[] decode = Base64.getDecoder().decode(svgBase64);
try (ByteArrayInputStream is = new ByteArrayInputStream(decode);
ByteArrayOutputStream os = new ByteArrayOutputStream();
GZIPInputStream zipIs = new GZIPInputStream(is)) {
PNGTranscoder transcoder = new PNGTranscoder();
TranscoderInput input = new TranscoderInput(zipIs);
TranscoderOutput output = new TranscoderOutput(os);
transcoder.transcode(input, output);
return os.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}