11.java程序员必知必会类库之word处理库

前言

正常业务中,可能涉及到和合作方签约电子合同,此时,我们需要先设计合同模板,维护固定内容,将可变的内容通过占位符替代,等签章的时候,生成pdf,然后可以根据设计的合同章的坐标,调用签章系统盖电子章。
此时涉及三步:

  1. 读取合同word模板,替换占位符,比如签约人,合同金额,签约时间等
  2. 将word文档转换为pdf文件
  3. 上传pdf文件到电子签章系统签章

这时候就需要我们掌握java代码读写word文档的技能。

1. 前期准备与说明

1.1 doc文件后缀与docx文件后缀区别

  1. 运行环境不同,docx格式的文件是Office2007及以上版本保存的新型文档,而doc是Word2003以及之前版本保存的文档。
  2. 它们所占用的内存空间不同,docx更加节省空间
  3. 它们的响应速度有所不同,docx比doc的响应速度更加快捷,并且更加方便修改文件。
  4. docx格式的文件本质上是一个ZIP文件,是docx文件的容器。而doc则容纳文字格式、脚本语言及复原等资讯的文件。
  5. docx文件改后缀为zip文件,解压后可以看到如下所示:
    在这里插入图片描述
    其中word里面是很多xml文件在这里插入图片描述
    因为doc文件版本比较老,现在主流都用的是docx文件,下面示例统一以docx文件为演示格式

1.2 测试合同docx文件准备

假设合同模板内容如下,可变的信息我们通过${变量名}代替,后续真实业务中,基于不同的用户和合作方,替换文档内容
在这里插入图片描述
合同模板我上传到csdn里面了下载资源

2 POI 处理文档

2.1 介绍

POI可以灵活操作基于word文档。Apache POl中的HWPF具体提供了读写MicrosoftWord格式文件的功能。

优点:跨平台支持windows unix和linux
缺点:操作word的功能比较弱,必须针对doc和docx两种文档格式写不同的代码,兼容性差。相对与对word文件的处理来说,POI更适合excel处理,对于word实现一些简单文件的操作凑合,不能设置样式且生成的word文件格式不够规范

综合来说:但是如果只是基于合同模板生成word稳定,不手动拼接word,则POI就已经能满足我们的业务需求。具体选哪种方案,视具体业务而定

2.2 pom坐标引入

<!--解析docx文档的XWPFDocument对象在这个包里-->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.9</version>
</dependency>
<!--解析doc文档的HWPFDocument对象在这个包里,操作docx文件可不要这个包,这里只是一起记录一下-->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>3.9</version>
</dependency>
<!--lombok组件,可以不接入,这里方便测试-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

2.3 使用

2.3.1 常用api

XWPFDocument类(文档类):
xd.getAllPictures(); 返回图片列表
xd.getParagraphs(); 返回段落列表
xd.getTables(); 返回表格列表
xd.getFooterList(); 返回脚脚列表
xd.getFootnotes(); 返回脚注列表
xd.getHeaderList(); 返回页眉列表
 
XWPFParagraph类(段落类):
xwpfParagraph.getText(); 返回段落的文本内容,包括图片中的文本和其中的sdt元素。
xwpfParagraph.getParagraphText(); 返回段落的文本,但不返回段落中的任何对象
xwpfParagraph.getRuns()); 返回段落run的列表
 
XWPFTable类(表格类):
xwpfTable.getRows(); 返回行对象XWPFTableRow
 
XWPFTableRow类(表格行类):
xwpfTableRow.getTableCells(); 返回单元格对象XWPFTableCell
xwpfTableCell.getParagraphs(); 返回单元格里面的段落
xwpfTableCell.getTables(); 返回单元格里面的表格

2.3.2 测试案例

测试demo:

@Slf4j
public class DocXUtils {
 
    public static void main(String[] args) {
        String filePath = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板.docx";
        String suffix = filePath.substring(filePath.lastIndexOf("."));
        if(".docx".equals(suffix)){
            System.out.println("文件类型是.docx");
            dealDocXFile(filePath);
        }
    }
 
