Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX and Spring - beans doesn't Autowire

I have problems with combining JavaFX and Spring. I have simple JavaFX application, which works fine. Now I am trying to add some Spring to it. I followed JavaFX 2 with Spring Tutorial. My code:

src/main
|
|_java/mycompany/imageviewer
|   |
|   |_Startup.java
|   |_controller/ImageViewController.java
|   |_dataprovider
|       |impl/DataProviderImpl.java
|   |_config
|       |_SpringFxmlLoader.java
|       |_SpringApplicationConfig.java
|_resources/mycompany/view/ImageViewer.fxml

Startup.java is file with main:

public class Startup extends Application {

    private static final SpringFxmlLoader loader = new SpringFxmlLoader();

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        ...    
        Parent root = (Parent) loader.load("/mycompany/imageviewer/view/ImageViewer.fxml","mycompany/imageviewer/bundle/bundle");
        Scene scene = new Scene(root);
        ...css etc...
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

ImageviewerController.java:

@Controller
public class ImageViewerController {
    private static final Logger LOG = Logger.getLogger(ImageViewerController.class);
    @FXML
    ...    
    @Autowired
    private DataProvider dataProvider;

    public ImageViewerController() {
        LOG.debug("Controller initialized. DataProvider is null: "+(dataProvider==null));
    }

DataProviderImpl.java:

@Service("dataProvider")
public class DataProviderImpl implements DataProvider {
    private static final Logger LOG = Logger.getLogger(DataProviderImpl.class);

    public DataProviderImpl() {
        LOG.debug("DataProviderImpl initialized.");
    }
    ...methods...
}

My SpringFxmlLoader looks similar to this in tutorial:

public class SpringFxmlLoader {

    private static final ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringApplicationConfig.class);

