0

I want to set some non-UI fields in the controller before the initialize method of the controller gets called automatically upon creation. As I understand it, the way to do it is to provide custom ControllerFactory, since initialize() gets called after ControllerFactory returns the created object. I wanted to use the following code as per this answer:

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

However, the ReflectUtil class (which is used in default setControllerFactory method) is part of com.sun.reflect.misc package, which I am not able to use, since compiling fails with error: package sun.reflect.misc does not exist.

As I understand it, I can't use sun packages, since this is not public API. So the question is: what do I do? I can't find any other examples of this, only the ones with ReflectUtil and, well, I want my ControllerFactory to comply with default workflow of JavaFX with @FXML annotations and all that, is this possible with some other DI framework like Jodd Petite, for example? Is there some other way to set the field? (other than to synchronize on it and wait in initialize() until the setter method gets called from other thread). Full code on github for context.

graynk
  • 131
  • 1
  • 2
  • 17
  • 3
    `Object controller = param.getConstructor().newInstance()`. This uses [`Class.getConstructor(Class...)`](https://docs.oracle.com/javase/10/docs/api/java/lang/Class.html#getConstructor(java.lang.Class...)) and [`Constructor.newInstance(Object...)`](https://docs.oracle.com/javase/10/docs/api/java/lang/reflect/Constructor.html#newInstance(java.lang.Object...)). – Slaw Aug 13 '18 at 12:45
  • @Slaw this works, thank you. If you post it as an answer I will accept it – graynk Aug 14 '18 at 04:59
  • what objects do you really want to inject and who will create them? in fxml there are mechanisms for creating and injecting objects that may be working for you. – mr mcwolf Aug 14 '18 at 05:41
  • @mrmcwolf in the example Swapper is an abstract class with two overrided methods (well, listener basically) that controllers will share and use the methods to let main controller know that scenes need to be switched. I'm making for myself something like DataFX but with less reflection, added caching and loading FXMLs in background thread. – graynk Aug 14 '18 at 06:13
  • so users create an instance of Swapper and want it to be injected into the controllers? – mr mcwolf Aug 14 '18 at 06:56
  • @mrmcwolf No, I create the instance of Swapper and inject it when I'm loading new scenes, then I just ask main controller to set the loaded scene. Here, I'll upload what I have for now on GitHub, hope it makes it clearer what I'm doing https://github.com/graynk/FlowerFX Check Flower.java – graynk Aug 14 '18 at 07:50

2 Answers2

1

If you want to create an instance via reflection then you need to use 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;
}

This code requires that your controller class has a public, no-argument constructor. If you want to inject your dependencies through the constructor you could do something like:

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

This code assumes that all subclasses of Swappable have a public, single-argument constructor that takes a Swapper.

If you want to get a non-public constructor you'll need to use Constructor.getDeclaredConstructor(Class...). Then you'd need to call setAccessible(true) on the Constructor before invoking it.

Couple things to remember if using Jigsaw modules (Java 9+) and this controller factory code is not in the same module as the controller class. Let's say the controller factory code is in module foo and the controller class is in module bar:

  • If using a public controller with a public constructor then bar must exports the controller class' package to at least foo
  • If using a non-public controller and/or constructor then the same thing must happen but with opens instead of exports

Otherwise an exception will be thrown.


1. If using a no-argument (not necessarily public) constructor you can bypass getConstructor and call Class.newInstance() directly. However, please note that this method has issues and has be deprecated since Java 9.

Slaw
  • 37,820
  • 8
  • 53
  • 80
0

Personally, using reflection for my own code is a sign of bad design.

Here's a suggestion that uses FXML mechanisms to inject a user instance of an object. For this purpose, an object is created that describes the context in which the application works. Object user entities are registered in this object. This imposes some constraint on users not to implement a direct interface but to inherit an abstract class that will implement the logic of registering the instance in the context.

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

In this case, the FXML file can be used fx:factory to use the swapper instance registered in ApplicationContext. Thus, FXMLLoader will inject the instance directly into the controller.

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

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

</GridPane>

and sample.Controller

public class Controller {

    @FXML
    private Swapper swapper;

}

Another solution is for the controller to initialize the fields using the ApplicationContext directly. So the swapper field does not bind to the FXML file.

public class Controller {

    private Swapper swapper;

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

In both versions, the user simply has to create an instance of AbstractSwapper before using FXMLLoader.

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

Also, there is an option to use FXMLLoader to inject the object. In this case it goes through fx:reference or through fx:copy (if you have copy constructor)

mr mcwolf
  • 2,574
  • 2
  • 14
  • 27
  • I thought about making it an abstract class, but users may want to extend their controllers from some other superclass and Java doesn't support multiple inheritance. Plus I don't want to add _even more_ boiler plate code for the user to write in _each_ of their controller classes/fxml files. And I don't do any reflection that doesn't happen with default construction in FXMLLoader anyway. I just use regular setter after object is constructed, but before `initialize()` is called, I just needed to know how to re-implement this default construction by reflection. So, I have accepted Slaw's answer. – graynk Aug 14 '18 at 10:04