    public static void dealDocXFile(String filePath){
 
        InputStream input = null;
        try {
            //实例化解析docx文档的对象
            input = new FileInputStream(filePath);
            XWPFDocument xd = new XWPFDocument(input);
            log.info("开始解析文档中的所有图片----------------");
            //1   getAllPackagePictures()此包装中的所有图片
            List<XWPFPictureData> xwpfPictureDataList2 = xd.getAllPictures();
            if(CollUtil.isNotEmpty(xwpfPictureDataList2)){
                for (XWPFPictureData xwpfPictureData : xwpfPictureDataList2) {
                    //图像的文件名
                    System.out.println("图片名称:" + xwpfPictureData.getFileName());
                }
            }
            log.info("开始解析文档中的所有段落----------------");
            //文本的段落
            List<XWPFParagraph> xwpfParagraphList = xd.getParagraphs();
            dealParagraph(xwpfParagraphList);
            log.info("开始解析文档中的所有表格----------------");
            //文本的表格
            List<XWPFTable> xwpfTableList = xd.getTables();
            dealTable(xwpfTableList);
            log.info("开始解析文档中的页脚----------------");
            //页脚列表
            List <XWPFFooter> xwpfFooterList = xd.getFooterList();
            //System.out.println("页脚列表size:"+xwpfFooterList.size());
            if(CollUtil.isNotEmpty(xwpfFooterList)){
                for (XWPFFooter xwpfFooter : xwpfFooterList) {
                    //页脚的文本的段落
                    dealParagraph(xwpfFooter.getParagraphs());
                    dealTable(xwpfFooter.getTables());
                    System.out.println("getText:"+xwpfFooter.getText());
                }
            }
            log.info("开始解析文档中的脚注----------------");
            //脚注列表
            List <XWPFFootnote> xwpfFootnoteList = xd.getFootnotes();
            for(XWPFFootnote xwpfFootnote : xwpfFootnoteList){
                //处理脚注段落
                dealParagraph(xwpfFootnote.getParagraphs());
                dealTable(xwpfFootnote.getTables());
            }
            log.info("开始解析文档中的页眉----------------");
            //页眉列表
            List <XWPFHeader> xwpfHeaderList = xd.getHeaderList();
            if(CollUtil.isNotEmpty(xwpfHeaderList)){
                for(XWPFHeader xwpfHeader : xwpfHeaderList){
                    dealParagraph(xwpfHeader.getParagraphs());
                    dealTable(xwpfHeader.getTables());
                    System.out.println(xwpfHeader.getText());
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件没有找到");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("发生io异常");
        }
 
    }
 
    public static void dealParagraph(List<XWPFParagraph> xwpfParagraphListList){
        if(CollUtil.isNotEmpty(xwpfParagraphListList)){
            for(XWPFParagraph xwpfParagraph : xwpfParagraphListList){
                if(ObjectUtil.isNull(xwpfParagraph) || StrUtil.isEmpty(xwpfParagraph.getText())){
                    continue;
                }
                System.out.println("______________________________________________________________");
                //返回段落的文本内容,包括图片中的文本和其中的sdt元素。String
                System.out.println("getText~~~~~~~~~~~:"+xwpfParagraph.getText());
                //返回段落的文本,但不返回段落中的任何对象.String
                System.out.println("getParagraphText:~~"+xwpfParagraph.getParagraphText());
                System.out.println("getRuns~~~~~~~~~~~:"+xwpfParagraph.getRuns());
            }
        }
 
    }
 
    public static void dealTable(List<XWPFTable> xwpfTableList){
        if(CollUtil.isNotEmpty(xwpfTableList)){
            for(XWPFTable xwpfTable : xwpfTableList){
                //遍历行
                for(XWPFTableRow xwpfTableRow : xwpfTable.getRows()){
                    //遍历单元格
                    for(XWPFTableCell xwpfTableCell : xwpfTableRow.getTableCells()){
                        //处理段落
                        dealParagraph(xwpfTableCell.getParagraphs());
                        dealTable(xwpfTableCell.getTables());
                    }
                }
            }
        }
    }
}

代码输出结果:

文件类型是.docx
2023-04-25 17:01:49.282 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的所有图片----------------
2023-04-25 17:01:49.282 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的所有段落----------------
______________________________________________________________
getText~~~~~~~~~~~:编号:${CONTRACT_NO}
getParagraphText:~~编号:${CONTRACT_NO}
getRuns~~~~~~~~~~~:[编号:, ${CONTRACT_NO}]
______________________________________________________________
getText~~~~~~~~~~~:${PRODUCT_NAME}产品开通申请
getParagraphText:~~${PRODUCT_NAME}产品开通申请
getRuns~~~~~~~~~~~:[${PRODUCT_NAME}, 产品开通申请]
______________________________________________________________
getText~~~~~~~~~~~:特别提示:本申请项下的以下${PRODUCT_NAME}产品服务(以下简称“本服务”),是由XXX为YYY所提供的服务
getParagraphText:~~特别提示:本申请项下的以下${PRODUCT_NAME}产品服务(以下简称“本服务”),是由XXX为YYY所提供的服务
getRuns~~~~~~~~~~~:[特别提示:,, 申请, 项下的以下, ${PRODUCT_NAME}, 产品服务, (以下简称“本服务”), ,是,, XXX为YYY, 所提供, 的服务]
______________________________________________________________
getText~~~~~~~~~~~:甲方: 
getParagraphText:~~甲方: 
getRuns~~~~~~~~~~~:[甲方,]
______________________________________________________________
getText~~~~~~~~~~~:用户姓名:${PERSONAL_NAME}
getParagraphText:~~用户姓名:${PERSONAL_NAME}
getRuns~~~~~~~~~~~:[用户姓名:, ${PERSONAL_NAME}]
______________________________________________________________
getText~~~~~~~~~~~:证件类型:${PERSONAL_ID_TYPE}
getParagraphText:~~证件类型:${PERSONAL_ID_TYPE}
getRuns~~~~~~~~~~~:[证件类型:, ${PERSONAL_ID_TYPE}]
______________________________________________________________
getText~~~~~~~~~~~:证件号码:${PERSONAL_ID_NO}
getParagraphText:~~证件号码:${PERSONAL_ID_NO}
getRuns~~~~~~~~~~~:[证件号码:, ${PERSONAL_ID_NO}]
______________________________________________________________
getText~~~~~~~~~~~:乙方: 
getParagraphText:~~乙方: 
getRuns~~~~~~~~~~~:[乙方,]
______________________________________________________________
getText~~~~~~~~~~~:公司名称:${ENT_NAME}
getParagraphText:~~公司名称:${ENT_NAME}
getRuns~~~~~~~~~~~:[公司名称,, ${ENT_NAME}]
______________________________________________________________
getText~~~~~~~~~~~:统一社会信用代码:${ENT_ID_NO}
getParagraphText:~~统一社会信用代码:${ENT_ID_NO}
getRuns~~~~~~~~~~~:[统一社会信用代码,, ${ENT_ID_NO}]
______________________________________________________________
getText~~~~~~~~~~~:甲方:${SIGNER_JIAFANG}
getParagraphText:~~甲方:${SIGNER_JIAFANG}
getRuns~~~~~~~~~~~:[甲方,, ${SIGNER_JIAFANG}]
______________________________________________________________
getText~~~~~~~~~~~:乙方:${SIGNER_YIFANG}}
getParagraphText:~~乙方:${SIGNER_YIFANG}}
getRuns~~~~~~~~~~~:[乙方,, ${SIGNER_, YI, FANG, }, }]
2023-04-25 17:01:49.317 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的所有表格----------------
2023-04-25 17:01:49.317 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的页脚----------------
2023-04-25 17:01:49.317 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的脚注----------------
2023-04-25 17:01:49.317 [main] INFO  com.wanlong.word.DocXUtils - 开始解析文档中的页眉----------------

