
简介针对Java开发中需要动态生成Word文档的场景这套源码项目基于POI实现了不依赖模板文件的图片插入与目录生成并细分为简单模式与复杂模式便于不同文档需求灵活选用。项目来自生产环境且代码包含注释与示例特别适合在OA系统、合同管理等场景中生成Word文件也适合正在使用POI进行Word图文混排、目录生成或希望绕开模板方案的开发者借鉴。资源以rar压缩包发布整体约22.61MB共97个文件包括35个Java源文件、38个class文件及18个依赖包同时附有配置文件和IDE工程文件导入后即可运行调试省去手动配置依赖的麻烦。目前已有535人学习或下载通过这套经过实际检验的示例可以快速掌握动态Word生成中的图片定位、文字插入与目录构建思路。 做Java后端的人迟早会被Office文档处理折磨一次。业务方经常甩过来一个需求把数据库里的数据导出成Word报告要带目录、要插图、字体要好看。Apache POI是绕不过去的方案但真正动手搞过几天就会明白一个道理——网上的资料很多能一次跑通的细节很少。这篇文章我从底层拆一遍把POI生成Word过程中最麻烦的图片插入、文字样式、目录生成三个硬骨头全部打通附能直接跑起来的源码顺便把Word里“下划线上打字但线不动”的经典问题一起解决了。如果你正在为导出带目录的Word文档发愁或者想搞懂POI对OOXML内容的真实控制方式这篇应该能省你不少时间。1. 动手之前先把需求拆透再决定技术方案1.1 核心需求解构先别急着写代码把需求里头的东西拆出来。标题里的“poi word 图片 文字 目录 源码”翻译成实际开发场景通常意味着这么几件事程序动态生成一个docx文档内容包含大量格式化文字比如标题、正文、加粗、下划线、缩进。需要往文档里插入若干张图片图片要能控制尺寸最好还能居中或文字环绕。文档开头需要带一个目录章节多了以后点一下跳转、看页码这是硬性要求。如果涉及表格还得处理单元格宽度、列宽这类看起来简单、其实很坑的操作。这些需求单独看都不复杂但凑在一起就会碰到POI不同API层次的问题。文字和图片用XWPF系列接口就能做目录却需要直接操作底层XML标签因为POI原生没有提供“插入一个目录对象”的现成API。1.2 技术选型与方案取舍POI处理Word有两套方案操作老格式.doc的HWPF以及处理新格式.docx的XWPF。别犹豫新的项目一律选XWPF。原因很直接.doc格式对样式、图片的处理能力非常弱API也很久没大更新而.docx本质上是一个zip压缩包里面是各种XML文件XWPF能比较完整地覆盖段落、表格、图片等核心元素。另一个容易踩的坑是版本选择。我推荐现在用5.x系列比如5.2.5因为4.x对某些OOXML标签的封装不够团队在自定义XML时经常要绕路。5.x对Java 8以上支持良好稳定性经过了很长时间验证。如果公司环境特别老只能JDK7那才考虑4.1.2否则别回头用老版本。2. 环境准备依赖与核心API基础2.1 Maven依赖与版本选择既然是源码实战环境先搞定。理论上只需要一个核心依赖POI会把其他依赖带进来dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency这个坐标同时包含了poi核心、ooxml-schema、XMLBeans这些基础库。实际开发中还可能要引入poi-scratchpad用来支持.doc老格式但这里用不到。有一点必须单独强调poi和poi-ooxml的版本必须强制一致。我见过很多次因为项目里其他地方依赖了旧版poi导致运行时NoSuchMethodError或者ClassNotFoundException排查半天发现是jar包版本冲突。一旦出现这类问题先检查依赖树里是否混入了不同版本的poi。2.2 XWPF核心对象与文档结构XWPF的对象模型和Word文档结构是一一对应的。XWPFDocument对应整个文档XWPFParagraph对应一个段落XWPFRun对应段落里同一格式的一段文字XWPFTable对应一个表格。图片和文字都挂在Run上表格里又有行和单元格。理解这个层级关系写代码就有章法了设置段落样式用XWPFParagraph的CTP底层对象设置字体用XWPFRun插入图片也走XWPFRun表格定宽则要同时操作表格对象和底层CTTbl节点。后面所有源码都是围绕这个结构展开的。3. 从零构造一份可用的Word报告3.1 创建文档骨架标题、段落与中文字体避坑直接上代码。以下是最基础的骨架包含标题和正文段落XWPFDocument document new XWPFDocument(); XWPFParagraph title document.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); title.setSpacingAfter(200); XWPFRun titleRun title.createRun(); titleRun.setText(项目技术方案报告); titleRun.setBold(true); titleRun.setFontSize(22); titleRun.setFontFamily(微软雅黑);这里有个坑光setFontFamily不行。POI设置中文字体时这个方法只改了西文字体中文字体还需要单独设置eastAsia属性否则中文在Word里可能默认变成等线或宋体和你想要的效果不一样private static void setChineseFont(XWPFRun run, String fontName) { run.setFontFamily(fontName); CTFonr rFonts run.getCTR().isSetRFonts() ? run.getCTR().getRFonts() : run.getCTR().addNewRFonts(); rFonts.setEastAsia(fontName); }这个方法建议直接沉淀成工具后续所有段落都调它。正文段落的常规操作也一样无非是fontSize、setSpacingAfter、行距这些。行距注意用setSpacingLineRule比如1.5倍行距对应LineSpacingRule.AUTO同时配setSpacingLine(360)这里的单位是240分之一磅。3.2 图片插入尺寸换算与图文混排图片是另一个新手重灾区。POI的XWPFRun.addPicture方法需要传入宽度和高度但单位是EMU不是你熟悉的px或cm。换算关系是1厘米 360000 EMU1像素在96DPI下约等于9525 EMU。如果直接拿图片原始像素传进去Word里显示会异常大。正确做法是读取图片宽高按想要的显示宽度等比例计算高度再把厘米转成EMUprivate static void addPictureToParagraph(XWPFParagraph paragraph, String imgPath, double targetWidthCm) throws Exception { BufferedImage image ImageIO.read(new File(imgPath)); int type imgPath.toLowerCase().endsWith(.png) ? XWPFDocument.PICTURE_TYPE_PNG : XWPFDocument.PICTURE_TYPE_JPEG; int emuWidth (int) (targetWidthCm * 360000); int emuHeight (int) (emuWidth * image.getHeight() / (double) image.getWidth()); try (FileInputStream fis new FileInputStream(imgPath)) { paragraph.createRun().addPicture(fis, type, imgPath, emuWidth, emuHeight); } }这里通过BufferedImage先拿原始宽高再按显示宽度等比缩放这样图片不会变形。注意addPicture会读取整个InputStream传完再关闭所以用try-with-resources包裹文件流非常稳妥。图片类型也要和文件真实格式一致JPEG格式传PNG类型或者反过来打开Word时都可能报错。3.3 段落对齐与分页控制图文混排时图片所在段落一般需要居中paragraph.setAlignment(ParagraphAlignment.CENTER);分页则用BreakType.PAGEXWPFParagraph pageBreak document.createParagraph(); pageBreak.createRun().addBreak(BreakType.PAGE);这些虽然都是小操作但是不写的话生成的文档排版会很乱。真实报告往往是标题、正文、图片、表格、附录这种结构每一节都需要明确控制。4. 目录生成POI没有直接API怎么办4.1 目录字段的原理这是全篇最核心的部分。POI没有XWPFDirectory这种现成类但Word里的目录本质上是“域Field”的一种。在OOXML里目录靠一组fldChar标签实现结构是这样的w:r w:fldChar w:fldCharTypebegin/ /w:r w:r w:instrText xml:spacepreserve TOC \o 1-3 \h \z \u /w:instrText /w:r w:r w:fldChar w:fldCharTypeseparate/ /w:r w:r w:t目录内容占位区/w:t /w:r w:r w:fldChar w:fldCharTypeend/ /w:r这段XML就是Word中“目录”域的内部结构。TOC开头的指令告诉Word去扫描所有应用了内置标题样式“标题1”到“标题3”的段落把它们收集成目录并显示页码。所以要让目录自动识别章节文档里的标题段落必须应用系统内置标题样式而不是仅手动调大字号加粗。4.2 源码实现在文档中插入TOC指令POI暴露了底层CTP对象我们可以通过拼接上面的域代码实现目录。代码如下private static XWPFParagraph createTOCPlaceholder(XWPFDocument document) { XWPFParagraph paragraph document.createParagraph(); CTP ctp paragraph.getCTP(); CTR r1 ctp.addNewR(); CTFldChar begin r1.addNewFldChar(); begin.setFldCharType(STFldCharType.BEGIN); CTR r2 ctp.addNewR(); CTText instr r2.addNewInstrText(); instr.setStringValue( TOC \\o \1-3\ \\h \\z \\u ); CTR r3 ctp.addNewR(); CTFldChar separate r3.addNewFldChar(); separate.setFldCharType(STFldCharType.SEPARATE); CTR r4 ctp.addNewR(); CTText placeholder r4.addNewT(); placeholder.setStringValue(请右键点击此处选择“更新域”生成目录。); CTR r5 ctp.addNewR(); CTFldChar end r5.addNewFldChar(); end.setFldCharType(STFldCharType.END); return paragraph; }这里一个关键点是begin、separate、end三个fldChar必须分别位于独立的CTRun里不能混在同一个Run中。否则Word打开文档时会认为域结构损坏直接报错或提示修复文档。4.3 让Word打开时自动提示更新域只插入TOC指令还不够。Word出于安全考虑默认不会在打开文档时自动执行更新域的指令用户打开后可能只看到提示文字看不到真正的目录。解决办法是在document.xml的settings部分加上updateFields配置private static void enableAutoUpdateFields(XWPFDocument document) { CTDocument1 ct document.getDocument(); CTSettings settings ct.isSetSettings() ? ct.getSettings() : ct.addNewSettings(); CTOnOff updateFields settings.isSetUpdateFields() ? settings.getUpdateFields() : settings.addNewUpdateFields(); updateFields.setVal(true); }加了这一段之后用户用Word或WPS打开文档会弹出一个“此文档包含的域可能需要更新”的提示确认后目录就能自动生成。如果没弹出来右键目录区域选“更新域”或者按F9也能手动刷新。建议在生成的文档开头加一句使用说明免得非技术同事打开后以为没生成目录。5. 表格、下划线样式与布局控制5.1 表格宽度与单元格定宽的真正写法POI设置表格宽度是很多人的噩梦。只设置cell宽度经常没用因为表格宽度、列宽、单元格宽度三个值必须逻辑一致缺一个都白搭。正确的姿势是同时设置表格总宽、gridCol各列宽和每个单元格的宽XWPFTable table document.createTable(3, 3); table.setWidth(100%); CTTbl ctTbl table.getCTTbl(); CTTblPr tblPr ctTbl.getTblPr() null ? ctTbl.addNewTblPr() : ctTbl.getTblPr(); // 固定表格布局 TblLayout layout tblPr.addNewTblLayout(); layout.setType(STTblLayoutType.FIXED); // 表格总宽单位是twip1厘米约等于567 twip CTTblWidth tblW tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW(); tblW.setW(BigInteger.valueOf(2835)); // 5cm tblW.setType(STTblWidth.DXA); // 列宽 CTTblGrid grid ctTbl.getTblGrid() null ? ctTbl.addNewTblGrid() : ctTbl.getTblGrid(); for (int i 0; i 3; i) { CTGridCol col grid.addNewGridCol(); col.setW(BigInteger.valueOf(945)); } // 每个单元格宽度 for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { CTTcPr tcPr cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr(); CTTblWidth tcW tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW(); tcW.setW(BigInteger.valueOf(945)); tcW.setType(STTblWidth.DXA); } }如果不设置表格布局为FIXEDWord在排版时还是会根据内容自动调整列宽导致你设的值失效。写表格时这个细节一定要加。5.2 用下边框实现“线不动输入”的填空效果很多做记录表、合同模板的需求会遇到这个问题用户希望在空白的下划线上方打字但下划线不能跟着文字往后跑。用文字加下划线的方式做不到因为文字撑开下划线字段长度线条一定会动。正确做法是用“段落下边框”模拟下划线。给段落底部画一条横线用户在线上方输入任意长度的文字底线始终固定在段落底部private static void addBottomBorderLine(XWPFParagraph paragraph) { CTPPr pPr paragraph.getCTP().getPPr() null ? paragraph.getCTP().addNewPPr() : paragraph.getCTP().getPPr(); CTPBdr pBdr pPr.isSetPBdr() ? pPr.getPBdr() : pPr.addNewPBdr(); CTBorder bottom pBdr.isSetBottom() ? pBdr.getBottom() : pBdr.addNewBottom(); bottom.setVal(STBorder.SINGLE); bottom.setSz(BigInteger.valueOf(8)); bottom.setColor(000000); paragraph.createRun().setText(); }这样一个空段落底部就有一条直线效果和真正下划线几乎一致又不会因为输入内容而移动。表格里做填空题、签名栏时这个方法可以说是解锁了刚需场景。6. 完整源码示例6.1 综合示例代码上面所有功能点整合起来从零生成一份带目录、图片、表格和填空线的Word文档。完整可运行代码如下public class WordReportGenerator { public static void main(String[] args) throws Exception { XWPFDocument document new XWPFDocument(); // 1. 标题 XWPFParagraph title document.createParagraph(); title.setAlignment(ParagraphAlignment.CENTER); XWPFRun titleRun title.createRun(); titleRun.setText(POI生成Word实战示例); titleRun.setBold(true); titleRun.setFontSize(22); setChineseFont(titleRun, 微软雅黑); // 2. 目录区 createTOCPlaceholder(document); enableAutoUpdateFields(document); // 3. 一级标题 XWPFParagraph h1 document.createParagraph(); XWPFRun h1Run h1.createRun(); h1Run.setText(第一章 概述); h1Run.setBold(true); h1Run.setFontSize(16); setChineseFont(h1Run, 微软雅黑); h1.getCTP().getPPr().addNewPStyle().setVal(Heading1); // 应用标题1样式 // 4. 正文段落 XWPFParagraph body document.createParagraph(); XWPFRun bodyRun body.createRun(); bodyRun.setText(这是正文内容用于演示POI插入普通文字段落。); bodyRun.setFontSize(12); setChineseFont(bodyRun, 宋体); // 5. 插入图片 XWPFParagraph imgPara document.createParagraph(); imgPara.setAlignment(ParagraphAlignment.CENTER); addPictureToParagraph(imgPara, cover.png, 12); // 6. 表格 XWPFTable table document.createTable(2, 2); setupTableWidth(table, 5, 5); // 7. 填空题下划线 XWPFParagraph linePara document.createParagraph(); addBottomBorderLine(linePara); try (FileOutputStream fos new FileOutputStream(report.docx)) { document.write(fos); } document.close(); System.out.println(生成成功); } // 本章前面所有工具方法都粘贴到这里即可运行 }6.2 运行结果验证生成后直接双击打开report.docx正常能看到标题格式、图片居中、表格宽度固定以及目录占位区。如果Word弹窗询问是否更新域点“是”就能看到真正的目录。很多人打开文档发现目录区只有“请右键点击此处更新域”这几句话就以为代码失败了其实不是。POI生成的是目录域指令页签到Word/WPS里去刷新。这是所有动态生成Word目录的方案都无法绕开的机制。7. 常见问题与排查技巧实录7.1 高频问题速查表把实际开发里经常出现的几个坑整理成一张表直接对着排查现象原因解决办法打开文档提示文件损坏或需要修复TOC字段的begin/end顺序或Run结构不正确检查fldChar是否各自独占一个CTR严格按begin、instrText、separate、end顺序生成中文显示正常但字体不是想要的只设置了西文字体未设置eastAsia用rFonts.setEastAsia显式设置中文字体图片插入后尺寸异常大直接传了像素值没有换算成EMU参考厘米转EMU的公式按比例计算表格宽度设置后无效只设置了单元格未设置表格布局及总宽度表格布局设为FIXED同时维护tblW、gridCol、tcW打开文档目录只有文字没页码Word未更新域或没启用updateFields添加updateFields配置或打开后按F9手动更新运行时报NoSuchMethodError项目里poi核心包版本冲突用mvn dependency:tree检查版本统一poi与poi-ooxml版本大文档生成时内存溢出大量图片或超长内容撑爆内存调整ZipSecureFile限制或分批写入段落必要时升级到64位JVM7.2 排查思路与避坑建议遇到POI相关的问题我的排查顺序永远是“先简化场景再验证最小代码”。比如目录不生效就新建一个只含标题TOC指令的文件看能不能更新出来图片显示不对就单独写一个只插入图片的类跑。快速隔离变量比反复查看生成结果直观得多。另外生成完docx后建议把文件名后缀改成zip解压直接看word/document.xml里的内容。POI输出的XML和标准Word结构哪里有偏差肉眼对照一次就明白了。这个习惯能帮你解决80%的隐藏问题。实际跑下来用POI生成Word这件事最大的成本不是API不熟而是细节太多。中文字体、图片单位、表格宽度、目录域每一个都能让人卡两三个小时。把这些基础工具函数沉淀好后面所有报告模板都可以复用同一套代码效率会高很多。最后再分享一个小技巧代码里所有setText之后如果发现文字丢失多半是CTText的xml:space没有设置preserve遇到时检查一下源码生成出的XML属性很快能找到问题。本文还有配套的精品资源点击获取