Spring注入的变量为null

时间:2016-03-30 19:08:53

标签: java spring javafx dependency-injection

我试图在JavaFx中使用Spring DI我有一个MainController类,它在AppConfig中作为Bean加载,然后是另一个MenuController类,它将使用MainService。但是,当被调用时,注入的服务为null。

问题

  • 可能是因为它没有注入变量?
  • 在setControllerFactory方法的示例中,他们返回了appContext.getBean(clazz),但我从配置文件中不知道如何访问上下文。如何以及如何设置工厂?
  • 我是否需要以递归方式连接Beans?

我的代码

App类:

public class App extends Application {

    private static final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);

    @Override
    public void start(Stage primaryStage) throws Exception {
        MainController mainController = context.getBean(MainController.class);
        Scene scene = new Scene(mainController.getView());
        primaryStage.setScene(scene);
        recursiveWire(context, mainController.getView());
        primaryStage.show();
    }  

    public void recursiveWire(AnnotationConfigApplicationContext context, Object root) throws Exception {
        context.getAutowireCapableBeanFactory().autowireBean(root);
        context.getAutowireCapableBeanFactory().initializeBean(root, null);

        for (Field field : root.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(FXML.class) && !Node.class.isAssignableFrom(field.getType())) {
                recursiveWire(context, field.get(root));
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }    
}

AppConfiguration类:

我已经尝试设置ControllerFactory,因为我看到了一些建议,但它没有改变结果。

@Configuration
public class AppConfiguration {

    @Bean
    @Scope("prototype")
    public MainService mainService() {
        return new InMemoryMainService();
    }

    @Bean
    @Scope("prototype")
    @DependsOn("mainService")
    public MainController mainController() throws IOException {
        return (MainController) loadController("/java/com/akos/fxml/Main.fxml");
    }

    @Bean
    @Scope("prototype")
    public MenuController menuController() throws IOException {
        return (MenuController) loadController("/java/com/akos/fxml/Menu.fxml");
    }

    protected Object loadController(String url) throws IOException {
        InputStream fxmlStream = null;
        try {
            fxmlStream = getClass().getResourceAsStream(url);
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(getClass().getResource(url));
            Node view = loader.load(fxmlStream);
            AbstractController  controller = loader.getController();
            loader.setControllerFactory(clazz -> controller);
            controller.setView(view);
            return controller;
        } finally {
            if (fxmlStream != null) {
                fxmlStream.close();
            }
        }
    }    
}

MenuController类:

这是我需要访问mainService的地方,但它是null。

public class MenuController extends AbstractController implements Initializable {
   ...

    @Inject
    MainService mainService;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        disableMenuElements();
        mainService.currentProgramProperty().addListener((observable, oldValue, newValue) -> {
        ...
        });
    }
}

1 个答案:

答案 0 :(得分:1)

问题在于事情发生的顺序。

从Spring的角度来看,当你请求bean时,它是通过在你的应用程序配置中调用menuController()方法创建的,然后@Inject - 注释字段被初始化(通过反射),然后它返回bean。

但是,menuController()方法通过加载fxml文件,然后从FXMLLoader检索控制器来创建控制器。作为initialize()进程的一部分,FXMLLoader调用控制器中的load()方法。显然,这发生在menuController()返回之前(因为它发生在loader.load()之前);所以在Spring有机会初始化注入的字段之前调用initialize()

最快的修复方法可能是为服务定义一个setter方法,并在初始化时简单地调用服务上的方法:

public class MenuController extends AbstractController implements Initializable {
    // ...

    private MainService mainService;

    @Inject
    public void setMainService(MainService mainService) {
        this.mainService = mainService ;
        mainService.currentProgramProperty().addListener((observable, oldValue, newValue) -> {
           //  ...
        });
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        disableMenuElements();
    }
}

当我使用Spring来管理我的JavaFX应用程序时,我倾向于完全使用不同的方法。我只是通过FXMLLoader告诉controllerFactory使用Spring来实例化控制器,而不是让控制器访问视图,然后从控制器中检索视图。然后当你在load()上调用FXMLLoader时,它从Spring请求控制器作为bean,因此FXMLLoader接收一个注入了所有依赖项的bean。然后,当它在控制器上调用initialize()时,依赖关系已经存在。

所以

@Configuration
public class AppConfiguration {

    @Bean
    // ??? surely a service should be singleton, not prototype, scope...
    @Scope("prototype")
    public MainService mainService() {
        return new InMemoryMainService();
    }

    @Bean
    @Scope("prototype")
    public MainController mainController() throws IOException {
        return new MainController();
    }

    @Bean
    @Scope("prototype")
    public MenuController menuController() throws IOException {
        return new MenuController();
    }


}

你的菜单控制器和你一样:

public class MenuController extends AbstractController implements Initializable {
    // ...

    @Inject
    MainService mainService;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        disableMenuElements();
        mainService.currentProgramProperty().addListener((observable, oldValue, newValue) -> {
            // ...
        });
    }
}

现在你可以做到

public class App extends Application {

    private static final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfiguration.class);

    @Override
    public void start(Stage primaryStage) throws Exception {
        // should fix name, you should not start your own pacakge names "java"
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/java/com/akos/fxml/Main.fxml"));
        loader.setControllerFactory(context::getBean);
        Scene scene = new Scene(loader.load());
        primaryStage.setScene(scene);
        primaryStage.show();
    }  

    public static void main(String[] args) {
        launch(args);
    }    
}

请注意,您可以通过调用

获取应用程序代码中对控制器的引用
MainController mainController = loader.getController();
在之后

您已拨打loader.load()。这为您提供了FXMLLoader创建的控制器的引用;即Spring创建的那个(因为控制器工厂指示FXMLLoader使用Spring)。 (在我看来,你真的不需要对控制器的引用;控制器具体知道如何在视图和模型(服务)之间进行通信;如果你想从外部更改UI,你应该更新模型这样做,然后控制器将观察模型中的变化并更新视图。)

我并不完全清楚“递归布线”应该做什么。如果您通过主fxml文件中的<fx:include>加载菜单,控制器工厂将传播到包含的fxml文件,因此MenuController也将从spring上下文实例化,因为它将具有根据需要注入服务。如果要将其加载到其他位置,则只需在加载时设置控制器工厂,如上面的主fxml文件所示。所有这些假设您的控制器在fxml文件中使用<fx:controller>指定,我认为必须是您的其他代码。