背景
我們需要通過?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>";
}