|
@@ -21,10 +21,25 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
|
|
|
private final Playwright playwright;
|
|
private final Playwright playwright;
|
|
|
private final Browser browser;
|
|
private final Browser browser;
|
|
|
private final double renderTimeoutMillis;
|
|
private final double renderTimeoutMillis;
|
|
|
|
|
+ private final boolean pageReuseEnabled;
|
|
|
|
|
+ private final int pageMaxRenderCount;
|
|
|
|
|
+ private BrowserContext reusableContext;
|
|
|
|
|
+ private Page reusablePage;
|
|
|
|
|
+ private int reusablePageRenderCount;
|
|
|
private boolean closed;
|
|
private boolean closed;
|
|
|
|
|
|
|
|
DefaultPlaywrightPdfWorker(double launchTimeoutMillis, double renderTimeoutMillis) {
|
|
DefaultPlaywrightPdfWorker(double launchTimeoutMillis, double renderTimeoutMillis) {
|
|
|
|
|
+ this(launchTimeoutMillis, renderTimeoutMillis, true, 200);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DefaultPlaywrightPdfWorker(
|
|
|
|
|
+ double launchTimeoutMillis,
|
|
|
|
|
+ double renderTimeoutMillis,
|
|
|
|
|
+ boolean pageReuseEnabled,
|
|
|
|
|
+ int pageMaxRenderCount) {
|
|
|
this.renderTimeoutMillis = renderTimeoutMillis;
|
|
this.renderTimeoutMillis = renderTimeoutMillis;
|
|
|
|
|
+ this.pageReuseEnabled = pageReuseEnabled;
|
|
|
|
|
+ this.pageMaxRenderCount = validatePageMaxRenderCount(pageMaxRenderCount);
|
|
|
|
|
|
|
|
Playwright initializedPlaywright = null;
|
|
Playwright initializedPlaywright = null;
|
|
|
Browser initializedBrowser = null;
|
|
Browser initializedBrowser = null;
|
|
@@ -46,6 +61,14 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
|
|
|
Objects.requireNonNull(htmlContent, "htmlContent");
|
|
Objects.requireNonNull(htmlContent, "htmlContent");
|
|
|
ensureOpen();
|
|
ensureOpen();
|
|
|
|
|
|
|
|
|
|
+ if (pageReuseEnabled) {
|
|
|
|
|
+ return generateWithReusablePage(htmlContent);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return generateWithDedicatedContext(htmlContent);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private byte[] generateWithDedicatedContext(String htmlContent) {
|
|
|
BrowserContext context = null;
|
|
BrowserContext context = null;
|
|
|
try {
|
|
try {
|
|
|
long stageStartedNanos = System.nanoTime();
|
|
long stageStartedNanos = System.nanoTime();
|
|
@@ -93,6 +116,97 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private byte[] generateWithReusablePage(String htmlContent) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Page page = reusablePage();
|
|
|
|
|
+ long stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ page.emulateMedia(new Page.EmulateMediaOptions().setMedia(Media.PRINT));
|
|
|
|
|
+ logStageCompleted("设置打印媒体", stageStartedNanos);
|
|
|
|
|
+
|
|
|
|
|
+ stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ page.setContent(htmlContent, new Page.SetContentOptions()
|
|
|
|
|
+ .setWaitUntil(WaitUntilState.LOAD)
|
|
|
|
|
+ .setTimeout(renderTimeoutMillis));
|
|
|
|
|
+ logStageCompleted("设置HTML内容", stageStartedNanos);
|
|
|
|
|
+
|
|
|
|
|
+ stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ page.evaluate("() => document.fonts ? document.fonts.ready.then(() => true) : true");
|
|
|
|
|
+ logStageCompleted("等待字体加载完成", stageStartedNanos);
|
|
|
|
|
+
|
|
|
|
|
+ stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ byte[] pdfBytes = page.pdf(new Page.PdfOptions()
|
|
|
|
|
+ .setFormat("A4")
|
|
|
|
|
+ .setPrintBackground(true)
|
|
|
|
|
+ .setPreferCSSPageSize(true)
|
|
|
|
|
+ .setMargin(new Margin()
|
|
|
|
|
+ .setTop("0")
|
|
|
|
|
+ .setRight("0")
|
|
|
|
|
+ .setBottom("0")
|
|
|
|
|
+ .setLeft("0")));
|
|
|
|
|
+ logStageCompleted("生成PDF文件", stageStartedNanos);
|
|
|
|
|
+ reusablePageRenderCount++;
|
|
|
|
|
+ return pdfBytes;
|
|
|
|
|
+ } catch (Exception exception) {
|
|
|
|
|
+ rebuildReusablePage("failure");
|
|
|
|
|
+ throw new IllegalStateException("Failed to generate PDF", exception);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Page reusablePage() {
|
|
|
|
|
+ if (reusablePage == null) {
|
|
|
|
|
+ createReusablePage("initial");
|
|
|
|
|
+ } else if (reusablePageRenderCount >= pageMaxRenderCount) {
|
|
|
|
|
+ rebuildReusablePage("max_render_count");
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean pageReused = reusablePageRenderCount > 0;
|
|
|
|
|
+ log.info(
|
|
|
|
|
+ "PDF页面已准备 pageReuseEnabled={} pageReused={} pageRenderCount={} pageMaxRenderCount={}",
|
|
|
|
|
+ true,
|
|
|
|
|
+ pageReused,
|
|
|
|
|
+ reusablePageRenderCount,
|
|
|
|
|
+ pageMaxRenderCount);
|
|
|
|
|
+ return reusablePage;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void createReusablePage(String reason) {
|
|
|
|
|
+ long stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ reusableContext = browser.newContext(new Browser.NewContextOptions().setLocale("zh-CN"));
|
|
|
|
|
+ logStageCompleted("创建浏览器上下文", stageStartedNanos);
|
|
|
|
|
+ reusableContext.setDefaultTimeout(renderTimeoutMillis);
|
|
|
|
|
+ reusableContext.setDefaultNavigationTimeout(renderTimeoutMillis);
|
|
|
|
|
+
|
|
|
|
|
+ stageStartedNanos = System.nanoTime();
|
|
|
|
|
+ reusablePage = reusableContext.newPage();
|
|
|
|
|
+ reusablePageRenderCount = 0;
|
|
|
|
|
+ logStageCompleted("创建页面", stageStartedNanos);
|
|
|
|
|
+ log.info(
|
|
|
|
|
+ "PDF复用页面已创建 reason={} pageReuseEnabled={} pageRenderCount={} pageMaxRenderCount={}",
|
|
|
|
|
+ reason,
|
|
|
|
|
+ true,
|
|
|
|
|
+ reusablePageRenderCount,
|
|
|
|
|
+ pageMaxRenderCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void rebuildReusablePage(String reason) {
|
|
|
|
|
+ closeReusablePage();
|
|
|
|
|
+ if (!closed) {
|
|
|
|
|
+ log.info(
|
|
|
|
|
+ "PDF复用页面已重建 reason={} pageReuseEnabled={} pageRenderCount={} pageMaxRenderCount={}",
|
|
|
|
|
+ reason,
|
|
|
|
|
+ true,
|
|
|
|
|
+ reusablePageRenderCount,
|
|
|
|
|
+ pageMaxRenderCount);
|
|
|
|
|
+ createReusablePage(reason);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void closeReusablePage() {
|
|
|
|
|
+ closeQuietly(reusableContext);
|
|
|
|
|
+ reusableContext = null;
|
|
|
|
|
+ reusablePage = null;
|
|
|
|
|
+ reusablePageRenderCount = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Override
|
|
@Override
|
|
|
public synchronized void close() {
|
|
public synchronized void close() {
|
|
|
if (closed) {
|
|
if (closed) {
|
|
@@ -101,10 +215,19 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
|
|
|
|
|
|
|
|
RuntimeException closeFailure = null;
|
|
RuntimeException closeFailure = null;
|
|
|
try {
|
|
try {
|
|
|
- browser.close();
|
|
|
|
|
|
|
+ closeReusablePage();
|
|
|
} catch (RuntimeException exception) {
|
|
} catch (RuntimeException exception) {
|
|
|
closeFailure = exception;
|
|
closeFailure = exception;
|
|
|
}
|
|
}
|
|
|
|
|
+ try {
|
|
|
|
|
+ browser.close();
|
|
|
|
|
+ } catch (RuntimeException exception) {
|
|
|
|
|
+ if (closeFailure == null) {
|
|
|
|
|
+ closeFailure = exception;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ closeFailure.addSuppressed(exception);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
playwright.close();
|
|
playwright.close();
|
|
|
} catch (RuntimeException exception) {
|
|
} catch (RuntimeException exception) {
|
|
@@ -132,6 +255,13 @@ final class DefaultPlaywrightPdfWorker implements PlaywrightPdfWorker {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private int validatePageMaxRenderCount(int pageMaxRenderCount) {
|
|
|
|
|
+ if (pageMaxRenderCount < 1) {
|
|
|
|
|
+ throw new IllegalArgumentException("pageMaxRenderCount must be at least 1");
|
|
|
|
|
+ }
|
|
|
|
|
+ return pageMaxRenderCount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static void closeQuietly(AutoCloseable closeable) {
|
|
private static void closeQuietly(AutoCloseable closeable) {
|
|
|
if (closeable == null) {
|
|
if (closeable == null) {
|
|
|
return;
|
|
return;
|