最近在做word模板导出的需求,本来意为是很简单,做起来才发现细节上有很多东西处理起来还是比较麻烦的(客户要求太多!!!)
因此我把涉及到基于word模板导出的这部分整理了一下,大家直接取经,内容有点多分了三部分写:文本段落、图表、表格。这三部分应该涵盖大部分应用场景了。
(需要完整代码的直接看最后位置!!!)
前言:
poi操作word原理同excel一样,先获取word对象,通过对象操作文件内容。poi实际上是将word解析成xml,然后在读取里面的内容。
逻辑:
1、获取word对象:
// 获取docx解析对象
//获取word模板
InputStream is = WordController.class.getResourceAsStream("/template/wordChartTemplate.docx");
XWPFDocument document = new XWPFDocument(is);
2、修改word模板,将需要封装的数据通过占位符代替
比如这里的${xxxx}就是占位符,我们通过代码可以将这部分替换成我们需要的内容。注意格式是自己定义的,并不是固定这种,自己选择什么格式,就按照对应格式处理,下面给指出。
3、封装我们的业务数据,根据你的具体业务填充数据,Map中的key是我们占位符中的值,value就是要展示在页面的值
// 替换word模板中占位符数据,根据业务自行封装
Map<String, String> params = new HashMap<>();
params.put("area1", "山东省");
params.put("area2", "河南省");
params.put("area3", "北京市");
params.put("area4", "天津市");
params.put("area5", "陕西省");
params.put("areaRate1", "42.15");
params.put("areaRate2", "22.35");
params.put("areaRate3", "42.35");
params.put("areaRate4", "23.11");
params.put("areaRate5", "15.34");
4、操作document文件对象,先获取段落,在获取段落中每段内容,根据我们的占位符格式判断是否存在,存在则更新我们占位符位置的内容。
private void changeText(XWPFDocument document, Map<String, String> params) {
//获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
//判断此段落时候需要进行替换
String text = paragraph.getText();
if(checkText(text)){
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
//替换模板原来位置
String value = changeValue(run.toString(), params);
if (Objects.nonNull(value)) {
run.setText(value, 0);
}
}
}
}
}
/**
* 匹配传入信息集合与模板
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static String changeValue(String value, Map<String, String> textMap){
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
//匹配模板与替换值 格式${key}
String key = "${"+textSet.getKey()+"}";
if(value.indexOf(key)!= -1){
value = value.replace(key, textSet.getValue());
}
}
//模板未匹配到区域替换为空
if(checkText(value)){
value = "";
}
return value;
}
/**
* 判断文本中时候包含$
* @param text 文本
* @return 包含返回true,不包含返回false
*/
public static boolean checkText(String text){
boolean check = false;
if(text.indexOf("$")!= -1){
check = true;
}
return check;
}
解释:String key = "${"+textSet.getKey()+"}";这段代码就是我们自定义的占位符格式,如果你是其他格式在这里做相应修改
ok,完成!!!
但是这时大部分情况可能会遇到下面问题:
可以看到有的成功、有的失败。之前说过poi本质就是把word解析成xml,那我们直接把word导出为xml看一下,用office可以快速把word导出为xml格式,这里我们把我们的模板导出为xml看下同样的占位符有啥不同???
在idea里打开看一下,发现即使我们占位符都是通过复制粘贴在word里,他的xml格式还是会发生变化。
有两种解决方案:
1、最简单的办法把模板里的占位符粘贴到文本,在复制粘贴到word。
2、或者直接修改xml,在把xml导出为word。可以在idea中看xml文件,格式化一下,这种方式只要没有把xml格式改错,基本解析就不会再出问题了。
修改后再次执行程序:
成功!!!
完整代码:
package com.javacoding.controller;
import cn.hutool.core.util.RandomUtil;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.drawingml.x2006.chart.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.util.*;
@RestController
@RequestMapping("/word")
public class WordController {
@RequestMapping("/export")
public void exportWord() throws Exception {
//获取word模板
InputStream is = WordController.class.getResourceAsStream("/template/wordChartTemplate.docx");
try {
ZipSecureFile.setMinInflateRatio(-1.0d);
// 获取docx解析对象
XWPFDocument document = new XWPFDocument(is);
// 解析替换文本段落对象
// 替换word模板中占位符数据,根据业务自行封装
Map<String, String> params = new HashMap<>();
params.put("area1", "山东省");
params.put("area2", "河南省");
params.put("area3", "北京市");
params.put("area4", "天津市");
params.put("area5", "陕西省");
params.put("areaRate1", "42.15");
params.put("areaRate2", "22.35");
params.put("areaRate3", "42.35");
params.put("areaRate4", "23.11");
params.put("areaRate5", "15.34");
changeText(document, params);
// 输出新文件
FileOutputStream fos = new FileOutputStream("D:\\test\\test.docx");
document.write(fos);
document.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private void changeText(XWPFDocument document, Map<String, String> params) {
//获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
//判断此段落时候需要进行替换
String text = paragraph.getText();
if(checkText(text)){
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
//替换模板原来位置
String value = changeValue(run.toString(), params);
if (Objects.nonNull(value)) {
run.setText(value, 0);
}
}
}
}
}
/**
* 判断文本中时候包含$
* @param text 文本
* @return 包含返回true,不包含返回false
*/
public static boolean checkText(String text){
boolean check = false;
if(text.indexOf("$")!= -1){
check = true;
}
return check;
}
/**
* 匹配传入信息集合与模板
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static String changeValue(String value, Map<String, String> textMap){
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
//匹配模板与替换值 格式${key}
String key = "${"+textSet.getKey()+"}";
if(value.indexOf(key)!= -1){
value = value.replace(key, textSet.getValue());
}
}
//模板未匹配到区域替换为空
if(checkText(value)){
value = "";
}
return value;
}
}