上面注意一个细节

  1. 合同模板甲乙方下面是留着签章的,但是为了签章能定位到具体的坐标位置,所以有两个隐藏的占位符
  2. 不同poi版本的jar包会有一些类不存在的问题,为了后面可以使用word转pdf,建议poi的版本不要太高

2.4 使用场景

2.4.1 按照模板替换报文

模板规则约定如下

  1. 占位符统一用${}包含,比如name,则文档占位符为 ${name}
  2. 签章位置加一个隐藏的占位符,肉眼看不到,但是实际模板有隐藏的字体,这里通过设置字体为白色隐藏,但是代码可以正常读取,主要用于后续签章好锁定坐标位置
  3. 签章占位符统一约定占位符为${sign}开头,比如甲方签章,则可以为 ${sign_jiafang}
2.4.1.1 校验占位符工具类
package com.wanlong.word;

import org.apache.poi.xwpf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * 校验word占位符
 */
public class CheckWordElementValue {

	private final static Logger logger = LoggerFactory.getLogger(CheckWordElementValue.class);

	public static List<String> getContractTemplateKey(InputStream is){

		logger.info("=== 校验word占位符--进入解析word文档方法 ===");

		List<String> list = new ArrayList<String>();
		try {
			logger.info("=== 校验word占位符--解析word文档 ===");
			XWPFDocument document = new XWPFDocument(is);
			//获取文本对象
			logger.info("=== 校验word占位符--获取文本对象 ===");
			List<XWPFParagraph> allParagraph = document.getParagraphs();

			logger.info("=== 校验word占位符--调用convertStr方法 ===");
			convertStr(allParagraph, list);
			logger.info("=== 校验word占位符--获取全部表格对象 ===");
			//获取全部表格对象
			List<XWPFTable> allTable = document.getTables();
			for (XWPFTable xwpfTable : allTable) {
				logger.info("=== 校验word占位符--获取表格行数据 ===");
				//获取表格行数据
				List<XWPFTableRow> rows = xwpfTable.getRows();
				for (XWPFTableRow xwpfTableRow : rows) {
					//获取表格单元格数据
					logger.info("=== 校验word占位符--获取表格单元格数据 ===");
					List<XWPFTableCell> cells = xwpfTableRow.getTableCells();
					for (XWPFTableCell xwpfTableCell : cells) {
						List<XWPFParagraph> paragraphs = xwpfTableCell.getParagraphs();
						convertStr(paragraphs, list);
					}
				}
			}
		} catch (IOException e) {
			logger.error("---=== 校验word是否还存在占位符时'文档解析异常' ===---");
		}
		return list;
	}

