调整COSWriter解决X-easypdf / PDFBOX生成大量数据时OOM问题

背景

业务需要生成一个15W数据左右的PDF交易报表。希望我们写在一个文件里,不拆分成多个PDF文件。

使用的技术组件

        <dependency>
            <groupId>wiki.xsx</groupId>
            <artifactId>x-easypdf-pdfbox</artifactId>
            <version>2.11.10</version>
        </dependency>
    

生成PDF方法

testPDF: 使用xeasypdf实现未做修改

testDynamicPdf: 使用了修改后的方法实现

package wiki.xsx.core.pdf.doc;

import org.junit.Test;
import wiki.xsx.core.pdf.component.table.XEasyPdfCell;
import wiki.xsx.core.pdf.component.table.XEasyPdfRow;
import wiki.xsx.core.pdf.component.table.XEasyPdfTable;
import wiki.xsx.core.pdf.component.text.XEasyPdfText;
import wiki.xsx.core.pdf.handler.XEasyPdfHandler;
import wiki.xsx.core.pdf.mark.XEasyPdfWatermark;

public class XEasyPdfDynamicTest {

    public static final int GENERATE_PAGE = 10000;

    @Test
    //原生办法,最好别执行,会内存溢出。
    public void testPdf() {
        // 定义pdf输出路径
        String outputPath = "D://out.pdf";

        XEasyPdfText titleText = XEasyPdfHandler.Text.build("明细");
        titleText.setHorizontalStyle(XEasyPdfPositionStyle.CENTER);
        titleText.setFontSize(32);
        titleText.setMarginTop(15);
        XEasyPdfWatermark watermark = XEasyPdfHandler.Watermark.build("账单");
        // 如果需要动态加Page,需要使用定制的对象;
        XEasyPdfDocument document = XEasyPdfHandler.Document.build();
        document.setGlobalHeader(XEasyPdfHandler.Header.build(titleText));
        document.setGlobalWatermark(watermark);

        int[] cellWidth = {130, 80, 80, 262};

        for (int current = 0; current < GENERATE_PAGE; current++) {
            XEasyPdfPage xEasyPdfPage = generatePage(current, cellWidth);
            document.addPage(xEasyPdfPage);
        }
        document.save(outputPath).close();
    }

    @Test
    public void testDynamicPdf() {
        // 定义pdf输出路径
        String outputPath = "D://out.pdf";

        XEasyPdfText titleText = XEasyPdfHandler.Text.build("明细");
        titleText.setHorizontalStyle(XEasyPdfPositionStyle.CENTER);
        titleText.setFontSize(32);
        titleText.setMarginTop(15);
        XEasyPdfWatermark watermark = XEasyPdfHandler.Watermark.build("账单");
        // 如果需要动态加Page,需要使用定制的对象;
        XEasyPdfDynamicPdfDocument document = new XEasyPdfDynamicPdfDocument();
        document.setGlobalHeader(XEasyPdfHandler.Header.build(titleText));
        document.setGlobalWatermark(watermark);

        int[] cellWidth = {130, 80, 80, 262};

        for (int current = 1; current <= GENERATE_PAGE; current++) {
            XEasyPdfPage xEasyPdfPage = generatePage(current, cellWidth);
            document.addPage(xEasyPdfPage);
            if (current % 100 == 0) {
                document.flush();
            }
        }
        document.dynamicSave(outputPath, new XEasyPdfDynamicPage(10000, document)).close();
    }

