【JAVA】实现word的DOCX/DOC文档内容替换、套打、支持表格内容替换。

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文件内容,只是方便查看做了隔断。

大概就是这样,具体内容细节按需调整、比如居中、字体类型等。

相关推荐
贺今宵2 小时前
装Maven并在idea上配置
java·maven·intellij-idea
企微自动化2 小时前
企业微信外部群自动化系统的异常处理机制设计
开发语言·python
墨&白.2 小时前
如何卸载/更新Mac上的R版本
开发语言·macos·r语言
qq_12498707532 小时前
基于springboot的幼儿园家校联动小程序的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序
技术小甜甜2 小时前
[Python] 使用 Tesseract 实现 OCR 文字识别全流程指南
开发语言·python·ocr·实用工具
leo__5202 小时前
MATLAB 实现 基分类器为决策树的 AdaBoost
开发语言·决策树·matlab
Alsn862 小时前
27.IDEA 专业版创建与打包 Java 命令行程序
java·ide·intellij-idea
老朱佩琪!2 小时前
Unity原型模式
开发语言·经验分享·unity·设计模式·原型模式
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于JAVA的车辆违章信息管理系统设计及实现为例,包含答辩的问题和答案
java·开发语言