	public static void convertStr(List<XWPFParagraph> allParagraph, List<String> list) {
		for(XWPFParagraph xwpfParagraph : allParagraph) {
			String str = xwpfParagraph.getRuns().toString().replaceAll(", ", "");
			Matcher matcher = matcher(str);
			while(matcher.find()) {
				list.add(matcher.group(1).trim());
			}
		}
	}

	public static Matcher matcher(String str) {
		Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);
		Matcher matcher = pattern.matcher(str);
		return matcher;
	}
}
2.4.1.2. word文档替换占位符工具类
package com.wanlong.word;

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.POIXMLDocument;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 通过word模板生成新的word工具类
 */
public class WorderToNewWordUtils {

    private final static Logger logger = LoggerFactory.getLogger(WorderToNewWordUtils.class);

    public static void changeText(XWPFDocument document, Map<String, Object> textMap) {
        logger.info("=== 进入文本段落方法 === ");
        // 获取段落集合
        List<XWPFParagraph> paragraphs = document.getParagraphs();

        for (XWPFParagraph paragraph : paragraphs) {
            logger.info("=== 开始遍历获取paragraph === ");
            // 判断此段落时候需要进行替换
            String text = paragraph.getText();
            logger.info("=== 获取paragraphs文本段落内容 === : " + text);
            if (checkText(text)) {
                List<XWPFRun> runs = paragraph.getRuns();
                logger.info("=== 获取paragraph.getRuns()列表里面单个runs元素内容 === ");
                for (XWPFRun run : runs) {
                    logger.info("=== 遍历XWPFRun内容=== :" + run.toString());
                    if (checkUserCode(run.toString())) {
                        logger.info("=== 进入签署人列表占位符替换方法 === :" + run.toString());
                        if (!run.toString().equals("${SIGN_TIME}")) {
                            CTRPr rpr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr();
                            logger.info("=== 判断占位符内容是否有字体颜色 === :" + rpr.getColor());
                            if (rpr.getColor() != null) {
                                rpr.unsetColor();
                            }
                            run.setColor("FFFFFF");
                            run.setText(changeUserCodeValue(run.toString(), textMap), 0);
                        }
                    } else {
                        logger.info("=== 普通占位符替换方法 === :" + run.toString());
                        run.setText(changeValue(run.toString(), textMap), 0);
                    }
                }
            }
        }
    }