    public static XEasyPdfPage generatePage(long current, int[] cellWidth) {
        // 这里构建一下页数;
        XEasyPdfTable table = XEasyPdfHandler.Table.build();
        XEasyPdfPage page = XEasyPdfHandler.Page.build();

        table.setMarginTop(30);
        table.setMarginLeft(20);
        table.enableCenterStyle();

        XEasyPdfRow headRow = XEasyPdfHandler.Table.Row.build();
        XEasyPdfCell headCell1 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[0]);
        headCell1.addContent(XEasyPdfHandler.Text.build("卡号"));
        XEasyPdfCell headCell2 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[1]);
        headCell2.addContent(XEasyPdfHandler.Text.build("下标"));
        XEasyPdfCell headCell3 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[2]);
        headCell3.addContent(XEasyPdfHandler.Text.build("金额"));
        XEasyPdfCell headCell4 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[3]);
        headCell4.addContent(XEasyPdfHandler.Text.build("描述"));
        headRow.addCell(headCell1, headCell2, headCell3, headCell4);

        table.addRow(headRow);
        page.addComponent(table);
        for (int i = 0; i < 14; i++) {
            // 14行一页;
            XEasyPdfRow row = XEasyPdfHandler.Table.Row.build();
            row.setHeight(50);
            XEasyPdfCell cell1 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[0]);
            cell1.addContent(XEasyPdfHandler.Text.build("123456"));
            XEasyPdfCell cell2 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[1]);
            cell2.addContent(XEasyPdfHandler.Text.build("j-" + current + ":i-" + i));
            XEasyPdfCell cell3 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[2]);
            cell3.addContent(XEasyPdfHandler.Text.build("20.1"));
            XEasyPdfCell cell4 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[3]);
            cell4.addContent(XEasyPdfHandler.Text.build("说明"));
            row.addCell(cell1, cell2, cell3, cell4);
            table.addRow(row);
        }
        return page;
    }
}

testPdf执行情况

Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: Java heap space
	at java.base/java.security.AccessController.wrapException(AccessController.java:828)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:716)
	at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$176/0x000001bb3a9bd290.run(Unknown Source)
	at java.base/java.security.AccessController.executePrivileged(AccessController.java:776)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)

java.lang.OutOfMemoryError: Java heap space
11月 16, 2023 4:15:07 下午 org.apache.pdfbox.cos.COSDocument finalize
警告: Warning: You did not close a PDF Document

Process finished with exit code -1

从JVM监控可以看出CPU与内存占用会随着PDF文件写入而逐渐增大。【很正常,因为他无法释放内存】

testDynamicPdf运行情况

源代码

基于源码fork的仓库地址【源码我没权限改,所以fork了一个】:

x-easypdf: 一个用搭积木的方式构建pdf的框架(基于pdfbox/fop)icon-default.png?t=N7T8https://gitee.com/crazyAsm/x-easypdf分支:FEATURE_Dynamic_Generate

OOM原因

超过1万页的数据,使用原版的COSWriter类会占用大量内存。

COSWriter在写文件时,会使用doWriterBody方法写入PDF的基础信息。如下:

protected void doWriteBody(COSDocument doc) throws IOException
    {
        COSDictionary trailer = doc.getTrailer();
        COSDictionary root = trailer.getCOSDictionary(COSName.ROOT);
        COSDictionary info = trailer.getCOSDictionary(COSName.INFO);
        COSDictionary encrypt = trailer.getCOSDictionary(COSName.ENCRYPT);
        if( root != null )
        {
            addObjectToWrite( root );
        }
        if( info != null )
        {
            addObjectToWrite( info );
        }

        doWriteObjects();
        willEncrypt = false;
        if( encrypt != null )
        {
            addObjectToWrite( encrypt );
        }

        doWriteObjects();
    }

可以看到会写入的信息有root、基础信息、与加密信息【因为这个不咋占内存,这里就不展开说明了】;然后会执行doWriteObjects();

 第一次写入时可以看出,写的是Type\Version\Page\MetaData这四个信息;

分别对应PDF文件内容的Type\Version\Page\MetaData:f

根据PDF的规则,实际Page栏的4 0 R 代表 第一页对应内容在4 0 obj 位置,有多少页Page就会有多少个引用键。4 0 obj 对应的是第一页的内容,内容又是由一堆引用键组成的。COSWriter的问题也就在这里,只要页数够大,内容够多,这里就会占用大量内存。

