跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 填充 Word 模板实战:文本、列表与复选框处理

Java 项目中使用 Aspose.Words 和 Poi-tl 实现 Word 模板填充。支持普通文本域、表格列表及复选框逻辑处理。通过反射解析实体类数据并映射到文档域,配置 License 避免水印。解决邮件合并区域不匹配问题,提供完整工具类与测试示例。

念念不忘发布于 2026/3/24更新于 2026/6/1618 浏览

最近在实际项目中遇到了 Java 填充 Word 模板的需求,涉及文本、列表以及复选框的勾选状态。这里整理了一套基于 Aspose.Words 和 Poi-tl 的工具方案,记录一下核心实现细节。

一、设置 Word 模板

在 Word 中选中需要填充的位置,点击'插入'->'文档部件'->'域',选择域名(MergeField),填入变量名称即可。

普通字段

直接在文档中插入域代码,例如 ${name}。

图片

填充效果如下:

图片

列表字段

操作与普通字段类似,区别在于需要在首行第一列插入列表开始域,首行最后一列插入结束域,中间放置正常字段。格式规范为:StartTable:<数组字段名> 和 EndTable:<数组字段名>。

图片

复选框

复选框的处理稍微特殊一些,直接通过域可能无法完美控制勾选状态。我们采用占位符配合代码逻辑的方式处理。

图片

二、代码实现

1. 引入依赖

主要使用 Hutool、Poi-tl、Aspose.Words 和 Gson。

<!-- hutool 工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.14</version>
</dependency>
<!-- word 模板数据解析 -->
<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.9.0-beta</version>
</dependency>
<!-- word/pdf 操作 -->
<dependency>
    <groupId>com.aspose</groupId>
    <artifactId>aspose-words</artifactId>
    <version>18.8</version>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.9</version>
</dependency>

2. 资源准备

将模板文件放入项目的 resources 目录下。如果需要使用 Aspose.Words 且不希望出现水印,需配置有效的 License 文件。

注意: 生产环境请务必使用正版授权文件,避免法律风险。将 License.xml 放在 resources 根路径下。

3. 核心代码