    public static void changeTable(XWPFDocument document, Map<String, Object> textMap) {
        logger.info("=== changeTable方法,判断table内容是替换还是动态生成 ===");
        List<XWPFTable> tables = document.getTables();
        for (int i = 0; i < tables.size(); i++) {
            XWPFTable table = tables.get(i);
            if (table.getRows().size() > 1) {
                logger.info("=== 校验table内容是否含有占位符标识===");
                if (checkText(table.getText())) {
                    List<XWPFTableRow> rows = table.getRows();
                    String text = table.getRow(1).getCell(0).getText();
                    logger.info("=== 获取table占位符标识=== :" + text);
                    if (checkText(text)) {
                        logger.info("=== table内容动态生成 ===");
                        insertTable(table, textMap);
                    } else {
                        logger.info("=== table内容替换 ===");
                        eachTable(rows, textMap);
                    }


                }
            }
            if (table.getRows().size() <= 1) {
                logger.info("=== table.getRows().size() <= 1 table内容替换 ===");
                if (checkText(table.getText())) {
                    List<XWPFTableRow> rows = table.getRows();
                    eachTable(rows, textMap);
                }
            }
        }
    }

    public static void insertTable(XWPFTable table, Map<String, Object> textMap) {
        logger.info("=== insertTable方法,创建table内容 ===");
        String value = null;
        String text = table.getRow(1).getCell(0).getText();
        logger.info("=== 获取动态生成table占位符 === :" + text);
        Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
        logger.info("=== 获取动态生成table占位符去匹配替换内容内容 ===");
        for (Map.Entry<String, Object> textSet : textSets) {
            String key = "${" + textSet.getKey() + "}";
            if (text.indexOf(key) != -1) {
                value = (String) textSet.getValue();
            }
        }
        if (StringUtils.isNotEmpty(value)) {
            table.getRow(1).getCell(0).removeParagraph(0);
            table.getRow(1).getCell(0).setText("");

            List<String[]> tableValue = getListValue(value);
            if (tableValue.size() >= 1) {
                for (int i = 1; i < tableValue.size(); i++) {
                    XWPFTableRow row = table.createRow();
                }
                List<XWPFTableRow> tableRow = table.getRows();
                logger.info("=== 校验动态生成table头的单元格格式是否与替换内容格式符合 ===");
                if (tableRow.get(0).getTableCells().size() == tableValue.get(0).length) {

                    for (int i = 1; i < tableRow.size(); i++) {

                        logger.info("=== 校验动态生成table单元格数据大小是否与替换内容数据大小符合 ===");
                        logger.info("=== for i  === :" + i);
                        logger.info("=== for tableValue.size()  === : " + tableValue.size());
                        if (i <= tableValue.size()) {

                            XWPFTableRow newRow = table.getRow(i);
                            List<XWPFTableCell> cells = newRow.getTableCells();
                            try {
                                for (int j = 0; j < cells.size(); j++) {
                                    logger.info("=== 遍历添加table单元格内容 ===");
                                    XWPFTableCell cell = cells.get(j);
                                    cell.setText(tableValue.get(i - 1)[j]);
                                    CTTc cttc = cell.getCTTc();
                                    CTP ctp = cttc.getPList().get(0);
                                    CTPPr ctppr = ctp.getPPr();
                                    if (ctppr == null) {
                                        ctppr = ctp.addNewPPr();
                                    }
                                    CTJc ctjc = ctppr.getJc();
                                    if (ctjc == null) {
                                        ctjc = ctppr.addNewJc();
                                    }
                                    ctjc.setVal(STJc.LEFT); //水平向左
                                }
                            } catch (Exception e) {
                                logger.error("---=== 动态插入table内容错误 ===---", e);
                                throw new RuntimeException(e);
                            }

                        }
                    }

                }
            }

        }
    }

