池化技术 & 预处理 优化模板word文档生成速度

原文地址

https://blog.fengqingmo.top/articles/147

业务背景:

用户填写多个表单,拿到表单里数据填充到word模板生成 word/pdf 给用户预览/下载。

问题:

响应时间较慢,可以优化

原方法

拿到表单数据(map)后,从模板word文档获取到 XWPFDocument(文档的抽象),遍历word文档的所有单元格,如果该单元格是字段名称,则填充其对应的value。

主要耗时在:

  1. 从文件流获取到一个文档对象(如下 一个word文档和一个只有相应类的对象的对比)

public class PersonalInfomation {
    private Integer age;
    private String sex;
    private String education;
    private String name;
}

  1. 遍历文档所有单元格

这两个操作在每次预览都需要走一遍,思路清晰,就是怎么去优化这两个操作

解决方法

  1. 预准备文档,维护一个文档对象队列,需要时从队列 poll,并且开定时任务扫描数量,低于指定数量后填充

刚开始想 给每个文档生成一个对象,然后每次获取的时候直接深拷贝一个出来,测试的时候发现从XWPFDocument对象深拷贝一个新的出来比直接从文件流读还慢

  1. 预处理字段,将所有字段在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即可

  • 16
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值