javaspring-bootautowired

Java spring bean is getting created and added to the context but not @Autowired


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.


Solution

  • 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.)