原文地址
blog.fengqingmo.top/articles/14…
业务背景:
用户填写多个表单,拿到表单里数据填充到word模板生成 word/pdf 给用户预览/下载。
问题:
响应时间较慢,可以优化
原方法
拿到表单数据(map)后,从模板word文档获取到 XWPFDocument(文档的抽象),遍历word文档的所有单元格,如果该单元格是字段名称,则填充其对应的value。
主要耗时在:
- 从文件流获取到一个文档对象(如下 一个word文档和一个只有相应类的对象的对比)
类
public class PersonalInfomation {
private Integer age;
private String sex;
private String education;
private String name;
}
- 遍历文档所有单元格
这两个操作在每次预览都需要走一遍,思路清晰,就是怎么去优化这两个操作
解决方法
- 预准备文档,维护一个文档对象队列,需要时从队列 poll,并且开定时任务扫描数量,低于指定数量后填充
刚开始想 给每个文档生成一个对象,然后每次获取的时候直接深拷贝一个出来,测试的时候发现从XWPFDocument对象深拷贝一个新的出来比直接从文件流读还慢
- 预处理字段,将所有字段在word文档内的位置提前处理好,这样生成的时候只需要遍历所有字段就行
主要类
文档池类
/**
* 文档池类,用于管理文档对象。
*/
@Slf4j
public class DocumentPool implements CommandLineRunner {
private static final ExecutorService EXECUTOR_SERVICE = ThreadPoolExecutorFactory.getThreadPoolExecutor();
/**
* 定时任务执行间隔时间 默认1s 一次
*/
private int interval = 1000;
/**
* 实际存放对象的地方
*/
public Map<String, ConcurrentLinkedQueue<XWPFDocument>> pool = new HashMap<>();
/**
* 是否初始化完成
*/
private boolean ready = false;
/**
* 文档 字段对应的位置
*/
private Map<String, ConcurrentHashMap<String, String>> fieldPositions = new HashMap<>();
/**
* 模板目录
*/
private String templateDirectory = "";
/**
* 每个文档的数量 视业务并发而定
*/
private int documentNumber = 5;
/**
* 字段前缀
*/
private String prefix = "0x52";
/**
* 构造函数,初始化文档池。
*
* @param templateDirectory 目录
*/
public DocumentPool(String templateDirectory) throws IOException {
this.templateDirectory = templateDirectory;
initializePool(templateDirectory);
}
public DocumentPool(String templateDirectory, int documentNumber) throws IOException {
this.templateDirectory = templateDirectory;
this.documentNumber = documentNumber;
initializePool(templateDirectory);
}
public DocumentPool(String templateDirectory, int documentNumber,int interval) throws IOException {
this.templateDirectory = templateDirectory;
this.documentNumber = documentNumber;
this.interval = interval;
initializePool(templateDirectory);
}
/**
* 初始化文档池,遍历目录下的所有word模板文件
*
* @param templateDirectory 文档模板目录的路径。
*/
public void initializePool(String templateDirectory) throws IOException {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:" + templateDirectory + "/**");
for (Resource resource : resources) {
if (StringUtils.hasText(resource.getFilename()) && (resource.getFilename().endsWith(".doc") || resource.getFilename().endsWith(".docx"))) {
String documentName = resource.getFilename();
ConcurrentHashMap<String, String> fieldPositions = new ConcurrentHashMap<>();
try (InputStream fis = resource.getInputStream()) {
XWPFDocument doc = new XWPFDocument(fis);
ConcurrentLinkedQueue<XWPFDocument> queue = new ConcurrentLinkedQueue<>();
for (int i = 0; i < documentNumber; i++) {
queue.add(doc);
}
fieldPositions = getFieldPositions(doc);
pool.put(documentName, queue);
} catch (IOException e) {
// 处理文件读取异常
log.error("文档池初始化失败");
}
this.fieldPositions.put(documentName, fieldPositions);
this.ready = true;
}
}
}
/**
* 获取文档中字段的位置信息。
*
* @param doc 文档对象。
* @return 字段名到行列位置的映射。
*/
public ConcurrentHashMap<String, String> getFieldPositions(XWPFDocument doc) {
ConcurrentHashMap<String, String> fieldPositions = new ConcurrentHashMap<>();
for (int i = 0; i < doc.getTables().size(); i++) {
XWPFTable table = doc.getTables().get(i);
for (int j = 0; j < table.getRows().size(); j++) {
XWPFTableRow row = table.getRows().get(j);
for (int k = 0; k < row.getTableCells().size(); k++) {
XWPFTableCell cell = row.getTableCells().get(k);
for (int l = 0; l < cell.getParagraphs().size(); l++) {
XWPFParagraph para = cell.getParagraphs().get(i);
String content = para.getRuns().toString();
System.out.println(content);
if (content.startsWith("[" + prefix)) {
//最后一个字符 是 ‘]'
String key = content.substring(5, content.length() - 1);
String value = getFieldPositionsValue(i, j, k, l);
fieldPositions.put(key, value);
}
}
}
}
}
System.out.println(fieldPositions);
return fieldPositions;
}
/**
* @param i 第几个表格 固定第一个
* @param j 第几个行
* @param k 第几列
* @param l 第几段 固定第一段
* @return 以逗号分隔组合的字符串
*/
private String getFieldPositionsValue(int i, int j, int k, int l) {
StringBuilder sb = new StringBuilder();
return sb.append(j).append(",").append(k).toString();
}
/**
* 根据文件名获取填充好的文档
*
* @param documentName 文件名。
* @param dataMap 需要填充的k-v值
* @return 填充好的文档
*/
public XWPFDocument getDocument(String documentName, Map<String, String> dataMap) {
XWPFDocument document = getBlankDocument(documentName);
if (document == null) {
throw new RuntimeException("文档池中没有找到空文档");
}
ConcurrentHashMap<String, String> fieldPositions = this.fieldPositions.get(documentName);
// 遍历字段位置映射,将传入的数据映射覆盖进去
for (Map.Entry<String, String> entry : fieldPositions.entrySet()) {
String fieldName = entry.getKey();
String position = entry.getValue();
String[] positions = position.split(",");
// 这里需要实现将数据映射覆盖到文档中的逻辑
XWPFTable table = document.getTables().get(0);
XWPFTableRow row1 = table.getRows().get(Integer.parseInt(positions[0]));
XWPFTableCell cell = row1.getTableCells().get(Integer.parseInt(positions[1])); // 假设每个单元格只包含一个段落
XWPFParagraph para = cell.getParagraphs().get(0);
// 清除原内容
para.getRuns().forEach(run -> run.setText("", 0));
para.createRun().setText(dataMap.getOrDefault(fieldName, ""));
}
return document;
}
/**
* 根据文档名获取空文档
*
* @param documentName 文档名
*/
public XWPFDocument getBlankDocument(String documentName) {
ConcurrentLinkedQueue<XWPFDocument> documents = pool.get(documentName);
XWPFDocument doc;
if (documents == null || documents.isEmpty()) {
doc = getXWPFDocument(documentName);
} else {
doc = documents.poll();
}
return doc;
}
/**
* 获取指定文件
*
* @param documentName 文件名
* @return
*/
private XWPFDocument getXWPFDocument(String documentName) {
XWPFDocument doc;
ClassPathResource resource = new ClassPathResource(templateDirectory + "/" + documentName);
try (InputStream fis = resource.getInputStream()) {
doc = new XWPFDocument(fis);
} catch (IOException e) {
// 处理文件读取异常
throw new RuntimeException("未找到此文件");
}
return doc;
}
@Override
public void run(String... args) {
EXECUTOR_SERVICE.execute(new Runnable() {
@Override
@SneakyThrows
public void run() {
if (ready) {
log.info("定时任务执行");
supplyDocument();
}
if(!EXECUTOR_SERVICE.isShutdown()){
Thread.sleep(interval);
EXECUTOR_SERVICE.execute(this);
}
}
});
}
/**
* 补充文档
*/
private void supplyDocument() {
for (Map.Entry<String, ConcurrentLinkedQueue<XWPFDocument>> map : pool.entrySet()) {
String fileName = map.getKey();
ConcurrentLinkedQueue<XWPFDocument> documents = map.getValue();
if (documents.size() < documentNumber) {
log.info(fileName + "数量小于指定数量,补充");
XWPFDocument document = getXWPFDocument(fileName);
for (int i = 0; i < documentNumber - documents.size(); i++) {
documents.add(document);
}
pool.put(fileName,documents);
}
}
}
}
文档池配置类
@Configuration
public class DocumentPoolConfig {
/**
* 模板文件目录
*/
private static String templateDirectory = "wordTemplate" ;
/**
* 文档数量
*/
private static int initNumber = 5;
/**
* 定时任务时间 单位: ms
*/
private static int internal = 5000;
@Bean
public DocumentPool init() throws IOException {
return new DocumentPool(templateDirectory,initNumber,internal);
}
}
测试类
@GetMapping("/demo")
public void demo(HttpServletResponse response, HttpServletRequest request){
Map<String,String> map = new HashMap<>();
map.put("name","风清默");
map.put("age","20");
map.put("sex","男");
map.put("education","本科");
// 线上 文件名在参数里 map从数据库获取
String fileName = "personal infomation.docx";
XWPFDocument document = documentPool.getDocument(fileName, map);
// 设置响应内容类型为docx文件
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment; filename="document.docx"");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream outputStream = response.getOutputStream()) {
document.write(baos);
ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray());
StreamUtils.copy(inputStream, outputStream);
outputStream.flush();
inputStream.close();
} catch (IOException e) {
throw new RuntimeException("文件导出失败: " + e.getMessage(), e);
}
}
启动应用在浏览器输入 localhost:8080/demo
原文档:
0x52是标记这个是一个字段,自己随便约定一个即可
生成结果:
代码地址
WordPool: 预处理word模板文件,加快word文档预览/下载响应速度 (gitee.com)
运行
本地启动
浏览器访问http://localhost:8080/demo即可