SpringBoot導(dǎo)出PDF的完整解決方案!
告別傳統(tǒng)限制,體驗真正的"所見即所得"PDF導(dǎo)出
一、什么是PlayWright?為什么它值得80K GitHub Stars?
PlayWright是微軟開源的現(xiàn)代化瀏覽器自動化工具,你可以理解為它是一個操作瀏覽器的庫,它不僅僅是一個測試框架,更是服務(wù)端Web操作的瑞士軍刀。與Selenium、Puppeteer等工具相比,PlayWright具有以下顛覆性優(yōu)勢:
- 跨瀏覽器原生支持:Chromium、Firefox、WebKit三大引擎
- 多語言支持:Java、Python等
- 自動等待機制:智能處理動態(tài)內(nèi)容加載
- 強大的PDF生成:企業(yè)級排版控制能力
- 高性能并行:現(xiàn)代異步架構(gòu)設(shè)計
但最讓我震撼的是它在服務(wù)端PDF導(dǎo)出方面的卓越表現(xiàn)——它能夠完美執(zhí)行JavaScript,這是傳統(tǒng)方案無法企及的!就跟你在瀏覽器中將網(wǎng)頁渲染完再按住Ctrl+P打印效果一樣的!??!
PlayWright-Java文檔:https://playwright.dev/java/docs/browsers
二、實戰(zhàn)演示:帶JavaScript的動態(tài)網(wǎng)頁導(dǎo)出PDF
讓我們通過一個完整的示例,使用PlayWright-Java展示PlayWright如何處理包含復(fù)雜JavaScript的頁面。
步驟1:安裝PlayWright
第一步:導(dǎo)入Maven依賴:
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.56.0</version>
</dependency>
第二步:安裝瀏覽器: 每個版本的Playwright都需要特定版本的瀏覽器二進制文件才能運行。你需要使用Playwright的CLI來安裝這些瀏覽器。每次發(fā)布時,Playwright 都會更新它支持的瀏覽器版本,使最新的 Playwright 隨時都能支持最新的瀏覽器。這意味著每次更新 Playwright 時,你可能需要重新運行 CLI 命令。install。請參閱第四章常見問題解決。
步驟2:示例頁面:動態(tài)數(shù)據(jù)報表
假設(shè)我們有一個包含圖表、動畫和異步數(shù)據(jù)加載的報表頁面: 這里我創(chuàng)建一個包含js的網(wǎng)頁模板,這里我直接使用模板字符串,很方便,也可以使用FreeMarker/Thymeleaf初步渲染HTML結(jié)構(gòu)再拿到頁面字符串。
public static String getPageContent(Map<String,Object> data){
String content = """
<!DOCTYPE html>
<html>
<head>
<title>銷售報表</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.chart-container {
height: 300px;
margin: 20px 0;
opacity: 0;
transition: opacity 1s;
}
.loaded { opacity: 1; }
</style>
</head>
<body>
<h1>2024年銷售數(shù)據(jù)分析</h1>
<div id="chart1" class="chart-container">
<canvas id="salesChart"></canvas>
</div>
<div id="dynamicContent">正在加載數(shù)據(jù)...</div>
<script>
// 直接把Java數(shù)據(jù)以json格式塞進來,就是這么方便!
const data = %s;
// 模擬異步數(shù)據(jù)加載
setTimeout(() => {
// 動態(tài)生成圖表
const ctx = document.getElementById('salesChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
datasets: [{
label: '銷售額',
data: [120, 190, 300, 500, 200, 300],
backgroundColor: 'rgba(75, 192, 192, 0.6)'
}]
}
});
// 動態(tài)更新內(nèi)容
document.getElementById('dynamicContent').innerHTML = `
<h3>數(shù)據(jù)分析結(jié)果</h3>
<p>最高銷售額:<strong>500萬元</strong>(4月份)</p>
<p>平均月增長:<strong>15%</strong></p>
`;
// 顯示動畫效果
document.getElementById('chart1').classList.add('loaded');
// 設(shè)置頁面就緒標志 - 這是關(guān)鍵!
window.pageReady = true;
}, 2000); // 模擬2秒數(shù)據(jù)加載
</script>
</body>
</html>
""";
/*
* 我們可以直接把json數(shù)據(jù)塞進頁面中,這直接免去了freeMarker模板引擎的工作
* 當然也可以用模板引擎初步渲染html結(jié)構(gòu)。
*/
return String.format(content, JSON.toJSONString(data))
}
這個頁面包含了:
- Chart.js動態(tài)圖表渲染
- CSS動畫效果
- 異步數(shù)據(jù)加載
- DOM動態(tài)更新
- JSON數(shù)據(jù)渲染
步驟3:創(chuàng)建PlayWrightUtil工具類
這個工具類包含兩個方法,一個是創(chuàng)建瀏覽器,一個是打印網(wǎng)頁內(nèi)容
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.Margin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* PlayWright無頭瀏覽器
* 官網(wǎng):https://playwright.dev/java/docs/browsers
*/
@Component
public class PlayWrightUtil {
private final static Logger logger = LoggerFactory.getLogger(PlayWrightUtil.class);
/* 拿到本地瀏覽器路徑 */
@Value("${chrome.path}")
private String CHROME_PATH;
/**
* 創(chuàng)建一個瀏覽器
* @return browser
*/
public Browser getBrowser() {
// 瀏覽器配置參數(shù)中的環(huán)境變量
Map<String, String> env = new HashMap<>();
Playwright playwright = null;
// 配置瀏覽器參數(shù)
BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions();
launchOptions.setHeadless(true);
launchOptions.setSlowMo(1000);
launchOptions.setArgs(Arrays.asList(
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-web-security",
"--disable-blink-features=AutomationControlled"
));
// 獲取本地下載好的瀏覽器
Path chromePath = Paths.get(CHROME_PATH);
// 優(yōu)先使用本地瀏覽器,如果沒找到本地瀏覽器則下載默認瀏覽器
if (Files.exists(chromePath)){
launchOptions.setExecutablePath(chromePath);
logger.info("已使用本地瀏覽器:{}", chromePath);
env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1"); // 設(shè)置為 "1" 以跳過下載瀏覽器
playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env));
}
else {
logger.error("已使用默認下載瀏覽器");
env.put("PLAYWRIGHT_SKIP_BROWSER_GC", "1"); // 移除舊的過時瀏覽器
playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env));
}
// 創(chuàng)建瀏覽器并返回
return playwright.chromium().launch(launchOptions);
}
/**
* 打印網(wǎng)頁內(nèi)容
* @param pageContent 網(wǎng)頁字符串
* @return pdf字節(jié)
*/
public byte[] printPage(String pageContent) {
// 拿到瀏覽器
Browser browser = getBrowser();
// 獲取瀏覽器上下文
BrowserContext context = browser.newContext();
// 創(chuàng)建一個頁面
Page page = context.newPage();
// 設(shè)置超時和重試策略
page.setDefaultTimeout(30000);
page.setDefaultNavigationTimeout(30000);
// 設(shè)置 HTML 內(nèi)容(包含 JavaScript)
page.setContent(pageContent);
// 監(jiān)聽網(wǎng)絡(luò)請求
page.onResponse(response -> {
logger.info("響應(yīng)網(wǎng)頁請求: {} - {}", response.status(), response.url());
});
// 等待 JavaScript 執(zhí)行完成
page.waitForLoadState(LoadState.NETWORKIDLE);
// 可以等待特定的 JavaScript 條件
page.waitForFunction("() => window.pageReady === true");
// 打印PDF
byte[] a4s = page.pdf(new Page.PdfOptions()
.setMargin(new Margin().setLeft("50").setTop("60").setRight("50").setBottom("60"))
.setPrintBackground(true)
.setFormat("A4")
.setPath(null)
.setDisplayHeaderFooter(true)
.setHeaderTemplate("""
<div style='font-size: 10px; margin:0 50px 0 50px; width: 100%; display:flex;justify-content: space-between;align-items:center;">'>
<span>填你自己的東西</span>
<p>生成日期:<span class='date'></span></p>
</div>
"""
)
.setFooterTemplate("""
<div style='font-size: 10px; margin:0 50px 0 50px; width: 100%; display: flex; justify-content: space-between;'>
<span>? xxxxx科技有限公司. 所有權(quán)利保留。</span>"
<span>第 <span class='pageNumber'></span> 頁 / 共 <span class='totalPages'></span> 頁</span>
</div>
"""
)
);
// 關(guān)閉瀏覽器
browser.close();
return a4s;
}
}
步驟4:創(chuàng)建PDFService類
- 本類中只有一個generatePdfAndUploadAsync方法用于異步打印pdf并上傳文件服務(wù)器。無頭瀏覽器打印pdf任務(wù)一般我們使用異步操作。
- PlayWright打印后返回的是byte[]格式數(shù)據(jù),需要轉(zhuǎn)為MutilpartFile文件格式才能上傳,可以自己封裝一個CustomMultipleFile類,這樣就無需導(dǎo)入第三方庫了。
PdfServiceImpl.java
@Service
public class PdfServiceImpl implements PdfService {
private final static Logger logger = LoggerFactory.getLogger(PdfServiceImpl.class);
@Resource
private BizExportService bizExportService;
@Resource
private DevFileApi devFileApi;
@Resource
private PlayWrightUtil playWrightUtil;
/**
* 生成PDF文件并上傳
*
* @param pageContent 網(wǎng)頁內(nèi)容
* @param exportId 導(dǎo)出任務(wù)ID
*/
@Async("taskExecutor")
@Override
public void generatePdfAndUploadAsync(String pageContent, String exportId) {
BizExport bizExport = bizExportService.queryEntity(exportId);
try {
// 傳入網(wǎng)頁字符串開始打印
byte[] bytes = playWrightUtil.printPage(pageContent);
logger.info("打印成功");
String fileName = bizExport.getExportId() + ".pdf";
// 構(gòu)造MultipartFile文件并上傳
MultipartFile multipartFile = new CustomMultipartFile(bytes, fileName,"application/octet-stream");
// 將文件上傳到Minio并返回文件URL
String fileUrl = devFileApi.storageFileWithReturnUrlMinio(multipartFile);
logger.info("上傳成功,文件地址:{}",fileUrl);
// 更新數(shù)據(jù)(這里根據(jù)自己的業(yè)務(wù)進行調(diào)整)
bizExport.setFileUrl(fileUrl);
bizExport.setStatus(BizExportStatusEnum.SUCCESS.getValue());
bizExportService.updateById(bizExport);
logger.info("文件已導(dǎo)出完成,請查看下載");
}catch (Exception e){
// 更新數(shù)據(jù)(這里根據(jù)自己的業(yè)務(wù)進行調(diào)整)
bizExport.setStatus(BizExportStatusEnum.FAILED.getValue());
bizExportService.updateById(bizExport);
logger.error("導(dǎo)出PDF任務(wù)執(zhí)行失敗,任務(wù)ID:{}", bizExport.getExportId());
throw new CommonException("導(dǎo)出PDF任務(wù)執(zhí)行失敗,任務(wù)ID:{}", bizExport.getExportId());
}
}
}
CustomMultiplartFile.java
public class CustomMultipartFile implements MultipartFile {
private final byte[] fileContent;
private final String originalFilename;
private final String contentType;
public CustomMultipartFile(byte[] fileContent, String originalFilename, String contentType) {
this.fileContent = fileContent != null ? fileContent : new byte[0];
this.originalFilename = originalFilename;
this.contentType = contentType;
}
@Override
public String getName() {
return "file";
}
@Override
public String getOriginalFilename() {
return this.originalFilename;
}
@Override
public String getContentType() {
return this.contentType;
}
@Override
public boolean isEmpty() {
return this.fileContent.length == 0;
}
@Override
public long getSize() {
return this.fileContent.length;
}
@Override
public byte[] getBytes() throws IOException {
return this.fileContent;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(this.fileContent);
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
try (FileOutputStream fos = new FileOutputStream(dest)) {
fos.write(this.fileContent);
}
}
}
步驟5:在業(yè)務(wù)中使用
public void createExport(BizExportAddParam addParam) {
// 準備數(shù)據(jù),可以轉(zhuǎn)為JSON,用String.format()塞入頁面中
List<Object> dataList = bizXXXService.getDataList();
Map<String,Object> pageData = new HashMap<>();
pageData.put("exportName",addParam.getName());
pageData.put("dataList",dataList);
// 更新狀態(tài)(正在導(dǎo)出)
BizExport bizExport = BeanUtil.toBean(addParam, BizExport.class);
bizExport.setStatus(BizExportStatusEnum.PROCESS.getValue());
bizExport.setOriginData(JSON.toJSONString(pageData));
bizExport.setQuestionNum(addParam.getQuestionIds().size());
this.save(bizExport);
// 異步打印并上傳
pdfService.generatePdfAndUploadAsync(PageUtil.getPageContent(pageData), bizExport.getExportId());
}
注意:異步任務(wù)不可以在同一個類中被調(diào)用,這將會失效。
三、PlayWright PDF導(dǎo)出代碼深度解析
代碼解析:
下面是我優(yōu)化后的完整工具類,每個配置都有詳細說明:
package vip.xiaonuo.biz.modular.export.utils;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.Margin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* PlayWright無頭瀏覽器PDF導(dǎo)出工具
* 核心技術(shù)亮點:完美支持JavaScript執(zhí)行,真正的動態(tài)內(nèi)容捕獲
*/
public class PlayWrightPDFExporter {
private static final Logger logger = LoggerFactory.getLogger(PlayWrightPDFExporter.class);
// 瀏覽器路徑配置 - 支持跨平臺,如果使用本地瀏覽器,需要提前下載好。
private static final String WINDOWS_CHROME_PATH = "D:/chrome-win64/chrome.exe";
private static final String LINUX_CHROME_PATH = "/usr/bin/google-chrome";
/**
* 智能瀏覽器實例管理
* 特性1:可以使用本地瀏覽器或自動下載可靠瀏覽器
* 特性2:自動降級,確保服務(wù)可用性
*/
public static Browser createBrowser() {
Map<String, String> env = new HashMap<>();
Playwright playwright = null;
BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
.setHeadless(true) // 無頭模式 - 服務(wù)端運行關(guān)鍵
.setArgs(Arrays.asList(
"--disable-web-security", // 禁用安全策略,避免跨域問題
"--disable-dev-shm-usage", // 解決Docker內(nèi)存問題
"--no-sandbox" // Linux環(huán)境必須
));
// 智能瀏覽器檢測:Windows -> Linux -> 自動下載
Path chromePath = detectChromePath();
if (Files.exists(chromePath)) {
launchOptions.setExecutablePath(chromePath);
logger.info("? 使用本地Chrome瀏覽器: {}", chromePath);
// 跳過自動下載
env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "1");
} else {
logger.warn("?? 本地瀏覽器未找到,使用PlayWright內(nèi)置瀏覽器");
}
playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env));
// 選擇Chromium(Chrome兼容性最好)
return playwright.chromium().launch(launchOptions);
}
/**
* 跨平臺瀏覽器路徑檢測
*/
private static Path detectChromePath() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
return Paths.get(WINDOWS_CHROME_PATH);
} else if (os.contains("linux") || os.contains("unix")) {
return Paths.get(LINUX_CHROME_PATH);
}
return Paths.get(""); // 返回空路徑觸發(fā)自動下載
}
/**
* 核心PDF導(dǎo)出方法 - 每個配置都是精華!
* htmlContent:網(wǎng)頁字符串
* title:打印標題
*/
public static byte[] exportToPDF(String htmlContent, String title) {
Browser browser = null;
try {
// 1. 創(chuàng)建瀏覽器實例
browser = createBrowser();
// 2. 創(chuàng)建瀏覽器上下文(類似隱身模式,隔離環(huán)境)
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080) // 視口大小,也可不設(shè)置
);
// 3. 創(chuàng)建新頁面
Page page = context.newPage();
// 4. 關(guān)鍵配置:超時和重試策略
page.setDefaultTimeout(30000); // 元素操作超時
page.setDefaultNavigationTimeout(60000); // 頁面加載超時
logger.info("?? 開始處理HTML內(nèi)容,長度: {} 字符", htmlContent.length());
// 5. 設(shè)置頁面HTML內(nèi)容(可以包含JavaScript),也可以直接請求網(wǎng)頁
page.setContent(htmlContent, new Page.SetContentOptions()
.setWaitUntil(WaitUntilState.NETWORKIDLE) // 等待網(wǎng)絡(luò)空閑
);
// 6. 網(wǎng)絡(luò)請求監(jiān)控(調(diào)試神器)監(jiān)控請求外部資源
page.onResponse(response -> {
if (response.status() != 200) {
logger.warn("?? 請求異常: {} - {}", response.status(), response.url());
}
});
// 7. 關(guān)鍵等待策略 - 確保所有動態(tài)內(nèi)容加載完成
// 等待1:網(wǎng)絡(luò)空閑(所有異步請求完成)
logger.info("? 等待網(wǎng)絡(luò)空閑...");
page.waitForLoadState(LoadState.NETWORKIDLE);
// 等待2:等待JavaScript自定義就緒標志
logger.info("? 等待JavaScript執(zhí)行完成...");
try {
page.waitForFunction("() => window.pageReady === true",
new Page.WaitForFunctionOptions().setTimeout(30000));
} catch (TimeoutException e) {
logger.warn("? 頁面就緒超時,繼續(xù)處理...");
}
// 等待3:確保圖表渲染完成(針對可視化頁面)
logger.info("? 等待圖表渲染...");
page.waitForFunction("() => {
const canvas = document.querySelector('canvas');
return canvas && canvas.width > 0;
}", new Page.WaitForFunctionOptions().setTimeout(10000));
// 8. 高級PDF配置 - 企業(yè)級排版控制
logger.info("?? 生成PDF中...");
Page.PdfOptions pdfOptions = new Page.PdfOptions()
// 頁面邊距:上、右、下、左
.setMargin(new Margin()
.setTop("1cm")
.setRight("1cm")
.setBottom("2cm") // 底部多留空間給頁腳
.setLeft("1cm"))
.setPrintBackground(true) // ? 打印背景色和圖片
.setFormat("A4") // 紙張規(guī)格
.setPreferredSize(210, 297) // A4尺寸(mm)
.setPath(null) // null表示返回字節(jié),不保存文件
.setDisplayHeaderFooter(true) // 顯示頁眉頁腳
// 頁眉模板:支持CSS和動態(tài)數(shù)據(jù)
.setHeaderTemplate("""
<div style="
font-size: 10px;
margin: 0 1cm;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
">
<span>${title}</span>
<span>生成時間: <span class="date"></span></span>
</div>
""".replace("${title}", title))
// 頁腳模板:自動頁碼計算
.setFooterTemplate("""
<div style="
font-size: 8px;
margin: 0 1cm;
width: 100%;
display: flex;
justify-content: space-between;
color: #666;
">
<span>機密文件 · 嚴禁外傳</span>
<span>第 <span class="pageNumber"></span> 頁 / 共 <span class="totalPages"></span> 頁</span>
</div>
""");
byte[] pdfBytes = page.pdf(pdfOptions);
logger.info("? PDF生成成功,大小: {} KB", pdfBytes.length / 1024);
return pdfBytes;
} catch (Exception e) {
logger.error("? PDF生成失敗", e);
throw new RuntimeException("PDF導(dǎo)出異常: " + e.getMessage(), e);
} finally {
// 9. 資源清理 - 防止內(nèi)存泄漏
if (browser != null) {
browser.close();
logger.info("?? 瀏覽器資源已釋放");
}
}
}
特性1:智能等待機制 - 解決動態(tài)內(nèi)容核心難題
// 三重等待確保萬無一失
page.waitForLoadState(LoadState.NETWORKIDLE); // 網(wǎng)絡(luò)請求完成
page.waitForFunction("() => window.pageReady === true"); // 業(yè)務(wù)邏輯完成
page.waitForFunction("() => canvas.width > 0"); // 圖表渲染完成
為什么這么重要?
- 傳統(tǒng)工具:直接生成,JavaScript沒執(zhí)行完
- PlayWright:等待所有異步操作完成,真正捕獲最終狀態(tài)
特性2:完整的PDF排版控制
.setHeaderTemplate("""
<div style="font-size: 10px;">
<span>${title}</span>
<span>生成時間: <span class="date"></span></span>
</div>
""")
強大之處:
class="date":自動替換為當前日期class="pageNumber"/class="totalPages":自動頁碼計算- 支持完整CSS樣式
特性3:跨平臺瀏覽器管理
private static Path detectChromePath() {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) return Paths.get(WINDOWS_CHROME_PATH);
if (os.contains("linux")) return Paths.get(LINUX_CHROME_PATH);
return Paths.get(""); // 觸發(fā)自動下載
}
智能降級策略:
- 優(yōu)先使用本地Chrome(性能最佳)
- 自動下載(零配置部署)
四、常見問題解決
問題1:關(guān)于瀏覽器的安裝、下載路徑、參數(shù)配置等問題?
答:請詳細閱讀:https://playwright.dev/java/docs/browsers#introduction
問題2:文檔中/報錯信息說使用PlayWright CLI下載系統(tǒng)依賴和瀏覽器,該如何下載呢?
答:CLI是PlayWright的腳手架,它的代碼地址在com.microsoft.playwright.CLI
如果你的項目是maven單模塊項目:
// 1. 先cd到pom所在目錄 cd xxxxx // 2. 執(zhí)行mvn install nvm install // 3. 使用CLI安裝系統(tǒng)依賴和瀏覽器。 // 如果你使用本地瀏覽器,刪除chromium參數(shù),保留install-deps參數(shù)只安裝系統(tǒng)依賴即可 mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps chromium"
如果你的項目是maven多模塊項目:
// 1. 先cd到項目的全局pom所在目錄,一般在根目錄下 cd xxxx // 2. 執(zhí)行mvn install mvn install // 3. 再cd到playwright被導(dǎo)入使用的模塊的pom目錄下 cd xxxx/xxxx // 4. 使用CLI安裝系統(tǒng)依賴和瀏覽器。 // 如果你使用本地瀏覽器,刪除chromium參數(shù),保留install-deps參數(shù)只安裝系統(tǒng)依賴即可 mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install-deps chromium"
注意:使用mvn指令需要安裝maven和jdk哦,再配置一下maven的鏡像,這些自己百度一下即可,本文不再贅述。不過我在win平臺開發(fā)中測試時并不要執(zhí)行這樣的命令去下載瀏覽器和依賴,PlayWright會自動執(zhí)行這些操作。但是?。?!在linux上就不得行了,即使你使用自己下載的瀏覽器也依然會報錯,因為缺失運行所需依賴。 所以就需要嚴格按照上述步驟走一遍。這一部分的詳細文檔請參考https://playwright.dev/java/docs/browsers#introduction和 https://playwright.dev/java/docs/ci-intro。系統(tǒng)依賴和瀏覽器只需要安裝一次即可,在linux平臺部署時,第一次我把源代碼進去執(zhí)行上述操作安裝系統(tǒng)依賴和瀏覽器,后面就不再需要了。
問題3:Linux平臺上怎么安裝瀏覽器呢?怎么找到安裝位置呢?
答:根據(jù)我親測,以Ubuntu平臺為例:
# 下載Chrome安裝包 wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb # 更新軟件包列表 sudo apt update # 安裝依賴(如果需要) sudo apt install -y libappindicator3-1 # 安裝Chrome sudo dpkg -i google-chrome-stable_current_amd64.deb # 如果出現(xiàn)依賴問題,修復(fù)安裝 sudo apt --fix-broken install # 查找安裝位置 which google-chrome # 或 which google-chrome-stable # 最后拿到安裝位置設(shè)置playwright調(diào)用本地瀏覽器路徑
若有其他問題,直接問AI就好了。
問題4:Linux上打印PDF缺失字體怎么辦?
答:我們可以先查看系統(tǒng)中有哪些字體,再安裝缺失的字體。這里我以Ubuntu平臺為例。
// 檢查ubuntu中安裝的中文字體 fc-list :lang=zh // 檢查ubuntu中安裝的所有字體 fc-list // 安裝宋體,字體自己下載,ttf格式。下載后先解壓,得到simsun.ttf文件 // 進入/usr/share/fonts/truetype目錄,創(chuàng)建文件夾simsun并將simsun.ttf拷貝進該目錄下 mkdir simsun cd simsun // 假設(shè)這里已經(jīng)拷貝好了simsun.ttf文件 // 執(zhí)行下面指令即可安裝完成 sudo mkfontscale sudo mkfontdir sudo fc-cache -fv // 再次查看已安裝字體 fc-list
五、實戰(zhàn)效果對比
傳統(tǒng)方案(iText、Flying Saucer):
? 靜態(tài)HTML渲染 ? 無法執(zhí)行JavaScript ? 圖表顯示為空白框 ? 動態(tài)內(nèi)容缺失
PlayWright方案:
? 真實瀏覽器環(huán)境 ? 完整JavaScript執(zhí)行 ? 圖表完美渲染 ? 動畫效果保持 ? 異步數(shù)據(jù)完整
六、性能優(yōu)化技巧
1. 瀏覽器實例復(fù)用
// 創(chuàng)建瀏覽器池,避免頻繁創(chuàng)建銷毀
@Component
public class BrowserPool {
private final BlockingQueue<Browser> browserQueue = new LinkedBlockingQueue<>(5);
public Browser getBrowser() {
// 池化管理實現(xiàn)
}
}
2. 資源攔截優(yōu)化
// 屏蔽不必要資源,提升加載速度
page.route("**/*.{png,jpg,jpeg,svg}", route -> route.abort());
3. 緩存策略
// 對相同內(nèi)容哈希緩存
String contentHash = DigestUtils.md5Hex(htmlContent);
if (cache.containsKey(contentHash)) {
return cache.get(contentHash);
}
七、為什么PlayWright是PDF導(dǎo)出的終極解決方案?
- 真正的瀏覽器環(huán)境:不是模擬,是真實的Chromium內(nèi)核
- 完整的Web標準支持:ES6+、CSS3、Web API全面兼容
- 智能等待機制:自動處理異步加載,無需人工估算時間
- 企業(yè)級PDF輸出:頁眉頁腳、頁碼、邊距精細控制
- 活躍的生態(tài):微軟官方維護,持續(xù)更新迭代
結(jié)語
經(jīng)過多個生產(chǎn)項目的實踐驗證,PlayWright已經(jīng)完全取代了我們之前使用的所有PDF導(dǎo)出方案。從簡單的靜態(tài)報表到復(fù)雜的動態(tài)儀表盤,它都能完美應(yīng)對。 特別讓人驚喜的是:那些需要先在前端"點擊生成報表"按鈕才能看到完整數(shù)據(jù)的復(fù)雜頁面,PlayWright也能輕松處理——因為它能執(zhí)行所有的交互JavaScript! 如果你正在為以下問題困擾:
- 圖表在PDF中顯示異常
- 動態(tài)數(shù)據(jù)無法導(dǎo)出
- 復(fù)雜布局錯亂
- 需要人工參與才能生成完整報表
那么,是時候體驗PlayWright帶來的技術(shù)革命了!它不僅僅是一個工具,更是改變你對"服務(wù)端Web操作"認知的鑰匙。
PlayWright-Java已在實際項目中驗證,可直接使用。建議根據(jù)文檔從簡單頁面開始,逐步體驗PlayWright的強大能力!
以上就是SpringBoot導(dǎo)出PDF的完整解決方案!的詳細內(nèi)容,更多關(guān)于SpringBoot導(dǎo)出PDF的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Java8 Stream API實現(xiàn)數(shù)據(jù)抽取收集
這篇文章主要介紹了基于Java8 Stream API實現(xiàn)數(shù)據(jù)抽取收集,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03
Java數(shù)據(jù)結(jié)構(gòu)之稀疏數(shù)組的實現(xiàn)與應(yīng)用
這篇文章主要為大家詳細介紹了Java數(shù)據(jù)結(jié)構(gòu)中稀疏數(shù)組的實現(xiàn)與應(yīng)用,文中的示例代碼講解詳細,具有一定的借鑒價值,感興趣的可以了解一下2022-10-10

