原文地址
https://blog.fengqingmo.top/articles/147
业务背景:
用户填写多个表单,拿到表单里数据填充到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即可