Program-Me-Rev
Program-Me-Rev

Reputation: 6624

How to autowire Spring beans into a FXML controller class via a Spring XML config file

I have a FXML controller that has some Spring Bean dependencies. I can't find a way to autowire them in time before the controller is loaded, since I'm using a custom FXML loader:

@Bean
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "prototype")
public UserProfile attachDocController() throws IOException {
    return (UserProfile) loadController("/myproject/Forms/userProfile.fxml");
}

FXMLLoader loader = null;

protected Object loadController(String url) throws IOException {
    loader = new FXMLLoader(getClass().getResource(url));
    loader.load();
    return loader.getController();
}

Using this approach, I can only autowire the beans by directly injecting them via the @Autowired annotation:

public class UserProfile {

    @Autowired
    MyDependency myDependency;

This leaves me dependent on Spring, and will leave me with code maintainability issues later on. How can I autowire dependencies from a Spring XML file configuration into a FXML controller class? Something like:

<bean id="UserProfile" class="myproject.controllerinjection.UserProfile" scope="prototype">
    <aop:scoped-proxy proxy-target-class="true"/>

    <property name="myDependency" ref="myDependency" />
</bean>

<bean id="myDependency" class="myproject.controllerinjection.MyDependency" scope="prototype">
    <aop:scoped-proxy proxy-target-class="true"/>
</bean>

This seems like a much better route, with long-term project maintainability in mind, as the project gets larger.

UPDATE:

I'me not really used to Lambda expressions. I've researched a bit, but integrating the suggestion by @James_D as follows:

protected Object loadBeanController(String url) throws IOException {
    loader = new FXMLLoader(getClass().getResource(url));
    ApplicationContext ctx = WakiliProject.getCtx();

    if (ctx != null) {
        System.out.println("Load Bean...............");
        loader.setControllerFactory(ctx::getBean);

    } else {
        System.out.println("No App.ctx...............");
    }

    return loader.getController();
}

gives a null pointer whenever I try calling a method of MyDependency. MyDependency myDependency never gets injected into UserProfile.

Upvotes: 2

Views: 1966

Answers (1)

James_D
James_D

Reputation: 209330

When you call FXMLLoader.load(), it loads the FXML file. If there is a fx:controller attribute in the root element, it creates a controller based on the class specified (and injects the fx:id-attributed elements into that controller instance, etc.). The loader then returns the root of the FXML file. The controller is intrinsically linked to that FXML root.

By default, the FXMLLoader maps a controller class to an instance by reflection, calling controllerClass.newInstance() (which effectively invokes the no-arg constructor of the controller class). You can configure this, overriding the default behavior, by specifying a controllerFactory on the FXMLLoader.

The controllerFactory is a function that maps a Class<?> object (constructed from the class name specified in the fx:controller attribute) to the controller instance. If you are using Spring to manage your controller instances, you just need this function to ask the Spring application context (bean factory) to generate the controller instance for you. So you can basically just do fxmlLoader.setControllerFactory(applicationContext::getBean);. With this setup, simply loading the fxml file via the FXMLLoader will cause the FXMLLoader to request the controller class from the application context. The application context can be configured in any of the ways that Spring allows.

So your config can look like

@Configuration
public class Config {
    @Bean
    @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "prototype")
    public UserProfile attachDocController() throws IOException {
        return new UserProfile();
    }
}

Of course, you can now inject dependencies in the config class:

    @Bean
    @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "prototype")
    public UserProfile attachDocController(MyDependency myDependency) throws IOException {
        return new UserProfile(myDependency);
    }

    @Bean
    public MyDependency createDependency() {
        return new MyDependencyImpl();
    }

Then in your UI work you can just do

FXMLLoader loader = new FXMLLoader(getClass().getResource("/myproject/Forms/userProfile.fxml"));
loader.setControllerFactory(applicationContext::getBean);
Parent root = loader.load();

// since everything can be initialized in the controller by D.I., you
// shouldn't need to access it, but if you do for some reason you can do

UserProfile controller = loader.getController();

where applicationContext is your Spring application context. This works whether the application context use XML configuration, annotation-based configuration, or Java configuration.

Update

If, for some reason, you cannot use Java 8 or later, a call to setControllerFactory that is Java 7 compatible looks like:

loader.setControllerFactory(new Callback<Class<?>, Object>() {
    @Override
    public Object call(Class<?> c) {
        return applicationContext.getBean(c);
    }
});

You would need applicationContext to be either a field or a final local variable for this to work in Java 7. Note that at the time of writing, Java 7 is not publicly supported by Oracle.

Upvotes: 3

Related Questions