Reputation: 41
I've been through a number of tutorials on integrating Spring DI with JavaFx but I've hit a wall that the simple examples dont cover (and I cant figure out).
I want clean separation between the view and presentation layers. I would like to use fxml to define composable views and Spring to wire it all together. Here's a concrete example:
Dashboard.fxml:
<GridPane fx:id="view"
fx:controller="com.scrub.presenters.DashboardPresenter"
xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml">
<children>
<TransactionHistoryPresenter fx:id="transactionHistory" />
</children>
</GridPane>
Main.java:
public void start(Stage primaryStage) throws Exception{
try {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppFactory.class);
SpringFxmlLoader loader = context.getBean(SpringFxmlLoader.class);
primaryStage.setScene(new Scene((Parent)loader.load("/views/dashboard.fxml")));
primaryStage.setTitle("Hello World");
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
SpringFxmlLoader.java:
public class SpringFxmlLoader {
@Autowired
ApplicationContext context;
public Object load(String url) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource(url));
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> aClass) {
return context.getBean(aClass);
}
});
return loader.load();
} catch(Exception e) {
e.printStackTrace();
throw new RuntimeException(String.format("Failed to load FXML file '%s'", url));
}
}
}
So when DashboardPresenter gets loaded the SpringFxmlLoader correctly injects the controller with the loader.setControllerFactory.
However, the custom TransactionHistoryPresenter control is loaded with a new instance and not from the spring context. It must be using its own FXMLLoader?
Any ideas how to make custom controls play nice with Spring? I really dont want to go down the path of having the controllers / presenters manually wiring them up.
Upvotes: 4
Views: 5006
Reputation: 197
It is possible.
Create custom BuilderFactory that delivers spring beans. Then assign it to the FXMLLoader fxmlLoader.setBuilderFactory(beanBuilderFactory);
@Component
public class BeanBuilderFactory implements BuilderFactory {
@Autowired
private ConfigurableApplicationContext context;
public BeanBuilderFactory() {
}
@Override
public Builder<?> getBuilder(Class<?> type) {
try {
Object bean = this.context.getBean(type);
if (bean.getClass().isAssignableFrom(type))
return new Builder() {
@Override
public Object build() {
return bean;
}
};
else
return null;
} catch (BeansException e) {
return null;
}
}
}
Upvotes: 0
Reputation: 611
The main problem here, is make sure that Spring is initialized on the same thread of the JavaFX application. This usually means that Spring code must be executed on the JavaFX application thread; other time-consuming jobs can of course be executed on their own thread.
This is the solution I put together using this tutorial and my own knowledge of Spring Boot:
@SpringBootApplication
@ImportResource("classpath:root-context.xml")
public class JavaFXSpringApplication extends Application {
private static final Logger log = LoggerFactory.getLogger(JavaFXSpringApplication.class);
private Messages messages;
private static String[] args;
@Override
public void start(final Stage primaryStage) {
// Bootstrap Spring context here.
ApplicationContext context = SpringApplication.run(JavaFXSpringApplication.class, args);
messages = context.getBean(Messages.class);
MainPaneController mainPaneController = context.getBean(MainPaneController.class);
// Create a Scene
Scene scene = new Scene((Parent) mainPaneController.getRoot());
scene.getStylesheets().add(getClass().getResource("/css/application.css").toExternalForm());
// Set the scene on the primary stage
primaryStage.setScene(scene);
// Any other shenanigans on the primary stage...
primaryStage.show();
}
public static void main(String[] args) {
JavaFXSpringApplication.args = args;
launch(args);
}
}
This class is both a JavaFX application entry point and a Spring Boot initialization entry point, hence the passing around of varargs. Importing an external configuration file makes it easier to keep the main class uncluttered while getting other Spring-related stuff ready (i.e. setting up Spring Data JPA, resource bundles, security...)
On the JavaFX "start" method, the main ApplicationContext is initialized and lives. Any bean used at this point must be retrieved via ApplicationContext.getBean(), but every other annotated bean (provided it is in a descendant package of this main class) will be accessible as always.
In particular, Controllers are declared in this other class:
@Configuration
@ComponentScan
public class ApplicationConfiguration {
@Bean
public MainPaneController mainPaneController() throws IOException {
return (MainPaneController) this.loadController("path/to/MainPane.fxml");
}
protected Object loadController(String url) throws IOException {
InputStream fxmlStream = null;
try {
fxmlStream = getClass().getResourceAsStream(url);
FXMLLoader loader = new FXMLLoader();
loader.load(fxmlStream);
return loader.getController();
} finally {
if (fxmlStream != null) {
fxmlStream.close();
}
}
}
}
You can see any Controller (I have just one, but it can be many) is annotated with @Bean and the whole class is a Configuration.
Finally, here is MainPaneController.
public class MainPaneController {
@Autowired
private Service aService;
@PostConstruct
public void init() {
// ...stuff to do with components...
}
/*
* FXML Fields
*/
@FXML
private Node root;
@FXML
private TextArea aTextArea;
@FXML
private TextField aTextField;
@FXML
private void sayButtonAction(ActionEvent event) {
aService.doStuff(aTextArea, aTextField);
}
}
This Controller is declared as a @Bean, so it can be @Autowired with and from any other @Beans (or Services, Components, etc.). Now for example you can have it answer to a button press and delegate logic performed on its fields to a @Service. Any component declared into the Spring-created Controllers will be managed by Spring and thus aware of the context.
It is all quite easy and straightforward to configure. Feel free to ask if you have any doubts.
Upvotes: 5