实体类定义
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FillWordDTO implements Serializable {
    private String name;
    private String age;
    private String yuyan;
    private String yingyu;
    private String deyu;
    private String fayu;
    private String zhengshu;
    private String yiji;
    private String erji;
    private List<ExperienceList> experienceList;

    public static FillWordDTO create() {
        FillWordDTO fillWordDTO = new FillWordDTO();
        fillWordDTO.setName("小王");
        fillWordDTO.setAge("18");
        // 模拟复选框状态
        fillWordDTO.setYuyan("☑");
        fillWordDTO.setYingyu("☑");
        fillWordDTO.setDeyu("□");
        fillWordDTO.setFayu("☑");
        fillWordDTO.setZhengshu("☑");
        fillWordDTO.setYiji("☑");
        fillWordDTO.setErji("□");
        fillWordDTO.setExperienceList(Arrays.asList(
            new ExperienceList("学校 A", "2020-01-01", "2020-01-01", "备注 A"),
            new ExperienceList("学校 B", "2020-01-01", "2020-01-01", "备注 B")
        ));
        return fillWordDTO;
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class ExperienceList {
    private String school;
    private String startTime;
    private String endTime;
    private String remark;
}
工具类实现
import com.aspose.words.*;
import com.aspose.words.net.System.Data.DataRow;
import com.aspose.words.net.System.Data.DataTable;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.beans.PropertyDescriptor;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ContractUtil {
    private ContractUtil() {}

    /**
     * 调整 BufferedImage 大小
     */
    public static BufferedImage resizeBufferedImage(BufferedImage source, int targetW, int targetH, boolean flag) {
        int type = source.getType();
        BufferedImage target = null;
        double sx = (double) targetW / source.getWidth();
        double sy = (double) targetH / source.getHeight();
        if (flag && sx > sy) {
            sx = sy;
            targetW = (int) (sx * source.getWidth());
        } else if (flag && sx <= sy) {
            sy = sx;
            targetH = (int) (sy * source.getHeight());
        }
        if (type == BufferedImage.TYPE_CUSTOM) {
            ColorModel cm = source.getColorModel();
            WritableRaster raster = cm.createCompatibleWritableRaster(targetW, targetH);
            boolean alphaPremultiplied = cm.isAlphaPremultiplied();
            target = new BufferedImage(cm, raster, alphaPremultiplied, null);
        } else {
            target = new BufferedImage(targetW, targetH, type);
        }
        Graphics2D g = target.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.drawRenderedImage(source, AffineTransform.getScaleInstance(sx, sy));
        g.dispose();
        return target;
    }

    /**
     * 填充 Word 模板(Object 数据格式)
     */
    public static byte[] fillWordDataByDomain(byte[] modelWordByte, Object obj) {
        try {
            Class<?> aClass = obj.getClass();
            Field[] fields = aClass.getDeclaredFields();
            Map<String, Object> data = new HashMap<>(fields.length);
            for (Field field : fields) {
                PropertyDescriptor pd = new PropertyDescriptor(field.getName(), aClass);
                Method method = pd.getReadMethod();
                String key = field.getName();
                Object value = method.invoke(obj);
                if (value != null) {
                    data.put(key, value);
                }
            }
            return fillWordDataByMap(modelWordByte, data);
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[0];
        }
    }

    /**
     * 填充 Word 模板(Map 数据格式)
     */
    public static byte[] fillWordDataByMap(byte[] file, Map<String, Object> data) throws Exception {
        byte[] ret = null;
        if (data == null || data.isEmpty()) {
            return ret;
        }
        try (InputStream is = new ByteArrayInputStream(file);
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            Document doc = new Document(is);
            DocumentBuilder builder = new DocumentBuilder(doc);
            Map<String, String> toData = new HashMap<>();
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                // 处理表格数据
                if (value instanceof List && !key.equals("checkboxOptions")) {
                    DataTable dataTable = fillListData((List) value, key, builder);
                    doc.getMailMerge().executeWithRegions(dataTable);
                }
                // 图片插入
                else if (value instanceof BufferedImage) {
                    builder.moveToMergeField(key);
                    builder.insertImage((BufferedImage) value);
                }
                // 其他普通字段正常填充
                else {
                    String valueStr = String.valueOf(value);
                    if (value != null && !"null".equals(valueStr)) {
                        toData.put(key, valueStr);
                    }
                }
            }
            // 执行普通字段合并
            String[] fieldNames = new String[toData.size()];
            String[] values = new String[toData.size()];
            int i = 0;
            for (Map.Entry<String, String> entry : toData.entrySet()) {
                fieldNames[i] = entry.getKey();
                values[i] = entry.getValue();
                i++;
            }
            doc.getMailMerge().execute(fieldNames, values);
            doc.save(out, SaveOptions.createSaveOptions(SaveFormat.DOCX));
            ret = out.toByteArray();
        }
        return ret;
    }

    /**
     * 勾选段落中的复选框字段
     */
    private static void checkTheCheckbox(Paragraph paragraph) throws Exception {
        FieldCollection fields = paragraph.getRange().getFields();
        int count = fields.getCount();
        for (int i = 0; i < count; i++) {
            com.aspose.words.Field field = fields.get(i);
            if (field.getType() == FieldType.FIELD_FORM_CHECK_BOX) {
                setCheckboxChecked(field, true);
            }
        }
    }

    private static void setCheckboxChecked(com.aspose.words.Field field, boolean checked) throws Exception {
        if (checked) {
            field.setResult("✓");
        } else {
            field.setResult("□");
        }
    }

    /**
     * 封装 list 数据到 Word 模板中(Word 表格)
     */
    private static DataTable fillListData(List<Object> list, String tableName, DocumentBuilder builder) throws Exception {
        DataTable dataTable = new DataTable(tableName);
        for (Object obj : list) {
            DataRow dataRow = dataTable.newRow();
            Class<?> objClass = obj.getClass();
            Field[] fields = objClass.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                Field field = fields[i];
                dataTable.getColumns().add(fields[i].getName());
                PropertyDescriptor pd = new PropertyDescriptor(field.getName(), objClass);
                Method method = pd.getReadMethod();
                dataRow.set(i, method.invoke(obj));
            }
            dataTable.getRows().add(dataRow);
        }
        return dataTable;
    }

    /**
     * 加载 License
     * 由于 Aspose 是收费软件,若没有有效 License,则会出现水印。
     */
    static {
        try {
            InputStream is = ContractUtil.class.getResourceAsStream("/License.xml");
            if (is != null) {
                License license = new License();
                license.setLicense(is);
            }
        } catch (Exception e) {
            // 忽略异常,若无 License 文件则保留默认行为
        }
    }
}

