首页 > 极客资料 博客日记

Java生成Word文档之 XDocReport 和 Poi-tl

2025-01-10 16:30:06极客资料围观2

这篇文章介绍了Java生成Word文档之 XDocReport 和 Poi-tl,分享给大家做个参考,收藏极客之家收获更多编程知识

近期参与的多个项目中,均涉及根据预定义模板生成Word文档以供前端下载的需求。以往,我们通常采用将Word文档转换为XML格式,并通过代码赋值变量的方式来实现这一功能。尽管此方法在技术层面可行,但当面对篇幅较长且包含大量变量的文档时,其弊端便显露无遗:代码冗长繁杂,模板维护困难,不利于后续的修改与扩展。

鉴于此,近期对市场上现有的解决方案进行了深入调研,旨在寻找一种更为高效、简洁的Word文档生成方式。经过综合评估,推荐以下两款优秀的组件,旨在为开发者提供更为便捷的开发体验。

方案 移植性 功能性 易用性
XDocReport Java跨平台 支持多种文档格式,强大的模板引擎,易于集成 模板与代码分离,易于管理与修改,适用于复杂文档,处理速度快
Poi-tl Java跨平台 轻量级模板引擎,专注于Word文档生成,简洁易用 模板语法简洁,降低维护成本,针对Word文档优化,性能稳定
Apache POI Java跨平台 Apache项目,封装了常见的文档操作,也可以操作底层XML结构 文档不全,这里有一个教程:Apache POI Word快速入门
Freemarker XML跨平台 需要手动转换Word为XML,代码量大 模板与代码紧密耦合,修改复杂,变量多时长文档生成效率低
OpenOffice 部署OpenOffice,移植性较差 - 需要了解OpenOffice的API
HTML浏览器导出 依赖浏览器的实现,移植性较差 HTML不能很好的兼容Word的格式,样式糟糕 -
Jacob、winlib Windows平台 - 复杂,完全不推荐使用

综上所述,XDocReport与Poi-tl两款组件在Word文档生成方面均表现出色,各有千秋。XDocReport功能全面,适用于大型企业级应用;而Poi-tl则以其轻量级、简洁易用的特点,更适合中小型项目及快速迭代开发场景。开发者可根据项目实际需求选择合适的组件,以提升开发效率与代码质量。

一、XDocReport

1. 简介

xdocreport是一个基于Apache POI和Velocity/Freemarker的Java库,主要用于生成和处理各种文档格式,如DOCX、ODT、PDF等。它通过模板引擎语法(如Freemarker、Velocity)将数据动态插入到文档中,支持多种格式的转换和文档生成。

代码托管地址:https://github.com/opensagres/xdocreport

2. 主要功能

  1. 支持多种模板引擎,如 Velocity、Freemarker 和 Mustache。
  2. 支持表格、图表、页眉和页脚等复杂布局。
  3. 支持在 Word、Excel 和 PowerPoint 文档中插入图片。
  4. 支持在 Excel 文档中创建数据透视表。
  5. 支持在 Word 文档中创建目录。
  6. 支持在 Word 文档中创建书签和超链接。
  7. 支持在 Word 文档中创建水印。
  8. 支持在 Word 文档中创建页码。
  9. 支持在 Word 文档中创建分节符。
  10. 支持在 Word 文档中创建页面背景。

3.基本原理

4. 快速开始

4.1 引入依赖

<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.core</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.document</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.template</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
    <version>2.0.6</version>
</dependency>

4.2 创建模板

插入能被正常替换的占位符是模板的核心,创建占位符:插入 - 文档部件 - 域 - 邮件合并 - 域代码 - 确定

插入域:Ctrl + F9

显示域:Alt + F9


4.3 生成文档

/**
 * 使用 xdocreport 生成 word
 * 一共需要5步,其中只有第4步需要开发者自行实现
 */
public static void main(String[] args) {
    try (
        // 1. 定义输入流,读取模板
        InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
        // 2. 定义输出流,输出文件
        OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
    ) {
        // 3. 读取模板,创建 IXDocReport 对象
        IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

        // 4. 创建上下文,设置变量
        IContext context = report.createContext();
        context.put("name", "张三");

        // 5. 生成 word
        report.process(context, out);
    } catch (Exception e) {
        log.error("生成word失败", e);
    }
}

