2

I have created the Spring boot JavaFX application with scene builder. When I add the WebView component in my FXML file I am getting the below error:

Caused by: javafx.fxml.LoadException: 
/C:/Users/pdhasar/WebProject/standalone-app/target/classes/WebView.fxml:12

    at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2605)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2583)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2445)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2413)
    at com.web.view.WebViewApplication.init(WebViewApplication.java:29)
    at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:841)
    ... 3 more
Caused by: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
    at javafx.fxml.JavaFXBuilder$ObjectBuilder.build(JavaFXBuilderFactory.java:278)
    at javafx.fxml.FXMLLoader$ValueElement.processEndElement(FXMLLoader.java:759)
    at javafx.fxml.FXMLLoader.processEndElement(FXMLLoader.java:2827)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2536)
    ... 7 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at sun.reflect.misc.Trampoline.invoke(MethodUtil.java:71)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at sun.reflect.misc.MethodUtil.invoke(MethodUtil.java:275)
    at javafx.fxml.JavaFXBuilder$ObjectBuilder.build(JavaFXBuilderFactory.java:270)
    ... 10 more
Caused by: java.lang.IllegalStateException: Not on FX application thread; currentThread = JavaFX-Launcher
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:204)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:438)
    at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1182)
    at javafx.scene.web.WebEngine.<init>(WebEngine.java:822)
    at javafx.scene.web.WebEngine.<init>(WebEngine.java:811)
    at javafx.scene.web.WebView.<init>(WebView.java:271)
    at javafx.scene.web.WebViewBuilder.build(WebViewBuilder.java:60)
    ... 21 more
2019-10-07 14:00:12.979  INFO 14680 --- [       Thread-6] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7f904074: startup date [Mon Oct 07 14:00:10 IST 2019]; root of context hierarchy

Below given the code with spring boot: WebViewApplication:

package com.web.view;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.web.view")
public class WebViewApplication extends Application {

    private ConfigurableApplicationContext context;
    private Parent rootNode;

    @Override
    public void init() throws Exception {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(WebViewApplication.class);
        context = builder.run(getParameters().getRaw().toArray(new String[0]));
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/WebView.fxml"));
        loader.setControllerFactory(context::getBean);
        rootNode = loader.load();
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();
        double width = visualBounds.getWidth();
        double height = visualBounds.getHeight();

        primaryStage.setScene(new Scene(rootNode, width, height));
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        context.close();
    }
}

WebViewController:

package com.web.view.controller;

import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import org.springframework.stereotype.Controller;

@Controller
public class WebViewController {

    @FXML private WebView webView;
    private WebEngine webEngine;

    @FXML
    private void handleWebViewButtonAction(ActionEvent event) {
        webEngine = webView.getEngine();
        webEngine.load("https://www.google.co.in");
    }
}

FXML:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.web.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.web.view.controller.WebViewController">
   <children>
      <WebView fx:id="webView" layoutX="-116.0" layoutY="-111.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
   </children>
</AnchorPane>

It seems the spring is trying to access the WebView and there is a restriction in javafx, which is not allowing spring to access it. Is there way to make the WebView work with spring boot. Can any one help me with this.

Praveen Dhasarathan
  • 648
  • 2
  • 11
  • 24

2 Answers2

1

Background on JavaFX threads

It is a limitation of the JavaFX system that not all controls can be constructed off of the JavaFX application thread. When this is the case for a given control or UI element, its documentation notes this.

In particular, the documentation for WebView states:

WebView objects must be created and accessed solely from the FX thread.

The init() method of a JavaFX application does not run on the JavaFX application thread. In my environment (OS X, JavaFX 13), the init() method of the JavaFX application is called by a thread named "JavaFX-Launcher". As can be seen if you place the following code in init():

System.out.println("Thread name: " + Thread.currentThread().getName());

Placing the same code in the start() method shows that it is running on a different thread, which has the name "JavaFX Application Thread". This is the correct thread on which to create a WebView.

The FXML loader constructs objects based upon the FXML file. If you try to load FXML off of the JavaFX application thread and it contains one of the kind of controls which must be constructed on the JavaFX application thread, you will get a failure (as you see). Most controls can be constructed off of the JavaFX thread, so most of the time it is not an issue. However, WebView must be constructed on the JavaFX application thread, so you can't load an FXML document with a WebView off of the JavaFX application thread.

Load FXML containing a WebView reference, from start() not init()

The solution is to move the location where you load the FXML which contains a WebView, from the init() method, to the start() method (suggested by kleopatra in comments) as demonstrated below:

import javafx.fxml.FXMLLoader;
import javafx.geometry.Rectangle2D;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan("com.web.view")
public class WebViewApplication extends Application {

    private ConfigurableApplicationContext context;
    private Parent rootNode;

    @Override
    public void init() throws Exception {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(WebViewApplication.class);
        context = builder.run(getParameters().getRaw().toArray(new String[0]));
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/WebView.fxml"));
        loader.setControllerFactory(context::getBean);
        rootNode = loader.load();

        Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();
        double width = visualBounds.getWidth();
        double height = visualBounds.getHeight();

        primaryStage.setScene(new Scene(rootNode, width, height));
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        context.close();
    }
}

Unrelated design advice

As an aside, I would advise not making your JavaFX application a Spring application.

Instead, create a separate class for the Spring application:

@SpringBootApplication
public class MySpringApplication {
    // spring application implementation.
}

And have your JavaFX application call run() on a SpringApplicationBuilder instance that has been initialized with your specific spring application class (i.e. in your JavaFX init() method have):

SpringApplicationBuilder builder = new SpringApplicationBuilder(
    MySpringApplication.class
);
context = builder.run(
    getParameters().getRaw().toArray(new String[0])
);

Otherwise, you may have two instances of your JavaFX application created at runtime (one by the JavaFX application), and another by the Spring application runner, which can be confusing. Plus, by separating the two, you get a better design through a stronger separation of concerns, with the JavaFX application responsible for processing JavaFX application lifecycle events and the Spring application responsible for spring lifecycle events.

Then you can launch and test your spring application completely independent of the JavaFX application, which should make testing and analysis of the code easier.

jewelsea
  • 150,031
  • 14
  • 366
  • 406
0

Finally I got the solution. After I added the below code in the init method, it worked

Platform.runLater(() -> { ... });
Praveen Dhasarathan
  • 648
  • 2
  • 11
  • 24
  • 3
    hmm ... wondering why you use the init method (which is doc'ed to be called off the fx app thread) at all - instead of wrapping it into runlater (which queues it for execution on the fx app thread) you could move it to start (which is doc'ed to be called on the fx thread), or why not? – kleopatra Oct 07 '19 at 11:45
  • It is failing in init not in the start. spring thread is trying to initialize and load the element and it fails. – Praveen Dhasarathan Oct 07 '19 at 16:53
  • Praveen, there is no "spring thread". In my environment (OS X, JavaFX 13), the `init()` method of the JavaFX application is called by a thread named `JavaFX-Launcher`. As can be seen by the following code in `init()` => `System.out.println("Thread name: " + Thread.currentThread().getName());`. – jewelsea Oct 07 '19 at 21:30