
长久以来,JavaFX 一直包含一个内置的 WebView
组件,这是在 Java 应用中渲染 Web 内容的一个稳定方案。然而,在更复杂或要求更高的使用场景中,它可能就不够用了。因此,许多开发者转向了像 JxBrowser 这样的替代方案。
在本迁移指南中,我们将详细介绍如何从 JavaFX WebView
迁移至 JxBrowser,并提供代码示例以及相关 JxBrowser 文档的链接。
本文侧重介绍如何 迁移至 JxBrowser。若想了解为何 要迁移,请参阅文章 JxBrowser 还是 JavaFX WebView
,我们在其中详细解析了这两种方案的技术和架构差异。
依赖项
将 JxBrowser 添加到项目中,就像将一些 JAR 文件添加到类路径中一样简单。例如,一个运行于 Windows 的 JavaFX 应用将需要以下文件:
jxbrowser-8.9.2.jar
. 该文件包含 JxBrowser 的大部分 API。jxbrowser-javafx-8.9.2.jar
. 该文件包含 JxBrowser 的 JavaFX 组件。jxbrowser-win64-8.9.2.jar
. 该文件包含适用于 64 位 Windows 的 Chromium 二进制文件。
你可以从 JxBrowser 8.9.2 版本发布说明页面下载所需文件。
如果您使用的是标准的 Maven 或 Gradle,只需像平常一样添加 Maven 仓库中的依赖项即可:
Maven
xml
<repositories>
<repository>
<id>com.teamdev</id>
<url>https://europe-maven.pkg.dev/jxbrowser/releases</url>
</repository>
</repositories>
<dependency>
<groupId>com.teamdev.jxbrowser</groupId>
<artifactId>jxbrowser-javafx</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>com.teamdev.jxbrowser</groupId>
<artifactId>jxbrowser-win64</artifactId>
<version>{version}</version>
</dependency>
Gradle
kotlin
plugins {
id("com.teamdev.jxbrowser") version "{gradle_plugin_version}"
}
jxbrowser {
version = "{version}"
}
dependencies {
implementation(jxbrowser.javafx)
implementation(jxbrowser.win64)
}
线程安全性
WebView
和 WebEngine
并非线程安全的;访问它们及其 DOM/JavaScript 对象时,必须始终仅从 JavaFX 应用程序线程进行。
而 JxBrowser 是线程安全的。您可以在不同线程中安全地使用 JxBrowser 对象。不过,在 JavaFX 应用线程中调用 JxBrowser API 时需谨慎,因为它的许多方法是阻塞式的。为了避免影响用户体验,我们通常建议不要在 JavaFX 应用线程中调用 JxBrowser。
迁移
创建浏览器
JavaFX 提供了可视化的 WebView
组件(可添加到场景中),以及非可视的 WebEngine
(包含实际的 Browser API)。
创建和使用方法如下:
java
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.load("https://example.com");
scene.getRoot().getChildren().add(webView);
JxBrowser 同样由可视化和非可视化部分组成。该库提供了非可视化的 Engine
和 Browser
对象,它们封装了 Browser API;还提供了一个可视化的 BrowserView
组件,可将其添加到场景中以显示加载的 Web 内容。
java
// 非可视化部分:
Engine engine = Engine.newInstance(HARDWARE_ACCELERATED);
Browser browser = engine.newBrowser();
browser.navigation().loadUrl("https://example.com");
// 可视化部分:
Platform.runLater(() -> {
BrowserView browserView = BrowserView.newInstance(browser);
scene.getRoot().getChildren().add(browserView);
});
在上述示例中,我们创建了一个非可视化的 Engine
实例,用于表示 Chromium 主进程。然后,我们创建一个非可视化的 Browser
实体,用于表示主进程中的特定浏览器------类似于 Google Chrome 中的浏览器标签页。最后,我们创建一个可视化的 BrowserView
节点并将其添加到场景中。
提示: 就像在 Google Chrome 中可以打开多个标签页一样,您可以在同一个
Engine
实例中创建多个Browser
对象。
如需了解 JxBrowser API 中主要组件、进程模型及其他架构细节,请查阅架构指南。
关闭浏览器
在 JavaFX 中,并不需要显式关闭 WebView
实例。通常将其从场景图(scene graph)中移除就已足够。
而在 JxBrowser 中,仅从场景图中移除 BrowserView
不会 关闭浏览器并释放所有已分配的资源。你必须手动关闭 Browser
和 Engine
对象:
JavaFX
java
scene.getRoot().getChildren().add(webView);
JxBrowser
java
// 关闭单个 Browser。
browser.close();
// 关闭 Engine。此操作将自动关闭其包含的所有 Browser。
engine.close();
页面导航
WebEngine
中的导航功能几乎可以直接转换为 JxBrowser 的调用方式:
JavaFX
java
webEngine.load("https://example.com");
webEngine.reload();
WebHistory history = webEngine.getHistory();
var currentIndex = history.getCurrentIndex();
var historySize = history.getEntries().size();
// 后退到上一个历史页面。
var previousPage = currentIndex - 1;
if (previousPage >= 0 && previousPage < historySize) {
history.go(previousPage);
}
// 前进到下一个历史页面。
var nextPage = currentIndex + 1;
if (nextPage < historySize) {
history.go(nextPage);
}
JxBrowser
java
Navigation navigation = browser.navigation();
navigation.loadUrl("https://example.com");
navigation.reload();
navigation.goBack();
navigation.goForward();
// 跳转到指定的历史记录索引
navigation.goToIndex(2);
导航监听器
在这两种解决方案中,加载过程都是在后台进行的,因此需要注册监听器来检测加载何时完成。
在 JavaFX 中,可以通过监听加载工作器的状态来实现:
java
var worker = webEngine.getLoadWorker();
worker.stateProperty().addListener((ov, oldState, newState) -> {
if (newState == State.SUCCEEDED) {
// 此处可以执行 JavaScript 并访问 DOM 树。
} else {
System.out.println("导航失败!");
}
});
在 JxBrowser 中,通知更加精细:
java
// 当导航操作完成时会触发该事件。
// 此时 frame 和 DOM 树可能尚未初始化。
navigation.on(NavigationFinished.class, event -> {
if (event.error() != OK) {
System.out.println("Navigation failed!");
}
});
// 当 frame 的文档加载完成,且可以访问 DOM 时,会触发此事件。
navigation.on(FrameDocumentLoadFinished.class, event -> {
// 此处可以执行 JavaScript 并访问 DOM 树。
});
// 此回调允许您在 frame 刚刚完成加载、但**尚未执行其自身的 JavaScript**之前
// 执行您的 JavaScript 代码。
browser.set(InjectJsCallback.class, params -> {
Frame frame = params.frame();
JsObject window = frame.executeJavaScript("window");
if (window != null) {
...
}
return Response.proceed();
});
JxBrowser 共提供九 种细粒度的导航事件。完整列表请参阅导航事件文档。
从 Java 调用 JavaScript
在这两种方案中,您都可以执行任意的 JavaScript 代码,在 Java 中获取 JavaScript 对象,并享受自动类型转换的便利:
JavaFX
java
// JavaScript 对象会被转换为 JSObject。
JSObject dialogs = (JSObject) webEngine.executeScript("dialogs");
dialogs.call("showError", "The card number is not correct!");
// JavaScript 字符串会被转换为 String。
String locale = (String) dialogs.getMember("locale");
JxBrowser
java
browser.mainFrame().ifPresent(frame -> {
// JavaScript 对象会被转换为 JsObject。
JsObject dialogs = frame.executeJavaScript("dialogs");
jsObject.call("showError", "The card number is not correct!");
// JavaScript 字符串会被转换为 String。
String locale = dialogs.property("locale");
});
JavaFX 会自动转换传入的 JavaScript 值。原始类型会被转换为对应的 Java 类型,JavaScript 对象则会被转换为 JSObject
实例。
JxBrowser 执行类似的转换,但为特定的 JavaScript 类型(例如函数、Promise
、ArrayBuffer
等)提供了专用的 Java 类型。在类型转换指南中可查看完整列表。
对于用于访问带索引的 JavaScript 对象的 JSObject.getSlot()
和 JSObject.setSlot()
方法,JxBrowser 没有直接的替代方案。
JavaScript 对象的生命周期
在 JavaFX 和 JxBrowser 中,只要对应的 JavaScript 对象还存在,JSObject
和 JsObject
实例就能正常工作。当 JavaScript 对象被垃圾回收或所在的 frame 加载了新的文档时,该对象就会失效。无论是在 JavaFX 还是 JxBrowser 中,尝试使用已失效的 JavaScript 对象都会抛出异常。
JavaScript 对象被垃圾回收的时间难以预测,因此在 JxBrowser 中,传递给 Java 的 JavaScript 对象会被保护,防止被垃圾回收,直到新文档加载时才会关闭。若想释放对该 JavaScript 对象的引用,使其可以被垃圾回收,可以调用 JsObject.close()
方法:
java
JsObject persistentObject = frame.executeJavaScript("dialogs");
persistentObject.close();
从 JavaScript 调用 Java
要从 JavaScript 调用 Java 代码,需要将 Java 对象注入到 JavaScript 环境中。这两种方案中的做法非常相似:
JavaFX
java
public static class GreetingService {
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
...
GreetingService greetings = new GreetingService();
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("greetings", greetings);
JxBrowser
java
@JsAccessible
public static class GreetingService {
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
...
GreetingService greetings = new GreetingService();
JsObject window = frame.executeScript("window");
window.putProperty("greetings", greetings);
成员访问权限
在 JavaFX 中,JavaScript 可以访问被注入的 Java 对象的所有公共成员。
而在 JxBrowser 中,需要显式地将 Java 类及其成员标记为允许被 JavaScript 访问:
java
// 类的所有公共成员都将可被访问。
@JsAccessible
public class AccessibleClass {
public String sayHelloTo(String firstName) {
...
}
}
// 仅该类的单个方法可被访问。
public class RestrictedClass {
@JsAccessible
public String sayHelloTo(String firstName) {
...
}
}
对于无法添加注解的类(例如标准库类),可以使用以下方式使其可访问:
java
JsAccessibleTypes.makeAccessible(java.util.HashMap.class);
更多关于如何使对象对 JavaScript 可访问的信息,请参阅 JavaScript 指南。
Java 对象的生命周期
JavaFX 对传递给 JavaScript 的 Java 对象使用弱引用 。这意味着如果该对象被垃圾回收,其对应的 JavaScript 对象会变为 undefined
。
而 JxBrowser 使用的是强引用,会防止 Java 对象被回收。只有在以下几种情况下,引用才会被移除:frame 加载了新文档;frame 被移除;或 browser 被关闭时。
代理配置
JavaFX 的 WebView 使用的是 Java 运行时自带的网络栈,因此会自动遵循 Java 的代理配置。
在 JxBrowser 中,Chromium 在单独的进程中使用其自身的网络,并遵循系统代理设置。如果您不想使用系统设置,可以为每个 Profile 单独配置代理:
JavaFX
java
// 配置代理设置。
System.setProperty("http.proxyHost", "proxy.com");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "proxy.com");
System.setProperty("https.proxyPort", "8081");
System.setProperty("nonProxyHosts", "example.com|microsoft.com");
// 配置代理身份验证。
Authenticator.setDefault(new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("username", "password".toCharArray());
}
});
JxBrowser
java
// 配置代理设置。
var profile = engine.profiles().defaultProfile();
var exceptions = "example.com,microsoft.com";
var proxyRules = "http=proxy.com:8080;https=proxy.com:8081";
profile.proxy().config(CustomProxyConfig.newInstance(proxyRules, exceptions));
// 配置代理身份验证。
profile.network().set(AuthenticateCallback.class, (params, tell) -> {
if (params.isProxy()) {
tell.authenticate("username", "password");
} else {
// 跳过其他身份验证请求。
tell.cancel();
}
});
DOM 访问
JavaFX 和 JxBrowser 提供了一组类似的功能来访问 DOM 树:
JavaFX
java
var document = webEngine.getDocument();
var element = document.getElementById("exit-app");
((EventTarget) element).addEventListener("click", listener, false);
JxBrowser
java
browser.mainFrame()
.flatMap(Frame::document)
.flatMap(document -> document.findElementById("exit-app"))
.ifPresent(element -> {
element.addEventListener(CLICK, listener, true);
});
在 JxBrowser 中,DOM 节点在 Java 和 JavaScript 之间传递时会自动转换为对应的 Java 类型。与 JsObject
类似,它们在浏览器中会被保护,不会被垃圾回收。如需手动释放资源,可调用 close()
方法,使其可被回收。
更多关于 DOM 操作的内容,请参阅 DOM 指南。
打印功能
JavaFX 提供了 API 来打印任何可视节点,包括 WebView
。您可以选择打印机、通过代码配置部分打印参数,或者向用户展示系统打印对话框。
JxBrowser 则使用 Chromium 的打印能力。它同样支持选择打印机、以编程方式设置打印参数,并在需要时显示 Chromium 的打印预览对话框。
JavaFX
java
var printer = findMyPrinter(Printer.getAllPrinters());
var job = PrinterJob.createPrinterJob(printer);
if (showDialogs) {
// 向用户显示系统对话框。
job.showPageSetupDialog(stage);
job.showPrintDialog(stage);
} else {
// 或者静默打印
var settings = printerJob.getJobSettings();
settings.setCopies(3);
settings.setCollation(COLLATED);
webView.getEngine().print(job);
printerJob.job();
}
JxBrowser
java
browser.set(PrintCallback.class, (params, tell) -> {
if (showDialogs) {
// 向用户显示系统对话框。
tell.showPrintPreview();
} else {
// 或静默打印。
tell.print();
}
});
// 注册 `PrintHtmlCallback` 用于打印 HTML 页面。
// 若从 PDF 文件发起打印,则需使用 `PrintPdfCallback`。
browser.set(PrintHtmlCallback.class, (params, tell) -> {
var printer = findMyPrinter(params.printers());
var job = printer.printJob();
var settings = job.settings();
settings.copies(3);
settings.enableCollatePrinting();
job.on(PrintCompleted.class, event -> {
System.out.println("Printing completed");
});
tell.proceed(printer);
});
browser.set(PrintPdfCallback.class, (params, tell) -> {
...
});
提示: 即使没有系统打印机,您也可以使用 Chromium 内置的 PDF 打印机:
params.printers().pdfPrinter()
。
有关如何配置打印功能的更多信息,请参阅打印指南。

