You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaFX PDFViewerFX:IDE运行快但Jar包加载PDF卡顿问题求助

问题:JavaFX中PDFViewerFX打包Jar后PDF加载极慢,IDE运行正常

我在JavaFX应用中使用PDFViewerFX库展示PDF时遇到问题:IDE中运行时PDF页面加载速度很快,但打包成Jar包后加载极慢甚至无响应。已将堆内存设置为3000MB,修改缓冲代码优化性能后IDE中效果明显,但Jar包无改善。同时尝试了Maven构建、OpenJDK18和Oracle JDK18,结果一致。相关代码如下:

public void loadPDF(URL url) throws IOException {
    loadPDF(new BufferedInputStream(url.openConnection().getInputStream()));
}
private static final Logger logger = LoggerFactory.getLogger(PDFDisplayer.class);
private static final int THREAD_POOL_SIZE = 5;

private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(THREAD_POOL_SIZE, new ThreadFactory() {
    private final ThreadGroup group = new ThreadGroup("my-thread-pool");
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, "my-thread-" + new AtomicInteger(1), 0);
        t.setDaemon(false);
        return t;
    }
});


private final ObjectProperty<Consumer<Task<String>>> onLoaderTaskPresent =
        new SimpleObjectProperty<>();

private boolean pdfJsLoaded;
public void loadPDF(InputStream inputStream) {
    if (inputStream == null)
        return;

    Task<String> task = buildLoadingTask(inputStream);

    final Consumer<Task<String>> onLoaderTaskPresent = this.onLoaderTaskPresent.get();
    if (onLoaderTaskPresent != null) {
        Platform.runLater(() -> onLoaderTaskPresent.accept(task));
    }
    THREAD_POOL.submit(task);
}

/**
 * @deprecated Use {@link #loadPDF(InputStream)} instead
 */
@Deprecated
public void displayPdf(InputStream inputStream) throws IOException {
    loadPDF(inputStream);
}
private void loadInputStream(InputStream inputStream) {
    Task<String> loadingTask = buildLoadingTask(inputStream);
    THREAD_POOL.submit(loadingTask);
    loadingTask.setOnSucceeded(event -> {
        String js = loadingTask.getValue();
        if (js != null) {
            try {
                webView.getEngine().executeScript(js);
            } catch (Exception ex) {
                if (!pdfJsLoaded) {
                    loadScript = js;
                }
            }
        }
        THREAD_POOL.shutdown();
    });
    loadingTask.setOnFailed(event -> {
        Throwable ex = event.getSource().getException();
        if (ex != null) {
            // Handle error more gracefully
            showErrorDialog("Failed to execute script", (Exception) ex);
        }
    });
}


private Task<String> buildLoadingTask(InputStream inputStream) {
    final Task<String> task = new Task<String>() {
        @Override
        protected String call() throws Exception {
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

                long contentSize = inputStream.available();
                long onePercent = contentSize / 100;

                int allReadBytesCount = 0;

                byte[] buf = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buf)) >= 0) {
                    allReadBytesCount += bytesRead;
                    outputStream.write(buf, 0, bytesRead);

                    if (onePercent > 0) {
                        double percent = allReadBytesCount / (double) onePercent;
                        updateProgress(percent, 100d);
                    }

                    if (this.isCancelled()) {
                        return null;
                    }
                }

                byte[] data = outputStream.toByteArray();
                String base64 = Base64.getEncoder().encodeToString(data);

                //JS Function declaration
                return "openFileFromBase64('" + base64 + "');";
            } finally {
                inputStream.close();
            }
        }
    };
    task.valueProperty().addListener((observable, oldValue, js) -> {
        if (js != null) {
            try {
                webView.getEngine().executeScript(js);
            } catch (Exception ex) {
                if (!pdfJsLoaded) loadScript = js;
            }
        }
    });
    return task;
}
@SuppressWarnings("all")
public void setSecondaryToolbarToggleVisibility(boolean value) {
    setVisibilityOf("secondaryToolbarToggle", value);

    String js;
    if (value){
        js = new StringBuilder()
                .append("var element = document.getElementsByClassName('verticalToolbarSeparator')[0];")
                .append("element.style.display = 'inherit';")
                .append("element.style.visibility = 'inherit';")
                .toString();
    } else {
        js = new StringBuilder()
                .append("var element = document.getElementsByClassName('verticalToolbarSeparator')[0];")
                .append("element.style.display = 'none';")
                .append("element.style.visibility = 'hidden';")
                .toString();
    }

    try {
        webView.getEngine().executeScript(js);
    } catch (Exception ex){
        if (!pdfJsLoaded) toExecuteWhenPDFJSLoaded += js;
    }
}

