ADDITIONAL SYSTEM INFORMATION :
OS:
Linux Mint 21.2 Cinnamon
Java:
openjdk version "20.0.2" 2023-07-18
OpenJDK Runtime Environment Temurin-20.0.2+9 (build 20.0.2+9)
OpenJDK 64-Bit Server VM Temurin-20.0.2+9 (build 20.0.2+9, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
On JavaFX 19 and 20, I cannot reliably register an EventListener multiple times on a WebView when reloading the page. Specifically if I unload the page (by loading "about:blank"), wait for a time, then reload the page again, I cannot reregister the same EventListener instance without breaking event handling.
If the GC happens to run during the time the page is unloaded, then after the page is loaded again and an event occurs, a NullPointerException is logged and my EventListener is not called.
In the attached example, the important details are:
1. A page is loaded into a WebView, and when the status becomes SUCCEEDED, an event listener is registered for the button on the page.
2. After the page's button is clicked, we unload the page by loading "about:blank". Then we request a GC cycle.
- The GC cycle is no something I do in real world code, but do it here for the sake of reproduction.
3. When we load the page again, we register the same EventListener instance.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
- Run the attached example.
- Click the "Open Page" button to open a web page in the WebView with a single button in it.
- Click the "Close Page" button in the WebView to close the page (load about:blank)
- Click the "Open Page" button again to reopen the web page.
- Click the "Close Page" button again.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The EventListener should be called and the "about:blank" page loaded into the WebView.
ACTUAL -
The EventListener is not invoked, and this NullPointerException is logged:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.web@20/com.sun.webkit.WebPage.twkProcessMouseEvent(Native Method)
at javafx.web@20/com.sun.webkit.WebPage.dispatchMouseEvent(WebPage.java:857)
at javafx.web@20/javafx.scene.web.WebView.processMouseEvent(WebView.java:1098)
at javafx.web@20/javafx.scene.web.WebView.lambda$registerEventHandlers$3(WebView.java:1224)
at javafx.base@20/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
at javafx.base@20/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at javafx.base@20/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232)
at javafx.base@20/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189)
at javafx.base@20/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base@20/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base@20/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base@20/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base@20/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.base@20/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics@20/javafx.scene.Scene$MouseHandler.process(Scene.java:3980)
at javafx.graphics@20/javafx.scene.Scene.processMouseEvent(Scene.java:1890)
at javafx.graphics@20/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2704)
at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:411)
at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:301)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:450)
at javafx.graphics@20/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
at javafx.graphics@20/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:449)
at javafx.graphics@20/com.sun.glass.ui.View.handleMouseEvent(View.java:551)
at javafx.graphics@20/com.sun.glass.ui.View.notifyMouse(View.java:937)
at javafx.graphics@20/com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
at javafx.graphics@20/com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:316)
at java.base/java.lang.Thread.run(Thread.java:1623)
---------- BEGIN SOURCE ----------
package com.kwvanderlinde;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
public class JavaFxNpe extends Application {
private final EventListener listener = this::onButtonClicked;
private WebView webView;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("NPE on WebView event");
Pane root = new VBox();
webView = new WebView();
final var button = new Button("Open Page");
button.setOnAction(event -> loadWebPage());
root.getChildren().add(button);
root.getChildren().add(webView);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
webView.getEngine().getLoadWorker().stateProperty().addListener(this::onStateChanged);
}
private void loadWebPage() {
webView.getEngine().loadContent("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Submit Test</title>
</head>
<body>
<button>Close Page</button>
</body>
</html>
""");
}
private void onStateChanged(ObservableValue<? extends Worker.State> observable,
Worker.State oldState,
Worker.State newState) {
if (newState != Worker.State.SUCCEEDED) {
return;
}
final var nodeList = webView.getEngine().getDocument().getElementsByTagName("button");
for (int i = 0; i < nodeList.getLength(); i++) {
final var eventTarget = (EventTarget) nodeList.item(i);
eventTarget.addEventListener("click", this.listener, true);
}
}
private void onButtonClicked(Event event) {
// On JFX thead
webView.getEngine().load("about:blank");
// Need to delay the GC to after this event is handled, but we also want a GC before we open
// the page again.
Platform.runLater(System::gc);
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Registering a different EventListener instance each time will avoid the issue. E.g., in the example code, I could use `this::onButtonClicked` instead of `this.listener` and everything will work fine.
FREQUENCY : always