解决思路

既然内存占用原因是写入时在内存中存放了太多的内容,那么解决思路也就很容易得出来:一页一页写就行了。

因为我用的事X-EasyPdf 所以基于这个改造了一下。【源码自己看下git仓库吧】

XEasyPdfDynamicCOSWriter:基于COSWriter改造的类目的:在doWriteObjet时,动态加载Page并写入;
XEasyPdfDynamicPage:动态页的实现,结合XEasyPDFDocument的flush方法,借助临时文件增量写页内容。
XEasyPdfDynamicPdfDocument:增加了个实现,写文件改用XEasyPdfDynamicCOSWriter类。

参考文章

https://zxyle.github.io/PDF-Explained/resources/pdf_reference_1.7.pdf

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/160774.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python中的字典

字典&#xff1a; 1.字典是一种可变容器模型&#xff0c;可以存储任意类型的对象&#xff0c;比如字符串&#xff0c;数字&#xff0c;元组等其他容 器模型 形式&#xff1a; d{key1&#xff1a;value1&#xff0c;key2&#xff1a;value2} 解释&#xff1a; 1.其中key1代表一…

如何使用Gitlab搭建属于自己的代码管理平台

大家好&#xff0c;我是Mandy。今天分享的主题内容是如何使用GitLab搭建属于自己的代码管理平台。 为什么会单独分享这篇文章呢&#xff0c;相信在很多的开发同学任职的公司中&#xff0c;都用到了gitlab来做代码管理平台&#xff0c;同时结合GitLab的一些自动化功能&#xff…

智慧能源太阳能光伏数据采集终端钡铼技术4G无线RTU

智慧能源太阳能光伏系统在当今的能源行业中扮演着越来越重要的角色&#xff0c;而钡铼技术有限公司的4G无线RTU&#xff08;远程终端单元&#xff09;作为数据采集终端&#xff0c;为智慧能源太阳能光伏系统的监测和管理提供了全新的解决方案。 首先&#xff0c;钡铼技术的4G无…

Linux基本指令(一)

前言&#xff1a;我们今天换个口味&#xff0c;我们来学习Linux&#xff0c;我们平时电脑上都只使用windows系统&#xff0c;但是作为后来者&#xff0c;Linux有着windows所没有的优点&#xff0c;那么我们今天就来学习Linux的一些基本指令。我会通过Xshell和阿里云云服务器进行…

振南技术干货集:比萨斜塔要倒了,倾斜传感器快来!(5)

注解目录 1、倾斜传感器的那些基础干货 1.1 典型应用场景 &#xff08;危楼、边坡、古建筑都是对倾斜敏感的。&#xff09; 1.2 倾斜传感器的原理 1.2.1 滚珠式倾斜开关 1.2.2 加速度式倾斜传感器 1)直接输出倾角 2)加速度计算倾角 3)倾角精度的提高 &#xff08;如果…

NC65 如何设置现金流量明细查询的查询框中核算账簿可多选??

NC65 如何设置现金流量明细查询的查询框中核算账簿可多选&#xff1f;&#xff1f; NC65 如何设置现金流量明细查询的查询框中核算账簿可多选&#xff1f;&#xff1f;效果如下图 解决方案二开&#xff0c;即在 nc.ui.gl.cashflowcase.CashFlowDetailQueryUI 的 onButtonQuer…

周年纪念篇

一周年纪念&#xff01; 凌晨逛手机版csdn时才突然发现已经错过一周年了&#xff0c;但我当闰年来纪念一下不过分吧hhh 浅浅的整些怀念的东西吧&#xff01; 这是人生第一段代码&#xff1a;不是hello world写不起&#xff0c;而是纯爱单推人更有性价比。 有这段代码在&#x…

【汇编】Loop指令、段前缀