@SuppressWarnings("all")
public void setVisibilityOf(String id, boolean value){
    String css;
    if (value) {
        css = new StringBuilder()
                .append("document.getElementById('" + id + "').style.display = 'inherit';")
                .append("document.getElementById('" + id + "').style.visibility = 'inherit';")
                .toString();
    } else {
        css = new StringBuilder()
                .append("document.getElementById('" + id + "').style.display = 'none';")
                .append("document.getElementById('" + id + "').style.visibility = 'hidden';")
                .toString();
    }

    try {
        webView.getEngine().executeScript(css);
    } catch (Exception ex) {
        if (!pdfJsLoaded) this.toExecuteWhenPDFJSLoaded += css;
    }
}

public int getActualPageNumber(){
    try {
        return (int) webView.getEngine().executeScript("PDFViewerApplication.page;");
    } catch (Exception e) {
        return 0;
    }
}
private void showErrorDialog(String message, Exception ex) {
    Alert alert = new Alert(Alert.AlertType.ERROR);
    alert.setTitle("Error");
    alert.setHeaderText(message);
    alert.setContentText(ex.getMessage());
    alert.showAndWait();
}
public int getTotalPageCount(){
    try {
        return (int) webView.getEngine().executeScript("PDFViewerApplication.pagesCount;");
    } catch (Exception e) {
        return 0;
    }
}

public void navigateByPage(int pageNum) {
    String jsCommand = "goToPage(" + pageNum + ");";
    try {
        webView.getEngine().executeScript(jsCommand);
    } catch (Exception ex) {
        if (!pdfJsLoaded) toExecuteWhenPDFJSLoaded += jsCommand;
    }
}

public void executeScript(String js) {
    try {
        this.webView.getEngine().executeScript(js);
    } catch (Exception ex) {
        if (!pdfJsLoaded) toExecuteWhenPDFJSLoaded += String.format("%s;", js);
    }
}

