Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。
POI生成excel是比较常用的技术之一,但是用来生成word相对来说比较少,今天演示一下利用POI生成word文档的整个流程,,当然方法有很多种,这是我感觉比较方便的一种。
需要实现的功能:
- 在文档中动态生成章节标题、文本段落、表格标题、表格、统计图等等。
- 给每个章节编号
- 删除没有替换的占位符
总体思路:
这种方法的本质其实是动态替换,动态替换的意思是在模板中写入很多某种特定格式的占位符(关键词)以及图的样例,如果前端需要生成这个章节的内容,那么,把这个关键词传到后端,再在模板中寻找关键词,有则替换成具体内容,无则不替换。最后把没有替换掉的占位符删除,从而达到动态替换的效果。
具体实现过程如下:
- 创建word模板.文档中包含若干个章节,章节中包含章节标题、文本段落、表格标题、表格、统计图的占位符。占位符的内容依据自己的实际情况而定。
- 加载模板,判断该章节是否存在,如果存在就替换具体数据。本示例数据都是模拟的,实际情况应该从数据库中查出。
- 再次遍历替换后的word文档,把没有替换掉的占位符删除。
示例
1. 创建模板
如下图所示:
2.替换数据
2.1 测试类
import org.apache.poi.xwpf.usermodel.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ResourceUtils;
import java.io.*;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PoiChartsTest {
@Autowired
private PoiPropsConfig config;
@Autowired
private PoiUtils poiUtils;
//预编译正则表达式,加快正则匹配速度
private Pattern pattern = Pattern.compile("\\$\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);
@Test
public void test() throws Exception {
XWPFDocument document = null;
InputStream inputStream=null;
try {
File file = ResourceUtils.getFile("D://poi.docx");
inputStream = new FileInputStream(file);
document = new XWPFDocument(inputStream);
} catch (Exception e) {
e.printStackTrace();
}
Iterator<XWPFParagraph> itPara = document.getParagraphsIterator();
//处理文字
while (itPara.hasNext()) {
XWPFParagraph paragraph = itPara.next();
String paraText = paragraph.getText();
//如果没有匹配到指定格式的关键词占位符(如${title}格式的)则不进行后续处理
if (!pattern.matcher(paraText).find()) {
continue;
}
//提取出文档模板占位符中的章节标题
String keyInParaText = paraText.split("\\$\\{")[1].split("\\}")[0];
//如果占位符是大标题
if ("title".equalsIgnoreCase(keyInParaText)) {
insertTitle(paragraph);
continue;
}
//如果占位符代表文本总描述
if ("totalDesc".equalsIgnoreCase(keyInParaText)) {
insertText(paragraph);
continue;
}
//如果占位符代表章节标题
if (keyInParaText.contains("section") && keyInParaText.contains("Title")) {
//获取章节类名
String name = keyInParaText.substring(0, 8);
//获取章节类的路径
String classPath = config.getSection().get(name);
//通过类路径获取类对象
BaseSection base = (BaseSection) Class.forName(classPath).newInstance();
base.replaceSectionTitle(document, paragraph);
continue;
}
//如果占位符代表章节文本描述
if (keyInParaText.contains("body")) {
String name = keyInParaText.substring(0, 8);
BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
base.replaceBody(paragraph);
continue;
}
//如果占位符代表表名
if (keyInParaText.contains("tableName")) {
String name = keyInParaText.substring(0, 8);
BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
base.replaceTableName(paragraph);
continue;
}
//如果占位符代表表
if (keyInParaText.endsWith("table")) {
String name = keyInParaText.substring(0, 8);
BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
base.insertTable(document, paragraph);
continue;
}
//如果占位符代表统计图
if (keyInParaText.endsWith("chart")) {
String name = keyInParaText.substring(0, 8);
paragraph.removeRun(0);
BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
base.replaceChart(document, keyInParaText);
continue;
}
//如果占位符代表图名
if (keyInParaText.contains("chartName")) {
String name = keyInParaText.substring(0, 8);
BaseSection base = (BaseSection) Class.forName(config.getSection().get(name)).newInstance();
base.replaceChartName(paragraph);
continue;
}
}
//再遍历一次文档,把没有替换的占位符段落删除
List<IBodyElement> elements = document.getBodyElements();
int indexTable = 0;
for (int k = 0; k < elements.size(); k++) {
IBodyElement bodyElement = elements.get(k);
//所有段落,如果有${}格式的段落便删除该段落
if (bodyElement.getElementType().equals(BodyElementType.PARAGRAPH)) {
XWPFParagraph p = (XWPFParagraph) bodyElement;
String paraText = p.getText();
boolean flag = false;
if (pattern.matcher(paraText).find()) {
flag = document.removeBodyElement(k);
if (flag) {
k--;
}
}
}
//如果是表格,那么给表格的前一个段落(即表名加上编号,如表1)
if (bodyElement.getElementType().equals(BodyElementType.TABLE)) {
indexTable++;
XWPFParagraph tableTitleParagraph = (XWPFParagraph) elements.get(k - 1);
StringBuilder tableTitleText = new StringBuilder(tableTitleParagraph.getParagraphText());
tableTitleText.insert(0, "表" + indexTable + " ");
poiUtils.setTableOrChartTitle(tableTitleParagraph, tableTitleText.toString());
}
}
//给章节与小节添加序号
poiUtils.init(document);
//导出word文档
FileOutputStream docxFos = new FileOutputStream("D://test1.docx");
document.write(docxFos);
docxFos.flush();
docxFos.close();
inputStream.close();
}
//插入大标题
public void insertTitle(XWPFParagraph paragraph) {
String title = "步步升超市报告";
List<XWPFRun> runs = paragraph.getRuns();
int runSize = runs.size();
/**Paragrap中每删除一个run,其所有的run对象就会动态变化,即不能同时遍历和删除*/
int haveRemoved = 0;
for (int runIndex = 0; runIndex < runSize; runIndex++) {
paragraph.removeRun(runIndex - haveRemoved);
haveRemoved++;
}
/**3.插入新的Run即将新的文本插入段落*/
XWPFRun createRun = paragraph.insertNewRun(0);
createRun.setText(title);
XWPFRun separtor = paragraph.insertNewRun(1);
/**两段之间添加换行*/
separtor.setText("\r");
//设置字体大小
createRun.setFontSize(22);
//是否加粗
createRun.setBold(true);
//设置字体
createRun.setFontFamily("宋体");
//设置居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
}
//插入文本描述
private void insertText(XWPFParagraph paragraph) {
String text = "步步升超市作为零售业的典型代表,它已经在全国迅速发展起来。在2018年上半年取得了不菲的成绩," +
"创造销售额230亿,本着方便于民,服务于民的宗旨,我们会继续努力。以下是详细信息报告:"
poiUtils.setTextPro(paragraph, text);
}
}
2.2 章节基类
因为要实现动态生成,所以要用到反射和多态。
import org.apache.poi.ooxml.POIXMLDocumentPart;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTChart;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTTitle;
import org.openxmlformats.schemas.drawingml.x2006.chart.CTTx;
import org.openxmlformats.