    public static void eachTable(List<XWPFTableRow> rows, Map<String, Object> textMap) {
        logger.info("=== 进入表格替换方法 ===");
        for (XWPFTableRow row : rows) {
            List<XWPFTableCell> cells = row.getTableCells();
            for (XWPFTableCell cell : cells) {
                //判断单元格是否需要替换
                logger.info("=== 判断表格替换单元格是否需要替换 ===");
                logger.info("=== 表格替换内容 === : " + cell.getText());
                if (checkText(cell.getText())) {
                    List<XWPFParagraph> paragraphs = cell.getParagraphs();
                    for (XWPFParagraph paragraph : paragraphs) {
                        List<XWPFRun> runs = paragraph.getRuns();
                        for (XWPFRun run : runs) {
                            logger.info("=== 表格替换占位符 === :" + cell.getText());
                            run.setText(changeValue(run.toString(), textMap), 0);
                        }
                    }
                }
            }
        }
    }

    public static List<String[]> getListValue(String value) {
        logger.info("=== 进入getListValue方法,解析table内容 ===");
        List<String[]> listValue = new ArrayList<String[]>();
        try {
            if (StringUtils.isNotBlank(value)) {
                String[] splitValue = value.split("\\|");
                try {
                    for (int i = 0; i < splitValue.length; i++) {
                        String[] splitLaterValue = splitValue[i].split(",", -1);
                        listValue.add(splitLaterValue);
                    }
                } catch (Exception e) {
                    logger.error("---=== 获取table内容,截取内容时出现错误 ===---", e);
                    throw new RuntimeException(e);
                }
            }

        } catch (Exception e) {
            logger.error("---=== 获取table内容,截取内容时出现错误 ===---", e);
            throw new RuntimeException(e);
        }
        return listValue;
    }


    public static boolean checkText(String text) {
        boolean check = false;
        if (text.indexOf("$") != -1) {
            check = true;
        }
        return check;

    }


    public static String changeValue(String value, Map<String, Object> textMap) {
        try {
            Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
            for (Map.Entry<String, Object> textSet : textSets) {
                String key = "${" + textSet.getKey() + "}";
                if (value.indexOf(key) != -1) {
                    value = (String) textSet.getValue();
                }
            }
        } catch (Exception e) {
            logger.error("---=== 比较${}格式与替换内容是否匹配错误  ===---", e);
            throw new RuntimeException();
        }
        return value;
    }

    public static String changeUserCodeValue(String value, Map<String, Object> textMap) {

        try {
            Set<Map.Entry<String, Object>> textSets = textMap.entrySet();
            for (Map.Entry<String, Object> textSet : textSets) {
                // 匹配模板与替换值 格式#  预计
                String key = "${" + textSet.getKey() + "}";
                if (value.indexOf(key) != -1) {
                    value = (String) textSet.getValue();
                }
            }
        } catch (Exception e) {
            logger.error("---=== 比较${}格式与替换内容是否匹配错误 ===---", e);
            throw new RuntimeException(e);
        }
        return value;
    }

    public static boolean checkUserCode(String text) {
        boolean check = false;
        if (text.indexOf("${SIGNER_") != -1) {
            check = true;
        }
        return check;
    }

