最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot導(dǎo)出PDF的完整解決方案!

 更新時間:2025年12月01日 09:00:10   作者:CyberShen  
本文介紹了PlayWright,一個現(xiàn)代化的瀏覽器自動化工具,適用于服務(wù)端Web操作,它具有跨瀏覽器支持、多語言、智能等待、強(qiáng)大的PDF生成和高性能并行處理等優(yōu)勢,通過一個實戰(zhàn)示例,展示了如何使用PlayWright-Java導(dǎo)出包含JavaScript的動態(tài)網(wǎng)頁為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等
  • 自動等待機(jī)制:智能處理動態(tài)內(nèi)容加載
  • 強(qiáng)大的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都需要特定版本的瀏覽器二進(jìn)制文件才能運(yùn)行。你需要使用Playwright的CLI來安裝這些瀏覽器。每次發(fā)布時,Playwright 都會更新它支持的瀏覽器版本,使最新的 Playwright 隨時都能支持最新的瀏覽器。這意味著每次更新 Playwright 時,你可能需要重新運(yùn)行 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格式塞進(jìn)來,就是這么方便!
                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è)置頁面就緒標(biāo)志 - 這是關(guān)鍵!
                    window.pageReady = true;

                }, 2000); // 模擬2秒數(shù)據(jù)加載
            </script>
        </body>
        </html>
    
    """;
    
    
    /*
     * 我們可以直接把json數(shù)據(jù)塞進(jìn)頁面中,這直接免去了freeMarker模板引擎的工作
     * 當(dāng)然也可以用模板引擎初步渲染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)先使用本地瀏覽器,如果沒找到本地瀏覽器則下載默認(rèn)瀏覽器
        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("已使用默認(rèn)下載瀏覽器");
            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ù)進(jìn)行調(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ù)進(jìn)行調(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) {
    // 準(zhǔn)備數(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)化后的完整工具類,每個配置都有詳細(xì)說明:

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ù)亮點(diǎn):完美支持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ù)端運(yùn)行關(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:打印標(biāo)題
     */
    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自定義就緒標(biāo)志
            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>機(jī)密文件 · 嚴(yán)禁外傳</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:智能等待機(jī)制 - 解決動態(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>
""")

強(qiáng)大之處:

  • class="date":自動替換為當(dāng)前日期
  • 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ā)自動下載
}

智能降級策略:

  1. 優(yōu)先使用本地Chrome(性能最佳)
  2. 自動下載(零配置部署)

四、常見問題解決

問題1:關(guān)于瀏覽器的安裝、下載路徑、參數(shù)配置等問題?

答:請詳細(xì)閱讀: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上就不得行了,即使你使用自己下載的瀏覽器也依然會報錯,因為缺失運(yùn)行所需依賴。 所以就需要嚴(yán)格按照上述步驟走一遍。這一部分的詳細(xì)文檔請參考https://playwright.dev/java/docs/browsers#introductionhttps://playwright.dev/java/docs/ci-intro。系統(tǒng)依賴和瀏覽器只需要安裝一次即可,在linux平臺部署時,第一次我把源代碼進(jìn)去執(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文件
// 進(jìn)入/usr/share/fonts/truetype目錄,創(chuàng)建文件夾simsun并將simsun.ttf拷貝進(jìn)該目錄下
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)出的終極解決方案?

  1. 真正的瀏覽器環(huán)境:不是模擬,是真實的Chromium內(nèi)核
  2. 完整的Web標(biāo)準(zhǔn)支持:ES6+、CSS3、Web API全面兼容
  3. 智能等待機(jī)制:自動處理異步加載,無需人工估算時間
  4. 企業(yè)級PDF輸出:頁眉頁腳、頁碼、邊距精細(xì)控制
  5. 活躍的生態(tài):微軟官方維護(hù),持續(xù)更新迭代

結(jié)語

經(jīng)過多個生產(chǎn)項目的實踐驗證,PlayWright已經(jīng)完全取代了我們之前使用的所有PDF導(dǎo)出方案。從簡單的靜態(tài)報表到復(fù)雜的動態(tài)儀表盤,它都能完美應(yīng)對。 特別讓人驚喜的是:那些需要先在前端"點(diǎn)擊生成報表"按鈕才能看到完整數(shù)據(jù)的復(fù)雜頁面,PlayWright也能輕松處理——因為它能執(zhí)行所有的交互JavaScript! 如果你正在為以下問題困擾:

  • 圖表在PDF中顯示異常
  • 動態(tài)數(shù)據(jù)無法導(dǎo)出
  • 復(fù)雜布局錯亂
  • 需要人工參與才能生成完整報表

那么,是時候體驗PlayWright帶來的技術(shù)革命了!它不僅僅是一個工具,更是改變你對"服務(wù)端Web操作"認(rèn)知的鑰匙。

PlayWright-Java已在實際項目中驗證,可直接使用。建議根據(jù)文檔從簡單頁面開始,逐步體驗PlayWright的強(qiáng)大能力!