private WebView createWebView() {
    WebView webView = new WebView();
    webView.setContextMenuEnabled(false);

    WebEngine engine = webView.getEngine();
    String url;
    try {
        URL resourceUrl = getClass().getResource("/pdfjs_2.2.228/web/viewer.html");
        engine.setJavaScriptEnabled(true);
        engine.load(resourceUrl.toString());

    } catch (Exception e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
    }
    engine.getLoadWorker()
            .stateProperty()
            .addListener(
                    new ChangeListener<Worker.State>() {
                        @Override
                        public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldValue, Worker.State newValue) {
                            JSObject window = (JSObject) engine.executeScript("window");
                            window.setMember("java", new JSLogListener());

                            if (newValue == Worker.State.SUCCEEDED) {
                                try {
                                    pdfJsLoaded = true;

                                    if (loadScript != null) {
                                        logger.debug("PDF already loaded");
                                        engine.executeScript(loadScript);
                                    }

                                    engine.executeScript(toExecuteWhenPDFJSLoaded);
                                    toExecuteWhenPDFJSLoaded = null;
                                    observable.removeListener(this);
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        }
                    });
    return webView;
}

public Parent toNode() {
    if (webView == null)
        return webView = createWebView();
    else
        return webView;
}

IDE与Jar包运行的核心差异

  • 资源访问效率:IDE中资源直接从本地文件系统读取,IO操作无压缩和解压开销;Jar包内的资源存储在压缩文件中,读取时需要先解压,IO效率大幅下降。
  • JVM优化策略:IDE运行时JVM默认启用更多即时编译(JIT)优化,代码执行速度更快;Jar包运行时JIT预热需要时间,首次加载时优化不足。
  • WebView资源加载:IDE中pdfjs的静态资源(HTML/JS/CSS)通过本地文件路径加载,WebEngine解析速度快;Jar中通过jar:协议加载资源,WebEngine的加载和解析流程更复杂,耗时更长。

解决办法

1. 优化Jar内资源读取逻辑

  • 替换inputStream.available():该方法在Jar内输入流中无法返回真实文件大小,会导致进度计算错误且影响IO效率。建议先将PDF资源复制到临时文件,再从临时文件读取:
    private Task<String> buildLoadingTask(InputStream inputStream) {
        return new Task<String>() {
            @Override
            protected String call() throws Exception {
                // 创建临时文件
                File tempPdf = File.createTempFile("temp", ".pdf");
                try (FileOutputStream fos = new FileOutputStream(tempPdf)) {
                    byte[] buf = new byte[16384]; // 增大缓冲区到16KB
                    int bytesRead;
                    long totalBytes = 0;
                    while ((bytesRead = inputStream.read(buf)) != -1) {
                        fos.write(buf, 0, bytesRead);
                        totalBytes += bytesRead;
                        updateProgress(totalBytes, tempPdf.length());
                        if (isCancelled()) return null;
                    }
                    // 返回临时文件路径给JS,避免Base64转换
                    return String.format("openFileFromPath('%s');", tempPdf.getAbsolutePath().replace("\\", "\\\\"));
                } finally {
                    inputStream.close();
                    // 可在应用退出时清理临时文件
                    tempPdf.deleteOnExit();
                }
            }
        };
    }
    
  • 增大缓冲区:将原代码中4096字节的缓冲区改为8192或16384字节,减少IO操作次数。

2. 改进WebView加载方式

将pdfjs的静态资源从Jar中提取到临时目录,用本地文件路径加载,避免jar:协议的性能开销:

private WebView createWebView() {
    WebView webView = new WebView();
    webView.setContextMenuEnabled(false);
    WebEngine engine = webView.getEngine();
    engine.setJavaScriptEnabled(true);

    try {
        // 创建临时目录存储pdfjs资源
        Path tempDir = Files.createTempDirectory("pdfjs-web");
        // 复制Jar内的pdfjs资源到临时目录
        copyResource("/pdfjs_2.2.228/web/viewer.html", tempDir.resolve("viewer.html"));
        copyResource("/pdfjs_2.2.228/web/viewer.js", tempDir.resolve("viewer.js"));
        copyResource("/pdfjs_2.2.228/web/viewer.css", tempDir.resolve("viewer.css"));
        // 复制其他必要资源(如images、locale等)

        // 加载本地路径的viewer.html
        File viewerFile = tempDir.resolve("viewer.html").toFile();
        engine.load(viewerFile.toURI().toString());
        // 标记临时目录退出时删除
        tempDir.toFile().deleteOnExit();
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 原监听器代码保持不变
    engine.getLoadWorker().stateProperty().addListener(...);
    return webView;
}

// 辅助方法:复制Jar内单个资源到目标路径
private void copyResource(String resourcePath, Path targetPath) throws IOException {
    try (InputStream is = getClass().getResourceAsStream(resourcePath)) {
        if (is == null) throw new FileNotFoundException("Resource not found: " + resourcePath);
        Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
    }
}

同时需要修改pdfjs的viewer.js,添加从本地文件路径加载PDF的方法:

function openFileFromPath(filePath) {
    const url = new URL(`file://${filePath}`);
    PDFViewerApplication.open(url);
}

3. 修复线程池使用问题

移除loadInputStream方法中的THREAD_POOL.shutdown()调用——全局线程池仅应在应用退出时关闭,否则后续PDF加载会因线程池已关闭而无法执行,导致性能下降甚至无响应。同时根据CPU核心数调整线程池大小:

private static final int THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors();
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(THREAD_POOL_SIZE, new ThreadFactory() {
    // 原线程工厂代码保持不变
});

// 在应用退出时关闭线程池,比如在主窗口关闭事件中
primaryStage.setOnCloseRequest(e -> {
    THREAD_POOL.shutdown();
    try {
        if (!THREAD_POOL.awaitTermination(5, TimeUnit.SECONDS)) {
            THREAD_POOL.shutdownNow();
        }
    } catch (InterruptedException ex) {
        THREAD_POOL.shutdownNow();
    }
});

4. 优化JVM启动参数

运行Jar包时添加以下参数,提升JIT预热速度和内存效率:

java -Xmx3000m -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -jar your-app.jar
  • -XX:+TieredCompilation:启用分层编译,平衡启动速度和运行性能
  • -XX:TieredStopAtLevel=1:快速启动JIT编译,减少首次加载的等待时间

5. 避免Base64转换开销

原代码将PDF转为Base64字符串传递给JS,大PDF会产生超长字符串,导致JS解析耗时。改用临时文件路径传递给pdfjs,彻底消除Base64转换的性能损耗(如步骤1中的代码所示)。


内容的提问来源于stack exchange,提问作者Ahmad Luay AlQatu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 01:13:18