业务需要生成一个15W数据左右的PDF交易报表。希望我们写在一个文件里,不拆分成多个PDF文件。
- <dependency>
- <groupId>wiki.xsxgroupId>
- <artifactId>x-easypdf-pdfboxartifactId>
- <version>2.11.10version>
- dependency>
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;
- }
- }
- 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文件写入而逐渐增大。【很正常,因为他无法释放内存】
基于源码fork的仓库地址【源码我没权限改,所以fork了一个】:
x-easypdf: 一个用搭积木的方式构建pdf的框架(基于pdfbox/fop)https://gitee.com/crazyAsm/x-easypdf分支:FEATURE_Dynamic_Generate
超过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