JRE 7 环境下的 docx 文档模板方案

17 阅读6分钟

摘要

在比较老的生产环境中,实现基于文档模板的生成 docx 类型文档的功能。

前言

开发工具:idea

jdk版本:1.7

poi版本:3.17

poi-tl版本:1.5.1

研发背景:用户提出要求,在老项目的导出doc文档功能基础上,进行优化,实现能够导出 docx 文档的功能。在尝试了很多种方案后,个人觉得 poi-tl 加 poi 混合方案,更加符合需求,且工作量相对较少些。

注:ai 工具提供了很大帮助

正文

一、方案比较和研究

(一)docx文档类型研究

docx 文档是一个压缩包,里面的document.xml是文档内容,其他文件是关联关系,样式等文件。

(二)先列出同事和我想到的所有方案,包括以下几种:

1、用freemarker生成xml文件,再压缩成docx(压缩这一步很麻烦,需要自己生成相关xml文件)

2、仍然使用系统生成doc,再通过poi等其他第三方库把doc转成docx文件中(用wps效果更好,但是需要联网,且有费用)

3、本地下载LibreOffice,java通过命令行调用LibreOffice进行文件转换(已实践,转出来的文件打开是xml代码,不能正常显示)

4、poi-tl 加 poi 混合方式(基本能够满足用户需求,可以通过配合公式转换ooxml功能,实现插入公式的功能)

二、poi-tl 加 poi 混合方式实现

(一)poi-tl 渲染docx类型文档模板

参考poi-tl的官方文档 deepoove.com/poi-tl/1.5.…

这个版本是java7的最高版本,可以满足基本的模板功能,但是不支持公式。

(二)LaTeX公式转换成ooxml

借助第三方工具实现,需要自己编写一部分代码.

(三)poi 插入公式到指定位置

可以先用poi-tl在渲染模板时,插入特定字符串到指定位置,再用poi操作docx文件在指定位置替换为公式ooxml代码。

后记

过程比较费时,尝试了各种方案后,发现均不理想。在ai帮助下,才找到一个比较符合需求的方案。成果比较简单,过程比较折磨。

最后,同事尝试使用了一个付费工具aspose,可以直接实现doc转docx,如果想要省事且能够承担费用的话,可以直接使用 aspose。

aspose官网:用于处理 Word 文档格式的专业本地和基于云的解决方案

附部分代码


    /**
     * 合同文档生成示例
     */
    public static void generateContractDocument() {
        try {
            // 准备合同数据
            ContractData contractData = new ContractData();
            
            // 合同基本信息
            contractData.setContractNo("HT20240120001");
            contractData.setContractName("软件开发服务合同");
            contractData.setPartyA("某某科技有限公司");
            contractData.setPartyB("某某客户公司");
            contractData.setSignDate( dateFormat.format(new Date()));
            contractData.setEffectiveDate("2024年01月25日");
            contractData.setExpireDate("2025年01月24日");
            
            // 项目信息
            contractData.setProjectName("OA办公系统开发项目");
            contractData.setProjectDuration("12个月");
            contractData.setTotalAmount(150000.00);
            contractData.setAmountInWords("人民币壹拾伍万元整");
            contractData.setDeposit(30000.00);
            
            // 条件数据
            contractData.setIncludeWarranty(true);
            contractData.setWarrantyPeriod("12个月");
            contractData.setIncludeTraining(true);
            contractData.setTrainingHours(16);
            
            // 循环数据 - 付款计划
//            List<PaymentItem> paymentItems = new ArrayList<PaymentItem>();
//            paymentItems.add(new PaymentItem("合同签订后3个工作日内", 30000.00, 20, "首付款"));
//            paymentItems.add(new PaymentItem("项目中期验收后", 75000.00, 50, "中期款"));
//            paymentItems.add(new PaymentItem("项目最终验收后", 45000.00, 30, "尾款"));
//            contractData.put("paymentItems", paymentItems);

            RowRenderData header = RowRenderData.build(new TextRenderData("FFFFFF", "姓名"), new TextRenderData("FFFFFF", "学历"));

            RowRenderData row0 = RowRenderData.build("张三", "研究生");
            RowRenderData row1 = RowRenderData.build("李四", "博士");
            RowRenderData row2 = RowRenderData.build("王五", "博士后");
            RowRenderData row3 = RowRenderData.build(new TextRenderData("000000", "Sayi卅一"), new TextRenderData("000000", "第一行内容\n第二行内容"));
            contractData.setPaymentItems(new MiniTableRenderData(header, Arrays.asList(row0, row1, row2, row3)));
            
            // 循环数据 - 服务内容
            contractData.setServices(new NumbericRenderData(new ArrayList<TextRenderData>() {
                {
                    add(new TextRenderData("系统需求分析与设计"));
                    add(new TextRenderData("核心功能模块开发"));
                    add(new TextRenderData("系统测试与调试"));
                    add(new TextRenderData("部署与实施"));
                    add(new TextRenderData("用户培训与技术支持"));
                }
            }));
            
            // 加载合同模板
            String templatePath = "/contract_template.docx";
            Configure.ConfigureBuilder builder = Configure.newBuilder();
            builder.setElMode( ELMode.SPEL_MODE);
            XWPFTemplate template = XWPFTemplate.compile(templatePath, builder.build()).render(contractData);
            
            // 输出合同
            String outputPath = "output/软件开发合同_" + contractData.getContractNo() + ".docx";
            FileOutputStream out = null;
            try {
                out = new FileOutputStream(outputPath);
                template.write(out);
                logger.info("合同生成成功: {}", outputPath);
            } finally {
                if (out != null) out.close();
                if (template != null) template.close();
            }
            
        } catch (Exception e) {
            logger.error("生成合同失败", e);
        }
    }