    public static boolean changWord(String inputUrl, String outputUrl,
                                    Map<String, Object> textMap, List<String[]> tableList) {

        //模板转换默认成功
        boolean changeFlag = true;
        try {
            //获取docx解析对象
            XWPFDocument document = new XWPFDocument(POIXMLDocument.openPackage(inputUrl));
            //解析替换文本段落对象
            WorderToNewWordUtils.changeText(document, textMap);
            //解析替换表格对象
            WorderToNewWordUtils.changeTable(document, textMap);

            //生成新的word
            File file = new File(outputUrl);
            FileOutputStream stream = new FileOutputStream(file);
            document.write(stream);
            stream.close();

        } catch (IOException e) {
            e.printStackTrace();
            changeFlag = false;
        }
        return changeFlag;
    }
}
2.4.1.3 word转pdf测试类
@Slf4j
public class TestWord {
    @Test
    public void testWord() throws Exception{

        String path="E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\";
        //维护待替换的参数和值
        Map<String,Object> map=new HashMap<>();
        map.put("PRODUCT_NAME","余额宝");
        map.put("PERSONAL_NAME","张三");
        map.put("PERSONAL_ID_TYPE","身份证");
        map.put("PERSONAL_ID_NO","620234199902122302X");
        map.put("CONTRACT_NO","hetong123456");
        map.put("ENT_NAME","支付宝测试有限公司");
        map.put("ENT_ID_NO","90232340341234");


        log.info("===  解析word文档 ===");
        XWPFDocument document = new XWPFDocument(new FileInputStream("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板.docx"));
        //解析替换文本对象
        log.info("===  解析替换文本对象 ===");
        WorderToNewWordUtils.changeText(document, map);
        //解析替换表格对象
        log.info("===  解析替换表格对象 ===");
        WorderToNewWordUtils.changeTable(document, map);

        log.info("===  生成word ===");
        InputStream foundWordInputStream = foundWord(document);
        log.info("===  校验生成的word是否含有占位符 ===");
        Boolean isContainPlaceholder = checkCreateWord(foundWordInputStream);

        if (isContainPlaceholder == true) {
            throw new Exception("文档没替换完毕");
        } else {
            log.info("===  调生成word文档方法 ===");
            document.write(new FileOutputStream(path + "result.docx"));
        }
    }
2.4.1.4. 生成替换后的合同如下

在这里插入图片描述

2.4.2 word转pdf

2.4.2.1 添加依赖
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.9</version>
</dependency>
<dependency>
    <groupId>fr.opensagres.xdocreport</groupId>
    <artifactId>org.apache.poi.xwpf.converter.pdf</artifactId>
    <version>1.0.4</version>
</dependency>
<!--        工具类用到apache的类,可以重新替换-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
2.4.2.2 测试类
@Test
public void testWordToPdf() throws Exception {
    String in = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\result.docx";
    FileInputStream inputStream = new FileInputStream(in);
    XWPFDocument document = new XWPFDocument(inputStream);

    String out = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\test.pdf";
    FileOutputStream outputStream = new FileOutputStream(out);
    PdfOptions pdfOptions = PdfOptions.create();
    PdfConverter.getInstance().convert(document, outputStream, pdfOptions);
}
2.4.2.3 转换结果

在这里插入图片描述

2.5 注意事项

  1. 在代码解析word文档的时候,可能会出现某些占位字符被分割解析,比如上面占位符的PERSONAL_ID_NO 变量,POI可能会切割为三个字符,PERSONAL ,ID,NO处理,此时替换的时候,会出现无法替换。此时建议调整模板的格式,多次尝试,直到能正常解析(可以尝试将完整字符放到记事本里,修改完粘贴过来)。因为模板调整完一般就不会变了,不建议代码做兼容处理
  2. POI的版本和POIConverter的POM版本会有关联关系,上面的版本号是经过测试的,版本不匹配会报错类不存在

3 POI-TL

3.1 介绍

poi-tl(poi template language)是Word模板引擎,使用Word模板和数据创建很棒的Word文档

3.2 pom坐标引入

<!--        word模板引擎-->
<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.8.2</version>
</dependency>

3.3 使用

3.3.1 合同模板

在这里插入图片描述

3.3.2 测试代码

package com.wanlong.word;

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.util.BytePictureUtils;
import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * @author wanlong
 * @version 1.0
 * @description:
 * @date 2023/4/25 10:11
 */
public class TestTLWord {

    @Test
    public void testCreate() throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("PRODUCT_NAME", "余额宝");
        map.put("PERSONAL_NAME", "张三");
        map.put("PERSONAL_ID_TYPE", "身份证");
        map.put("PERSONAL_ID_NO", "620234199902122302X");
        map.put("CONTRACT_NO", "hetong123456");
        map.put("ENT_NAME", "支付宝测试有限公司");
        map.put("ENT_ID_NO", "90232340341234");
        // 读取本地磁盘图片
        map.put("QIANZHANG_JIAFANG", new PictureRenderData(100, 100, "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\picture\\logo.jpeg"));
        map.put("QIANZHANG_YIFANG", new PictureRenderData(100, 100, "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\picture\\logo.jpeg"));

//        // 通过url读取网络图片
//        map.put("picture", new PictureRenderData(200, 400, ".png", BytePictureUtils.getUrlByteArray("https://res.wx.qq.com/a/wx_fed/weixin_portal/res/static/img/1EtCRvm.png")));

