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