Javascript bridgeupcall to JavaFX (via JSObject.setMember() method) breaks when distributing - Stack Overflow

The ProblemI spent several hours trying to determine why my distributed code fails and yet my source co

The Problem

I spent several hours trying to determine why my distributed code fails and yet my source code when debugging with the IDE (NetBeans) works without issue. I have found a solution and am posting to help others that might have similar issues. BTW: I'm a self-taught programmer and might be missing a few fundamental concepts -- feel free to educate me.

Background Information

Using a WebView control within JavaFX application I load a webpage from an html file. I want to use JavaScript to handle the HTML side of things but I also need to freely pass information between Java and JavaScript (both directions). Works great to use the WebEngine.executeScript() method for Java initiated transfers and to use JSObject.setMember() in Java to set up a way for JavaScript to initiate information transfer to Java.

Setting up the link (this way breaks later):

/*Simple example class that gives method for 
JavaScript to send text to Java debugger console*/
public static class JavaLink {
    public void showMsg(String msg) {
        System.out.println(msg);
    }
}

...

/*This can be added in the initialize() method of  
the FXML controller with a reference to the WebEngine*/
public void initialize(URL url, ResourceBundle rb) {
    webE = webView.getEngine();

    //Retrieve a reference to the JavaScript window object
    JSObject jsObj = (JSObject)webE.executeScript("window");
    jsObj.setMember("javaLink", new JavaLink());
    /*Now in our JavaScript code we can type javaLink.showMsg("Hello!");
    which will send 'Hello!' to the debugger console*/
}

The code above will work great until distributing it and attempting to run the JAR file. After hours of searching and testing different tweaks I finally narrowed the problem down to the JavaLink object itself (I eventually learned that you can use try-catch blocks in JavaScript which enabled me to catch the error: "TypeError: showMsg is not a function...").

The Problem

I spent several hours trying to determine why my distributed code fails and yet my source code when debugging with the IDE (NetBeans) works without issue. I have found a solution and am posting to help others that might have similar issues. BTW: I'm a self-taught programmer and might be missing a few fundamental concepts -- feel free to educate me.

Background Information

Using a WebView control within JavaFX application I load a webpage from an html file. I want to use JavaScript to handle the HTML side of things but I also need to freely pass information between Java and JavaScript (both directions). Works great to use the WebEngine.executeScript() method for Java initiated transfers and to use JSObject.setMember() in Java to set up a way for JavaScript to initiate information transfer to Java.

Setting up the link (this way breaks later):

/*Simple example class that gives method for 
JavaScript to send text to Java debugger console*/
public static class JavaLink {
    public void showMsg(String msg) {
        System.out.println(msg);
    }
}

...

/*This can be added in the initialize() method of  
the FXML controller with a reference to the WebEngine*/
public void initialize(URL url, ResourceBundle rb) {
    webE = webView.getEngine();

    //Retrieve a reference to the JavaScript window object
    JSObject jsObj = (JSObject)webE.executeScript("window");
    jsObj.setMember("javaLink", new JavaLink());
    /*Now in our JavaScript code we can type javaLink.showMsg("Hello!");
    which will send 'Hello!' to the debugger console*/
}

The code above will work great until distributing it and attempting to run the JAR file. After hours of searching and testing different tweaks I finally narrowed the problem down to the JavaLink object itself (I eventually learned that you can use try-catch blocks in JavaScript which enabled me to catch the error: "TypeError: showMsg is not a function...").

Share Improve this question asked Mar 8, 2017 at 0:49 DatuPutiDatuPuti 6387 silver badges17 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 6

The Solution

I found that declaring a global variable to hold an instance of the JavaLink class and passing that as a parameter into the setMember() method fixes it so that the app now runs both in the IDE as well as a standalone JAR:

JavaLink jl;

...

    jl = new JavaLink();

    //replace anonymous instantiation of JavaLink with global variable
    jsObj.setMember("javaLink", jl); 

Why!?

I'm guessing this has to do with garbage collection and that the JVM does not keep a reference to JavaLink unless you force it to by declaring a global variable. Any other ideas?

