Search code examples
javascriptjavajavafxwebengine

JavaFX WebEngine JavaScript Upcalls


In java Windows 10 pro x64 jre 1.8.0_60 the following code produces the intended output (After clicking the html button):

Hello World

However in java Windows 10 pro x64 jre 1.8.0_152 there seems to be some sort of disconnect, as it will not output anything to console upon clicking the button

Why on the latest version of java (at the time 152) is my code giving unpredictable and typically unwanted results. I've attempted to give the minimum code to create the scenario below.

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class Main extends Application {

public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage primaryStage) {
    WebView browser = new WebView();
    final WebEngine webEngine = browser.getEngine();
    webEngine.setJavaScriptEnabled(true);
    webEngine.load("https://api.ipify.org/?format=json");
    webEngine.getLoadWorker().stateProperty().addListener(
            new ChangeListener<Worker.State>() {
                @Override
                public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
                    if (newState == Worker.State.SUCCEEDED) {
                        JSObject jso = (JSObject) webEngine.executeScript("window");
                        webEngine.executeScript(
                                "var button = document.createElement(\"button\");\n" +
                                        "button.innerHTML = \"Do Something\";\n" +
                                        "var body = document.getElementsByTagName(\"body\")[0];\n" +
                                        "body.appendChild(button);\n" +
                                        "button.addEventListener (\"click\", function() {java.exit();});");
                        jso.setMember("java", new Bridge());

                    }
                }
            });
    BorderPane panel = new BorderPane(browser);
    Scene scene = new Scene(panel, 700, 700);
    primaryStage.setScene(scene);
    primaryStage.show();
}

public class Bridge {
    public void exit() {
        System.out.println("Hello World");
    }
}}

Solution

  • You are passing new Bridge() to the setMember method. Since no variable holds the Bridge instance, it gets garbage-collected before you ever press the button.

    From the WebEngine documentation:

    Note that in the above example, the application holds a reference to the JavaApplication instance. This is required for the callback from JavaScript to execute the desired method.

    In the following example, the application does not hold a reference to the Java object:

    JSObject window = (JSObject) webEngine.executeScript("window");
    window.setMember("app", new JavaApplication());

    In this case, since the property value is a local object, "new JavaApplication()", the value may be garbage collected in next GC cycle.

    When a user clicks the link, it does not guarantee to execute the callback method exit.

    Keep your Bridge object in a field to prevent garbage collection of it:

    new ChangeListener<Worker.State>() {
        private final Bridge bridge = new Bridge();
    
        @Override
        public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
            if (newState == Worker.State.SUCCEEDED) {
    
                // ...
    
                jso.setMember("java", bridge);
            }
        }
    

    Why didn’t it happen in earlier versions of Java? Because different Java releases are free to change the timing and behavior of garbage collection. You got lucky, but in later versions, your luck ran out.