I am currently working on a JavaFX-based application where users can interact with places marked on a world map. To do this, I use an approach similar to that described in http://captaincasa.blogspot.de/2014/01/javafx-and-osm-openstreetmap.html ([1]).
However, I have to deal with the difficult debugging task associated with a Javascript callback variable embedded in an embedded HTML page using the WebEngine setMember () method (see also https://docs.oracle.com/javase/8/javafx/ embedded-browser-tutorial / js-javafx.htm ([2]) for the official tutorial).
When you run the program for some time, the callback variable loses its state unpredictably! To demonstrate this behavior, I developed a minimal working / unsuccessful example. I am using jdk1.8.0_121 64-bit on a machine running Windows 10.
A JavaFx application is as follows:
import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import javafx.application.Application; import javafx.concurrent.Worker.State; import javafx.scene.Scene; import javafx.scene.layout.AnchorPane; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import netscape.javascript.JSObject; public class WebViewJsCallbackTest extends Application { private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); public static void main(String[] args) { launch(args); } public class JavaScriptBridge { public void callback(String data) { System.out.println("callback retrieved: " + data); } } @Override public void start(Stage primaryStage) throws Exception { WebView webView = new WebView(); primaryStage.setScene(new Scene(new AnchorPane(webView))); primaryStage.show(); final WebEngine webEngine = webView.getEngine(); webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm()); webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> { if (newValue == State.SUCCEEDED) { JSObject window = (JSObject) webEngine.executeScript("window"); window.setMember("javaApp", new JavaScriptBridge()); } }); webEngine.setOnAlert(event -> { System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData()); }); } }
The HTML file "page.html" is as follows:
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script type="text/javascript"> var javaApp = null; function javaCallback(data) { try { alert("javaApp=" + javaApp + "(type=" + typeof javaApp + "), data=" + data); javaApp.callback(data); } catch (e) { alert("caugt exception: " + e); } } </script> </head> <body> <button onclick="javaCallback('Test')">Send data to Java</button> <button onclick="setInterval(function(){ javaCallback('Test'); }, 1000)">Send data to Java in endless loop</button> </body> </html>
The state of the javaApp
callback variable can be seen by clicking the "Send data to Java in endless loop"
button. He will constantly try to run the callback method using javaApp.callback
, which displays some message about registering in the Java application. Alerts are used as an additional communication channel to restore health (they always seem to work and are currently being used as workers, but this is not how things can be ...).
If everything works as expected, write a log each time that looks like the following lines:
callback retrieved: Test 2017/01/27 21:26:11 alerted: javaApp=webviewtests.WebViewJsCallbackTest$JavaScriptBridge@51fa c693(type=object), data=Test
However, after a while (something from 2 to 7 minutes), callbacks no longer return, but only records are printed, such as the following line:
2017/01/27 21:32:01 alerted: javaApp=undefined(type=object), data=Test
Printing the variable now gives 'undefined'
instead of the path of the Java instance. A strange observation is that the state of javaApp
not true "undefined". using typeof
returns object
, javaApp === undefined
evaluates to false
. This is consistent with the fact that callback-call does not throw an exception (otherwise, a warning will start, starting with "caugt exception: "
).
Using Java VisualVM showed that the time of failure coincides with the time when the garbage collector is activated. This can be seen by observing the memory consumption of the heap, which drops from approx. 60MB to 16MB due to GC.
What is happening there? Do you know how I can further debug this problem? I could not find the error associated with this ...
Thanks so much for your advice!
PS: the problem was reproduced much faster if you included Javascript code to display the world map through the Flyer (cf [1]). Loading or moving the card in most cases instantly forced the GC to do its job. By debugging this original problem, I traced the problem to the minimum example presented here.