首页 > 编程语言 >Java生成Word文档之 XDocReport 和 Poi-tl

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

时间:2025-01-10 16:16:16浏览次数:1  
标签:static java XDocReport PATH tl Word new import 模板

近期参与的多个项目中,均涉及根据预定义模板生成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日'))}} 

标签:static,java,XDocReport,PATH,tl,Word,new,import,模板
From: https://www.cnblogs.com/dafengdeai/p/18664134

相关文章

  • 请求被中止: 未能创建 SSL/TLS 安全通道”的原因及解决办法
    4个解决办法,我用的第四个方法就解决了,注册表手动添加的重启后不管用,第四个方法直接用程序改一下方便 首先得保证服务器是否支持tls1.2去注册表里查或者百度怎么查,基本大多数都用的是1.2      1.  代码前加这个 ServicePointManager.Expect100Continu......
  • CRC校验:原理、计算方法、优缺点及MATLAB代码示例
    引言        在数字通信和数据存储领域,数据的完整性和可靠性是至关重要的。为了确保数据在传输或存储过程中不发生错误,人们开发了许多错误检测与校正技术。其中,循环冗余校验(CyclicRedundancyCheck,简称CRC)是一种广泛应用的错误检测机制。本文将详细介绍CRC校验的基本......
  • ETL之kettle版本安装包免费下载地址
    想真正学习或者提升自己的ETL领域知识的朋友欢迎进群,一起学习,共同进步。由于群内人员较多无法直接扫描进入,公众号后台加我微信入群,备注kettle/hop。1、群里经常有小伙伴询问kettle安装包或者私下找我要,今天群里的三倍镜大佬分享了一个kettle全家桶版本,今天我整理下分享给大家。......
  • PCIe扫盲——TLP路由之Address Routing
    地址路由(AddressRouting)的地址包括IO和Memory。对于Memory请求来说,32bit的地址使用3DW的Header,64bit的地址使用4DW的Header。而IO请求则只能使用32bit的地址,即只能使用3DW的Header。注:再次强调,IO请求是为了兼容早期的PCI设备的,在新的PCIe设备中禁止使用。3DW和4DW的TLPHeader......
  • 【YashanDB知识库】kettle做增量同步,出现报错:Unrecognized VM option 'MaxPermSize-25
    本文内容来自YashanDB官网,原文内容请见https://www.yashandb.com/newsinfo/7863039.html?templateId=1718516问题现象kettle在增量同步过程,出现报错:UnrecognizedVMoption'MaxPermSize=256m'问题的风险及影响无法使用kettle做增量同步,导致迁移进度会有所影响问题影响的版......
  • 【MATLAB源码-第51期】基于matlab的粒子群算法(PSO)的栅格地图路径规划。
    操作环境:MATLAB2022a1、算法描述粒子群算法(ParticleSwarmOptimization,简称PSO)是一种模拟鸟群觅食行为的启发式优化方法。以下是其详细描述:基本思想:鸟群在寻找食物时,每只鸟都会观察自己和其他鸟之间的距离,以及当前找到的食物的位置。每只鸟都会向自己历史上找到的最好食......
  • GitLab安装与配置
    由于图片和格式解析问题,为了更好阅读体验可前往阅读原文对于gitlab的安装本人都是在arm架构Centos7.9虚拟机上进行的,请悉知本人安装环境,或者与本人的环境保持一致,以便产生不必要的疑惑这里使用docker安装Gitlab,机器上需要先安装docker,如果你还不知道docker,可以先看看我的往期......
  • 基于CNN卷积神经网络的金融数据预测matlab仿真,对比BP,RBF,LSTM
    1.程序功能描述基于CNN卷积神经网络的金融数据预测matlab仿真,对比BP神经网络,RBF神经网络,LSTM网络.对比预测结果和预测误差。2.测试软件版本以及运行结果展示MATLAB2022A版本运行 3.核心程序fori=1:floor(length(data1)/5);p1w(5*i-4:5*i,1)=[p1(i......
  • Python Matplotlib 教程- Matplotlib 如何进行数据点标记
    PythonMatplotlib数据点标记在数据可视化中,数据点标记是非常重要的部分。无论是绘制折线图、散点图还是柱状图,清晰地标记关键数据点可以帮助观众快速理解图表的核心信息。本篇文章将详细介绍如何在Python的Matplotlib中实现数据点标记,从基础使用到高级自定义,帮助新手快速上......
  • Python Matplotlib 教程- Matplotlib 如何自定义样式
    PythonMatplotlib样式和自定义Matplotlib是一个强大的Python数据可视化库,提供丰富的绘图功能。在实际使用中,熟悉其样式和自定义技巧可以让你的图表更加专业和美观。本文将简要介绍Matplotlib样式和一些常用的自定义方法。1.设置全局样式Matplotlib提供了一些预设的样......