Search code examples
javajavafxreflectionfxmlfxmlloader

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


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.


Solution

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