JXLS 库导出复杂 Excel

发布于:2025-07-04 ⋅ 阅读:(14) ⋅ 点赞:(0)

JXLS 库导出复杂 Excel

  JXLS java 库导出复杂 excel。JXLS 简述、与 Apache POI 区别、使用姿势、核心特性(如 XLS 区域、单数据绑定、循环、条件判断、公式支持、多工作表、图像插入、合并单元格、单元格样式)、各功能使用示例等。


—— 2025 年 5 月 21 日 甲辰年四月二十四 小满

版本

  • 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),设计内容格式、样式等。如下示例:

one

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}。

  最终模板将如下:

two

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);
}

  生成结果如下:

three

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)。示例如下:

    • 模板:

      four

      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);
      
    • 结果:

      five

  • 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 为区域数组,数组第一个元素指定条件为真时要显示的区域,数组第二个元素指定条件为假时要显示的区域。示例如下:

  • 模板:

    six

    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"])
    
  • 结果:

    seven

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 属性表示工作表列表,其元素中应包含每个工作表的名称和数据。示例如下:

  • 模板:

    eight

    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");
    
  • 结果:

nineten

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);
    
  • 结果:

    eleven

3.8 合并单元格

  可通过 jx:mergeCells 命令合并单元格。

jx:mergeCells(cols="3", rows="3", minCols="1", minRows="1", lastCell="D4")
cols: 要合并的列数
rows: 要合并的行数
minCols: 要合并的最小列数
minRows: 要合并的最小行数
lastCell: 要合并的单元格范围
注:动态变化值则可通过 Context 上下文传入
  • 示例一:

    • 模板:

      twelve

      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 指表头占一行
      
    • 结果:

      twelve

  • 示例二:

    • 模板:

      fourteen

      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);
      
    • 结果:

      fifteen

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();
}

网站公告

今日签到

点亮在社区的每一天
去签到