Reputation: 6624
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
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