5. Demo演示

5.1 文本输出

import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 文本输出
 * 创建域 占位符使用 ${var}
 * 域的创建方式:插入 - 文档部件 - 域 - 邮件合并 - 域代码 - 确定
 * 注意:占位符中的 MERGEFIELD 不可删除
 * 会保留模板中的样式
 * @author dafeng
 * @date 2024/12/27 9:59
 */
@Slf4j
public class Demo2 {
    // 模板路径
    private static final String TEMP_PATH = "D:\\deployment\\test\\xdoc-report\\demo2-temp.docx";
    // 输出文档路径
    private static final String OUT_PATH = "D:\\deployment\\test\\xdoc-report\\demo2-out.docx";

    /**
     * 使用 xdocreport 生成 word
     */
    public static void main(String[] args) {
        try (
                // 1. 定义输入流,读取模板
                InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
                // 2. 定义输出流,输出文件
                OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
        ) {
            // 3. 读取模板,创建 IXDocReport 对象
            IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

            // 4. 创建上下文,设置变量
            IContext context = report.createContext();
            // 填充变量
            setContext(context);

            // 5. 生成 word
            report.process(context, out);
        } catch (Exception e) {
            log.error("生成word失败", e);
        }
    }

    /**
     * 自定义变量填充
     */
    private static void setContext(IContext context) {
        context.put("name", "张三");
    }
}

5.2 对象输出

import com.xajw.xdocreport.vo.User;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Date;
import java.util.HashMap;

/**
 * 复杂对象
 * ${var.v} ${var.v.x}
 *
 * @author dafeng
 * @date 2024/12/27 9:59
 */
@Slf4j
public class Demo3 {
    // 模板路径
    private static final String TEMP_PATH = "D:\\deployment\\test\\xdoc-report\\demo3-temp.docx";
    // 输出文档路径
    private static final String OUT_PATH = "D:\\deployment\\test\\xdoc-report\\demo3-out.docx";

    /**
     * 使用 xdocreport 生成 word
     */
    public static void main(String[] args) {
        try (
                // 1. 定义输入流,读取模板
                InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
                // 2. 定义输出流,输出文件
                OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
        ) {
            // 3. 读取模板,创建 IXDocReport 对象
            IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

            // 4. 创建上下文,设置变量
            IContext context = report.createContext();
            // 填充变量
            setContext(context);

            // 5. 生成 word
            report.process(context, out);
        } catch (Exception e) {
            log.error("生成word失败", e);
        }
    }

    /**
     * 自定义变量填充
     */
    private static void setContext(IContext context) {
        User user = new User("张三", 18, new Date(), new User.Address("中国", "陕西", "西安"));
        context.put("user", user);

        HashMap<String, Object> map = new HashMap<String, Object>(){{
            put("name", "李四");
            put("age", 19);
            put("birthday", new Date());
            put("address", new User.Address("中国", "四川", "成都"));
        }};
        context.put("map", map);
    }
}

5.3 列表循环