三、测试

在 main 方法中调用,实际场景可改为返回 Response 或上传至 OSS 后返回链接。

public static void main(String[] args) throws IOException {
    FillWordDTO fillWordDTO = FillWordDTO.create();
    // 读取模板文件
    byte[] modelByte = Files.readAllBytes(Paths.get("src/main/resources/templates/test.docx"));
    // 调用工具类
    byte[] resultByte = ContractUtil.fillWordDataByDomain(modelByte, fillWordDTO);
    // 输出结果
    File resultFile = new File("demo.docx");
    FileOutputStream fos = new FileOutputStream(resultFile);
    fos.write(resultByte);
    fos.close();
}

四、注意事项

编辑 Word 域代码时,有时隐藏代码会导致填充失败,报错信息如: Found end of mail merge region 'experienceList' that does not match start of mail merge region 'jlList'.

排查方法: 在 Word 中依次点击「文件→选项→高级」,在「显示文档内容」区域勾选「显示域代码而非域值」。找到报错的域代码后删除,重新添加正确的域即可解决。

目录

  1. 一、设置 Word 模板
  2. 普通字段
  3. 列表字段
  4. 复选框
  5. 二、代码实现
  6. 1. 引入依赖
  7. 2. 资源准备
  8. 3. 核心代码
  9. 实体类定义
  10. 工具类实现
  11. 三、测试
  12. 四、注意事项
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Python 办公自动化:PPT 高级功能与常用操作
  • Mac mini 安装 OpenClaw 并对接飞书
  • Flutter 三方库 eth_sig_util 在鸿蒙端的适配与以太坊签名应用实践
  • LLaMA 2/3、Qwen 与 DeepSeek 开源大模型技术对比分析
  • OpenCode 开源 AI 编程助手工具介绍
  • Flutter for OpenHarmony 项目 Lint 静态检查配置实战
  • OpenClaw MacOS 安装前环境变量设置教程
  • Spring Boot Web 三大核心交互实战:表单、AJAX 与 JSON
  • FauxPilot:开源 GitHub Copilot 替代方案本地部署指南
  • Java版LeetCode热题100之跳跃游戏:贪心算法的完美应用
  • 清华团队 Nature 发表仿生多模态触觉传感器 SuperTac 研究
  • FARS 全自动科研系统:从多智能体架构到工业化科研范式
  • Openclaw 开源仿生爪:原理、应用与生态解析
  • 零代码上手!用 Rokid 灵珠平台,5 步搭建专属旅游 AR 智能体
  • Python 量化回测框架 backtesting.py 实战教程
  • OpenClaw 本地推理方案:基于 Ollama 部署开源模型替代云端 Token 消耗
  • YOLOv8 目标追踪实战:卡尔曼滤波数学原理与 Python 实现
  • Java 9 到 Java 25 语言演进与技术革新解析
  • JavaScript Proxy 代理机制与核心方法详解

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online