用户代理
在 JavaFX 中,您可以自定义 Browser 的用户代理(User-Agent)。
在 JxBrowser 中,您可以自定义单个 Browser 的用户代理,也可以自定义整个 Engine:
JavaFX
java
webEngine.setUserAgent("custom user agent");
JxBrowser
java
// 在 Engine 启动时配置全局用户代理。
var opts = EngineOptions
.newBuilder(HARDWARE_ACCELERATED)
.userAgent("custom user agent")
.build();
var engine = Engine.newInstance(opts);
// 或者,为特定 Browser 配置 UI。
browser.userAgent("custom user agent");
用户数据目录
在 JavaFX 中,用户数据目录用于存储本地存储中的数据。您可以显式配置该目录,或者 Engine 会根据操作系统和用户偏好自动选择。
在 JxBrowser 中,用户数据目录存储所有用户数据,包括缓存、本地存储和其他相关信息。您可以在启动 Engine 时配置该目录,或者 JxBrowser 会使用临时目录,该目录将在 Engine
关闭时被删除:
JavaFX
java
webEngine.setUserDataDirectory(new File("/path/to/directory"));
JxBrowser
java
var opts = EngineOptions
.newBuilder(HARDWARE_ACCELERATED)
.userDataDir(Paths.get("/path/to/directory"))
.build();
var engine = Engine.newInstance(opts);
请注意,同一个用户数据目录不能被单个或不同 Java 应用中运行的多个 Engine
实例同时使用。尝试使用同一个用户数据目录将导致 Engine
创建过程中抛出异常。
弹出窗口
在 JavaFX 中,当网页想要在新窗口中打开内容时,WebEngine
不会创建新窗口。相反,它会用新窗口替换当前加载的页面。
通过注册自定义弹出处理器,可以更改此行为:
java
webEngine.setCreatePopupHandler(features -> {
if (noPopups) {
// 返回 null 会取消弹出窗口的创建。
return null;
}
// 通过创建新的 WebView,可以指示 JavaFX 为新弹出窗口使用它。
var popupView = new WebView();
scene.getRoot().getChildren().add(popupView);
return popupView.getEngine();
});
在 JxBrowser 中,所有弹出窗口默认都是被抑制的。要更改此设置,需注册 CreatePopupCallback
:
java
browser.set(CreatePopupCallback.class, params -> {
return noPopups
? CreatePopupCallback.Response.suppress()
: CreatePopupCallback.Response.create();
}
});
如果允许创建弹出窗口且 BrowserView
在 UI 中可见,JxBrowser 会在新的 Stage
中打开弹出窗口。你可以在 OpenBrowserPopupCallback
中自定义此行为:
java
browser.set(OpenBrowserPopupCallback.class, params -> {
var popupBrowser = params.popupBrowser();
var popupBounds = params.initialBounds();
Platform.runLater(() -> {
var popupView = BrowserView.newInstance(browser);
scene.getRoot().getChildren().add(popupView);
});
return OpenBrowserPopupCallback.Response.proceed();
});
有关处理弹出对话框的更多信息,请参阅弹出窗口指南。
JavaScript 对话框
JavaFX 和 JxBrowser 都允许您自定义 JavaScript 对话框(如 confirm、prompt 和 alert)的行为:
JavaFX
java
webEngine.setConfirmHandler(value -> {
if (silent) {
return null;
} else {
return showMyConfirmDialog();
}
});
webEngine.setPromptHandler(promptData -> {
if (silent) {
return null;
} else {
return showMyPromptDialog(promptData);
}
});
webEngine.setOnAlert(event -> System.out.println("Alert happened!"));
JxBrowser
java
browser.set(ConfirmCallback.class, (params, action) -> {
if (silent) {
action.cancel();
} else {
var result = showMyConfirmDialog(params);
if (result) {
action.ok();
} else {
action.cancel();
}
}
});
browser.set(PromptCallback.class, (params, action) -> {
if (silent) {
action.cancel();
} else {
action.ok(showMyPromptDialog(params));
}
});
browser.set(AlertCallback.class, (params, action) -> {
System.out.println("Alert happened");
action.ok();
});
如果您不配置这些处理程序,JavaFX 默认会抑制对话框 ------ confirm 对话框返回 false
,prompt 对话框返回空字符串。而 JxBrowser 则会调用 JavaFX 的默认对话框实现来显示这些对话框。
您可以阅读对话框指南,了解如何自定义文件选择器、身份验证和其他类型的对话框。
自定义 CSS
在 JavaFX 中,可以通过设置样式表文件路径,或使用包含样式的 Data URL 来注入自定义 CSS。
在 JxBrowser 中,可以通过将 CSS 样式作为字符串传入来注入自定义样式:
JavaFX
java
webEngine.setUserStyleSheetLocation("file:///path/theme.css");
JxBrowser
java
// 此回调在文档准备就绪后触发,此时可注入 CSS。
browser.set(InjectCssCallback.class, params -> {
var styles = readFile("file:///path/theme.css")
return InjectCssCallback.Response.inject(styles);
});
总结
JavaFX WebView
和 JxBrowser 都提供了类似的功能,从 WebView
迁移至 JxBrowser 不会给您带来太多麻烦。
在本指南中,我们提供了迁移 WebView
大部分功能的代码示例,并附上了相关文档的链接。
尽管实际项目中的迁移工作可能会比较复杂,但我们相信通过本指南可以大大简化这一过程,并为您提供一个清晰的起点。