import com.xajw.xdocreport.vo.User;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 列表循环
 * [#list varList as var] ${var} [/#list]
 * 占位符需要有开始和结束
 *
 * @author dafeng
 * @date 2024/12/27 9:59
 */
@Slf4j
public class Demo4 {
    // 模板路径
    private static final String TEMP_PATH = "D:\\deployment\\test\\xdoc-report\\demo4-temp.docx";
    // 输出文档路径
    private static final String OUT_PATH = "D:\\deployment\\test\\xdoc-report\\demo4-out.docx";

    /**
     * 使用 xdocreport 生成 word
     */
    public static void main(String[] args) {
        try (
                // 1. 定义输入流,读取模板
                InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
                // 2. 定义输出流,输出文件
                OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
        ) {
            // 3. 读取模板,创建 IXDocReport 对象
            IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

            // 4. 创建上下文,设置变量
            IContext context = report.createContext();
            // 填充变量
            setContext(context);

            // 5. 生成 word
            report.process(context, out);
        } catch (Exception e) {
            log.error("生成word失败", e);
        }
    }

    /**
     * 自定义变量填充
     */
    private static void setContext(IContext context) {
        List<String> varList = new ArrayList<String>() {{
            add("张三");
            add("李四");
            add("王五");
        }};
        context.put("varList", varList);

        List<User> userList = new ArrayList<User>(){{
            add(new User("张三", 18, new Date(), new User.Address("中国", "陕西", "西安")));
            add(new User("李四", 19, new Date(), new User.Address("中国", "四川", "成都")));
            add(new User("王五", 20, new Date(), new User.Address("中国", "河南", "郑州")));
        }};
        context.put("userList", userList);

        List<List<String>> table = new ArrayList<List<String>>(){{
            add(new ArrayList<String>(){{
                add("第一行:第一列");
                add("第一行:第二列");
            }});
            add(new ArrayList<String>(){{
                add("第二行:第一列");
                add("第二行:第二列");
            }});
            add(new ArrayList<String>(){{
                add("第三行:第一列");
                add("第三行:第二列");
            }});
        }};
        context.put("table", table);
    }
}

5.4 表格输出

import com.xajw.xdocreport.vo.Dtl;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import lombok.extern.slf4j.Slf4j;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

/**
 * 表格输出
 *
 * @author dafeng
 * @date 2024/12/27 9:59
 */
@Slf4j
public class Demo5 {
    // 模板路径
    private static final String TEMP_PATH = "D:\\deployment\\test\\xdoc-report\\demo5-temp.docx";
    // 输出文档路径
    private static final String OUT_PATH = "D:\\deployment\\test\\xdoc-report\\demo5-out.docx";

    /**
     * 使用 xdocreport 生成 word
     */
    public static void main(String[] args) {
        try (
                // 1. 定义输入流,读取模板
                InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
                // 2. 定义输出流,输出文件
                OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
        ) {
            // 3. 读取模板,创建 IXDocReport 对象
            IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

            // 4. 创建上下文,设置变量
            IContext context = report.createContext();
            // 填充变量
            setContext(context);

            // 5. 生成 word
            report.process(context, out);
        } catch (Exception e) {
            log.error("生成word失败", e);
        }
    }

    /**
     * 自定义变量填充
     */
    private static void setContext(IContext context) {
        List<Dtl> list2 = new ArrayList<Dtl>(){{
            add(new Dtl("001", "国铁", "中国", new ArrayList<Dtl.Tender>(){{
                add(new Dtl.Tender("张三", 18.0, "无"));
                add(new Dtl.Tender("李四", 19.0, "无"));
                add(new Dtl.Tender("王五", 20.0, "无"));
            }}));
            add(new Dtl("002", "电子所", "北京", new ArrayList<Dtl.Tender>(){{
                add(new Dtl.Tender("赵六", 21.0, "无"));
                add(new Dtl.Tender("钱七", 22.0, "无"));
                add(new Dtl.Tender("孙八", 23.0, "无"));
            }}));
            add(new Dtl("003", "经纬公司", "陕西", new ArrayList<Dtl.Tender>(){{
                add(new Dtl.Tender("周九", 24.0, "无"));
                add(new Dtl.Tender("吴十", 25.0, "无"));
                add(new Dtl.Tender("郑十一", 26.0, "无"));
            }}));
        }};
        context.put("dtlList", list2);
    }
}

开头编辑域 "@before-row[# list dtlList as dtl]"

结尾编辑域 "@after-row[/# list]"

@before-row和@after-row需要成对出现

编号 单位 地址
«@before-row[#list itemList as item»«${item.code}» «${item.unit}» «${item.address}»«@after-row[/#list]»

子表格开头编辑域和结尾编辑域不添加 @before-row和@after-row

这种实现方式子表格会多出一行空行

编号 单位 中标人 单价
«@before-row[#list itemList as item»«${item.code}» «${item.unit}» «[#list item.tender as tender]»«${tender.spName}» «${tender.price}»
«[/#list]»«@after-row[/#list]»

移除多余的空行 xxx为子表格循环对象

"[#if (!xxx_has_next)]"

"[#else]"

"[/#if]"

如下:在子表格最后一行只保留一个单元格,其他的单元格需删掉(如上图所示)

编号 单位 中标人 单价
«@before-row[#list itemList as item»«${item.code}» «${item.unit}» «[#list item.tender as tender]»«${tender.spName}» «${tender.price}»«[#if (!tender_has_next)]»«[#else]»
«[/#if]»«[/#list]»«@after-row[/#list]»

如下:

红色为最外层循环

蓝色为子表格循环

绿色为隐藏子表格空白行

编号 中标人
«@before-row[#list itemList as item»«${item.code}» «[#list item.tender as tender]»«${tender.spName}»«[#if (!tender_has_next)]»«[#else]»
«[/#if]»«[/#list]»«@after-row[/#list]»

5.5 图片输出


import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider;
import fr.opensagres.xdocreport.document.images.FileImageProvider;
import fr.opensagres.xdocreport.document.images.IImageProvider;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
import fr.opensagres.xdocreport.template.formatter.NullImageBehaviour;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 图片输出
 *
 * @author dafeng
 * @date 2024/12/27 9:59
 */
@Slf4j
public class Demo6 {
    // 模板路径
    private static final String TEMP_PATH = "D:\\deployment\\test\\xdoc-report\\demo6-temp.docx";
    // 输出文档路径
    private static final String OUT_PATH = "D:\\deployment\\test\\xdoc-report\\demo6-out.docx";

    /**
     * 使用 xdocreport 生成 word
     */
    public static void main(String[] args) {
        try (
                // 1. 定义输入流,读取模板
                InputStream ins = Files.newInputStream(Paths.get(TEMP_PATH));
                // 2. 定义输出流,输出文件
                OutputStream out = Files.newOutputStream(Paths.get(OUT_PATH))
        ) {
            // 3. 读取模板,创建 IXDocReport 对象
            IXDocReport report = XDocReportRegistry.getRegistry().loadReport(ins, TemplateEngineKind.Freemarker);

            // 4. 创建上下文,设置变量
            IContext context = report.createContext();
            // 填充变量
            setContext(report, context);

            // 5. 生成 word
            report.process(context, out);
        } catch (Exception e) {
            log.error("生成word失败", e);
        }
    }

    /**
     * 自定义变量填充
     */
    private static void setContext(IXDocReport report, IContext context) throws XDocReportException {
        FieldsMetadata metadata = report.createFieldsMetadata();
        // 1. 单个图片
        File img = new File("D:\\deployment\\test\\xdoc-report\\1.png");
        context.put("img", new FileImageProvider(img));
        metadata.addFieldAsImage("img");

        // 2. 循环图片
        IImageProvider p1 = new FileImageProvider(new File("D:\\deployment\\test\\xdoc-report\\1.png"));
        IImageProvider p2 = new FileImageProvider(new File("D:\\deployment\\test\\xdoc-report\\2.png"));
        IImageProvider p3 = new FileImageProvider(new File("D:\\deployment\\test\\xdoc-report\\3.png"));
        IImageProvider p4 = new FileImageProvider(new File("D:\\deployment\\test\\xdoc-report\\4.png"));

        List<Map<String, IImageProvider>> list = new ArrayList<>();
        list.add(Map.of("pic", p1));
        list.add(Map.of("pic", p2));
        list.add(Map.of("pic", p3));
        list.add(Map.of("pic", p4));

        context.put("picList", list);

        //映射:picture为模板中书签名,item.pic为word模板循环中的变量名
        metadata.addFieldAsImage("picture","item.pic", NullImageBehaviour.RemoveImageTemplate);
    }

    private static void setContext1(IXDocReport report, IContext context) {
        File file = new File("D:\\deployment\\test\\xdoc-report\\a.jpg");
        try (FileInputStream in = new FileInputStream(file)){
            FieldsMetadata metadata = report.createFieldsMetadata();
            metadata.addFieldAsImage("img");
            context.put("img", new ByteArrayImageProvider(in));
        }catch (Exception e){
            log.error("获取图片失败", e);
        }
    }
}

二、Poi-tl

1. 简介

poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中。模板是Docx格式的Word文档,你可以使用Microsoft office、WPS Office、Pages等任何你喜欢的软件制作模板,也可以使用Apache POI代码来生成模板。

所有的标签都是以{{开头,以}}结尾,标签可以出现在任何位置,包括页眉,页脚,表格内部,文本框等,表格布局可以设计出很多优秀专业的文档,推荐使用表格布局。

poi-tl模板遵循“所见即所得”的设计,模板和标签的样式会被完全保留。

代码托管地址:https://github.com/Sayi/poi-tl

指导文档地址:https://deepoove.com/poi-tl/

2. 主要功能

Word模板引擎功能 描述
文本 将标签渲染为文本
图片 将标签渲染为图片
表格 将标签渲染为表格
列表 将标签渲染为列表
图表 条形图(3D条形图)、柱形图(3D柱形图)、面积图(3D面积图)、折线图(3D折线图)、雷达图、饼图(3D饼图)、散点图等图表渲染
If Condition判断 根据条件隐藏或者显示某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Foreach Loop循环 根据集合循环某些文档内容(包括文本、段落、图片、表格、列表、图表等)
Loop表格行 循环复制渲染表格的某一行
Loop表格列 循环复制渲染表格的某一列
Loop有序列表 支持有序列表的循环,同时支持多级列表
Highlight代码高亮 word中代码块高亮展示,支持26种语言和上百种着色样式
Markdown 将Markdown渲染为word文档
Word批注 完整的批注功能,创建批注、修改批注等
Word附件 Word中插入附件
SDT内容控件 内容控件内标签支持
Textbox文本框 文本框内标签支持
图片替换 将原有图片替换成另一张图片
书签、锚点、超链接 支持设置书签,文档内锚点和超链接功能
Expression Language 完全支持SpringEL表达式,可以扩展更多的表达式:OGNL, MVEL…
样式 模板即样式,同时代码也可以设置样式
模板嵌套 模板包含子模板,子模板再包含子模板
合并 Word合并Merge,也可以在指定位置进行合并
用户自定义函数(插件) 插件化设计,在文档任何位置执行函数

3. 开发环境和依赖

  • JDK1.8+
  • Apache POI5.2.2+

4. 快速开始

4.1 引入依赖

<dependency>
  <groupId>com.deepoove</groupId>
  <artifactId>poi-tl</artifactId>
  <version>1.12.2</version>
</dependency>

4.2 创建模板

注:Poi-tl 创建模板相较于XDocReport比较简单,无需使用域,直接使用 {{var}} 即可

4.3 生成文档

public static void main(String[] args) throws Exception {
    XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(
    new HashMap<String, Object>(){{
        put("title", "Hi, poi-tl Word模板引擎");
    }});  
    template.writeAndClose(new FileOutputStream(OUT_PATH));
}

5. Demo演示

5.1 文本输出

  • 模板代码
{{title}}
  • Java代码
import com.deepoove.poi.XWPFTemplate;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

/**
 * poi-tl Word模板引擎
 *
 * @author dafeng
 * @date 2024/12/20 13:41
 */
public class PoiTlUtil {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\output.docx";

    public static void main(String[] args) throws Exception {
        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() {
        return new HashMap<String, Object>() {{
            // 文本
            put("title", "Hi, poi-tl Word模板引擎");
        }};
    }
}

5.2 图片输出

  • 模板代码
{{@image}}
{{@svg}}
{{@image1}}
{{@streamImg}}
{{@urlImg}}
{{@buffered}}
  • Java代码

/**
 * poi-tl Word模板引擎
 *
 * @author dafeng
 * @date 2024/12/20 13:41
 */
public class PoiTlUtil {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\output.docx";

    public static void main(String[] args) throws Exception {
        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() throws FileNotFoundException {
        return new HashMap<String, Object>() {{
            // 指定图片路径
            put("image", "F:\\Z-图片\\a.jpg");

            // svg图片
            put("svg", "https://img1.baidu.com/it/u=1960110688,1786190632&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281");

            // 图片文件
            put("image1", Pictures.ofLocal("F:\\a.jpg").size(120, 120).create());

            // 图片流
            put("streamImg", Pictures.ofStream(new FileInputStream("F:\\logo.png"), PictureType.PNG)
                    .size(100, 120).create());

            // 网络图片(注意网络耗时对系统可能的性能影响)
            put("urlImg", Pictures.ofUrl("http://xxx.com/icecream.png").size(100, 100).create());

            // java图片,我们可以利用Java生成图表插入到word文档中
            put("buffered", Pictures.ofBufferedImage(new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB), PictureType.PNG).size(100, 100).create());
        }};
    }
}

5.3 列表输出

  • 模板代码
{{*list}}
  • Java代码
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.Numberings;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

/**
 * poi-tl Word模板引擎
 *
 * @author dafeng
 * @date 2024/12/20 13:41
 */
public class PoiTlUtil {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\output.docx";

    public static void main(String[] args) throws Exception {
        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() throws FileNotFoundException {
        return new HashMap<String, Object>() {{
            // 列表
            put("list", Numberings.create("Plug-in grammar", "Supports word text, pictures, table...", "Not just templates"));
        }};
    }
}

5.4 表格输出

  • 模板代码
{{#table0}}

{{#table1}}

{{#table2}}
  • Java代码
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.MergeCellRule;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.data.Tables;
import com.deepoove.poi.data.style.BorderStyle;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

/**
 * poi-tl Word模板引擎
 *
 * @author dafeng
 * @date 2024/12/20 13:41
 */
public class PoiTlUtil2 {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\output.docx";

    public static void main(String[] args) throws Exception {
        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() throws FileNotFoundException {
        return new HashMap<String, Object>() {{
            
            // 3. 表格
            // 一个2行2列的表格
            put("table0", Tables.of(new String[][]{
                    new String[]{"00", "01"},
                    new String[]{"10", "11"}
            }).border(BorderStyle.DEFAULT).create());

            // 第0行居中且背景为蓝色的表格
            RowRenderData row0 = Rows.of("姓名", "学历").textColor("FFFFFF")
                    .bgColor("4472C4").center().create();
            RowRenderData row1 = Rows.create("李四", "博士");
            put("table1", Tables.create(row0, row1));


            // 合并第1行所有单元格的表格
            RowRenderData row2 = Rows.of("列0", "列1", "列2").center().bgColor("4472C4").create();
            RowRenderData row3 = Rows.create("没有数据", null, null);
            MergeCellRule rule = MergeCellRule.builder().map(MergeCellRule.Grid.of(1, 0), MergeCellRule.Grid.of(1, 2)).build();
            put("table2", Tables.of(row2, row3).mergeRule(rule).create());

        }};
    }
}

5.5 表格行循环

货物明细和人工费在同一个表格中,货物明细需要展示所有货物,人工费需要展示所有费用。{{goods}} 是个标准的标签,将 {{goods}} 置于循环行的上一行,循环行设置要循环的标签和内容,注意此时的标签应该使用 [] ,以此来区别 poi-tl 的默认标签语法。同理,{{labors}}置于循环行的上一行

  • 模板

  • Java代码

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.xajw.export.app.tl.vo.DetailData;
import com.xajw.export.app.tl.vo.Goods;
import com.xajw.export.app.tl.vo.Labor;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * Poi-tl 表格行循环
 * @author dafeng
 * @date 2024/12/20 17:06
 */
public class PoiTlTableUtil1 {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template_table.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\template_table_out.docx";

    public static void main(String[] args) throws Exception {
        // 绑定插件
        LoopRowTableRenderPolicy rowPolicy = new LoopRowTableRenderPolicy(); // 表格行循环
        Configure config = Configure.builder()
                .bind("goods", rowPolicy)
                .bind("labors", rowPolicy)
                .build();

        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() {
        return new HashMap<String, Object>() {{
            List<Goods> goods = new ArrayList<>();
            List<Labor> labors = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                goods.add(new Goods(i + 1, "商品" + i, "描述" + i, 10, 20, 30, 40));
                labors.add(new Labor("类别" + i, 10, 20, 30));
            }

            put("goods", goods);
            put("labors", labors);
            put("total", 1220);
        }};
    }
}
  • 生成文档

5.6 表格列循环

  • 模板

  • Java代码

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.plugin.table.LoopColumnTableRenderPolicy;
import com.xajw.export.app.tl.vo.DetailData;
import com.xajw.export.app.tl.vo.Goods;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * Poi-tl 表格列循环
 *
 * @author dafeng
 * @date 2024/12/20 17:06
 */
public class PoiTlTableUtil1 {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template_table.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\template_table_out.docx";

    public static void main(String[] args) throws Exception {
        // 绑定插件
        LoopColumnTableRenderPolicy columnPolicy = new LoopColumnTableRenderPolicy(); // 表格列循环
        Configure config = Configure.builder()
                .bind("products", columnPolicy)
                .build();

        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() {
        return new HashMap<String, Object>() {{
            List<Goods> products = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                products.add(new Goods(i + 1, "商品" + i, "描述" + i, 10, 20, 30, 40));
            }

            put("products", products);
            put("total", 1220);
        }};
    }
}
  • 生成文档

5.7 动态表格

当需求中的表格更加复杂的时候,我们完全可以设计好那些固定的部分,将需要动态渲染的部分单元格交给自定义模板渲染策略。poi-tl提供了抽象表格策略 DynamicTableRenderPolicy 来实现这样的功能。

  • 模板

  • Java代码

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.Rows;
import com.deepoove.poi.plugin.table.LoopColumnTableRenderPolicy;
import com.xajw.export.app.tl.vo.DetailData;
import com.xajw.export.app.tl.vo.Goods;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * Poi-tl 动态表格
 *
 * @author dafeng
 * @date 2024/12/20 17:06
 */
public class PoiTlTableUtil {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template_table.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\template_table_out.docx";

    public static void main(String[] args) throws Exception {
        // 绑定插件
        DetailTablePolicy detailTablePolicy = new DetailTablePolicy(); // 动态表格表格
        Configure config = Configure.builder()
                .bind("detailTable", detailTablePolicy)
                .build();

        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH, config).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() {
        DetailData detailTable = new DetailData();

        RowRenderData good = Rows.of("4", "墙纸", "书房+卧室", "1500", "/", "400", "1600").center().create();
        List<RowRenderData> goods = Arrays.asList(good, good, good);
        detailTable.setGoods(goods);

        RowRenderData labor = Rows.of("油漆工", "2", "200", "400").center().create();
        List<RowRenderData> labors = Arrays.asList(labor, labor, labor, labor);
        detailTable.setLabors(labors);

        return new HashMap<String, Object>() {{
            put("detailTable", detailTable);
            put("total", 1220);
        }};
    }
}
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.policy.DynamicTableRenderPolicy;
import com.deepoove.poi.policy.TableRenderPolicy;
import com.deepoove.poi.util.TableTools;
import com.xajw.export.app.tl.vo.DetailData;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import java.util.List;

public class DetailTablePolicy extends DynamicTableRenderPolicy {

    // 货品填充数据所在行数
    int goodsStartRow = 2;
    // 人工费填充数据所在行数
    int laborsStartRow = 5;

    @Override
    public void render(XWPFTable table, Object data) throws Exception {
        if (null == data) return;
        DetailData detailData = (DetailData) data;

        // 人工费 先创建表格后面的行数据
        List<RowRenderData> labors = detailData.getLabors();
        table.removeRow(laborsStartRow);
        // 循环插入行
        for (RowRenderData labor : labors) {
            XWPFTableRow insertNewTableRow = table.insertNewTableRow(laborsStartRow);
            for (int j = 0; j < 7; j++) {
                insertNewTableRow.createCell();
            }
            // 合并单元格
            TableTools.mergeCellsHorizonal(table, laborsStartRow, 0, 3);
            // 单行渲染
            TableRenderPolicy.Helper.renderRow(table.getRow(laborsStartRow), labor);
        }

        // 货物
        List<RowRenderData> goods = detailData.getGoods();
        table.removeRow(goodsStartRow);
        for (RowRenderData good : goods) {
            XWPFTableRow insertNewTableRow = table.insertNewTableRow(goodsStartRow);
            for (int j = 0; j < 7; j++) {
                insertNewTableRow.createCell();
            }
            TableRenderPolicy.Helper.renderRow(table.getRow(goodsStartRow), good);
        }
    }
}
  • 输出文档

5.8 区块对

  • 模板代码
{{?announce}}
Top of the world!
{{/announce}}

{{?person}}
Hi {{name}}!,{{age}}
{{/person}}

{{?paragraphList}}
{{content}}
{{/paragraphList}}
{{?produces}}
 {{_index+1}}. {{=#this}}  {{_is_first}} 	{{_is_last}}	{{_has_next}}	{{_is_even_item}}	{{_is_odd_item}}
 {{/produces}}
  • Java代码
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import com.deepoove.poi.XWPFTemplate;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * poi-tl Word模板引擎
 *
 * @author dafeng
 * @date 2024/12/20 13:41
 */
public class PoiTlUtil {
    private static final String TEMP_PATH = "D:\\deployment\\test\\tl\\template.docx";
    private static final String OUT_PATH = "D:\\deployment\\test\\tl\\output.docx";

    public static void main(String[] args) throws Exception {
        XWPFTemplate template = XWPFTemplate.compile(TEMP_PATH).render(genData());
        template.writeAndClose(Files.newOutputStream(Paths.get(OUT_PATH)));
    }

    private static Object genData() throws FileNotFoundException {
        return new HashMap<String, Object>() {{
            
            // 5. 区块对
            put("announce", false);

            Map<String, String> person = MapUtil
                    .builder("name", "张三")
                    .put("age", "18")
                    .build();
            put("person", person);

            List<Map> wordDataList = new ArrayList<>();
            wordDataList.add(MapUtil.builder("content", "明月几时有,把酒问青天。").build());
            wordDataList.add(MapUtil.builder("content", "不知天上宫阙,今夕是何年?").build());
            wordDataList.add(MapUtil.builder("content", "我欲乘风归去,又恐琼楼玉宇,高处不胜寒。").build());
            wordDataList.add(MapUtil.builder("content", "大江东去,浪淘尽,千古风流人物。").build());
            put("paragraphList", wordDataList);

            put("produces", ListUtil.of("application/json", "application/xml", "text/html", "text/plain"));
        }};
    }
}

6. 配置

poi-tl提供了类 Configure 来配置常用的设置,使用方式如下:

ConfigureBuilder builder = Configure.builder();
XWPFTemplate.compile("template.docx", builder.buid());

6.1 前后缀

组件默认使用 {{}} 的方式来致敬Google CTemplate,如果你更偏爱freemarker ${} 的方式:

builder.buildGramer("${", "}");

6.2 标签类型

组件默认的图片标签是以@开始,若希望使用%开始作为图片标签:

builder.addPlugin('%', new PictureRenderPolicy());

也可以自由更改的标签标识类型

builder.addPlugin('@', new TableRenderPolicy());
builder.addPlugin('#', new PictureRenderPolicy());

这样{{@var}}就变成了表格标签,{{#var}}变成了图片标签,虽然不建议改变默认标签标识,但是从中可以看到poi-tl插件的灵活度,在插件章节中我们将会看到如何自定义自己的标签。

6.3 标签格式

标签默认支持中文、字母、数字、下划线的组合,但可以通过正则表达式来配置标签的规则,如不允许中文:

builder.buildGrammerRegex("[\\w]+(\\.[\\w]+)*");

若允许除了标签前后缀外的任意字符:

builder.buildGrammerRegex(RegexUtils.createGeneral("{{", "}}"));

6.4 EL表达式

Spring Expression Language 是一个强大的表达式语言,支持在运行时查询和操作对象图,可作为独立组件使用,需要引入相应的依赖:

官方文档:https://docs.spring.io/spring-framework/docs/5.3.18/reference/html/core.html#expressions

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-expression</artifactId>
  <version>5.3.18</version>
</dependency>

为了在模板标签中使用SpringEL表达式,需要将标签配置为SpringEL模式:

builder.useSpringEL();
{{name}}

{{name.toUpperCase()}} 

{{name == 'poi-tl'}} 

{{empty?:'这个字段为空'}} 

{{sex ? '男' : '女'}}

{{new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(time)}}  

{{price/10000 + '万元'}} 

{{dogs[0].name}}  

{{localDate.format(T(java.time.format.DateTimeFormatter).ofPattern('yyyy年MM月dd日'))}} 

版权声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云