EasyPoi介绍
EasyPoi是一个Java的Excel和Word处理库,主要用于将Java对象转换为Excel或Word文档,并且可以从Excel或Word文档中读取数据到Java对象。本文将重点介绍如何使用EasyPoi写Word文档。
- 引入EasyPoi依赖
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.3</version>
</dependency>
- 创建Word模板
可以使用Microsoft Word或其他能够创建Word模板的软件来设计Word文档的模板文件。模板文件中可以使用占位符来标记需要替换的内容。例如,在Word文档中可以插入占位符${name}表示需要替换的内容。
- 创建Java对象
使用Java对象来存放需要写入Word文档的数据。
@Data
public class User {
private String name;
private int age;
}
- 使用EasyPoi写入Word文档
创建一个WordExportUtil对象,利用模板和Java对象生成Word文档。
import cn.afterturn.easypoi.word.WordExportUtil;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;
public class WriteWordDemo {
public static void main(String[] args) throws Exception {
// 创建模板和数据对象
User user = new User("Tom", 18);
Map<String, Object> map = new HashMap<>();
map.put("name", user.getName());
map.put("age", user.getAge());
// 加载模板文件
XWPFDocument doc = WordExportUtil.exportWord07(
"user_template.docx", map);
// 输出文件
FileOutputStream fos = new FileOutputStream("user.docx");
doc.write(fos);
fos.close();
doc.close();
}
}
使用WordExportUtil.exportWord07方法可以生成.docx格式的Word文档,第一个参数为Word模板文件名,第二个参数是一个Map对象,其中包含了模板中所有的占位符和对应的数据。
基于EasyPoi实现段落循环
EasyPoi写word模版导出比较方便。但是如果你希望在写word模版的时候将段落进行循环,那就实现不了。
下面的方法基于EasyPoi实现了word段落的循环。
import cn.afterturn.easypoi.cache.WordCache;
import cn.afterturn.easypoi.entity.ImageEntity;
import cn.afterturn.easypoi.util.PoiElUtil;
import cn.afterturn.easypoi.util.PoiPublicUtil;
import cn.afterturn.easypoi.word.entity.MyXWPFDocument;
import cn.afterturn.easypoi.word.entity.params.ExcelListEntity;
import cn.afterturn.easypoi.word.parse.excel.ExcelMapParse;
import cn.hutool.core.bean.BeanUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.util.*;
public class WordParagraphHolder {
private static final Logger logger = LoggerFactory.getLogger(WordParagraphHolder1.class);
private XWPFDocument sourceDocument;
private String targetFileName;
private Map<String, Object> data;
public WordParagraphHolder(XWPFDocument sourceDocument, String targetFileName, Map<String, Object> data) {
this.sourceDocument = sourceDocument;
this.targetFileName = targetFileName;
this.data = data;
}
public String execute() throws Exception {
MyXWPFDocument second = null;
FileOutputStream fos2 = null;
try {
// 循环生成段落
Map<String, Object> data = this.data;
this.parseAllParagraphic(sourceDocument.getParagraphs(), data);
File targetFile = new File(targetFileName);
FileOutputStream fos = FileUtils.openOutputStream(targetFile);
sourceDocument.write(fos);
// 关闭流
fos.close();
sourceDocument.close();
// 循环段落赋值
second = WordCache.getXWPFDocument(targetFile.getPath());
this.evalAllParagraphic(second.getParagraphs(), data);
fos2 = FileUtils.openOutputStream(targetFile);
second.write(fos2);
} catch (Exception e) {
logger.error("生成word段落失败:", e);
return "";
} finally {
// 关闭流
fos2.close();
second.close();
}
return targetFileName;
}
private void evalAllParagraphic(List<XWPFParagraph> paragraphs, Map<String, Object> map) throws Exception {
int size = paragraphs.size();
Map<String, Integer> indexMap = new HashMap<>();
for (int i = 0; i < size; ++i) {
XWPFParagraph paragraph = (XWPFParagraph) paragraphs.get(i);
logger.info("段落内容:{}", paragraph.getText());
if (paragraph.getText().indexOf("(") != -1) {
Object obj = checkThisParagraphNeedIterator(paragraph, map);
String listKey = getParagraphListKey(paragraph);
Integer index = indexMap.getOrDefault(listKey, 0);
if (Objects.nonNull(obj) && obj instanceof List && index < ((List) obj).size()) {
Object o = ((List) obj).get(index);
parseParagraph(paragraph, o, listKey);
index++;
indexMap.put(listKey, index);
}
}
}
}
private void parseAllParagraphic(List<XWPFParagraph> paragraphs, Map<String, Object> map) throws Exception {
int size = paragraphs.size();
List<XWPFParagraph> paragraphList = new ArrayList<>();
// 循环找出需要复制的段落
for (int i = 0; i < size; ++i) {
XWPFParagraph paragraph = (XWPFParagraph) paragraphs.get(i);
if (paragraph.getText().indexOf("(") != -1) {
Object obj = checkThisParagraphNeedIterator(paragraph, map);
if (Objects.nonNull(obj) && obj instanceof List) {
paragraphList.add(paragraph);
}
}
}
// 复制段落
for (XWPFParagraph paragraph : paragraphList) {
System.out.println(paragraph.getText());
Object obj = checkThisParagraphNeedIterator(paragraph, map);
addParagraph(paragraph, (List) obj);
// 删除模板段落
XWPFDocument document = paragraph.getDocument();
document.removeBodyElement(document.getPosOfParagraph(paragraph));
}
}
/**
* 复制段落
*
* @param source 原段落
* @param doc
*/
public XWPFParagraph createParagraph(XWPFParagraph source, XWPFDocument doc) {
// 使用游标创建一个新行
XmlCursor cursor = source.getCTP().newCursor();
XWPFParagraph newParagraph = doc.insertNewParagraph(cursor);
newParagraph.getCTP().set(source.getCTP().copy());
return newParagraph;
}
private Object checkThisParagraphNeedIterator(XWPFParagraph paragraph, Map<String, Object> map) throws Exception {
String text = paragraph.getText().trim();
if (text != null && text.contains("fe:") && text.startsWith("(")) {
text = text.replace("!fe:", "").replace("$fe:", "").replace("fe:", "").replace("(", "");
String[] keys = text.replaceAll("\\s{1,}", " ").trim().split(" ");
Object result = PoiPublicUtil.getParamsValue(keys[0], map);
return Objects.nonNull(result) ? result : new ArrayList(0);
} else {
return null;
}
}
private String getParagraphListKey(XWPFParagraph paragraph) throws Exception {
String text = paragraph.getText().trim();
if (text != null && text.contains("fe:") && text.startsWith("(")) {
text = text.replace("!fe:", "").replace("$fe:", "").replace("fe:", "").replace("(", "");
String[] keys = text.replaceAll("\\s{1,}", " ").trim().split(" ");
return keys[0];
} else {
return null;
}
}
/**
* 赋值段落
*
* @param paragraph
* @param obj
* @throws Exception
*/
public void parseParagraph(XWPFParagraph paragraph, Object obj, String listKey) throws Exception {
String listname = paragraph.getText().trim();
boolean contains = listname.contains("fe:");
if (!contains) {
return;
}
Map<String, Object> objectMap = BeanUtil.beanToMap(obj);
parseThisParagraph(paragraph, objectMap, listKey);
}
/**
* 增加段落
*
* @param paragraph
* @param list
* @throws Exception
*/
public void addParagraph(XWPFParagraph paragraph, List<Object> list) throws Exception {
XWPFParagraph currentParagraph = paragraph;
System.out.println("start for each data list :" + list.size());
Iterator var11 = list.iterator();
while (var11.hasNext()) {
Object obj = var11.next();
this.createParagraph(currentParagraph, currentParagraph.getDocument());
}
}
/**
* 遍历段落赋值
*
* @param paragraph
* @param map
* @throws Exception
*/
private void parseThisParagraph(XWPFParagraph paragraph, Map<String, Object> map, String listKey) throws Exception {
XWPFRun currentRun = null;
String currentText = "";
Boolean isfinde = false;
List<Integer> runIndex = new ArrayList();
XWPFRun preRun = null;
for (int i = 0; i < paragraph.getRuns().size(); ++i) {
XWPFRun run = (XWPFRun) paragraph.getRuns().get(i);
String text = run.getText(0);
if (!StringUtils.isEmpty(text)) {
if (isfinde) {
currentText = currentText + text;
if (currentText.indexOf("[") == -1) {
isfinde = false;
runIndex.clear();
} else {
runIndex.add(i);
}
if (currentText.indexOf("]") != -1) {
this.changeValues(paragraph, currentRun, currentText, runIndex, map);
currentText = "";
isfinde = false;
}
} else if (text.indexOf("[") >= 0) {
currentText = text;
isfinde = true;
currentRun = run;
} else {
currentText = "";
}
if (currentText.indexOf("]") != -1) {
this.changeValues(paragraph, currentRun, currentText, runIndex, map);
isfinde = false;
}
}
// 去除多余的字符串
if (!StringUtils.isEmpty(text)) {
if (text.indexOf("(") != -1) {
preRun = run;
} else if (text.indexOf("$fe") != -1) {
// run.setText("", 0);
// 清除第一个(
preRun.setText("", 0);
// 防止自定义前缀未被拆分为多个run,根据自定义关键字删除多余前缀。例如($fe:resultList
if (text.indexOf(listKey) != -1) {
String lastText = run.getText(0);
if (StringUtils.isNotBlank(lastText) && text.indexOf(listKey) != -1) {
String replace = lastText.substring(text.indexOf(listKey) + listKey.length());
run.setText(replace, 0);
}
}
}
// 清除最后一个)
if (i == paragraph.getRuns().size() - 1) {
if (text.indexOf(")") != -1) {
if (text.length() >= 1) {
String lastText = run.getText(0);
String replace = lastText.replace(")", "");
run.setText(replace, 0);
}
}
}
}
}
}
private void changeValues(XWPFParagraph paragraph, XWPFRun currentRun, String currentText, List<Integer> runIndex, Map<String, Object> map) throws Exception {
Object obj = getRealValue(currentText, map);
if (obj instanceof ImageEntity) {
currentRun.setText("", 0);
ExcelMapParse.addAnImage((ImageEntity) obj, currentRun);
} else {
currentText = obj.toString();
PoiPublicUtil.setWordText(currentRun, currentText);
}
for (int k = 0; k < runIndex.size(); ++k) {
((XWPFRun) paragraph.getRuns().get((Integer) runIndex.get(k))).setText("", 0);
}
runIndex.clear();
}
public static Object getRealValue(String currentText, Map<String, Object> map) throws Exception {
String params = "";
while (currentText.indexOf("[") != -1) {
params = currentText.substring(currentText.indexOf("[") + 1, currentText.indexOf("]"));
Object obj = PoiElUtil.eval(params.trim(), map);
if (obj instanceof ImageEntity || obj instanceof List || obj instanceof ExcelListEntity) {
return obj;
}
if (obj != null) {
currentText = currentText.replace("[" + params + "]", obj.toString());
} else {
currentText = currentText.replace("[" + params + "]", "");
}
}
return currentText;
}
/**
* 复制表格段落
*
* @param source 原段落
* @param cell
*/
public XWPFParagraph copyTableParagraph(XWPFParagraph source, XWPFTableCell cell) {
// 使用游标创建一个新行
XmlCursor cursor = source.getCTP().newCursor();
// 在游标位置插入新段落
XWPFParagraph newParagraph = cell.insertNewParagraph(cursor);
// 复制底层 XML 对象
XmlObject copy = source.getCTP().copy();
newParagraph.getCTP().set(copy);
// 关闭游标
cursor.dispose();
return newParagraph;
}
/**
* 表格段落复制
*
* @param document
* @param data
*/
private void createTableParagraph(XWPFDocument document, Map<String, Object> data) {
Object resultList = data.get("resultList");
if (Objects.isNull(resultList)) {
return;
}
List list = (List) resultList;
List<XWPFTable> tables = document.getTables();
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
XWPFParagraph xwpfParagraph = null;
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String listname = paragraph.getText().trim();
if (listname.startsWith("($fe")) {
xwpfParagraph = paragraph;
}
}
if (Objects.nonNull(xwpfParagraph)) {
for (int i = 0; i < list.size() - 1; i++) {
copyTableParagraph(xwpfParagraph, cell);
}
}
}
}
}
}
/**
* 表格段落赋值
*
* @param document
* @param date
*/
private void evalTableParagraph(XWPFDocument document, Map<String, Object> date) {
List<XWPFTable> tables = document.getTables();
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
List<XWPFParagraph> paragraphs = cell.getParagraphs();
List<XWPFParagraph> addList = new ArrayList<>();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String listname = paragraph.getText().trim();
if (listname.startsWith("($fe")) {
addList.add(paragraph);
}
}
try {
evalAllParagraphic(addList, date);
} catch (Exception e) {
logger.error("单元格{}段落赋值异常", cell.getText(), e);
}
}
}
}
}
}
具体的调用逻辑是先执行一次常规占位符的替换,然后再进行段落的替换。WordParagraphHolder 中也是先生成段落,再进行段落赋值。
代码如下
public static File exportWord(Map<String, Object> params, File templateFile, File writeFile) {
FileOutputStream fos = null;
FileOutputStream outputStream = null;
try {
long start = System.currentTimeMillis();
logger.info("开始写入word文件");
//获取模板文档
logger.info("开始写入word模板文件路径:{}", templateFile.getPath());
// 写入word常规替换
XWPFDocument doc = WordExportUtil.exportWord07(templateFile.getPath(), params);
// 循环写段落
WordParagraphHolder paragraphHolder = new WordParagraphHolder(doc, writeFile.getPath(), params);
String targetFileName = paragraphHolder.execute();
logger.info("写入word文件完成,耗时{}", (System.currentTimeMillis() - start));
return new File(targetFileName);
} catch (Exception e) {
logger.error("写入word文件异常:", e);
} finally {
IOUtils.closeQuietly(fos);
IOUtils.closeQuietly(outputStream);
}
return null;
}
使用方法
新建word插入如下内容:
($fe:resultList [createDate],我使用[number]元,购买苹果[amount]个,截止日期[endDate],使用支付方式[type])
其中resultList 为入参list的名称,可以自由更换但是要和代码里的对应上,因为我们要生成多个段落,所以参数要构造为list的形式。
**[createDate]**内createDate为list中实体的字段名称,其他的为固定格式,不要修改,生成完成后会自动删除多余的字符串。
执行下面的测试代码:
public static void main(String[] args) throws Exception {
Map<String, Object> date = createDate();
String sourceFile = "d:/temp/模版word.docx";
String targetFile = "d:/temp/输出结果.docx";
MyXWPFDocument first = WordCache.getXWPFDocument(sourceFile);
WordParagraphHolder test = new WordParagraphHolder(first, targetFile, date);
test.execute();
}
private static Map<String, Object> createDate() {
//填充数据
List<WordExportBatch> resultList = new ArrayList<>();
WordExportBatch wordExport = new WordExportBatch();
WordExportBatch wordExport1 = new WordExportBatch();
wordExport.setCreateDate("2022/9/30");
wordExport1.setCreateDate("2022/9/28");
wordExport.setNumber("11");
wordExport1.setNumber("15");
wordExport.setAmount("1234.5");
wordExport1.setAmount("2345.77");
wordExport.setEndDate("2022/12/31");
wordExport1.setEndDate("2022/11/30");
wordExport.setType("支付宝");
wordExport1.setType("微信");
resultList.add(wordExport);
resultList.add(wordExport1);
//准备数据
Map<String, Object> params = new HashMap<>();
params.put("resultList", resultList);
return params;
}
看到输出如下就大功告成了。
补充pom依赖
有时候项目里poi的版本与easypoi李的poi会冲突,需要解决一下冲突。
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.1</version>
</dependency>
word表格里段落复制
奇怪的需求千千万,遇到了需要在单元格内进行段落的循环复制。需要在生产段落的基础上做一下调整。增加创建表格段落的方法,并修改execute()执行方法。
public String execute() throws Exception {
MyXWPFDocument second = null;
FileOutputStream fos2 = null;
try {
Map<String, Object> data = this.data;
// 循环生成段落
this.parseAllParagraphic(sourceDocument.getParagraphs(), data);
// 循环生成表格段落
this.createTableParagraph(sourceDocument, data);
File targetFile = new File(targetFileName);
FileOutputStream fos = FileUtils.openOutputStream(targetFile);
sourceDocument.write(fos);
// 关闭流
fos.close();
sourceDocument.close();
second = WordCache.getXWPFDocument(targetFile.getPath());
// 循环段落赋值
this.evalAllParagraphic(second.getParagraphs(), data);
// 循环表格段落赋值
this.evalTableParagraph(second, data);
fos2 = FileUtils.openOutputStream(targetFile);
second.write(fos2);
} catch (Exception e) {
logger.error("生成word段落失败:", e);
return "";
} finally {
// 关闭流
fos2.close();
second.close();
}
return targetFileName;
}
/**
* 复制表格段落
*
* @param source 原段落
* @param cell
*/
public XWPFParagraph copyTableParagraph(XWPFParagraph source, XWPFTableCell cell) {
// 使用游标创建一个新行
XmlCursor cursor = source.getCTP().newCursor();
// 在游标位置插入新段落
XWPFParagraph newParagraph = cell.insertNewParagraph(cursor);
// 复制底层 XML 对象
XmlObject copy = source.getCTP().copy();
newParagraph.getCTP().set(copy);
// 关闭游标
cursor.dispose();
return newParagraph;
}
/**
* 表格段落复制
*
* @param document
* @param data
*/
private void createTableParagraph(XWPFDocument document, Map<String, Object> data) {
Object resultList = data.get("resultList");
if (Objects.isNull(resultList)) {
return;
}
List list = (List) resultList;
List<XWPFTable> tables = document.getTables();
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
XWPFParagraph xwpfParagraph = null;
List<XWPFParagraph> paragraphs = cell.getParagraphs();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String listname = paragraph.getText().trim();
if (listname.startsWith("($fe")) {
xwpfParagraph = paragraph;
}
}
if (Objects.nonNull(xwpfParagraph)) {
for (int i = 0; i < list.size() - 1; i++) {
copyTableParagraph(xwpfParagraph, cell);
}
}
}
}
}
}
/**
* 表格段落赋值
*
* @param document
* @param date
*/
private void evalTableParagraph(XWPFDocument document, Map<String, Object> date) {
List<XWPFTable> tables = document.getTables();
for (XWPFTable table : tables) {
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
List<XWPFParagraph> paragraphs = cell.getParagraphs();
List<XWPFParagraph> addList = new ArrayList<>();
for (int i = 0; i < paragraphs.size(); i++) {
XWPFParagraph paragraph = paragraphs.get(i);
String listname = paragraph.getText().trim();
if (listname.startsWith("($fe")) {
addList.add(paragraph);
}
}
try {
evalAllParagraphic(addList, date);
} catch (Exception e) {
logger.error("单元格{}段落赋值异常", cell.getText(), e);
}
}
}
}
}
public static void main(String[] args) throws Exception {
Map<String, Object> date = createDate();
String sourceFile = "d:/temp/模版word1.docx";
String targetFile = "d:/temp/输出结果1.docx";
MyXWPFDocument first = WordCache.getXWPFDocument(sourceFile);
WordParagraphHolder1 test = new WordParagraphHolder1(first, targetFile, date);
test.execute();
}
private static Map<String, Object> createDate() {
//填充数据
List<WordExportBatch> resultList = new ArrayList<>();
WordExportBatch wordExport = new WordExportBatch();
WordExportBatch wordExport1 = new WordExportBatch();
wordExport.setCreateDate("2022/9/30");
wordExport1.setCreateDate("2022/9/28");
wordExport.setNumber("11");
wordExport1.setNumber("15");
wordExport.setAmount("1234.5");
wordExport1.setAmount("2345.77");
wordExport.setEndDate("2022/12/31");
wordExport1.setEndDate("2022/11/30");
wordExport.setType("支付宝");
wordExport1.setType("微信");
resultList.add(wordExport);
resultList.add(wordExport1);
//准备数据
Map<String, Object> params = new HashMap<>();
params.put("resultList", resultList);
return params;
}
($fe:resultList [createDate],我使用[number]元,购买苹果[amount]个,截止日期[endDate],使用支付方式[type])
在word中新建表格,在对应的单元格里写入上面需要循环的内容。执行测试代码就可以看到输出结果。