    public Object load(String url, String resources) {
        FXMLLoader loader = new FXMLLoader();
        loader.setControllerFactory(clazz -> applicationContext.getBean(clazz));
        try {
            return loader.load(getClass().getResource(url), ResourceBundle.getBundle(resources));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

My SpringApplicationConfig:

@Configuration
@ComponentScan(basePackages = {"mycompany.imageviewer.controller", "mycompany.imageviewer.dataprovider.impl" })
public class SpringApplicationConfig {
    private static final Logger LOG = Logger.getLogger(SpringApplicationConfig.class);

    @Bean
    public DataProvider dataProvider() {
        LOG.debug("Initializing dataProvider via SpringApplicationConfig");
        return new DataProviderImpl();
    }

    @Bean
    public ImageViewerController imageViewerController() {
        LOG.debug("Initializing ImageViewerController via SpringApplicationConfig");
        return new ImageViewerController();
    }
}

In my app I have ImageViewer.fxml with binded controller:

<AnchorPane fx:controller="mycompany.imageviewer.controller.ImageViewerController" xmlns="http://javafx.com/javafx/8.0.51" xmlns:fx="http://javafx.com/fxml/1" >

When I run program I get logs:

DEBUG [main] mycompany.imageviewer.controller.ImageViewerController:74 - Controller initialized. DataProviderImpl is null: true
DEBUG [main] mycompany.imageviewer.dataprovider.impl.DataProviderImpl:22 - DataProviderImpl initialized.
DEBUG [JavaFX Application Thread] mycompany.imageviewer.controller.ImageViewerController:74 - Controller initialized. DataProviderImpl is null: true

Which shows, that my controller is initialized twice, and the dataProvider is not properly binded. What made me confused, was that when I accidentaly wrote wrong basePackages in ComponentScan in this way with wrong packages:

@ComponentScan(basePackages = {"mycompany.imageviewer.dataprovider.controller", "mycompany.imageviewer.dataprovider.dataprovider.impl" })

Beans initialize methods in SpringApplicationConfig.java run and I get logs from them:

2015-09-06 16:52:29,420 DEBUG [main] com.capgemini.starterkit.imageviewer.config.SpringApplicationConfig:19 - Initializing dataProvider via SpringApplicationConfig
2015-09-06 16:52:29,431 DEBUG [main] com.capgemini.starterkit.imageviewer.dataprovider.impl.DataProviderImpl:22 - DataProviderImpl initialized.
DEBUG [main] mycompany.imageviewer.config.SpringApplicationConfig:25 - Initializing ImageViewerController via SpringApplicationConfig
DEBUG [main] mycompany.imageviewer.controller.ImageViewerController:74 - Controller initialized. DataProviderImpl is null: true
DEBUG [JavaFX Application Thread] mycompany.imageviewer.controller.ImageViewerController:74 - Controller initialized. DataProviderImpl is null: true

When I run basePackages = "com.capgemini.starterkit.imageviewer" effect is the same like in the first case. I am new to spring and probably I made some simple mistakes, but I am not able to find them, so if anybody can help me configure spring that would be great.:-)

like image 857
sus-mateusz Avatar asked Sep 06 '15 14:09

sus-mateusz


People also ask

Why Autowired is not working?

When @Autowired doesn't work. There are several reasons @Autowired might not work. When a new instance is created not by Spring but by for example manually calling a constructor, the instance of the class will not be registered in the Spring context and thus not available for dependency injection.

What is difference between @bean and @autowire?

@Bean is just for the metadata definition to create the bean(equivalent to tag). @Autowired is to inject the dependancy into a bean(equivalent to ref XML tag/attribute).

What can I use instead of Autowired?

Since Spring is the most popular DI and IOC container for Java application, @Autowired is more common and @Inject is lesser-known, but from a portability point of view, it's better to use @Inject. Spring 3.0 supports JSR-330 annotation, so if you are using Spring 3.0 or higher release, prefer @Inject over @Autowired.


1 Answers

The FXMLLoader.load(URL, ResourceBundle) method you are calling is a static method - so it actually pays no attention to the FXMLLoader instance you instantiated, and consequently ignores the controllerFactory which references your Spring bean factory.

Rewrite your SpringFXMLLoader class as follows:

public class SpringFxmlLoader {

    private static final ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringApplicationConfig.class);

    public Object load(String url, String resources) {
        FXMLLoader loader = new FXMLLoader();
        loader.setControllerFactory(clazz -> applicationContext.getBean(clazz));
        loader.setLocation(getClass().getResource(url));
        loader.setResources(ResourceBundle.getBundle(resources));
        try {
            return loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

This uses the instance method loader.load() which will use your controller factory: i.e. it will use Spring to instantiate the controller.

The reason you see the controller loaded twice is that by default the bean factory gives the controller singleton scope and makes it eagerly created, so as soon as you create the bean factory (applicationContext) it creates a controller. That controller will have its dataProvider initialized (but only after the constructor has completed, of course). Then the call to the static FXMLLoader.load(...) method creates a second controller by the usual mechanism (i.e. by calling its no-arg constructor). That instance will not have its dataProvider initialized at any time.

As an aside, you probably don't want controllers to be singletons. If you were to load your FXML file twice, to get two instances of the Parent, you would likely need each instance to have its own controller, otherwise strange behavior would ensue. I would recommend making the controller a prototype (which means the bean factory will create a new instance every time one is requested, instead of reusing a single instance). You can do this with the following in your config class:

@Configuration
@ComponentScan(basePackages = {"mycompany.imageviewer.controller", "mycompany.imageviewer.dataprovider.impl" })
public class SpringApplicationConfig {
    private static final Logger LOG = Logger.getLogger(SpringApplicationConfig.class);

    @Bean
    public DataProvider dataProvider() {
        LOG.debug("Initializing dataProvider via SpringApplicationConfig");
        return new DataProviderImpl();
    }

    @Bean
    @Scope("prototype")
    public ImageViewerController imageViewerController() {
        LOG.debug("Initializing ImageViewerController via SpringApplicationConfig");
        return new ImageViewerController();
    }
}
like image 139
James_D Avatar answered Oct 07 '22 02:10

James_D