/**
 * 插入公式示例
 */
public static void main( String[] args ) {
    try {
        String filePath = inputDocx;
        String bookmarkName = "aaaaa";  // 修改为你的书签名

        // 1. 查找书签 - 表格
        BookmarkLocation location = findBookmarkInTables( filePath, bookmarkName );

        if ( location != null ) {
            System.out.println( "找到书签: " + location );

            // 2. 在书签位置插入
//                String ooxml = "<w:r><w:t>插入到表格书签位置</w:t></w:r>";
            String ooxml = "<?xml version="1.0" encoding="UTF-8"?>\n" +
                    "<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" " +
                    "xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">" +
                    "<m:oMathPara><m:oMathParaPr><m:jc m:val="left"/></m:oMathParaPr><m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mml="http://www.w3.org/1998/Math/MathML"><m:rad><m:radPr><m:degHide m:val="on"/></m:radPr><m:deg/><m:e><m:r><m:t>2</m:t></m:r></m:e></m:rad></m:oMath></m:oMathPara>" +
                    "</w:p>";

            insertAtBookmarkLocation( filePath, output, ooxml, bookmarkName );
            System.out.println( "插入完成" );
        } else {
            System.out.println( "未找到书签: " + bookmarkName );
        }


        bookmarkName = "bbbbb";  // 修改为你的书签名

        // 1. 查找书签 - 段落
        location = findBookmarkInTables( filePath, bookmarkName );

        if ( location != null ) {
            System.out.println( "找到书签: " + location );

            // 2. 在书签位置插入
//                String ooxml = "<w:r><w:t>插入到表格书签位置</w:t></w:r>";
            String ooxml = "<?xml version="1.0" encoding="UTF-8"?>\n" +
                    "<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" " +
                    "xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">" +
                    "<m:oMathPara><m:oMathParaPr><m:jc m:val="left"/></m:oMathParaPr><m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:mml="http://www.w3.org/1998/Math/MathML"><m:rad><m:radPr><m:degHide m:val="on"/></m:radPr><m:deg/><m:e><m:r><m:t>2</m:t></m:r></m:e></m:rad></m:oMath></m:oMathPara>" +
                    "</w:p>";

            insertAtBookmarkLocation( output, output, ooxml, bookmarkName );
            System.out.println( "插入完成" );
        } else {
            System.out.println( "未找到书签: " + bookmarkName );
        }

    } catch ( Exception e ) {
        e.printStackTrace();
    }
}
/**
 * 在书签位置插入(支持表格)
 */
public static void insertAtBookmarkLocation( String inputPath, String outputPath,
                                             String ooxmlContent, String bookmarkName ) throws Exception {

    BookmarkLocation location = findBookmarkInTables( inputPath, bookmarkName );
    if ( location == null ) {
        throw new RuntimeException( "未找到书签: " + bookmarkName );
    }

    try ( FileInputStream fis = new FileInputStream( inputPath );
          XWPFDocument document = new XWPFDocument( fis ) ) {

        if ( location.type == BookmarkType.PARAGRAPH ) {
            // 在段落中书签后插入
            insertAfterParagraph( document, location.paragraphIndex, ooxmlContent );
        } else {
            // 在表格单元格中书签后插入
            insertInTableCell( document, location, ooxmlContent );
        }

        // 保存
        try ( FileOutputStream fos = new FileOutputStream( outputPath ) ) {
            document.write( fos );
        }
    }
}

/**
 * 在段落后插入
 */
