如果项目内要对Microsoft Word文件进行处理,最容易想到的第三方库就是aspose.words了。除了直接对Word文件的内容进行操作外, aspose.words还提供了一套模板方法, 使我们可以在代码内只关注数据的抽取,然后将数据套用到事先写好的模板中去,直接生成处理好的Word文件。
以下是一小段模板示例:
这里需要用到Word的域功能来制作模板, 用法为插入 - 文档部件 - 选择MergeField - 输入域名称 - 确定
动态生成文件的方法如下:
File directory = new File("src/main/resources/templates");
String realPath = directory.getCanonicalPath();
JSONObject params = JSON.parseObject(contract.getParamJson());
params.put("firstPartyName", contract.getFirstPartyName());
params.put("secondPartyName", contract.getSecondPartyName());
params.put("firstPartyCode", contract.getFirstPartyCode());
params.put("secondPartyCode", contract.getSecondPartyCode());
Document doc = new Document(realPath + "/template.docx" );
doc.getMailMerge().execute(params.keySet().toArray(new String[0]), params.values().toArray());
doc.save(response.getOutputStream(), SaveFormat.PDF);
使用doc.getMailMerge().execute(array, array)这个方法, 依次传入keySet和valueSet, 就可以把模板内对应名称的域转换为我们传入的值了。
这里我们只用到了单一值的替换, 如果要传入一个List并且让aspose.words遍历这个列表来生成数据, 就比较麻烦, 因为asopse.words原生并不支持List转模板。
可以看一下官方的解释:
我们需要实现IMailMergeDataSource这个接口, 然后将它的实例传给 doc.getMailMerge().executeWithRegions(IMailMergeDataSource), 来实现值的替换。这里有几个关键方法:aspose.words会调用moveNext()来在每一次遍历结束后访问下一个元素, 并在每次遍历时循环调用getValue()来获取当前元素的每个属性值。
也就是说我们需要把List进行封装:
package com.framework.core.aspose;
import com.aspose.words.IMailMergeDataSource;
import com.framework.core.uitls.BeanUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 封装循环实体,实体用map进行分装
*
* @author chris hong
*/
//类上的泛型要与List的泛型对应
public class ListDataSource<T> implements IMailMergeDataSource {
private List<T> dataList;
//构造时需要传入List的泛型, 以实现通过反射调用
private Class<T> tClass;
private int index;
//word模板中的域名称
private String tableName;
/**
* @param dataList 数据集
* @param tableName 与模板中的Name对应
*/
public ListDataSource(List<T> dataList, String tableName, Class<T> tClass) {
this.dataList = dataList;
this.tableName = tableName;
this.tClass = tClass;
index = -1;
}
/**
* @param data 单个数据集
* @param tableName 与模板中的Name对应
*/
public ListDataSource(T data, String tableName, Class<T> tClass) {
if (this.dataList == null) {
this.dataList = new ArrayList<>();
this.dataList.add(data);
}
this.tClass = tClass;
this.tableName = tableName;
index = -1;
}
/**
* 获取结果集总数
*/
private int getCount() {
return this.dataList.size();
}
public String getTableName() {
return this.tableName;
}
/**
* 实现接口 获取当前index指向数据行的数据 将数据存入args数组中即可
*
* @return ***返回false则不绑定数据***
*/
public boolean getValue(String key, Object[] args) {
if (index < 0 || index >= this.getCount()) {
return false;
}
//这里aspose传入的key, 就是模板中的域名, 我们需要通过反射, 获取对应属性的getter, 再调用这个getter来获取属性值
if (args != null && args.length > 0) {
Method getter = BeanUtil.getReadMethod(key, tClass);
if (getter != null) {
try {
args[0] = getter.invoke(this.dataList.get(index));
return true;
} catch (IllegalAccessException e) {
return false;
} catch (InvocationTargetException e) {
return false;
}
}
return false;
} else {
return false;
}
}
/**
* 实现接口 判断是否还有下一条记录
*/
@Override
public boolean moveNext() throws Exception {
index += 1;
if (index >= this.getCount()) {
return false;
}
return true;
}
@Override
public IMailMergeDataSource getChildDataSource(String arg0)
throws Exception {
// TODO Auto-generated method stub
return null;
}
}
模板写法如下:
模板里的'list'对应的就是ListDataSource的tableName, 'name'对应的是实体类中的一个属性名
示例代码如下:
List<BusPatient> list = dataPackService.getScreenedList(new PageBounds(), params);
ListDataSource<BusPatient> dataSource = new ListDataSource<>(list, "list", BusPatient.class);
doc.getMailMerge().executeWithRegions(dataSource);
doc.save(response.getOutputStream(), SaveFormat.PDF);
生成后的文件如下:
这样, 就完成了列表的循环。如果有多个List需要遍历,那么就需要创建多个ListDataSource示例并多次调用doc.getMailMerge().executeWithRegions()。
网上现有的资料很多都说aspose.words只支持List<Map<String, Object>>的封装, 不支持直接使用实体类, 其实是对 IMailMergeDataSource的理解不足导致的, 只要理解 IMailMergeDataSource的实现方法, 让aspose.words变得更贴近我们的使用习惯并不是很难。