背景
我们需要通过 Java 动态导出 Word 文档,基于预定义的 模板文件(如 .docx
格式)。模板中包含 表格,程序需要完成以下操作:
-
替换模板中的文本(如占位符 ${设备类型} 等)。
-
替换模板中的图片(如占位符 {{图片_作业现场}} )。
模板示例
模板文件(如 template.docx
)结构大致如下:
maven依赖
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.17</version></dependency> <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.17</version></dependency><dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.12.1</version></dependency>
Controller
@ApiOperation(notes = "模板导出", value = "使用模板导出文档")
@RequestMapping(value = "/exportByTemplate", method = RequestMethod.GET)
public void exportByTemplate(HttpServletResponse response) {try {// 1. 设置响应头response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");response.setHeader("Content-Disposition", "attachment;filename=report.docx");// 2. 准备数据Map<String, Object> data = new HashMap<>();data.put("设备类型", "开关");data.put("属地运维单位", "湘江公司");data.put("作业现场", new String[]{"D:\\upload\\upload\\2025\\04\\14\\20250414070702.jpg","D:\\upload\\upload\\2025\\04\\14\\20250414070720.jpg"});// 3. 调用生成方法pdPointProblemService.generateFromTemplate(response,"D:\\1.docx", // 模板路径data);} catch (Exception e) {e.printStackTrace();// 异常处理(略)}
}
ServiceImpl
@Override
public void generateFromTemplate(HttpServletResponse response,String templatePath,Map<String, Object> data) throws Exception {// 1. 初始化文档(不使用try-with-resources)FileInputStream fis = new FileInputStream(templatePath);XWPFDocument doc = new XWPFDocument(fis);try {// 2. 执行替换replaceText(doc, data);replaceImages(doc, data);OutputStream out = response.getOutputStream();doc.write(out);out.flush();} finally {if (fis != null) {fis.close();}}
}private void replaceText(XWPFDocument doc, Map<String, Object> data) {// 替换段落中的文本for (XWPFParagraph p : doc.getParagraphs()) {replaceTextInParagraph(p, data);}// 替换表格中的文本for (XWPFTable table : doc.getTables()) {for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph p : cell.getParagraphs()) {replaceTextInParagraph(p, data);}}}}
}private void replaceTextInParagraph(XWPFParagraph paragraph, Map<String, Object> data) {// 1. 合并段落内所有Run的文本String fullText = mergeAllRuns(paragraph);if (!fullText.contains("${")) return;// 2. 执行全局替换String newText = replacePlaceholders(fullText, data);// 3. 清空原有Run的文本(保留样式)clearRunTexts(paragraph);// 4. 将新文本写入第一个Run(保留原始格式)if (!paragraph.getRuns().isEmpty()) {XWPFRun firstRun = paragraph.getRuns().get(0);firstRun.setText(newText, 0);} else {paragraph.createRun().setText(newText);}
}/*** 正则替换完整文本*/
private String replacePlaceholders(String text, Map<String, Object> data) {Pattern pattern = Pattern.compile("\\$\\{(.+?)}");Matcher matcher = pattern.matcher(text);StringBuffer sb = new StringBuffer();while (matcher.find()) {String key = matcher.group(1);Object value = data.getOrDefault(key, "");matcher.appendReplacement(sb, Matcher.quoteReplacement(value.toString()));}matcher.appendTail(sb);return sb.toString();
}/*** 清空所有Run的文本(保留样式)*/
private void clearRunTexts(XWPFParagraph paragraph) {for (XWPFRun run : paragraph.getRuns()) {run.setText("", 0); // 清空文本但保留Run对象}
}private void replaceImages(XWPFDocument doc, Map<String, Object> data) throws Exception {// 1. 处理普通段落for (XWPFParagraph p : doc.getParagraphs()) {processParagraphForImages(p, data);}// 2. 处理表格内的段落for (XWPFTable table : doc.getTables()) {for (XWPFTableRow row : table.getRows()) {for (XWPFTableCell cell : row.getTableCells()) {for (XWPFParagraph p : cell.getParagraphs()) {processParagraphForImages(p, data);}}}}
}/*** 统一处理段落中的图片占位符*/
private void processParagraphForImages(XWPFParagraph p, Map<String, Object> data) throws Exception {// 合并段落内所有Run的文本String mergedText = mergeAllRuns(p);if (mergedText.isEmpty()) return;// 正则匹配图片占位符Matcher matcher = Pattern.compile("\\{\\{图片_(.+?)}}").matcher(mergedText);if (!matcher.find()) return;String placeholder = matcher.group(0);String fieldName = matcher.group(1);// 清理占位符clearPlaceholderRuns(p, placeholder);// 插入图片if (data.containsKey(fieldName)) {
// String imagePath = (String) data.get(fieldName);
// insertImage(p, imagePath);String[] imageList = (String[]) data.get(fieldName);insertImageList(p,imageList);}
}private void insertImageList(XWPFParagraph paragraph, String[] imagePaths) throws Exception {for (String imagePath : imagePaths) {File imageFile = new File(imagePath);if (!imageFile.exists()) {System.out.println("图片文件不存在: " + imagePath);}FileInputStream fis = new FileInputStream(imageFile);byte[] bytes = IOUtils.toByteArray(fis);fis.close();int format = getImageFormat(imagePath);// 添加图片到文档中,返回的是图片IDString blipId = paragraph.getDocument().addPictureData(bytes, format);// 创建图片关联的 CTDrawingint id = paragraph.getDocument().getNextPicNameNumber(format);XWPFRun run = paragraph.createRun();int width = 300; // pxint height = 200; // pxint widthEmu = Units.toEMU(width);int heightEmu = Units.toEMU(height);String picXml = getPicXml(blipId, widthEmu, heightEmu, id);// 读取为 CTInlineCTInline inline = run.getCTR().addNewDrawing().addNewInline();XmlToken xmlToken = XmlToken.Factory.parse(picXml);inline.set(xmlToken);// 设置图片的大小和描述inline.setDistT(0);inline.setDistB(0);inline.setDistL(0);inline.setDistR(0);CTPositiveSize2D extent = inline.addNewExtent();extent.setCx(widthEmu);extent.setCy(heightEmu);CTNonVisualDrawingProps docPr = inline.addNewDocPr();docPr.setId(id);docPr.setName("图片_" + id);docPr.setDescr("描述_" + id);// 可选:图片之间加个换行run.addBreak();}
}/*** 合并段落内所有Run的文本*/
private String mergeAllRuns(XWPFParagraph paragraph) {StringBuilder sb = new StringBuilder();for (XWPFRun run : paragraph.getRuns()) {String text = run.getText(0);if (text != null) {sb.append(text);}}return sb.toString();
}/*** 处理占位符跨多个Run的情况,并删除相关Run*/
private void clearPlaceholderRuns(XWPFParagraph paragraph, String placeholder) {List<XWPFRun> runs = paragraph.getRuns();if (runs == null || runs.isEmpty()) {return;}StringBuilder allText = new StringBuilder();List<Integer> runPositions = new ArrayList<>();// 收集每个run的起始位置for (XWPFRun run : runs) {runPositions.add(allText.length());String text = run.getText(0);if (text != null) {allText.append(text);}}String fullText = allText.toString();int startIndex = fullText.indexOf(placeholder);if (startIndex == -1) {return; // 找不到占位符,不处理}int endIndex = startIndex + placeholder.length();// 找到涉及到的 run 范围int runStart = -1;int runEnd = -1;for (int i = 0; i < runPositions.size(); i++) {int runPos = runPositions.get(i);if (runStart == -1 && runPos <= startIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) > startIndex)) {runStart = i;}if (runPos <= endIndex && (i == runPositions.size() - 1 || runPositions.get(i + 1) >= endIndex)) {runEnd = i;break;}}// 删除 run,注意:从后往前删,避免下标错乱for (int i = runEnd; i >= runStart; i--) {paragraph.removeRun(i);}
}/*** 获取图片格式类型*/
private int getImageFormat(String fileName) {String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();switch (extension) {case "jpg":case "jpeg": return XWPFDocument.PICTURE_TYPE_JPEG;case "png": return XWPFDocument.PICTURE_TYPE_PNG;default: return XWPFDocument.PICTURE_TYPE_JPEG;}
}private static String getPicXml(String blipId, int widthEmu, int heightEmu, int id) {return"<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">" +" <a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +" <pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">" +" <pic:nvPicPr>" +" <pic:cNvPr id=\"" + id + "\" name=\"Generated\"/>" +" <pic:cNvPicPr/>" +" </pic:nvPicPr>" +" <pic:blipFill>" +" <a:blip r:embed=\"" + blipId + "\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"/>" +" <a:stretch><a:fillRect/></a:stretch>" +" </pic:blipFill>" +" <pic:spPr>" +" <a:xfrm>" +" <a:off x=\"0\" y=\"0\"/>" +" <a:ext cx=\"" + widthEmu + "\" cy=\"" + heightEmu + "\"/>" +" </a:xfrm>" +" <a:prstGeom prst=\"rect\">" +" <a:avLst/>" +" </a:prstGeom>" +" </pic:spPr>" +" </pic:pic>" +" </a:graphicData>" +"</a:graphic>";
}