JavaFX fxml - How to use Spring DI with nested custom controls? - java

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.

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.

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;
}
}
}

Related

How does the boundary class connect with the controller class in Java with JavaFX + Spring Boot?

I'm making a Spring Boot + JavaFX application for a college project. Since we're studying the BCE (Boundary/Control/Entitity) Pattern from the Unified Process, we have to implement a use case with that in mind.
I'm programming the use case in Java, using JavaFX as UI and Spring Boot as a framework.
In the JavaFX fxml file, when you define the controller class attribute, I think that it should be the boundary class, rather than the controller itself because the first method of the app goes to the boundary object. The problem is that later I don't know how to link the boundary class to the controller in the Spring Context because if I define a Controller controller attribute in the boundary class, the program won't compile.
This is my Spring/JavaFX application class:
#SpringBootApplication
public class SpringApp extends Application {
public static ConfigurableApplicationContext applicationContext;
public static Parent root;
public static Stage stage;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
applicationContext = SpringApplication.run(SpringApp.class);
FXMLLoader loader = new FXMLLoader(SpringApp.class.getResource("/app.fxml"));
loader.setControllerFactory(applicationContext::getBean);
Scene scene = new Scene(loader.load(), 640, 360, false, SceneAntialiasing.BALANCED);
primaryStage.setScene(scene);
primaryStage.show();
}
}
And this is a portion of the boundary class (defined as a controller in the fxml file):
public class SaveNewClientBoundary implements Initializable {
#FXML
public ComboBox<Client> cboClients;
#Autowired
private ClientRepo clientRepo;
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
saveNewClientOption();
}
public void saveNewClientOption(){
showClients();
}
public void showClients() {
cboClients.setItems(FXCollections.observableArrayList(clientRepo.findAll()));
}
Now, this works and all, but it's not using the pattern properly (because of the findAll() method). What I'd like to do is to delegate the communication with entity objects to a controller, so a controller would look like this:
#Component
public class SaveNewClientController {
private List<Client> clients;
#Autowired
private ClientRepo clientRepo;
public void saveNewClientOption() {
this.searchAllClients();
}
public void searchAllClients() {
this.clients = clientRepo.findAll();
}
public List<Client> getClients() {
return clients;
}
If I do this, however, I don't get how to let the boundary object access the clients that the controller looks for. Basically, I don't understand how to instantiate the controller and link it to the Boundary class (and link the boundary class to the controller) once the program starts.
I ended up fixing it by implementing a circular dependency between the boundary class and the controller: Setter/Field Injection
Before that, I was trying to create an instance of the controller in the Boundary class initialize() method, but it was throwing a Controller NullPointer exception because that's not how Spring Boot works.
Thanks for the other replies.

Using an autowired singleton bean in a non spring managed java class

Alright, this might seem pretty stupid to all the veterans out there, but bear with me here, as I'm only finding my way around Spring & Spring Boot.
I've got a Controller class here,
#RestController
public class Controller {
private static final Logger logger = LogManager.getLogger(Controller.class);
private static Controller controller = null;
#Autowired
private ApplicationParameters applicationParameters;
public static Controller getInstance() {
if (controller == null) {
synchronized (Controller.class) {
if (controller == null) {
controller = new Controller();
}
}
}
return controller;
}
public Controller() {}
public ApplicationParameters getApplicationParameters() {
return applicationParameters;
}
#RequestMapping("/")
public void init() {
try {
for (Entry<String, String> prop : applicationParameters.getProperties().entrySet())
logger.info("Loaded System Property: " + prop.getKey() + " -> " + prop.getValue());
Utils.concatenate("key1", "key2");
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
which autowires the ApplicationParameters bean with properties from a Property file.
Utils Class
public class Utils {
protected static final Logger logger = LogManager.getLogger(Utils.class);
//Need to get the value of the property keys propKey1 & propKey2 and concat them.
public static String concatenate(String propKey1, String propKey2) throws Exception {
if(StringUtils.isNoneEmpty(propKey2) && StringUtils.isNoneEmpty(propKey1)) {
return Controller.getInstance().getApplicationParameters().getProperties().get(propKey1) + Controller.getInstance().getApplicationParameters().getProperties().get(propKey2)
} else {
logger.error("System Property is undefined." );
return null;
}
}
So, I'd like use this autowired ApplicationParameters bean as a singleton instance throughout the lifecycle of my project.
For instance, I'd like to use it in the Utils class. Clearly Utils class is not spring managed, its just a regular old java class.
So I'd like to know how to use fully initialized applicationParameters in my Utils class.
This is what I've tried so far:
Autowiring the ApplicationParameters again in the Utils class, like this,
public class Utils {
#Autowired
private ApplicationParameters applicationParameters;
protected static final Logger logger = LogManager.getLogger(Utils.class);
But applicationParameters will be null here as, I'm presuming, this is because, Utils is not a spring managed bean.
Make Controller class singleton. (Not sure how to go about doing this as init() needs to get invoked when web server starts, then where to call getInstance()?)
Hence, would someone be so kind as to assist a novice here.
P.S. The Utils class is shown only as a sample to bring home the fact that, a spring managed autowired bean has to be used in a regular java class.
You could make the spring context accessible from outside with a helper class like this one:
public class SpringContextUtil implements ApplicationContextAware {
static ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
Then, you could do something like this: SpringContextUtil.getApplicationContext.getBean("applicationParameters")
As a first rule, don't. A second don't either. Only if you really must, as there is no garantuee that this will work reliable as there is no way that everything has been properly initialized when this method is called. Instead try re-working your util to be a spring managed class as well.
If you really want, ditch most of your code as you are trying to be too smart in your code. Use this hack (yes it is a hack imho and should be avoided if necessary!).
public class SpringUtil {
private static final ApplicationContext ctx;
SpringUtil(ApplicationContext ctx) {
SpringUtil.ctx=ctx;
}
public static Controller getController() {
return this.ctx.getBean(Controller.class);
}
public static ApplicationParameters getApplicationParameters() {
return ctx.getBean(ApplicationParameters.class);
}
}
Then cleanup your controller
#RestController
public class Controller {
private static final Logger logger = LogManager.getLogger(Controller.class);
#Autowired
private ApplicationParameters applicationParameters;
#GetMapping("/")
public void init() {
try {
for (Entry<String, String> prop : applicationParameters.getProperties().entrySet())
logger.info("Loaded System Property: " + prop.getKey() + " -> " + prop.getValue());
Utils.concatenate("key1", "key2");
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
THen use the SpringUtil to obtain the ApplicationParameters instead of the controller
public class Utils {
protected static final Logger logger = LogManager.getLogger(Utils.class);
//Need to get the value of the property keys propKey1 & propKey2 and concat them.
public static String concatenate(String propKey1, String propKey2) throws Exception {
if(StringUtils.isNoneEmpty(propKey2) && StringUtils.isNoneEmpty(propKey1)) {
return SpringUtils.getApplicationParameters().getProperties().get(propKey1) + SpringUtils.getApplicationParameters().getProperties().get(propKey2)
} else {
logger.error("System Property is undefined." );
return null;
}
}
However this is quite a hack and might work in 90% of the cases. Also there is quite a design flaw/smell as you are doing a lot of getter chaining in your class. So all in all you are probably better of refactoring the Utils to make use of regular method calls and proper design techniques.

JavaFX SpringBoot SpringJDBC SQLite CRUD Application - configuration

I'm developing a JavaFX CRUD application with SpringBoot + SpringJDBC + SQLite . I'm using Eclipse IDE.
STORY:
I'm developing this application as StepByStep process. And I achieved JavaFX+SQLite CRUD application with Old School JDBC Connection. But After Integrating SpringBoot + SpringJDBC I get error. I think the error in passing application configuration to all the files.
Main.Class
#SpringBootApplication
public class Main {
public static void main(String[] args) {
Application.launch(MyApplication.class, args);
}
}
AND MyApplication.Class (has no annotation)
public class MyApplication extends Application {
protected ConfigurableApplicationContext applicationContext;
#Override
public void init() throws Exception {
applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
}
#Override
public void stop() throws Exception {
applicationContext.close();
Platform.exit();
}
#Override
public void start(Stage stage) throws Exception {
stage.setTitle("ExYouTub");
stage.setOnCloseRequest(x -> {
Platform.exit();
});
stage.setResizable(false);
stage.setScene(new Scene(FXMLLoader.load(getClass().getResource("../Sample.fxml"))));
stage.show();
}
}
AND AppConfig.class
#Configuration
#ConditionalOnClass(DataSource.class)
#Profile("sqlite")
#ComponentScan(basePackages= "com.fz")
#PropertySource("classpath:data/config.properties")
public class AppConfig {
#Autowired
Environment environment;
private final String DB_URL = "fz.db.url";
#Bean
DataSource dataSource() {
final DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl(environment.getProperty(DB_URL));
return driverManagerDataSource;
}
}
AND SampleController.class
#Controller
public class SampleController implements Initializable {
//-- un-necessary lines are ignored to copy
#Autowired
#Qualifier("studentsDAOImpl")
private StudentsDAO studentsDAO;
#Override
public void initialize(URL location, ResourceBundle resources) {
tableViewList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
setColumnProperties();
loadStudentsDetails();
}
private void loadStudentsDetails() {
studentsList.clear();
studentsList.addAll(studentsDAO.getAllStudents()); // this is line 83
tableViewList.setItems(studentsList);
}
}
AND Error report
Caused by: java.lang.NullPointerException
at com.fz.SampleController.loadStudentsDetails(SampleController.java:83)
at com.fz.SampleController.initialize(SampleController.java:78)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2573)
... 17 more
And Up to now my guess on this error is that, the configuration is not working properly - i think so. I need suggestion and help me improve on this.
The default behaviour for FXMLLoader to create the controller instance is to simply instantiate it, which means no dependencies get injected. To get injection, you rather need the Spring ApplicationContext to manage the instance creation.
Josh Long has issued a Spring Tips installment demonstrating how to do this step by step: https://spring.io/blog/2019/01/16/spring-tips-javafx
However, this is a quite manual and repetitive process. This is what https://github.com/rgielen/javafx-weaver tries to solve. You can find a documentation and example code here: https://github.com/rgielen/javafx-weaver/tree/master/samples/springboot-sample
Disclaimer: I created the library, so I'm biased :)

JavaFX and Spring: issue with autowiring

I am trying to use JavaFX and Spring together. I am using Spring Boot, in particular.
My issue is that I have both Spring autowired fields and JavaFX "autowired" fields in the FXML controller.
I put a breakpoint in the constructor controller and it is actually being invoked twice: one by Spring, which autowires #Autowired fields only, one by JavaFX, which initialize #FXML fields only.
Here's my main code:
#SpringBootApplication
public class MySpringAndJavaFXApp extends AbstractJavaFXAndSpringApplication {
#Override
public void start(Stage window) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/fxml/home.fxml"));
window.setTitle("My Title");
window.setScene(new Scene(root, 500, 300));
window.show();
}
public static void main(String[] args) {
launchApp(MySpringAndJavaFXApp.class, args);
}
}
Here's the class I am extending:
public abstract class JavaFXAndSpringApplication extends Application {
protected static void launchApp(Class<? extends JavaFXAndSpringApplication> classePrincipaleJavaFX, String[] args) {
launch(classePrincipaleJavaFX,args);
}
private ConfigurableApplicationContext applicationContext;
#Override
public void init() throws Exception {
applicationContext = SpringApplication.run(getClass());
applicationContext.getAutowireCapableBeanFactory().autowireBean(this);
}
#Override
public void stop() throws Exception {
super.stop();
applicationContext.close();
}
}
An example of a class being Spring-managed and JavaFX-managed:
#Component
public class MixedController {
#FXML
private TextField aTextField;
#Autowired
private MyService myService; // MyService is a #Component
public MixedController() {
System.out.println("I am here"); // debugger goes twice here
}
}
How I can easily fix my issue?
I attempted using a custom FXML loader (e.g. JavaFX and Spring - beans doesn't Autowire). However, if I load the home.fxml file with such loader, I get an error on the data provider (i.e. Spring is not getting my database configuration correctly) and I would prefer another approach.

Vaadin Spring scoped objects: is it possible to create a Vaadin unit test without starting up a servlet container?

vaadin-spring introduces a couple of Spring scoped objects, the vaadin-session and the vaadin-ui scope. It is necessary to have these two scopes bound before referencing any Vaadin objects in your spring context if:
they are decorated with the #VaadinSessionScope or #UIScope annotations, or
they through some dependency chain reference any bean that is decorated this way.
All runs perfectly well when you start it up in a servlet container like jboss or tomcat. The question is:
If you would like to load a spring application context that contains any of the vaadin beans so decorated for unit testing purposes, how can you create a minimal test that allows the context to be loaded and accessed without starting up a web application container?
Spring MVC is very good at this but when you're using vaadin-spring it's not as straightforward - the relevant vaadin components are highly connected.
(The following example of how to construct a set of Vaadin components to allow access through the abovementioned scopes does not include configuration of the full container, just the minimum required to get a functioning application context.)
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(loader = AnnotationConfigWebContextLoader.class)
#WebAppConfiguration
public class SpringConfigurationTest extends Assert {
#Configuration
#ComponentScan({ "org.example" }) // contains SomeClassReferencingASpringVaadinBean.class
public static class Config {
}
#Autowired
WebApplicationContext applicationContext;
class MyDeploymentConfiguration extends DefaultDeploymentConfiguration {
public MyDeploymentConfiguration(Class<?> servletClass, Properties initParameters) {
super(servletClass, initParameters);
initParameters.put(Constants.SERVLET_PARAMETER_UI_PROVIDER, DefaultUIProvider.class.getName());
}
}
class MyVaadinServlet extends VaadinServlet {
#Override
public String getServletName() {
return getClass().getSimpleName();
}
}
class MyUI extends UI {
#Override
protected void init(VaadinRequest request) {
}
}
#Before
public void setupVaadinScopes() throws Exception {
MyVaadinServlet vaadinServlet = new MyVaadinServlet();
MyDeploymentConfiguration deploymentConfiguration = new MyDeploymentConfiguration(MyVaadinServlet.class,
new Properties());
VaadinServletService vaadinService = new VaadinServletService(vaadinServlet, deploymentConfiguration);
VaadinServletRequest vaadinRequest = new VaadinServletRequest(new MockHttpServletRequest(), vaadinService);
// creates vaadin session and vaadin ui, binds them to thread
VaadinSession vaadinSession = vaadinService.findVaadinSession(vaadinRequest);
Integer uiId = Integer.valueOf(vaadinSession.getNextUIid());
UI ui = new MyUI();
ui.setSession(vaadinSession);
UI.setCurrent(ui);
ui.doInit(vaadinRequest, uiId, null);
vaadinSession.addUI(ui);
}
#Test
public void test0() {
try {
applicationContext.getBean(SomeClassReferencingASpringVaadinBean.class);
} catch (Exception e) {
fail("scopes were probably not set up correctly");
}
}
}

Categories