private static void insertAfterParagraph( XWPFDocument document, int paragraphIndex, String ooxmlContent ) throws IOException, ParserConfigurationException, SAXException {

    // 3. 修复OOXML
    String fixedOoxml = fixOoxml( ooxmlContent );

    // 4. 解析OOXML
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware( true );
    DocumentBuilder builder = factory.newDocumentBuilder();
    org.w3c.dom.Document xmlDoc = builder.parse(
            new ByteArrayInputStream( fixedOoxml.getBytes( "UTF-8" ) ) );

    CTBody body = document.getDocument().getBody();
    Node bodyNode = body.getDomNode();

    org.w3c.dom.Element ooxmlElement = xmlDoc.getDocumentElement();
    Node importedNode = bodyNode.getOwnerDocument().importNode( ooxmlElement, true );

    // 5. 找到书签所在的段落节点
    List<XWPFParagraph> paragraphs = document.getParagraphs();
    if ( paragraphIndex >= 0 && paragraphIndex < paragraphs.size() ) {
        CTP bookmarkCTP = paragraphs.get( paragraphIndex ).getCTP();
        Node bookmarkNode = bookmarkCTP.getDomNode();

        // 6. 在书签段落后插入
        if ( bookmarkNode.getNextSibling() != null ) {
            bodyNode.insertBefore( importedNode, bookmarkNode.getNextSibling() );
        } else {
            // 插入到末尾
            bodyNode.appendChild( importedNode );
        }
    } else {
        // 插入到文档开头
        if ( bodyNode.getFirstChild() != null ) {
            bodyNode.insertBefore( importedNode, bodyNode.getFirstChild() );
        } else {
            bodyNode.appendChild( importedNode );
        }
    }
}

    /**
     * 在表格单元格中插入
     */
    private static void insertInTableCell( XWPFDocument document, BookmarkLocation location, String ooxmlContent ) throws ParserConfigurationException, IOException, SAXException {
        // 获取表格
        XWPFTable table = document.getTables().get( location.tableIndex );
        XWPFTableRow row = table.getRows().get( location.rowIndex );
        XWPFTableCell cell = row.getTableCells().get( location.cellIndex );

        // 获取单元格中的段落列表
        List<XWPFParagraph> cellParagraphs = cell.getParagraphs();

        // 3. 修复OOXML
        String fixedOoxml = fixOoxml( ooxmlContent );

        // 4. 解析OOXML
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware( true );
        DocumentBuilder builder = factory.newDocumentBuilder();
        org.w3c.dom.Document xmlDoc = builder.parse(
                new ByteArrayInputStream( fixedOoxml.getBytes( "UTF-8" ) ) );

        CTBody body = document.getDocument().getBody();
        Node bodyNode = body.getDomNode();

        org.w3c.dom.Element ooxmlElement = xmlDoc.getDocumentElement();
        Node importedNode = bodyNode.getOwnerDocument().importNode( ooxmlElement, true );

        if ( location.cellParagraphIndex >= 0 && location.cellParagraphIndex < cellParagraphs.size() ) {
            CTP bookmarkCTP = cellParagraphs.get( location.cellParagraphIndex ).getCTP();
            Node bookmarkNode = bookmarkCTP.getDomNode();

            // 6. 在书签段落后插入
            if ( bookmarkNode.getNextSibling() != null ) {
                bodyNode.insertBefore( importedNode, bookmarkNode.getNextSibling() );
            } else {
                // 插入到末尾
//                bodyNode.appendChild( importedNode );
                bookmarkNode.getParentNode().appendChild( importedNode );
            }
        } else {
            // 插入到文档开头
            if ( bodyNode.getFirstChild() != null ) {
                bodyNode.insertBefore( importedNode, bodyNode.getFirstChild() );
            } else {
                bodyNode.appendChild( importedNode );
            }
        }

    }