        File file = new File("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\合同文档模板2.docx");
        XWPFTemplate template = XWPFTemplate.compile(file).render(map);

        FileOutputStream out = new FileOutputStream(new File("E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\testtl.docx"));
        template.write(out);
        out.flush();
        out.close();
        template.close();
    }
}

3.3.3 运行结果

在这里插入图片描述

3.3.4 word转pdf

这里要留意,如果用了POI-TL的jar做模板替换,因为这个jar依赖了OOXML的高版本jar包,所以转换PDF不能用上面的converterjar包,否则两个jar包都对ooxml有依赖,但是版本不一样,会有问题
笔者尝试了好几个版本都不太行,最后折中换了一个其他依赖,实现word转PDF.

3.3.4.1 添加依赖
<dependency>
    <groupId>com.documents4j</groupId>
    <artifactId>documents4j-local</artifactId>
    <version>1.0.3</version>
</dependency>
<dependency>
    <groupId>com.documents4j</groupId>
    <artifactId>documents4j-transformer-msoffice-word</artifactId>
    <version>1.0.3</version>
</dependency>
3.3.4.2 测试类
@Test
public void testWordToPdf() throws Exception {
    String in = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\testtl.docx";
    String out = "E:\\weixinData\\WeChat Files\\wxid_gv8xbkloz0wc22\\FileStorage\\File\\2023-03\\test\\src\\main\\resources\\word\\test3.pdf";
    InputStream docxInputStream = null;
    OutputStream outputStream = null;
    IConverter converter=null;
    try {
        docxInputStream = new FileInputStream(new File(in));
        outputStream = new FileOutputStream(new File(out));
        converter = LocalConverter.builder().build();
        converter.convert(docxInputStream)
                .as(DocumentType.DOCX)
                .to(outputStream)
                .as(DocumentType.PDF)
                .execute();
    } catch (Exception e) {
        log.error("word转pdf异常",e);
        e.printStackTrace();
    } finally {
        outputStream.close();
        docxInputStream.close();
        converter.kill();
    }
}
3.3.4.3 运行结果

PDF正常生成
在这里插入图片描述

3.4 注意事项

  1. 项目里面如果还依赖oomxml版本的话,要留意版本不要冲突,否则会有奇怪的报错,这里可以看到,只需要引用一个依赖,即可完成word文本占位符替换。POI-Tl包本身会有ooxml的一些引用
    在这里插入图片描述
  2. 可以看到POI-TL在模板操作替换方面,使用极其简单,这个jar包初衷就是模板操作word
  3. 更多关于POI-TL的使用,参考文末的文档网站
  4. 如果有需求在一个项目中既实现word模板替换生成新的word文档,又想将word转换为PDF,笔者个人建议选择第一种方案,第二种方案相对来说没有经过我完整的生产验证,第一种是经过我们验证的。

4 其他类似类库简介

4.1 FreeMarker

FreeMarker生成word文档的功能是由XML+FreeMarker来实现的。先把word文件另存为xml,在xml文件中插入特殊的字符串占位符,将xml翻译为FreeMarker模板,最后用Java来解析FreeMarker模板,编码调用FreeMarker实现文本替换并输出Doc。
优点:比Java2word功能强大,代码相对简单,跨平台,也是纯Java编程。
缺点:模板制作复杂,生成的文件本质上是xml,不是真正的word文件格式,有很多常用的word格式无法处理或表现怪异,比如:超链、换行、乱码、部分生成的文件打不开等。

4.2 Java2word

Java2word是一个在Java程序中调用MS Office Word文档的组件(类库)。
该组件提供了一组简单的接口,以便Java程序调用他的服务操作Word文档。
这些服务包括:打开文档、新建文档、查找文字、替换文字,插入文字、插入图片、插入表格,在书签处插入文字、插入图片、插入表格等。
优点:足够简单,操作起来要比FreeMarker简单的多。
缺点:没有FreeMarker强大,不能够根据模版生成Word文档,word的文档的样式等信息都不能够很好的操作。

参考文档:

http://deepoove.com/poi-tl/
https://www.freesion.com/article/22151181181/

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值