如何将自定义字段注入添加到 FXMLLoader 的默认 ControllerFactory?

How do I add injection of custom fields to the default ControllerFactory of FXMLLoader?

我想在创建时自动调用控制器的 initialize 方法之前在控制器中设置一些非 UI 字段。据我了解,这样做的方法是提供自定义 ControllerFactory,因为 initialize()ControllerFactory returns 之后被调用 创建的对象。我想根据 this 答案使用以下代码:

FXMLLoader loader = new FXMLLoader(mainFXML); // some .fxml file to load
loader.setControllerFactory(param -> {
    Object controller = null;
    try {
        controller = ReflectUtil.newInstance(param); // this is default behaviour
    } catch (InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    }
    if (controller instanceof Swappable) {
        ((Swappable) controller).setSwapper(swapper); // this is what I want to add
    }
    return controller;
});

但是,ReflectUtil class(默认使用 setControllerFactory method)是 com.sun.reflect.misc 包的一部分,我无法使用使用,因为编译失败 error: package sun.reflect.misc does not exist.

据我所知,我不能使用 sun 包,因为这不是 public API。所以问题是:我该怎么办?我找不到任何其他示例,只有那些带有 ReflectUtil 的示例,好吧,我希望我的 ControllerFactory 符合带有 @FXML 注释的 JavaFX 的默认工作流等等,这是否可能与其他一些 DI例如,像 Jodd Petite 这样的框架?还有其他方法可以设置字段吗? (除了同步它并在 initialize() 中等待,直到 setter 方法被其他线程调用)。 Full code 关于 github 的上下文。

就个人而言,对我自己的代码使用反射是糟糕设计的标志。

建议使用 FXML 机制注入对象的用户实例。为此,创建了一个描述应用程序工作环境的对象。对象用户实体在该对象中注册。这对用户施加了一些限制,使其不能实现直接接口,而是继承抽象 class 来实现在上下文中注册实例的逻辑。

public interface Swapper {
}
public abstract class AbstractSwapper implements Swapper {

    public AbstractSwapper() {
        ApplicationContext.getInstance().setSwapper(this);
    }

}
public class ApplicationContext {
    private static ApplicationContext instance;

    private Swapper swapper;

    private ApplicationContext() {

    }

    public synchronized static ApplicationContext getInstance() {
        if(instance == null) {
            instance = new ApplicationContext();
        }

        return instance;
    }

    public synchronized static Swapper swapperFactory() {
        Swapper swapper = getInstance().getSwapper();

        if(swapper == null) {
            swapper = new AbstractSwapper() {

            };

            getInstance().setSwapper(swapper);
        }

        return swapper;
    }

    public Swapper getSwapper() {
        return swapper;
    }

    public void setSwapper(Swapper swapper) {
        this.swapper = swapper;
    }
}

在这种情况下,可以使用FXML文件fx:factory来使用在ApplicationContext中注册的swapper实例。因此,FXMLLoader 会将实例直接注入到控制器中。

<GridPane fx:controller="sample.Controller" xmlns:fx="http://javafx.com/fxml" >

    <fx:define>
        <ApplicationContext fx:factory="swapperFactory" fx:id="swapper"/>
    </fx:define>

</GridPane>

和sample.Controller

public class Controller {

    @FXML
    private Swapper swapper;

}

另一种解决方案是控制器直接使用 ApplicationContext 初始化字段。所以 swapper 字段没有绑定到 FXML 文件。

public class Controller {

    private Swapper swapper;

    @FXML
    private void initialize() {
        swapper = ApplicationContext.swapperFactory();
    }
}

在这两个版本中,用户只需在使用 FXMLLoader 之前创建 AbstractSwapper 的实例。

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        AbstractSwapper s = new AbstractSwapper() {

        };

        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }


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

此外,还有一个选项可以使用 FXMLLoader 来注入对象。在这种情况下,它通过 fx:reference 或通过 fx:copy (如果你有复制构造函数)

如果你想通过反射创建一个实例,那么你需要使用Class.getConstructor(Class...)1 followed by Constructor.newInstance(Object...)

FXMLLoader loader = new FXMLLoader(/* some location */);
loader.setControllerFactory(param -> {
    Object controller;
    try {
        controller = param.getConstructor().newInstance();
    } catch (ReflectiveOperationException ex) {
        throw new RuntimeException(ex);
    }
    if (controller instanceof Swappable) {
        ((Swappable) controller).setSwapper(swapper);
    }
    return controller;
}

此代码要求您的控制器 class 具有 public 无参数构造函数。如果你想通过构造函数注入依赖项,你可以这样做:

FXMLLoader loader = new FXMLLoader(/* some location */);
loader.setControllerFactory(param -> {
    Object controller;
    try {
        if (Swappable.class.isAssignableFrom(param)) {
            controller = param.getConstructor(Swapper.class).newInstance(swapper);
        } else {
            controller = param.getConstructor().newInstance();
        }
    } catch (ReflectiveOperationException ex) {
        throw new RuntimeException(ex);
    }
    return controller;
}

此代码假定 Swappable 的所有子 class 都有一个 public,采用 Swapper.

的单参数构造函数

如果你想得到一个 non-public 构造函数你需要在调用它之前在 Constructor 上使用 Constructor.getDeclaredConstructor(Class...). Then you'd need to call setAccessible(true) .

如果使用 Jigsaw 模块 (Java 9+) 并且此控制器工厂代码与控制器 class 不在同一个模块中,请记住几件事。假设控制器工厂代码在模块 foo 中,控制器 class 在模块 bar:

  • 如果使用带有 public 构造函数的 public 控制器,则 bar 必须 exports 将控制器 class' 封装到至少 foo
  • 如果使用 non-public controller and/or controller 那么同样的事情一定会发生但是用 opens 而不是 exports

否则会抛出异常


1.如果使用无参数(不一定是 public)构造函数,您可以绕过 getConstructor 并直接调用 Class.newInstance()。但是,请注意此方法存在问题,自 Java 9.

以来已被弃用