Just to demo that the hypothesis postulated in @DatuPuti's answer appears to be correct. Here's a quick test. Pressing the HTML button increments the counter in the HTML page, and also outputs to the system console. After forcing garbage collection by pressing the "GC" button, the updates to the system console stop:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewCallbackGCTest extends Application {

    private final String HTML = 
              "<html>"
            + "  <head>"
            + "    <script>"
            + "    var count = 0 ;"
            + "    function doCallback() {"
            + "      count++ ;"
            + "      javaLink.showMsg('Hello world '+count);"
            + "      document.getElementById('test').innerHTML='test '+count;"
            + "    }"
            + "    </script>"
            + "  </head>"
            + "  <body>"
            + "    <div>"
            + "      <button onclick='doCallback()'>Call Java</button>"
            + "    </div>"
            + "    <div id='test'></div>"
            + "  </body>"
            + "</html>" ;

    @Override
    public void start(Stage primaryStage) {
        WebView webView = new WebView();
        webView.getEngine().loadContent(HTML);

        JSObject js = (JSObject) webView.getEngine().executeScript("window");
        js.setMember("javaLink", new JavaLink());

        Button gc = new Button("GC");
        gc.setOnAction(e -> System.gc());

        BorderPane root = new BorderPane(webView);
        BorderPane.setAlignment(gc, Pos.CENTER);
        BorderPane.setMargin(gc, new Insets(5));
        root.setBottom(gc);
        primaryStage.setScene(new Scene(root, 400, 400));
        primaryStage.show();
    }

    public static class JavaLink {
        public void showMsg(String msg) {
            System.out.println(msg);
        }
    }

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

Of course, as stated in the other answer, if you declare an instance variable

private JavaLink jl = new JavaLink();

and replace

js.setMember("javaLink", new JavaLink());

with

js.setMember("javaLink", jl);

the problem is fixed, and the updates continue to appear in the console after calling System.gc();.

The solution from @DatuPuti works fine but partially. In my case, I've got multiple nested classes in the JavaLink class. Before doing what @DatuPuti exposes all the nested classes link broke and after applying the exposed solution, only the class being called is the one that keeps the link, all the rest are still breaking. This is the model of my JavaLink class:

public class JavaLink{

    public NestedClass1 ClassName1;
    public NestedClass2 ClassName2;

    //Constructor
    public JavaLink(){
        ClassName1 = new NestedClass1();
        ClassName2 = new NestedClass2();
    }

    //Nested classes being called from Javascript
    public class NestedClass1(){}
    public class NestedClass2(){}

}

The link would be created like this:

JavaLink javaLink;
...

    javaLink = new JavaLink();
    jsObject.setMember("JavaLink", javaLink);

Then, from Javascript I call classes methods like this:

JavaLink.ClassName1.method();
JavaLink.ClassName2.method();

And here es the probem: When the crash occurs calling ClassName1 methods, ClassName2 unlinks and it's not available anymore using Javascript. The same happens if I the crash occurs while calling ClassName2 methods.

The solution that works for me (in addition to the exposed solution):

Besides declaring JavaLink in the higher scope possible, declaring all the nested classes will make them keep the link. For example (keeping the same reference from the example class model):

JavaLink javaLink;
JavaLink.NestedClass1 nestedClass1;
JavaLink.NestedClass2 nestedClass2;

    javaLink = new JavaLink();
    nestedClass1 = javaLink.new NestedClass1();
    nestedClass2 = javaLink.new NestedClass2();

    //Creating an Object array to store classes instances
    Object[] javaLinkClasses = new Object[2];
    javaLinkClasses[0] = nestedClass1;
    javaLinkClasses[1] = nestedClass2;
    jsObject.setMember("JavaLink", javaLinkClasses); //Setting member as an array

And then finally, in order to call methods from nested classes in Javascript, object reallocation is needed, just like this (Javascript):

JavaLink.NestedClass1 = JavaLink[0];
JavaLink.NestedClass2 = JavaLink[1];

//Now we are able to call again methods from nested classes without them unlinking
JavaLink.NestedClass1.method();

I hope this helps people facing the same issue. I'm using Java JDK 1.8 with IntelliJIDEA.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744260977a4565632.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信