基于 Apache POI 的体检报告 Word 生成实战文档
一 项目目标与总体设计
- 目标:基于模板快速生成排版规范的体检报告,支持文本替换、动态表格、图片插入,并可一键导出PDF用于归档与打印。
- 技术选型:
- Apache POI XWPF:操作.docx模板,完成占位符替换、表格行循环、图片插入等。
- LibreOffice headless:将.docx转换为.pdf,跨平台、稳定可靠。
- 工程结构建议:
- 模板:resources/templates/health_report_template.docx
- 领域模型:体检人信息、体检项目结果、影像图片等
- 服务:模板解析、数据填充、PDF 转换、HTTP 下载
- 关键约束:
- 模板必须使用.docx(XWPF 不支持直接操作.doc);占位符需保证在同一XWPFRun内,避免替换失败。
二 快速开始与最小可用示例
- Maven 依赖(建议版本 ≥5.2.3):
<dependency><groupId>org.apache.poi</groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency>- 最小可用流程:读取模板 → 替换占位符 → 写出文件
// 1) 读取模板try(InputStreamis=newClassPathResource("templates/health_report_template.docx").getInputStream();XWPFDocumentdoc=newXWPFDocument(is)){// 2) 简单文本替换(占位符格式:${key})Map<String,String>params=Map.of("name","张三","gender","男","age","28","examDate","2025-12-01");replaceTextInDoc(doc,params);// 3) 写出 .docxtry(FileOutputStreamout=newFileOutputStream("target/体检报告_张三.docx")){doc.write(out);}}- 占位符替换工具方法(核心要点:逐段遍历,逐 Run 替换,避免跨 Run 失效)
publicstaticvoidreplaceTextInDoc(XWPFDocumentdoc,Map<String,String>params){for(XWPFParagraphp:doc.getParagraphs()){List<XWPFRun>runs=p.getRuns();if(runs.isEmpty())continue;Stringtext=p.getText();if(text==null||!text.contains("${"))continue;// 简单策略:将整段文本一次性替换(要求占位符不被 Run 拆分)for(Map.Entry<String,String>e:params.entrySet()){Stringph="${"+e.getKey()+"}";if(text.contains(ph)){text=text.replace(ph,e.getValue()==null?"":e.getValue());}}// 写回第一个 Run,清空其余 Run,避免残留格式runs.get(0).setText(text,0);for(inti=runs.size()-1;i>0;i--){p.removeRun(i);}}}- 运行后将在 target 目录生成:体检报告_张三.docx。
三 核心能力实现
- 动态表格(体检项目明细)
- 模板中预留一个表格,约定第一行是表头,第二行是“数据模板行”(POI 会复用该行样式创建新行)。
// 体检项目明细staticclassItem{Stringproject;Stringresult;Stringunit;StringrefLow;StringrefHigh;Stringconclusion;}publicstaticvoidfillTable(XWPFDocumentdoc,List<Item>items){List<XWPFTable>tables=doc.getTables();if(tables.isEmpty())return;XWPFTabletable=tables.get(0);// 取第一个表格// 约定:第2行为模板行(索引1),从它之后插入数据行XWPFTableRowtpl=table.getRow(1);for(inti=0;i<items.size();i++){XWPFTableRowrow=table.insertNewTableRowAfter(tpl);// 复制模板行的单元格样式(浅拷贝,POI 默认行为)for(intc=0;c<tpl.getTableCells().size();c++){XWPFTableCellsrc=tpl.getCell(c);XWPFTableCelldst=row.getCell(c);if(dst==null)dst=row.addNewTableCell();// 简单文本填充(如需保留样式,可深拷贝 CTR)dst.setText(getCellText(items.get(i),c));}}// 可选:移除模板行table.removeRow(1);}privatestaticStringgetCellText(Itemit,intcol){returnswitch(col){case0->it.project;case1->it.result;case2->it.unit;case3->it.refLow;case4->it.refHigh;case5->it.conclusion;default->"";};}- 图片插入(体检影像、签名等)
- 使用 POI 的 addPicture,尺寸以EMU为单位(1 英寸 =914400EMU)。
publicstaticvoidinsertImage(XWPFDocumentdoc,StringimgPath,intwidthInch,intheightInch)throwsException{try(FileInputStreamis=newFileInputStream(imgPath)){// 添加图片数据并返回索引(可选)intidx=doc.addPictureData(is,XWPFDocument.PICTURE_TYPE_PNG);XWPFParagraphp=doc.createParagraph();XWPFRunrun=p.createRun();run.addPicture(is,XWPFDocument.PICTURE_TYPE_PNG,imgPath,Units.toEMU(widthInch*914400),Units.toEMU(heightInch*914400));}}- 表格样式与对齐(居中、宽度)
- 通过底层CTTbl/ CTTblPr/ CTJc设置表格居中与宽度(单位DXA,常用全宽约9000)。
importstaticorg.openxmlformats.schemas.wordprocessingml.x2006.main.STJc.*;publicstaticvoidsetTableStyle(XWPFTabletable){CTTblPrtblPr=table.getCTTbl().addNewTblPr();CTJcjc=tblPr.addNewJc();jc.setVal(STJc.CENTER);CTTblWidthwidth=tblPr.addNewTblW();width.setW(BigInteger.valueOf(9000));width.setType(STTblWidth.DXA);}- 模板占位符被 Run 拆分的处理
- 现象:占位符被 Word 样式拆到多个XWPFRun,导致简单替换失效。
- 解决思路:
- 在 Word 中将占位符粘贴为“无格式文本”,保证整体位于同一 Run;或
- 在代码中合并被拆分的 Run,再替换(遍历 Run,定位
${与},合并中间 Run 的文本后再替换)。
四 模板规范与最佳实践
- 模板规范
- 使用.docx;所有占位符统一为${key},避免特殊字符与 XML 冲突。
- 表格循环:预留“表头 + 模板行”,模板行用于复制生成数据行。
- 图片占位:预留位置段落,或在代码中指定插入点。
- 样式与编号:尽量用 Word 样式(标题、正文、表格样式),避免手工格式影响复用。
- 运行与资源
- 模板放置于classpath,使用ClassPathResource读取,便于 JAR 包部署。
- 所有流使用 try-with-resources 关闭,避免文件句柄泄漏。
- 性能与稳定性
- 大数据量(>1万行)建议分页生成多个文档或导出CSV/Excel附件。
- 图片压缩后再写入,避免体积过大。
- 可维护性
- 将“文本替换、表格填充、图片插入”封装为独立组件,便于单元测试与复用。
五 导出 PDF 与 HTTP 下载
- LibreOffice 转换(跨平台、稳定)
publicstaticbooleanconvertDocxToPdf(StringinDocx,StringoutDir){Stringos=System.getProperty("os.name").toLowerCase();Stringcmd;if(os.contains("win")){cmd="cmd /c start /wait soffice --headless --invisible --convert-to pdf:writer_pdf_Export "+inDocx+" --outdir "+outDir;}else{cmd="libreoffice --headless --invisible --convert-to pdf:writer_pdf_Export "+inDocx+" --outdir "+outDir;}try{Processp=Runtime.getRuntime().exec(cmd);intexit=p.waitFor();returnexit==0;}catch(Exceptione){e.printStackTrace();returnfalse;}}- Spring Boot 下载接口示例
@GetMapping("/report/export")publicvoidexport(HttpServletResponseresp)throwsException{// 1) 生成 .docxStringdocx="target/体检报告_张三.docx";// 生成逻辑// 2) 转 PDFStringpdf=docx.replace(".docx",".pdf");convertDocxToPdf(docx,"target");// 3) 输出 PDFresp.setContentType("application/pdf");resp.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode("体检报告.pdf","UTF-8"));Files.copy(Paths.get(pdf),resp.getOutputStream());resp.getOutputStream().flush();}- 提示
- 服务器需安装LibreOffice;Windows 下建议使用安装路径下的 soffice.exe。
- 转换是外部进程,注意超时与异常捕获。
六 常见问题与排查清单
- 占位符未被替换
- 检查占位符是否被样式拆分到多个XWPFRun;统一为无格式文本或合并 Run 后再替换。
- 生成后 Word 损坏
- 避免并发写同一文档;确保所有流关闭;POI 版本升级到稳定版(≥5.2.3)。
- 图片不显示或变形
- 尺寸单位使用EMU;确认图片格式与 addPicture 类型一致;必要时压缩图片。
- 表格样式丢失
- 通过复制模板行样式或操作底层CTTc/CTP保留格式;必要时设置表格居中与宽度。
- Linux 转 PDF 失败
- 检查 LibreOffice 是否安装、命令路径、权限与可用字体;查看进程退出码与日志。
七 一键运行与扩展建议
- 一键运行步骤
- 准备模板:resources/templates/health_report_template.docx(含name、{name}、name、{gender}、age、{age}、age、{examDate} 与“项目明细”表格)。
- 运行单元测试或直接执行 main 方法生成.docx与.pdf。
- 打开 PDF 校验排版、表格、图片与编码(中文)。
- 扩展建议
- 模板引擎化:将模板标签升级为##{foreachRows}##、##{foreachTable}##等,实现表格行/表格级循环与更灵活的布局。
- 页眉页脚与页码:操作XWPFHeaderFooter与底层CTP添加页码域。
- 电子签名/二维码:生成图片后插入页脚或指定区域。
- 多格式导出:同时支持.docx/.pdf;大数据量导出Excel汇总。
- 异步与缓存:报告生成放入异步任务,生成后缓存 PDF,避免重复渲染。
附 模板占位符与表格示例
- 文本占位符示例
- 姓名:${name}
- 性别:${gender}
- 年龄:${age}
- 体检日期:${examDate}
- 表格示例(项目明细)
项目 结果 单位 参考低 参考高 结论 身高 175 cm 体重 68 kg 收缩压 118 mmHg 90 120 正常 舒张压 76 mmHg 60 80 正常
将以上表格的第二行作为“模板行”,程序会复制该行生成数据行,最终移除模板行。
参考要点
- 使用Apache POI XWPF操作.docx模板,完成文本替换、表格行循环与图片插入;模板占位符需位于同一XWPFRun内,避免替换失败。
- 通过LibreOffice headless将.docx转换为.pdf,适合服务器批量导出与归档。
- 图片插入需使用EMU单位设置宽高;表格可通过底层CTTbl/CTJc设置居中与全宽,保证打印版式稳定。