调整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();}@Testpublic 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 spaceat 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 DocumentProcess 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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux通过端口号找到对应的服务及其安装位置

Linux服务器中&#xff0c;通过端口号找到对应的服务及其安装位置&#xff0c;需要两步操作&#xff0c;如下&#xff1a; 第一步&#xff1a;根据端口号&#xff0c;确定对应的进程号&#xff08;以redis服务为例&#xff09; netstat -antup|grep 6379第二步&#xff1a;通…

Zabbix5.0部署

环境 主机名 IP 类型server01192.168.134.165zabbix-serverserver02 192.168.134.166zabbix-agent 官方部署文档 1 .安装yum源 [rootserver01 ~]# rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-rel…

cp: can‘t stat ‘/usr/share/zoneinfo/Asia/Shanghai‘: No such file or directory

目录 问题描述问题分析解决方案容器时区验证 问题描述 使用下面的 Dockerfile 为 youlai-boot 项目制作镜像设置容器时区报错。 # 基础镜像 FROM openjdk:17-jdk-alpine # 时区修改 RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \&& echo Asia/Sha…

JVM:字节码文件,类的生命周期,类加载器

JVM&#xff1a;字节码文件&#xff0c;类的生命周期&#xff0c;类加载器 为什么要学这门课程 1. 初识JVM1.1. 什么是JVM1.2. JVM的功能1.3. 常见的JVM 2. 字节码文件详解2.1. Java虚拟机的组成2.2. 字节码文件的组成2.2.1. 以正确的姿势打开文…

需求管理>需求的变更流程

1.需求的变更流程 一个大型软件系统的需求总是有变化的。为了降低项目开发的风险&#xff0c;需要一个好的变更控制过程。如下图所示为需求变更管理过程。 在需求管理过程中需求的变更是受严格管控的&#xff0c;其流程为&#xff1a; 1、问题分析和变更描述。这是识别和分析需…

进阶理解:leetcode115.不同的子序列(细节深度)

这道题是困难题&#xff0c;本章是针对于动态规划解决&#xff0c;对于思路进行一个全面透彻的讲解&#xff0c;但是并不是对于基础讲解思路&#xff0c;而是渗透到递推式和dp填数的详解&#xff0c;如果有读者不清楚基本的解题思路&#xff0c;请看我的这篇文章算法训练营DAY5…

【LeetCode刷题-滑动窗口】--992.K个不同整数的子数组

992.K个不同整数的子数组 思路&#xff1a; class Solution {public int subarraysWithKDistinct(int[] nums, int k) {return atMostKDistinct(nums,k) - atMostKDistinct(nums,k-1);}//最多包含K个不同整数的子区间个数private int atMostKDistinct(int[] a,int k){int len …

关于苏州立讯公司国产替代案例(使用我公司H82409S网络变压器和E1152E01A-YG网口连接器产品)

关于苏州立讯公司国产替代案例&#xff08;使用我们公司的H82409S网络变压器和E1152E01A-YG网口连接器产品&#xff09; 苏州立讯公司是一家专注于通信设备制造的企业&#xff0c;他们在其产品中选择了我们公司的H82409S网络变压器和E1152E01A-YG网口连接器&#xff0c;以实现…

C++初阶 日期类的实现(上)

目录 一、前置准备 1.1获得每月的天数 1.2获得每年的天数 1.3构造函数&#xff0c;析构函数和拷贝构造函数 二、日期与天数的,-,,-实现 2.1运算符重载 2.2运算符的实现 2.3-运算符的实现 2.4-运算符的实现 三、&#xff0c;--的实现 3.1前置&#xff0c;后置的实现 …

计算机网络秋招面试题

自己在秋招过程中遇到的计算机网络的面试题 OSI七层网络模型 DNS&#xff1a;应用层协议 根据域名查IP地址 DNS查询⽅式有哪些&#xff1f; 递归解析 局部DNS服务器⾃⼰负责向其他DNS服务器进⾏查询&#xff0c;⼀般是先向该域名的根域服务器查询&#xff0c;再由根域名服…

【论文阅读笔记】Supervised Contrastive Learning

【论文阅读笔记】Supervised Contrastive Learning 摘要 自监督批次对比方法扩展到完全监督的环境中&#xff0c;以有效利用标签信息提出两种监督对比损失的可能版本 介绍 交叉熵损失函数的不足之处&#xff0c;对噪声标签的不鲁棒性和可能导致交叉的边际&#xff0c;降低了…

C语言真的需要头文件吗?

C语言真的需要头文件吗&#xff1f; 头文件的作用是什么&#xff1f; 如果你直接定义了函数&#xff0c;当然不需要头文件。 因为调用函数&#xff0c;你得知道函数的参数有多少&#xff0c;都什么类型的&#xff0c;返回值是什么&#xff0c;这样才能调用。最近很多小伙伴找…