调整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

x-easypdf基于pdfbox构建而来,极大降低使用门槛,以组件化的形式进行pdf的构建。简单易用,仅需一行代码,便可完成pdf的操作。 x-easypdf特性: 1、轻量级 仅添加pdfbox相关依赖,无其他任何依赖 2、简单易用 仅需一行代码,便可完成pdf的操作 3、自动换行分页 文本超出单行显示,即可自动换行;内容超出单页显示,即可自动分页 4、模板填充 提供内置方法,可轻松实现模板填充 5、组件化 页面所有内容均采用组件化形式进行构建,使用不同的组件组合方式,即可构造出理想的文档 6、扩展灵活 只需实现系统提供的接口,即可完成自定义的组件扩展 x-easypdf软件架构: 1、document(文档):PDF文档 2、page(页面):若干个页面组成PDF文档 3、watermark(水印):每个页面可设置页面级别的独立水印,也可设置文档级别的全局水印,优先级为:页面级别>文档级别 4、header(页眉):每个页面可设置页面级别的独立页眉,也可设置文档级别的全局页眉,优先级为:页面级别>文档级别 5、footer(页脚):每个页面可设置页面级别的独立页脚,也可设置文档级别的全局页脚,优先级为:页面级别>文档级别 6、component(组件):核心,每个页面由若干个组件构成 text(文本组件):已提供,文本写入组件 line(线条组件):已提供,线条写入组件 image(图片组件):已提供,图片写入组件 table(表格组件):已提供,表格写入组件,cell(单元格)->row(行)->table(表格) rect(方形组件):已提供,方形写入组件 后续将添加更多其他方便实用的组件。。。 x-easypdf安装教程: mvn clean install   x-easypdf 更新日志: v2.2.0 新特性: 1、新增文档改变页面尺寸方法 2、新增文档提取器简单表格的文本内容提取(单行单列) 3、表格组件功能增强,已支持添加图片与文本,更灵活的表格定义 原有变更: 1、移除XEasyPdfUtil工具类 2、文档XEasyPdfDocument#image方法变更为imager,返回值变为XEasyPdfDocumentImager(文档图像器) 3、文档操作完毕,需手动关闭文档(调用close方法关闭) 问题修复: 1、修复字体错误问题(issue#I2BGJ1,issue#I2BGM3)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值