背景
业务需要生成一个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)https://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