I have a JavaFX application with Spring. I have a bean in a Configuration class which is being correctly added to the SpringContext, and can be retrieved manually, but it is not getting autowired into a class annotated @Component. I have summarised the code below.
@SpringBootApplication
public class BoothApplication extends javafx.application.Application {
// this is temporarily static for debug purposes
public static ConfigurableApplicationContext springContext;
private Parent rootNode;
private Scene threeCardScene, debugScene, ezzieScene;
@Autowired
private StateMachineConfiguration stateMachineSupplier;
@Autowired
private GameModel gameModel;
@Override
public void start(Stage prinaryStage) throws IOException {
<snip>
mainStage.show();
}
public static void main(String[] args) {
launch();
}
@Override
public void init() throws Exception {
springContext = SpringApplication.run(BoothApplication.class);
FXMLLoader fxmlLoader = new FXMLLoader(this.getClass().getResource("/org/overworld/tarotbooth/ezzie.fxml"));
fxmlLoader.setControllerFactory(springContext::getBean);
rootNode = fxmlLoader.load();
}
@Override
public void stop() throws Exception {
springContext.close();
}
}
I have a bean that is correctly initialised:
@Configuration
public class StateMachineConfiguration {
@Bean
public StateMachine<State, Trigger> stateMachine() {
StateMachineConfig<State, Trigger> config = new StateMachineConfig<State, Trigger>();
/* @formatter:off */
config.configure(State.IDLE)
.permit(Trigger.APPROACH_SENSOR, State.CURIOUS)
.permit(Trigger.PRESENCE_SENSOR, State.ENGAGED)
.onEntry(StateMachineConfiguration::idle)
.ignore(Trigger.PAST_READ)
.ignore(Trigger.PRESENT_READ)
.ignore(Trigger.FUTURE_READ)
.ignore(Trigger.PRINTER_ERROR)
.ignore(Trigger.TIMEOUT)
.ignore(Trigger.ADVANCE)
.ignore(Trigger.BAD_PLACEMENT);
<snip>
/* @formatter:on */
// config.generateDotFileInto(System.out, true);
StateMachine<State, Trigger> stateMachine = new StateMachine<State, Trigger>(State.IDLE, config);
stateMachine.fireInitialTransition();
return stateMachine;
}
private static void idle() {
System.out.println("idle");
}
}
I can access the bean directly via springContext.getBean, but it is not getting autowired:
@Component
public class DebugController implements javafx.fxml.Initializable {
@Autowired
private StateMachine<State, Trigger> stateMachine;
@Autowired
private GameModel gameModel; // this is also null
@Autowired
private Deck deck; // this is also null
@FXML
private ToggleButton approachToggle;
@Override
public void initialize(URL url, ResourceBundle rb) {
// this way works
stateMachine = BoothApplication.springContext.getBean(StateMachine.class);
//this way doesn't, statemachine is null
approachToggle.setOnAction(e -> stateMachine.fire(Trigger.APPROACH_SENSOR));
}
}
All the package hierarchy is under the package where the @SpringBootApplication is located, it's not being missed in the componentscan. It's like the DebugController is not getting injected. I've checked all the imports for imposters of the same name. I doubt it's anything with the generics as the other autowired fields are null too.
Okay, after a night of sleep I got it. When I introduced spring constructor injection to DebugController I got an error on the missing no-arg constructor and I realised from the stack trace that DebugController is being created by JavaFX not by Spring, thus there is a spring bean created by @Component and another class created by JavaFX with nothing wired in it. I thought I might have fixed this in the init method, but actually I just created an new FXMLLoader and set the setControllerFactory() on it, but when I was creating the scenes elsewhere in my code I was using a new FXMLLoader, so the controller factory on that new FXMLLoader was not set to the spring::getBean(), so spring wasn't injecting anything inside that new loader.
Bad:
threeCardScene = new Scene(
new FXMLLoader(BoothApplication.class.getResource("threeCardSpread.fxml")).load(),
640, 480);
Working:
threeCardScene = new Scene(
FXMLLoader.load(BoothApplication.class.getResource("threeCardSpread.fxml"), null,
null, springContext::getBean), 640, 480);
So instead of creating a new FXMLLoader each time, I use that static method of the class that takes a controller factory, and pass spring::getBean each time. (You could of course create a new FXMLLoader for each scene, set the controller factory and then call the load nonstatic method on it, but this is cleaner.)