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


    背景

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

    使用的技术组件

    1. <dependency>
    2. <groupId>wiki.xsxgroupId>
    3. <artifactId>x-easypdf-pdfboxartifactId>
    4. <version>2.11.10version>
    5. dependency>

    生成PDF方法

    testPDF: 使用xeasypdf实现未做修改

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

    1. package wiki.xsx.core.pdf.doc;
    2. import org.junit.Test;
    3. import wiki.xsx.core.pdf.component.table.XEasyPdfCell;
    4. import wiki.xsx.core.pdf.component.table.XEasyPdfRow;
    5. import wiki.xsx.core.pdf.component.table.XEasyPdfTable;
    6. import wiki.xsx.core.pdf.component.text.XEasyPdfText;
    7. import wiki.xsx.core.pdf.handler.XEasyPdfHandler;
    8. import wiki.xsx.core.pdf.mark.XEasyPdfWatermark;
    9. public class XEasyPdfDynamicTest {
    10. public static final int GENERATE_PAGE = 10000;
    11. @Test
    12. //原生办法,最好别执行,会内存溢出。
    13. public void testPdf() {
    14. // 定义pdf输出路径
    15. String outputPath = "D://out.pdf";
    16. XEasyPdfText titleText = XEasyPdfHandler.Text.build("明细");
    17. titleText.setHorizontalStyle(XEasyPdfPositionStyle.CENTER);
    18. titleText.setFontSize(32);
    19. titleText.setMarginTop(15);
    20. XEasyPdfWatermark watermark = XEasyPdfHandler.Watermark.build("账单");
    21. // 如果需要动态加Page,需要使用定制的对象;
    22. XEasyPdfDocument document = XEasyPdfHandler.Document.build();
    23. document.setGlobalHeader(XEasyPdfHandler.Header.build(titleText));
    24. document.setGlobalWatermark(watermark);
    25. int[] cellWidth = {130, 80, 80, 262};
    26. for (int current = 0; current < GENERATE_PAGE; current++) {
    27. XEasyPdfPage xEasyPdfPage = generatePage(current, cellWidth);
    28. document.addPage(xEasyPdfPage);
    29. }
    30. document.save(outputPath).close();
    31. }
    32. @Test
    33. public void testDynamicPdf() {
    34. // 定义pdf输出路径
    35. String outputPath = "D://out.pdf";
    36. XEasyPdfText titleText = XEasyPdfHandler.Text.build("明细");
    37. titleText.setHorizontalStyle(XEasyPdfPositionStyle.CENTER);
    38. titleText.setFontSize(32);
    39. titleText.setMarginTop(15);
    40. XEasyPdfWatermark watermark = XEasyPdfHandler.Watermark.build("账单");
    41. // 如果需要动态加Page,需要使用定制的对象;
    42. XEasyPdfDynamicPdfDocument document = new XEasyPdfDynamicPdfDocument();
    43. document.setGlobalHeader(XEasyPdfHandler.Header.build(titleText));
    44. document.setGlobalWatermark(watermark);
    45. int[] cellWidth = {130, 80, 80, 262};
    46. for (int current = 1; current <= GENERATE_PAGE; current++) {
    47. XEasyPdfPage xEasyPdfPage = generatePage(current, cellWidth);
    48. document.addPage(xEasyPdfPage);
    49. if (current % 100 == 0) {
    50. document.flush();
    51. }
    52. }
    53. document.dynamicSave(outputPath, new XEasyPdfDynamicPage(10000, document)).close();
    54. }
    55. public static XEasyPdfPage generatePage(long current, int[] cellWidth) {
    56. // 这里构建一下页数;
    57. XEasyPdfTable table = XEasyPdfHandler.Table.build();
    58. XEasyPdfPage page = XEasyPdfHandler.Page.build();
    59. table.setMarginTop(30);
    60. table.setMarginLeft(20);
    61. table.enableCenterStyle();
    62. XEasyPdfRow headRow = XEasyPdfHandler.Table.Row.build();
    63. XEasyPdfCell headCell1 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[0]);
    64. headCell1.addContent(XEasyPdfHandler.Text.build("卡号"));
    65. XEasyPdfCell headCell2 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[1]);
    66. headCell2.addContent(XEasyPdfHandler.Text.build("下标"));
    67. XEasyPdfCell headCell3 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[2]);
    68. headCell3.addContent(XEasyPdfHandler.Text.build("金额"));
    69. XEasyPdfCell headCell4 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[3]);
    70. headCell4.addContent(XEasyPdfHandler.Text.build("描述"));
    71. headRow.addCell(headCell1, headCell2, headCell3, headCell4);
    72. table.addRow(headRow);
    73. page.addComponent(table);
    74. for (int i = 0; i < 14; i++) {
    75. // 14行一页;
    76. XEasyPdfRow row = XEasyPdfHandler.Table.Row.build();
    77. row.setHeight(50);
    78. XEasyPdfCell cell1 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[0]);
    79. cell1.addContent(XEasyPdfHandler.Text.build("123456"));
    80. XEasyPdfCell cell2 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[1]);
    81. cell2.addContent(XEasyPdfHandler.Text.build("j-" + current + ":i-" + i));
    82. XEasyPdfCell cell3 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[2]);
    83. cell3.addContent(XEasyPdfHandler.Text.build("20.1"));
    84. XEasyPdfCell cell4 = XEasyPdfHandler.Table.Row.Cell.build(cellWidth[3]);
    85. cell4.addContent(XEasyPdfHandler.Text.build("说明"));
    86. row.addCell(cell1, cell2, cell3, cell4);
    87. table.addRow(row);
    88. }
    89. return page;
    90. }
    91. }

    testPdf执行情况

    1. Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: Java heap space
    2. at java.base/java.security.AccessController.wrapException(AccessController.java:828)
    3. at java.base/java.security.AccessController.doPrivileged(AccessController.java:716)
    4. at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
    5. at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
    6. at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
    7. at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
    8. at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$176/0x000001bb3a9bd290.run(Unknown Source)
    9. at java.base/java.security.AccessController.executePrivileged(AccessController.java:776)
    10. at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    11. at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
    12. at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    13. at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    14. at java.base/java.lang.Thread.run(Thread.java:833)
    15. java.lang.OutOfMemoryError: Java heap space
    16. 1116, 2023 4:15:07 下午 org.apache.pdfbox.cos.COSDocument finalize
    17. 警告: Warning: You did not close a PDF Document
    18. 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的基础信息。如下:

    1. protected void doWriteBody(COSDocument doc) throws IOException
    2. {
    3. COSDictionary trailer = doc.getTrailer();
    4. COSDictionary root = trailer.getCOSDictionary(COSName.ROOT);
    5. COSDictionary info = trailer.getCOSDictionary(COSName.INFO);
    6. COSDictionary encrypt = trailer.getCOSDictionary(COSName.ENCRYPT);
    7. if( root != null )
    8. {
    9. addObjectToWrite( root );
    10. }
    11. if( info != null )
    12. {
    13. addObjectToWrite( info );
    14. }
    15. doWriteObjects();
    16. willEncrypt = false;
    17. if( encrypt != null )
    18. {
    19. addObjectToWrite( encrypt );
    20. }
    21. doWriteObjects();
    22. }

    可以看到会写入的信息有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

  • 相关阅读:
    使用easyx制作一个绘画程序
    JVM内存区域划分
    mac连接easyconnnect显示“本地环境出现异常”
    什么是短网址?如何调用接口生成短地址?
    JavaWeb简单实例——DBCP数据库连接池
    【LeetCode】35.复杂链表的复制
    ArcGIS 10.7之 栅格影像裁剪操作
    Go 中的方法
    一文搞懂 MySQL 索引数据结构
    多校联测13 菜
  • 原文地址:https://blog.csdn.net/u013205724/article/details/134443649