private static String fixOoxml( String ooxml ) {
    if ( !ooxml.contains( "xmlns:w=" ) ) {
        ooxml = ooxml.replaceFirst( "<([^>]+)>",
                "<$1 xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">" );
    }
    return ooxml;
}

/**
 * 简单查找表格中的书签
 */
public static BookmarkLocation findBookmarkInTables( String filePath, String bookmarkName ) throws Exception {
    try ( FileInputStream fis = new FileInputStream( filePath );
          XWPFDocument document = new XWPFDocument( fis ) ) {

        // 1. 先检查普通段落
        List<XWPFParagraph> paragraphs = document.getParagraphs();
        for ( int i = 0; i < paragraphs.size(); i++ ) {
            if ( hasBookmark( paragraphs.get( i ), bookmarkName ) ) {
                return new BookmarkLocation( BookmarkType.PARAGRAPH, i, -1, -1, -1, -1 );
            }
        }

        // 2. 检查表格
        List<XWPFTable> tables = document.getTables();
        for ( int tableIndex = 0; tableIndex < tables.size(); tableIndex++ ) {
            XWPFTable table = tables.get( tableIndex );

            List<XWPFTableRow> rows = table.getRows();
            for ( int rowIndex = 0; rowIndex < rows.size(); rowIndex++ ) {
                XWPFTableRow row = rows.get( rowIndex );

                List<XWPFTableCell> cells = row.getTableCells();
                for ( int cellIndex = 0; cellIndex < cells.size(); cellIndex++ ) {
                    XWPFTableCell cell = cells.get( cellIndex );

                    List<XWPFParagraph> cellParagraphs = cell.getParagraphs();
                    for ( int paraIndex = 0; paraIndex < cellParagraphs.size(); paraIndex++ ) {
                        if ( hasBookmark( cellParagraphs.get( paraIndex ), bookmarkName ) ) {
                            return new BookmarkLocation( BookmarkType.TABLE, -1,
                                    tableIndex, rowIndex, cellIndex, paraIndex );
                        }
                    }
                }
            }
        }
    }

    return null;
}

private static boolean hasBookmark( XWPFParagraph paragraph, String bookmarkName ) {
    for ( CTBookmark bookmark : paragraph.getCTP().getBookmarkStartList() ) {
        if ( bookmarkName.equals( bookmark.getName() ) ) {
            return true;
        }
    }
    return false;
}

/**
 * 书签位置信息
 */
public static class BookmarkLocation {
    public BookmarkType type;
    public int paragraphIndex;  // 用于段落
    public int tableIndex;      // 用于表格
    public int rowIndex;
    public int cellIndex;
    public int cellParagraphIndex;

    public BookmarkLocation( BookmarkType type, int paragraphIndex,
                             int tableIndex, int rowIndex, int cellIndex, int cellParagraphIndex ) {
        this.type = type;
        this.paragraphIndex = paragraphIndex;
        this.tableIndex = tableIndex;
        this.rowIndex = rowIndex;
        this.cellIndex = cellIndex;
        this.cellParagraphIndex = cellParagraphIndex;
    }

    @Override
    public String toString() {
        if ( type == BookmarkType.PARAGRAPH ) {
            return String.format( "书签在段落[%d]", paragraphIndex );
        } else {
            return String.format( "书签在表格[%d] 行[%d] 列[%d] 段落[%d]",
                    tableIndex, rowIndex, cellIndex, cellParagraphIndex );
        }
    }
}

public enum BookmarkType {
    PARAGRAPH, TABLE
}
/**
 * 转换LaTeX为MathML - 简化版本(改为静态方法)
 */
private static String convertLatexToMathML(String latex, boolean inline) throws Exception {
    String cacheKey = latex + "_" + inline;
    if (latexCache.containsKey(cacheKey)) {
        return latexCache.get(cacheKey);
    }
    // 处理度数、分、秒符号
    latex = latex
            .replace("°", "^{\circ}");

    for (Map.Entry<String, String> entry : specialCharsMap.entrySet()) {
        latex = latex.replace(entry.getKey(), entry.getValue());
    }

    SnuggleSession session = engine.createSession();

    // 直接使用前端格式,不进行任何修复
    String wrappedLatex = inline ? "$" + latex + "$" : "\[" + latex + "\]";

    SnuggleInput input = new SnuggleInput(wrappedLatex);
    session.parseInput(input);

    if (!session.getErrors().isEmpty()) {
        throw new RuntimeException("LaTeX解析错误:" + session.getErrors());
    }

    String mathML = session.buildXMLString();
    if (ValidateUtil.isNotEmpty(mathML)) {
        for (Map.Entry<String, String> entry : specialCharsMap.entrySet()) {
            mathML = mathML.replace(entry.getValue(), entry.getKey());
        }
    }
    latexCache.put(cacheKey, mathML);
    return mathML;
}

docx模板

{{contractName}}

 

合同编号:{{contractNo}}

 

甲方(委托方):{{partyA}}

乙方(受托方):{{partyB}}

 

一、合同基本信息

  1. 项目名称:{{projectName}}

  2. 合同期限:{{projectDuration}}

  3. 签约日期:{{signDate}}

  4. 生效日期:{{effectiveDate}}

  5. 终止日期:{{expireDate}}

 

二、合同金额

  1. 合同总金额:{{totalAmount}}元(大写:{{amountInWords}})

  2. 订金:{{deposit}}元

 

三、服务内容

{{*services}}

 

四、付款计划

{{#paymentItems}}

 

五、其他条款

{{includeWarranty? ' 1. 质保期:'+ warrantyPeriod: ''}}

 

{{ includeTraining? '2. 培训服务:'+ trainingHours + ' 小时': ''}}

 

甲方(盖章):____________________

乙方(盖章):____________________

签约日期:{{signDate}}