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
相关产品推荐
相关产品推荐