以上就是SpringBoot導(dǎo)出PDF的完整解決方案!的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot導(dǎo)出PDF的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 關(guān)于JSON解析中獲取不存在的key問題

    關(guān)于JSON解析中獲取不存在的key問題

    這篇文章主要介紹了關(guān)于JSON解析中獲取不存在的key問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • 詳細(xì)學(xué)習(xí)Java Cookie技術(shù)(用戶登錄、瀏覽、訪問權(quán)限)

    詳細(xì)學(xué)習(xí)Java Cookie技術(shù)(用戶登錄、瀏覽、訪問權(quán)限)

    這篇文章主要為大家詳細(xì)介紹了Java Cookie技術(shù),顯示用戶上次登錄的時間、顯示用戶最近瀏覽的若干個圖片(按比例縮放)等,感興趣的小伙伴們可以參考一下
    2016-08-08
  • 使用Java Servlet生成動態(tài)二維碼的實現(xiàn)步驟

    使用Java Servlet生成動態(tài)二維碼的實現(xiàn)步驟

    在現(xiàn)代互聯(lián)網(wǎng)時代,二維碼廣泛應(yīng)用于各個領(lǐng)域,包括支付、認(rèn)證、信息傳遞等,在Web開發(fā)中,通過Java Servlet生成動態(tài)二維碼是一個常見的需求,本文將介紹如何使用Java Servlet結(jié)合Google的ZXing庫生成動態(tài)二維碼,需要的朋友可以參考下
    2023-11-11
  • SpringBoot如何接收數(shù)組參數(shù)的方法

    SpringBoot如何接收數(shù)組參數(shù)的方法

    這篇文章主要介紹了SpringBoot如何接收數(shù)組參數(shù)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-12-12
  • spring中12種@Transactional的失效場景(小結(jié))

    spring中12種@Transactional的失效場景(小結(jié))

    日常我們進(jìn)行業(yè)務(wù)開發(fā)時,基本上使用的都是聲明式事務(wù),即為使用@Transactional注解的方式,本文主要介紹了spring中12種@Transactional的失效場景,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Springboot中的@ConditionalOnBean注解詳細(xì)解讀

    Springboot中的@ConditionalOnBean注解詳細(xì)解讀

    這篇文章主要介紹了Springboot中的@ConditionalOnBean注解詳細(xì)解讀,@ConditionalOnMissingBean注解兩個類,一個Computer類,一個配置類,想要完成;如果容器中沒有Computer類,就注入備用電腦Computer類,如果有Computer就不注入,需要的朋友可以參考下
    2023-11-11
  • 二叉樹遞歸迭代及morris層序前中后序遍歷詳解

    二叉樹遞歸迭代及morris層序前中后序遍歷詳解

    這篇文章主要為大家介紹了二叉樹遞歸迭代詳解及二叉樹的morris遍歷、層序遍歷、前序遍歷、中序遍歷、后序遍歷示例分析,有需要的朋友可以借鑒參考下
    2021-11-11
  • Java集合中的Iterator迭代器接口詳解

    Java集合中的Iterator迭代器接口詳解

    這篇文章主要介紹了Java集合中的Iterator迭代器接口詳解,Iterator,被稱為迭代器接口,本身并不提供存儲對象的能力,主要用于遍歷Collection中的元素,需要的朋友可以參考下
    2023-11-11
  • Spring AspectJ AOP框架注解原理解析

    Spring AspectJ AOP框架注解原理解析

    這篇文章主要介紹了Spring AspectJ AOP框架注解原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-09-09
  • Java JVM原理與調(diào)優(yōu)_動力節(jié)點(diǎn)Java學(xué)院整理

    Java JVM原理與調(diào)優(yōu)_動力節(jié)點(diǎn)Java學(xué)院整理

    JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫,JVM是一種用于計算設(shè)備的規(guī)范,它是一個虛構(gòu)出來的計算機(jī),是通過在實際的計算機(jī)上仿真模擬各種計算機(jī)功能來實現(xiàn)的。下面通過本文給大家介紹jvm原理與調(diào)優(yōu)相關(guān)知識,感興趣的朋友一起學(xué)習(xí)吧
    2017-04-04

最新評論

夏邑县| 东港市| 平远县| 乐陵市| 鸡泽县| 牡丹江市| 万安县| 陕西省| 修武县| 华蓥市| 乡城县| 眉山市| 聊城市| 开鲁县| 盘锦市| 湘西| 沁阳市| 正蓝旗| 城步| 荔浦县| 青浦区| 类乌齐县| 中超| 桓仁| 比如县| 无为县| 杭锦后旗| 保定市| 乌恰县| 青神县| 阜平县| 靖安县| 富民县| 谷城县| 额济纳旗| 乌鲁木齐市| 格尔木市| 大荔县| 沁阳市| 南木林县| 宣武区|