JAVA实现DOCX/DOC文档内容替换、套打、支持表格内容替换。
要替换的数据结构Map<String, Object>,
word文档要替换内容的格式写法:
1、文本识别:${属性字段}
2、表格识别:${table:数组对象属性名称} (本文章仅支持docx实现表格的内容替换 )
真实数据,如:
java
{
"key1": "value1",
"key2": "value2",
"key3": [
{"k3_1": "v3_1_1", "k3_2": "v3_2_1", "k3_3": "v3_3_1", "k3_4": "v3_4_1"},
{"k3_1": "v3_1_2", "k3_2": "v3_2_2", "k3_3": "v3_3_2", "k3_4": "v3_4_2"},
{"k3_1": "v3_1_3", "k3_2": "v3_2_3", "k3_3": "v3_3_3", "k3_4": "v3_4_3"}
]
}
模板文件内容:


执行结果预览:

maven依赖引入:
html
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
代码实现:
java
package com.sso.common.utils.poi;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import java.io.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Word模板填充工具类(支持多数组-多表格精准匹配,兼容低版本POI)
* 核心能力:
* 1. 普通段落占位符替换(${xxx})
* 2. 多表格绑定多数组(基于${table:数组字段名}标记)
* 3. 日期字段自动转换(yyyy-MM-dd → yyyy年MM月dd日)
* 4. 兼容POI 3.17及以上版本(移除setRow方法)
*
* @author Administrator
*/
public class WordTemplateUtil {
// 通用占位符正则:匹配${xxx}格式(捕获组1为占位符key)
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)}");
// 表格数组标记正则:匹配${table:数组字段名}格式(捕获组1为数组字段名)
private static final Pattern TABLE_MARK_PATTERN = Pattern.compile("\\$\\{table:([^}]+)}");
// 日期格式正则:匹配yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
private static final Pattern DATE_PATTERN = Pattern.compile("^(\\d{4}-\\d{2}-\\d{2})(\\s\\d{2}:\\d{2}:\\d{2})?$");
// 日期解析器(线程安全)
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 目标日期格式(中文显示)
private static final DateTimeFormatter TARGET_FORMATTER = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
java
/**
* 生成填充后的Word文档字节流(入口方法)
*
* @param templatePhysicalPath Word模板文件物理路径(支持.doc/.docx)
* @param params 填充参数(key=占位符key,value=填充值;支持普通字段+数组字段)
* @return 填充后的Word字节流
* @throws Exception 文件读取/写入异常
*/
public static ByteArrayOutputStream generateWordStream(String templatePhysicalPath, Map<String, Object> params) throws Exception {
// 1. 模板文件校验
File templateFile = new File(templatePhysicalPath);
if (!templateFile.exists()) {
throw new FileNotFoundException("Word模板文件不存在:" + templatePhysicalPath);
}
if (templateFile.length() == 0) {
throw new IOException("Word模板文件为空:" + templatePhysicalPath);
}
// 2. 按文件后缀分处理(.docx优先,.doc兼容)
String fileName = templateFile.getName();
if (fileName.endsWith(".docx")) {
return generateDocxStream(templatePhysicalPath, params);
} else if (fileName.endsWith(".doc")) {
return generateDocStream(templatePhysicalPath, params);
} else {
throw new IllegalArgumentException("不支持的文件格式,仅支持.doc/.docx:" + fileName);
}
}
java
/**
* 处理.docx格式文档(核心逻辑)
*
* @param templatePath 模板路径
* @param params 填充参数
* @return 填充后的字节流
* @throws Exception IO/POI操作异常
*/
private static ByteArrayOutputStream generateDocxStream(String templatePath, Map<String, Object> params) throws Exception {
InputStream in = new FileInputStream(templatePath);
XWPFDocument doc = new XWPFDocument(in);
// ========== 第一步:处理普通段落占位符(非表格内的${xxx}) ==========
processNormalParagraphPlaceholders(doc, params);
// ========== 第二步:处理表格占位符(多数组-多表格精准匹配) ==========
processTableArrayPlaceholders(doc, params);
// ========== 最终输出 ==========
ByteArrayOutputStream out = new ByteArrayOutputStream();
doc.write(out);
// 关闭资源
doc.close();
in.close();
return out;
}
java
/**
* 处理普通段落占位符(非表格内的文本)
*
* @param doc XWPF文档对象
* @param params 填充参数
*/
private static void processNormalParagraphPlaceholders(XWPFDocument doc, Map<String, Object> params) {
for (XWPFParagraph paragraph : doc.getParagraphs()) {
List<XWPFRun> runs = new ArrayList<>(paragraph.getRuns());
Set<XWPFRun> processedRuns = new HashSet<>(); // 标记已处理的Run,避免重复
for (int i = 0; i < runs.size(); i++) {
XWPFRun currentRun = runs.get(i);
if (processedRuns.contains(currentRun)) {
continue;
}
String currentText = currentRun.getText(0);
if (currentText == null || currentText.isEmpty()) {
continue;
}
// 拼接跨Run占位符(解决占位符被拆分到多个Run的问题,如${在Run1,xxx在Run2,}在Run3)
String fullPlaceholder = null;
int endRunIndex = i;
// 场景1:当前Run以${开头,向后拼接直到找到}
if (currentText.startsWith("${")) {
fullPlaceholder = currentText;
for (int j = i + 1; j < runs.size(); j++) {
XWPFRun nextRun = runs.get(j);
String nextText = nextRun.getText(0);
if (nextText == null) break;
fullPlaceholder += nextText;
endRunIndex = j;
if (nextText.contains("}")) {
break;
}
}
}
// 场景2:当前Run包含},向前拼接直到找到${
else if (currentText.contains("}")) {
fullPlaceholder = currentText;
for (int j = i - 1; j >= 0; j--) {
XWPFRun prevRun = runs.get(j);
String prevText = prevRun.getText(0);
if (prevText == null) break;
fullPlaceholder = prevText + fullPlaceholder;
endRunIndex = j;
if (prevText.startsWith("${")) {
break;
}
}
}
// 替换完整占位符
if (fullPlaceholder != null && fullPlaceholder.contains("${") && fullPlaceholder.contains("}")) {
String replacedFullText = replacePlaceholders(fullPlaceholder, params);
// 第一个Run写入替换后的文本,其余Run清空
XWPFRun firstRun = runs.get(i);
firstRun.setText(replacedFullText, 0);
processedRuns.add(firstRun);
for (int j = i + 1; j <= endRunIndex; j++) {
XWPFRun run = runs.get(j);
run.setText("", 0);
processedRuns.add(run);
}
i = endRunIndex; // 跳过已处理的Run
continue;
}
// 单个Run内的占位符替换
String replacedText = replacePlaceholders(currentText, params);
currentRun.setText(replacedText, 0);
processedRuns.add(currentRun);
}
}
}
java
/**
* 提取模板行中的所有占位符key(仅保留模板存在的key,用于后续精准匹配)
*
* @param templateRow 模板行
* @return 模板占位符key集合
*/
private static Set<String> extractTemplatePlaceholderKeys(XWPFTableRow templateRow) {
Set<String> placeholderKeys = new HashSet<>();
for (XWPFTableCell cell : templateRow.getTableCells()) {
for (XWPFParagraph para : cell.getParagraphs()) {
StringBuilder fullText = new StringBuilder();
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
fullText.append(text);
}
}
// 匹配单元格内所有占位符${xxx},提取key
Matcher matcher = PLACEHOLDER_PATTERN.matcher(fullText.toString());
while (matcher.find()) {
String key = matcher.group(1).replaceAll("\\s+", ""); // 去空格
if (!key.startsWith("table:")) { // 排除表格标记
placeholderKeys.add(key);
}
}
}
}
System.out.println("【模板占位符提取】模板行包含的key:" + placeholderKeys);
return placeholderKeys;
}
java
/**
* 处理表格数组占位符(多数组-多表格精准匹配)
*
* @param doc XWPF文档对象
* @param params 填充参数(包含数组字段)
*/
private static void processTableArrayPlaceholders(XWPFDocument doc, Map<String, Object> params) {
// 提取所有数组参数(key=数组字段名,value=List<Map>)
Map<String, List<Map<String, Object>>> arrayParams = getArrayParams(params);
if (arrayParams.isEmpty()) {
// 无数组参数,仅处理表格内普通占位符
for (XWPFTable table : doc.getTables()) {
replaceTableNormal(table, params);
}
return;
}
// 遍历所有表格,匹配数组标记
for (XWPFTable table : doc.getTables()) {
// 1. 提取表格专属标记(如${table:账单明细表} → 账单明细表)
String tableMark = getTableMark(table);
if (tableMark == null || !arrayParams.containsKey(tableMark)) {
// 无标记/标记不匹配,处理表格内普通占位符
replaceTableNormal(table, params);
continue;
}
// 2. 获取当前表格对应的数组数据
List<Map<String, Object>> arrayData = arrayParams.get(tableMark);
if (arrayData.isEmpty()) {
// 数组为空,清空模板行
clearTableTemplateRow(table, tableMark);
continue;
}
// 3. 查找表格模板行(包含子字段占位符的行)
XWPFTableRow templateRow = findTableTemplateRow(table, arrayData.get(0).keySet());
if (templateRow == null) {
replaceTableNormal(table, params);
continue;
}
// 4. 复制模板行,生成新行并填充数据
generateTableDataRows(table, templateRow, arrayData, tableMark);
// 5. 清理模板行和标记占位符
cleanTableTemplate(table, templateRow, tableMark);
// 6. 处理表格内剩余普通占位符
replaceTableNormal(table, params);
}
}
java
/**
* 提取参数中的数组字段(value为List<Map>类型)
*
* @param params 填充参数
* @return 数组字段映射(key=数组字段名,value=List<Map>)
*/
private static Map<String, List<Map<String, Object>>> getArrayParams(Map<String, Object> params) {
Map<String, List<Map<String, Object>>> arrayParams = new HashMap<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// 过滤出List且元素为Map的参数
if (value instanceof List) {
List<?> list = (List<?>) value;
if (!list.isEmpty() && list.get(0) instanceof Map) {
arrayParams.put(key, (List<Map<String, Object>>) value);
}
}
}
return arrayParams;
}
java
/**
* 提取表格的专属数组标记
*
* @param table 表格对象
* @return 数组字段名(如账单明细表),无则返回null
*/
private static String getTableMark(XWPFTable table) {
String tableText = getTableFullText(table);
Matcher markMatcher = TABLE_MARK_PATTERN.matcher(tableText);
if (markMatcher.find()) {
String tableMark = markMatcher.group(1).trim();
System.out.println("【表格标记提取】找到表格标记:" + tableMark);
return tableMark;
}
return null;
}
java
/**
* 查找表格模板行(包含子字段占位符的行)
*
* @param table 表格对象
* @param subFieldKeys 数组子字段key集合
* @return 模板行,无则返回null
*/
private static XWPFTableRow findTableTemplateRow(XWPFTable table, Set<String> subFieldKeys) {
for (XWPFTableRow row : table.getRows()) {
String rowText = getTableRowText(row);
// 排除标记行,只找包含子占位符的行
if (rowText.contains("${") && !TABLE_MARK_PATTERN.matcher(rowText).find()) {
for (String subKey : subFieldKeys) {
String placeholder = "${" + subKey + "}";
if (rowText.contains(placeholder)) {
System.out.println("【模板行查找】找到模板行,包含占位符:" + placeholder);
return row;
}
}
}
}
// 兜底:取表格第二行(表头+数据行结构)或第一行
List<XWPFTableRow> rows = table.getRows();
if (rows.size() >= 2) {
System.out.println("【模板行查找】未匹配到占位符,取表格第二行作为模板行");
return rows.get(1);
} else if (!rows.isEmpty()) {
System.out.println("【模板行查找】未匹配到占位符,取表格第一行作为模板行");
return rows.get(0);
}
return null;
}
java
/**
* 生成表格数据行
*/
private static void generateTableDataRows(XWPFTable table, XWPFTableRow templateRow, List<Map<String, Object>> arrayData, String tableMark) {
int templateRowIndex = table.getRows().indexOf(templateRow);
Set<String> templateKeys = extractTemplatePlaceholderKeys(templateRow);
System.out.println("【数据行生成】模板行索引:" + templateRowIndex + ",需生成数据行数:" + arrayData.size());
System.out.println("【模板行处理】已删除原始模板行,避免占位符残留");
// 1. 遍历生成新行
for (int i = arrayData.size() - 1; i >= 0; i--) {
Map<String, Object> rowData = arrayData.get(i);
Map<String, Object> filteredRowData = new HashMap<>();
for (String key : templateKeys) {
filteredRowData.put(key, rowData.getOrDefault(key, ""));
}
System.out.println("【数据行填充】第" + (i + 1) + "行过滤后数据:" + filteredRowData);
// 2. 创建空行+空单元格(优化版:按列拷贝对应单元格的边框)
XWPFTableRow newRow = table.insertNewTableRow(templateRowIndex + 1);
int templateCellCount = templateRow.getTableCells().size();
for (int c = 0; c < templateCellCount; c++) {
//新格子
XWPFTableCell newCell = newRow.createCell();
//模板格子(利用其格式样式)
XWPFTableCell templateCell = templateRow.getCell(c);
if (templateCell != null) {
// 1. 拷贝模板单元格的边框样式(核心:继承原边框)
CTTc templateCttc = templateCell.getCTTc();
CTTcPr templateTcPr = templateCttc.getTcPr() == null ? templateCttc.addNewTcPr() : templateCttc.getTcPr();
CTTcBorders templateBdr = templateTcPr.getTcBorders() == null ? templateTcPr.addNewTcBorders() : templateTcPr.getTcBorders();
// 2. 给新单元格设置相同边框
CTTc newCttc = newCell.getCTTc();
CTTcPr newTcPr = newCttc.getTcPr() == null ? newCttc.addNewTcPr() : newCttc.getTcPr();
// 深拷贝边框,避免引用共享
newTcPr.setTcBorders((CTTcBorders) templateBdr.copy());
// 3. 额外:拷贝模板单元格的列宽(避免宽度为0)
if (templateCell.getWidth() != 0) {
newCell.setWidth(String.valueOf(templateCell.getWidth()));
}
// 4. 拷贝垂直对齐样式
newCell.setVerticalAlignment(templateCell.getVerticalAlignment());
}
}
System.out.println("【新行创建】第" + (i + 1) + "行已创建" + templateCellCount + "个空单元格");
// 3. 兼容版强制填充
forceFillNewRow(newRow, filteredRowData, templateRow, templateKeys);
}
// 4. 删除模板行
table.removeRow(templateRowIndex);
// 5. 清除表格标记
clearTableMark(table, tableMark);
}
java
/**
* 强制填充新行(动态适配模板key顺序,不再固定参数)
*
* @param newRow 新行
* @param rowData 行数据
* @param templateRow 模板行(用于动态提取key顺序)
* @param templateKeys 模板行提取的key集合
*/
private static void forceFillNewRow(XWPFTableRow newRow,
Map<String, Object> rowData,
XWPFTableRow templateRow, // 新增:传入模板行
Set<String> templateKeys) {
// ========== 动态提取模板单元格的key顺序 ==========
List<String> orderedKeys = extractDynamicOrderedKeys(templateRow, templateKeys);
System.out.println("【强制填充】动态提取模板key顺序:" + orderedKeys);
List<XWPFTableCell> cells = newRow.getTableCells();
System.out.println("【强制填充】准备填充" + cells.size() + "个单元格,模板key顺序:" + orderedKeys);
for (int idx = 0; idx < orderedKeys.size(); idx++) {
if (idx >= cells.size()) {
break;
}
XWPFTableCell cell = cells.get(idx);
String key = orderedKeys.get(idx);
Object valueObj = rowData.get(key);
String value = valueObj != null ? valueObj.toString() : "";
// 日期格式转换
value = convertDateString(value);
System.out.println("【单元格填充】第" + (idx + 1) + "列 → key:" + key + " → 值:" + value);
// 保留原有清空段落逻辑(POI 3.17兼容)
List<XWPFParagraph> paras = cell.getParagraphs();
for (int p = paras.size() - 1; p >= 0; p--) {
cell.removeParagraph(p);
}
// 重新创建段落并写入值(保留样式)
XWPFParagraph newPara = cell.addParagraph();
XWPFRun newRun = newPara.createRun();
newRun.setText(value, 0);
// 格式设置(兼容所有版本)
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
newPara.setAlignment(ParagraphAlignment.CENTER);
}
}
java
/**
* 从模板行中动态提取单元格对应的key顺序(核心:按模板单元格实际顺序)
*
* @param templateRow 模板行
* @param templateKeys 模板行提取的key集合
* @return 按单元格顺序排列的key列表
*/
private static List<String> extractDynamicOrderedKeys(XWPFTableRow templateRow, Set<String> templateKeys) {
List<String> orderedKeys = new ArrayList<>();
// 遍历模板行的每个单元格,按顺序提取占位符key
for (XWPFTableCell cell : templateRow.getTableCells()) {
StringBuilder cellText = new StringBuilder();
// 拼接单元格内所有文本(解决占位符拆分到多个Run的问题)
for (XWPFParagraph para : cell.getParagraphs()) {
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
cellText.append(text);
}
}
}
// 匹配单元格内的占位符${xxx},提取key
Matcher matcher = PLACEHOLDER_PATTERN.matcher(cellText.toString());
if (matcher.find()) {
String key = matcher.group(1).trim();
// 仅保留模板key集合中存在的key(过滤无效占位符)
if (templateKeys.contains(key)) {
orderedKeys.add(key);
}
}
}
// 兜底:若动态提取失败,保留原固定列表(兼容旧模板)
if (orderedKeys.isEmpty() && !templateKeys.isEmpty()) {
orderedKeys.addAll(templateKeys);
System.out.println("【动态提取key失败】兜底使用模板key集合:" + orderedKeys);
}
return orderedKeys;
}
java
/**
* 清除表格标记
*/
private static void clearTableMark(XWPFTable table, String mark) {
String markText = "${table:" + mark + "}";
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
List<XWPFParagraph> paras = cell.getParagraphs();
for (XWPFParagraph para : paras) {
// 读取段落文本
StringBuilder sb = new StringBuilder();
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
sb.append(text);
}
}
String cellText = sb.toString();
String newText = cellText.replace(markText, "");
// 清空Run(按索引删除)
List<XWPFRun> runs = para.getRuns();
for (int r = runs.size() - 1; r >= 0; r--) {
para.removeRun(r);
}
// 重新写入文本
XWPFRun newRun = para.createRun();
newRun.setText(newText, 0);
}
}
}
}
java
/**
* 清空表格行所有文本(保留格式结构)
*
* @param row 表格行
*/
private static void clearTableRowText(XWPFTableRow row) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph para : cell.getParagraphs()) {
for (XWPFRun run : para.getRuns()) {
run.setText("", 0);
}
}
}
}
java
/**
* 清理表格模板行和标记占位符
*
* @param table 表格对象
* @param templateRow 模板行
* @param tableMark 表格标记
*/
private static void cleanTableTemplate(XWPFTable table, XWPFTableRow templateRow, String tableMark) {
// 1. 删除模板行
table.removeRow(table.getRows().indexOf(templateRow));
// 2. 清除表格标记占位符(如${table:账单明细表})
clearTableMarkPlaceholder(table, tableMark);
System.out.println("【模板清理】已删除模板行,清除表格标记:" + tableMark);
}
java
/**
* 替换表格行内占位符(核心修复:确保占位符完整替换)
*
* @param row 表格行
* @param rowData 行数据
*/
private static void replaceTableRowPlaceholders(XWPFTableRow row, Map<String, Object> rowData) {
// 打印当前行要填充的所有数据key(确认数据正确)
System.out.println("【当前行数据key】:" + rowData.keySet());
for (int cellIdx = 0; cellIdx < row.getTableCells().size(); cellIdx++) {
XWPFTableCell cell = row.getTableCells().get(cellIdx);
// 遍历单元格内所有段落(单单元格通常只有1个段落)
for (XWPFParagraph para : cell.getParagraphs()) {
// 1. 拼接单元格内所有Run的文本(含隐藏字符)
StringBuilder cellText = new StringBuilder();
List<XWPFRun> runs = para.getRuns();
for (XWPFRun run : runs) {
String t = run.getText(0);
cellText.append(t == null ? "" : t);
}
String originalCellText = cellText.toString().trim();
// 2. 打印单元格原始文本(关键!看是否有隐藏字符)
System.out.println("【单元格" + (cellIdx + 1) + "】原始文本:[" + originalCellText + "]");
// 3. 打印单元格文本的字符编码(排查全角/隐藏字符)
if (!originalCellText.isEmpty()) {
System.out.println("【单元格" + (cellIdx + 1) + "】字符编码:");
for (char c : originalCellText.toCharArray()) {
System.out.print((int) c + " ");
}
System.out.println();
}
}
}
}
java
/**
* 通用占位符替换方法(核心)
*
* @param text 包含占位符的文本
* @param params 填充参数
* @return 替换后的文本
*/
private static String replacePlaceholders(String text, Map<String, Object> params) {
Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1).trim(); // 去除key前后空格(兼容模板手误)
if (key.startsWith("table:")) {
continue; // 跳过表格标记占位符(单独处理)
}
// 获取填充值(兼容空值)
Object valueObj = params.getOrDefault(key, "");
String replacement = valueObj != null ? valueObj.toString() : "";
// 日期格式转换(yyyy-MM-dd → yyyy年MM月dd日)
replacement = convertDateString(replacement);
// 转义特殊字符(避免正则替换异常)
replacement = Matcher.quoteReplacement(replacement);
System.out.println("【占位符替换】key:" + key + ",原值:" + matcher.group(0) + ",替换值:" + replacement);
// 替换占位符
matcher.appendReplacement(sb, replacement);
}
matcher.appendTail(sb);
return sb.toString();
}
java
/**
* 日期字符串格式转换
*
* @param input 原始日期字符串(yyyy-MM-dd / yyyy-MM-dd HH:mm:ss)
* @return 中文格式日期(yyyy年MM月dd日),非日期格式返回原字符串
*/
private static String convertDateString(String input) {
if (input == null || input.trim().isEmpty()) {
return input;
}
Matcher dateMatcher = DATE_PATTERN.matcher(input.trim());
if (!dateMatcher.matches()) {
return input; // 非日期格式,返回原字符串
}
try {
// 处理yyyy-MM-dd格式
if (input.trim().length() == 10) {
LocalDate date = LocalDate.parse(input.trim(), DATE_FORMATTER);
return date.format(TARGET_FORMATTER);
}
// 处理yyyy-MM-dd HH:mm:ss格式
else if (input.trim().length() == 19) {
LocalDateTime dateTime = LocalDateTime.parse(input.trim(), DATETIME_FORMATTER);
return dateTime.format(TARGET_FORMATTER);
}
} catch (DateTimeParseException e) {
System.out.println("【日期转换失败】输入:" + input + ",异常:" + e.getMessage());
}
return input;
}
java
// ------------------------ 以下为辅助方法 ------------------------
/**
* 获取表格完整文本(所有单元格+段落+Run)
*
* @param table 表格对象
* @return 表格文本
*/
private static String getTableFullText(XWPFTable table) {
StringBuilder sb = new StringBuilder();
for (XWPFTableRow row : table.getRows()) {
sb.append(getTableRowText(row));
}
return sb.toString();
}
java
/**
* 获取表格行完整文本
*
* @param row 表格行
* @return 行文本
*/
private static String getTableRowText(XWPFTableRow row) {
StringBuilder sb = new StringBuilder();
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph para : cell.getParagraphs()) {
for (XWPFRun run : para.getRuns()) {
String text = run.getText(0);
if (text != null) {
sb.append(text);
}
}
}
}
return sb.toString();
}
java
/**
* 清空表格模板行(数组为空时)
*
* @param table 表格对象
* @param tableMark 表格标记
*/
private static void clearTableTemplateRow(XWPFTable table, String tableMark) {
String tableText = getTableFullText(table);
for (XWPFTableRow row : table.getRows()) {
String rowText = getTableRowText(row);
// 清空包含子占位符的行(排除标记行)
if (rowText.contains("${") && !rowText.contains("${table:" + tableMark + "}")) {
clearTableRowText(row);
}
}
clearTableMarkPlaceholder(table, tableMark);
System.out.println("【模板行清空】数组为空,已清空表格模板行:" + tableMark);
}
java
/**
* 清除表格标记占位符(如${table:账单明细表})
*
* @param table 表格对象
* @param tableMark 表格标记
*/
private static void clearTableMarkPlaceholder(XWPFTable table, String tableMark) {
String markPlaceholder = "${table:" + tableMark + "}";
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph para : cell.getParagraphs()) {
List<XWPFRun> runs = new ArrayList<>(para.getRuns());
for (XWPFRun run : runs) {
String text = run.getText(0);
if (text != null && text.contains(markPlaceholder)) {
String newText = text.replace(markPlaceholder, "");
run.setText(newText, 0);
}
}
}
}
}
}
java
/**
* 处理表格内普通占位符(非数组相关)
*
* @param table 表格对象
* @param params 填充参数
*/
private static void replaceTableNormal(XWPFTable table, Map<String, Object> params) {
table.getRows().forEach(row -> {
row.getTableCells().forEach(cell -> {
cell.getParagraphs().forEach(paragraph -> {
List<XWPFRun> runs = new ArrayList<>(paragraph.getRuns());
Set<XWPFRun> processedRuns = new HashSet<>();
for (int i = 0; i < runs.size(); i++) {
XWPFRun currentRun = runs.get(i);
if (processedRuns.contains(currentRun)) {
continue;
}
String currentText = currentRun.getText(0);
if (currentText == null || currentText.isEmpty()) {
continue;
}
// 拼接跨Run占位符
String fullPlaceholder = null;
int endRunIndex = i;
if (currentText.startsWith("${")) {
fullPlaceholder = currentText;
for (int j = i + 1; j < runs.size(); j++) {
XWPFRun nextRun = runs.get(j);
String nextText = nextRun.getText(0);
if (nextText == null) {
break;
}
fullPlaceholder += nextText;
endRunIndex = j;
if (nextText.contains("}")) {
break;
}
}
} else if (currentText.contains("}") && i > 0) {
fullPlaceholder = currentText;
for (int j = i - 1; j >= 0; j--) {
XWPFRun prevRun = runs.get(j);
String prevText = prevRun.getText(0);
if (prevText == null) {
break;
}
fullPlaceholder = prevText + fullPlaceholder;
endRunIndex = j;
if (prevText.startsWith("${")) {
break;
}
}
}
// 替换完整占位符
if (fullPlaceholder != null && fullPlaceholder.contains("${") && fullPlaceholder.contains("}")) {
String replacedFullText = replacePlaceholders(fullPlaceholder, params);
XWPFRun firstRun = runs.get(i);
firstRun.setText(replacedFullText, 0);
processedRuns.add(firstRun);
for (int j = i + 1; j <= endRunIndex; j++) {
XWPFRun run = runs.get(j);
run.setText("", 0);
processedRuns.add(run);
}
i = endRunIndex;
continue;
}
// 单个Run替换
String replacedText = replacePlaceholders(currentText, params);
currentRun.setText(replacedText, 0);
processedRuns.add(currentRun);
}
});
});
});
}
java
/**
* 处理.doc格式文档(兼容逻辑,功能较弱)
*
* @param templatePath 模板路径
* @param params 填充参数
* @return 填充后的字节流
* @throws Exception IO/POI操作异常
*/
private static ByteArrayOutputStream generateDocStream(String templatePath, Map<String, Object> params) throws Exception {
InputStream in = new FileInputStream(templatePath);
HWPFDocument doc = new HWPFDocument(in);
// 简单文本替换(.doc格式不支持复杂表格数组处理)
Range range = doc.getRange();
String originalText = range.text();
String replacedText = replacePlaceholders(originalText, params);
range.replaceText(originalText, replacedText);
ByteArrayOutputStream out = new ByteArrayOutputStream();
doc.write(out);
doc.close();
in.close();
return out;
}
java
public static void main(String[] args) {
// 1. 测试JSON数据
String dataStr = "{\n" +
" \"key1\": \"value1\",\n" +
" \"key2\": \"value2\",\n" +
" \"key3\": [\n" +
" {\"k3_1\": \"v3_1_1\", \"k3_2\": \"v3_2_1\", \"k3_3\": \"v3_3_1\", \"k3_4\": \"v3_4_1\"},\n" +
" {\"k3_1\": \"v3_1_2\", \"k3_2\": \"v3_2_2\", \"k3_3\": \"v3_3_2\", \"k3_4\": \"v3_4_2\"},\n" +
" {\"k3_1\": \"v3_1_3\", \"k3_2\": \"v3_2_3\", \"k3_3\": \"v3_3_3\", \"k3_4\": \"v3_4_3\"}\n" +
" ]\n" +
"}";
// 2. 关键修复:JSON解析为带泛型的Map(避免List<Map>被识别为JSONArray)
Map<String, Object> params = JSON.parseObject(
dataStr,
new TypeReference<Map<String, Object>>() {
} // 泛型解析,保证类型正确
);
// 3. 模板路径和生成路径配置
String templatePath = "D:/temp/测试模版.docx";
String outputDir = "D:/temp/gen/";
String outputPath = outputDir + "生成的文档_" + System.currentTimeMillis() + ".docx";
try {
// 4. 校验模板文件是否存在
File templateFile = new File(templatePath);
if (!templateFile.exists()) {
System.err.println("模板文件不存在:" + templatePath);
return;
}
// 5. 校验输出目录,不存在则创建
File outDirFile = new File(outputDir);
if (!outDirFile.exists()) {
boolean mkdirSuccess = outDirFile.mkdirs();
if (!mkdirSuccess) {
System.err.println("输出目录创建失败:" + outputDir);
return;
}
}
// 6. 生成Word字节流(调用工具类核心方法)
ByteArrayOutputStream byteArrayOutputStream = generateWordStream(templatePath, params);
// 7. 将字节流写入文件(try-with-resources 自动关闭流)
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
byteArrayOutputStream.writeTo(fos);
System.out.println("Word文档生成成功!路径:" + outputPath);
}
// 8. 关闭字节流(兜底)
byteArrayOutputStream.close();
} catch (FileNotFoundException e) {
System.err.println("文件未找到:" + e.getMessage());
throw new RuntimeException("文件路径错误", e);
} catch (IOException e) {
System.err.println("IO异常:" + e.getMessage());
throw new RuntimeException("生成Word失败(IO错误)", e);
} catch (Exception e) {
System.err.println("未知异常:" + e.getMessage());
throw new RuntimeException("生成Word失败", e);
}
}
}
以上是一个完整的java文件内容,只是方便查看做了隔断。
大概就是这样,具体内容细节按需调整、比如居中、字体类型等。