文章目录 前言一、Loop指令1.1 Loop指令是什么&#xff1f;1.2 他的条件是什么&#xff1f;1.3 例子示例1示例2 1.4 要点总结 二、段前缀2.1 为什么要引入他2.2 对策 总结 前言 在计算机编程的世界里&#xff0c;了解底层的硬件操作是提升程序员能力的关键一步。汇编语言作为一…

【用unity实现100个游戏之15】开发一个类保卫萝卜的Unity2D塔防游戏5(附项目源码,完结)

文章目录 最终效果前言简单绘制一下环境显示当前波数生成不同的敌人控制游戏运行速度游戏结束最终效果扩展源码完结最终效果 前言 本期是本项目的最后一篇,主要内容是配置环境、生成不同敌人、结束重开。 简单绘制一下环境 环境可以按自己喜好,去找一些瓦片,想怎么配置怎…

基于热交换算法优化概率神经网络PNN的分类预测 - 附代码

基于热交换算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于热交换算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于热交换优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…

电脑技巧:低配置的旧电脑也可以使用Win11系统了

目录 一、优点和适用场景 二、下载以及版本说明 三、安装说明 四、Tiny11 设置安装简体中文语言方法 Windows 11&#xff0c;作为微软最新一代操作系统&#xff0c;固然有其强大的功能和卓越的性能&#xff0c;但有时&#xff0c;更轻、更小、更快速的需求也在用户的考虑范…

LLM大模型权重量化实战

大型语言模型 (LLM) 以其广泛的计算要求而闻名。 通常&#xff0c;模型的大小是通过将参数数量&#xff08;大小&#xff09;乘以这些值的精度&#xff08;数据类型&#xff09;来计算的。 然而&#xff0c;为了节省内存&#xff0c;可以通过称为量化的过程使用较低精度的数据类…

某app c++层3处魔改md5详解

hello everybody,本期是安卓逆向so层魔改md5教学,干货满满,可以细细品味,重点介绍的是so层魔改md5的处理. 常见的魔改md5有: 1:明文加密前处理 2:改初始化魔数 3:改k表中的值 4:改循环左移的次数 本期遇到的是124.且循环左移的次数是动态的,需要前面的加密结果处理生成 目录…

[C/C++]数据结构 链表(单向链表,双向链表)

前言: 上一文中我们介绍了顺序表的特点及实现,但是顺序表由于每次扩容都是呈二倍增长(扩容大小是自己定义的),可能会造成空间的大量浪费,但是链表却可以解决这个问题. 概念及结构: 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接…

halcon识别验证码,先训练后识别

识别验证码图片&#xff0c;使用halcon 21.05 下面代码识别准确率100% 目录 训练&#xff0c;图片打标签使用代码创建分类器&#xff1b;识别验证码&#xff0c;检验识别效果使用“助手”加载训练文件&#xff0c;加载训练分类器&#xff0c;察看收集的字符&#xff0c;训练识别…

Theory behind GAN

假如要生成一些人脸图&#xff0c;实际上就是想要找到一个分布&#xff0c;从这个分布内sample出来的图片像是人脸&#xff0c;分布之外生成的就不像人脸。而GAN要做的就是找到这个distribution。 在GAN之前用的是Maximum Likelihood Estimation。 Maximum Likelihood Estimat…

【C++】类和对象(5)--运算符重载

目录 一 概念 二 运算符重载的实现 三 关于时间的所有运算符重载 四 默认赋值运算符 五 const取地址操作符重载 一 概念 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也具有其返回值类型&#xff0c;函数名字以及参数…

Android 13.0 Launcher3仿ios长按app图标实现抖动动画开始拖拽停止动画

1.概述 在13.0的系统rom定制化开发中,在对系统原生Launcher3的定制需求中,也有好多功能定制的,在ios等电子产品中 的一些好用的功能,也是可以被拿来借用的,所以在最近的产品开发需求中,需求要求模仿ios的 功能实现长按app图标实现抖动动画,接下来看如何分析该功能的实现…

基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码

基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于静电放电算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于静电放电优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…