简介:这份资源面向需要在Java环境中批量生成Word文档的开发者,尤其是使用Apache POI处理Office文件的初中级程序员。它围绕“模板+数据”的思路,演示如何读取预先设计好的.docx模板,替换段落、表格中的占位符,并输出个性化文档,适用于报告、合同等自动化生成场景。压缩包共11个文件,约7.37MB,包含5个jar依赖包(poi、poi-ooxml、xmlbeans、dom4j等)、3个java源码示例、2个docx模板以及1张示意图,覆盖从依赖引入到文档读写的完整链路。目前已有4138人学习下载,说明该方案在实际开发中具有较高参考价值。读者可从中获取可运行的模板替换代码、XWPFDocument与XWPFTable的操作范例,以及处理复杂表格和资源关闭的排错思路,快速搭建自己的文档生成工具。
1. 从一份“套模板”需求说起:poi 生成word 到底能省掉多少手工活
合同、周报、验收单、通知书——只要格式固定、字段可变,就有人在一遍遍复制粘贴改内容。我见过一个后台系统,运营每天要出两百份格式完全一致的确认函,手动改到怀疑人生。后来用 Apache POI 的 XWPF 模块配合一份预置的 .docx 模板,把变量替换进去,批量生成压缩包下载,整个流程从两小时压到三分钟。这就是 poi 生成word 最典型的落地场景:模板不动,数据在动。
它适合谁?适合已经能写 Java、手上有固定 Word 版式、需要程序化填充字段的开发者。不适合想从零画复杂图表、做像素级排版的人——POI 对 Word 的控制力远不如对 Excel 那么细腻,这是选型前必须认清的边界。下面把我拆过的模板填充方案完整走一遍,包括依赖、模板语法、代码骨架和几个让我翻过车的坑。
2. 环境与依赖:XWPF 和 HWPF 怎么选,版本怎么锁
2.1 为什么是 XWPF 而不是 HWPF
POI 操作 Word 有两个入口:HWPF 对应 .doc(97-2003 二进制格式),XWPF 对应 .docx(OOXML 格式)。现在绝大多数模板都是 .docx,XWPF 的 API 也更完整,段落、表格、页眉页脚、图片都能碰。HWPF 维护活跃度低,遇到复杂 .doc 容易出玄学问题。所以除非甲方硬塞给你一批 .doc 老文件,否则一律走 XWPF。
选型确认后,依赖要锁版本。POI 各模块版本必须一致,混用会出现NoSuchMethodError这种运行时才炸的问题。常见做法是用 Maven 统一管理:
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.5</version> </dependency> <!-- poi-ooxml 会传递引入 poi 核心包,无需重复声明 -->参数说明:poi-ooxml是操作 .docx 的主包,内部依赖poi、poi-ooxml-lite、xmlbeans等。版本号建议固定,不要用LATEST,否则不同环境拉到的版本不一致,本地能跑服务器报错。如果项目里已经有其他组件依赖了旧版 POI,用mvn dependency:tree查一下冲突,必要时用<exclusions>排掉旧版。
2.2 模板文件放哪、怎么读
模板不要放在 classpath 里用getResourceAsStream直接读——XWPFDocument 从流构造后,如果还要写回,流关闭时机不好控制。我一般把模板放独立目录,用绝对路径或配置项指定:
// 模板路径从配置读取,避免硬编码 String templatePath = config.getTemplateDir() + "/confirm_template.docx"; try (FileInputStream fis = new FileInputStream(templatePath); XWPFDocument doc = new XWPFDocument(fis)) { // 后续替换逻辑 // ... try (FileOutputStream fos = new FileOutputStream(outputPath)) { doc.write(fos); } }逻辑说明:XWPFDocument实现了Closeable,用 try-with-resources 确保底层 XML 结构释放。先读模板构造文档对象,替换完再写出到目标路径。注意输出流要在文档对象还活着的时候写,顺序反了会写出空文件。参数上,templatePath和outputPath建议都走配置,方便测试环境和生产环境切换目录。
3. 模板占位符设计:用 {{key}} 还是 ${key},替换逻辑怎么写
3.1 占位符格式的选择
模板里怎么标记变量,直接决定替换代码的复杂度。常见三种写法:${name}、{{name}}、@name@。${}和某些模板引擎语法撞车,@在 Word 里容易被自动更正干扰。我一般用{{name}},双花括号在正文里显眼,运营编辑模板时不容易误删,正则匹配也干净。
但有个前提:占位符必须写在同一个 run 里。Word 的段落由多个 run 组成,你看到的{{name}}在底层可能被拆成{{、name、}}三个 run——这是 POI 替换最经典的翻车点。解决办法有两个:一是编辑模板时一次性输入完整占位符,不要中途改字体;二是代码里做跨 run 合并。先看基础替换:
/** * 替换段落中的占位符 * @param paragraph 段落对象 * @param data 键值对,key 不带花括号 */ private void replaceInParagraph(XWPFParagraph paragraph, Map<String, String> data) { // 先把段落所有 run 的文本拼起来,判断是否含占位符 StringBuilder fullText = new StringBuilder(); for (XWPFRun run : paragraph.getRuns()) { fullText.append(run.getText(0) == null ? "" : run.getText(0)); } String text = fullText.toString(); if (!text.contains("{{")) { return; // 没有占位符,跳过 } // 逐个 key 替换 for (Map.Entry<String, String> entry : data.entrySet()) { String placeholder = "{{" + entry.getKey() + "}}"; if (text.contains(placeholder)) { text = text.replace(placeholder, entry.getValue() == null ? "" : entry.getValue()); } } // 把替换后的文本写回第一个 run,清空其余 run if (paragraph.getRuns().size() > 0) { XWPFRun firstRun = paragraph.getRuns().get(0); firstRun.setText(text, 0); for (int i = paragraph.getRuns().size() - 1; i > 0; i--) { paragraph.removeRun(i); } } }逻辑说明:先把段落内所有 run 的文本拼成一个完整字符串,在这个字符串上做替换,再把结果塞回第一个 run,删掉多余的 run。这样即使占位符被拆散也能正确替换。参数上,data的 key 不带花括号,方法内部自动补{{}}。run.getText(0)的参数 0 表示取第一个文本片段,一个 run 可能有多个片段,这里简化处理。
提示:这种“合并再写回”的方式会丢失段落内不同 run 的字体差异。如果模板里一段话中部分文字加粗、部分不加粗,替换后格式会统一成第一个 run 的样式。对格式要求高的场景,需要按 run 边界精细处理,或者约定占位符独占一个 run。
3.2 表格里的占位符怎么处理
表格是模板填充的重灾区。XWPFTable 由行和单元格组成,单元格里又是段落。遍历逻辑要三层嵌套:
// 遍历文档所有表格 for (XWPFTable table : doc.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { // 单元格内可能有多个段落 for (XWPFParagraph p : cell.getParagraphs()) { replaceInParagraph(p, data); } } } }逻辑说明:doc.getTables()只返回顶层表格,嵌套表格需要递归进单元格再取getTables()。参数上没什么可调的,关键是别漏掉单元格里的段落——有人直接对 cell 调getText()再 setText,那样会丢掉段落格式。如果表格行需要动态增删(比如明细行数量不定),那就不只是替换,要配合table.createRow()和table.removeRow()做行复制,这块复杂度上一个台阶,建议模板里预留一行“样板行”,代码复制它再填充。
4. 避坑与排查:五个让我加班到凌晨的 POI 问题
4.1 占位符替换后格式全乱
现象:替换完打开文档,原本加粗的字段变成普通字体,或者整段字体统一了。原因:前面说的合并 run 再写回,丢失了 run 级格式。解决:如果格式要求严格,改用“按 run 逐个匹配”策略——遍历 run,对每个 run 的文本单独替换,不跨 run 合并。代价是占位符被拆散时匹配不到,所以模板编辑规范要跟上:占位符必须连续输入、不换字体。
4.2 生成的文档打开提示“内容有问题”
现象:Word 弹出“发现不可读取的内容”,点修复后内容还在但提示烦人。原因:通常是写出时流没关干净,或者文档对象在写出后被修改。解决:确保doc.write(fos)之后先关fos再关doc,用 try-with-resources 的嵌套顺序控制。另外检查模板本身是否损坏——用 Word 打开模板另存一次再试。
4.3 中文乱码或方框
现象:替换进去的中文显示成问号或方框。原因:字体问题。模板里占位符所在 run 的字体不支持中文,或者代码 setText 时没指定字体。解决:在模板里把占位符的字体设成宋体或微软雅黑,替换后继承该字体。如果代码里新建 run,要显式run.setFontFamily("宋体")。
4.4 页眉页脚里的占位符没被替换
现象:正文都替换了,页眉的公司名还是{{company}}。原因:doc.getParagraphs()和doc.getTables()不包含页眉页脚。解决:单独取页眉页脚遍历:
// 处理页眉 for (XWPFHeader header : doc.getHeaderList()) { for (XWPFParagraph p : header.getParagraphs()) { replaceInParagraph(p, data); } for (XWPFTable t : header.getTables()) { // 同样遍历表格 } } // 页脚同理,用 doc.getFooterList()4.5 大文档生成慢、内存飙高
现象:批量生成几百份时 GC 频繁,甚至 OOM。原因:每份都重新读模板、构造 XWPFDocument,对象没及时释放。解决:模板只读一次缓存成字节数组,每次生成用new XWPFDocument(new ByteArrayInputStream(templateBytes))构造,避免重复 IO。生成完立即 close,不要攒着。如果并发高,用线程池控制同时生成的数量,POI 对象不是线程安全的,别共享。
5. 进阶技巧:动态表格行与批量生成的收尾习惯
模板里明细行数量不固定,是 poi 生成word 里最考验人的部分。我的做法是在模板中放一行“样板行”,占位符写成{{item_name}}、{{item_qty}},代码里找到这行,按数据条数复制:
// 假设样板行是表格第 2 行(索引 1) XWPFTableRow templateRow = table.getRow(1); int dataSize = itemList.size(); // 先按需插入新行 for (int i = 1; i < dataSize; i++) { // 复制样板行的 XML 结构 XWPFTableRow newRow = table.insertNewTableRow(1 + i); newRow.getCtRow().set(templateRow.getCtRow().copy()); } // 再逐行填充 for (int i = 0; i < dataSize; i++) { XWPFTableRow row = table.getRow(1 + i); Map<String, String> rowData = new HashMap<>(); rowData.put("item_name", itemList.get(i).getName()); rowData.put("item_qty", String.valueOf(itemList.get(i).getQty())); for (XWPFTableCell cell : row.getTableCells()) { for (XWPFParagraph p : cell.getParagraphs()) { replaceInParagraph(p, rowData); } } } // 删掉原始样板行(如果它没被数据覆盖) if (dataSize == 0) { table.removeRow(1); }逻辑说明:getCtRow().copy()是底层 XML 级别的复制,能保留行内所有格式,比手动 createRow 再设样式靠谱。参数上,样板行索引要按实际模板调整,插入位置从样板行之后开始。注意insertNewTableRow只是插入空行,必须紧接着 copy XML,否则新行是空的。
批量生成时,我习惯把每份文档单独写出到以业务编号命名的文件,最后用 ZIP 打包。验证环节别偷懒:随机抽三份打开,检查字段是否对位、表格行数是否正确、页眉有没有漏。从那以后我每次改完模板都强制走一遍“三份抽检”,因为 POI 的坑往往藏在你看不见的 run 边界里。希望帮到你。
本文还有配套的精品资源,点击获取