Montag, 9. Dezember 2013

Adam Bien´s afterburner.fx internals explained..

Today I am writing about Adam´s framework afterburner.fx. This is a MVP framework for dependency injection into JavaFX apps. It is very small, containing only two classes.

What could you do with this afternburner.fx? What are the restrictions?

Let´s start with the project init. You will need only a normal pom.xml, plain no special libs are needed. I am using JDK8 because of the simple JavaFX config. (no config ;-) )

Convention over Configuration:
CoC is the main in this framework. This means, that you don´t need to configure something. But you have to follow the base structure that this framework is expecting.

As app-base-pkg I am using org.rapidpm.demo.jaxenter.blog008. (you could get all from my git repo under https://bitbucket.org/rapidpm/jaxenter.de-0008-afterburner ) The main class will be Main, this is the JavaFX Application Class with the basic boostrapping intro. The sup-pkg orig contains the pkg presentation with only one GUI module called demo. For every GUI Module you will need two classes. The first one is a class with a name ending with View and the second one will end with Presenter. In our examplke you will find the two classes, DemoView and DemoPresenter.

The DemoPresenter is nothing else as the Controller class for the View, declared inside the fxml file.
The fxml File itself must be named demo.fxml and at the same place as the Presenter/Controller class.

 The DemoView is the GUI Component itself and must extend the FXMLView class from the framework.

The View - FXMView 
The DemoView have a default constructor, calling the init method. init(Class clazz, String conventionalName) 


public FXMLView() {
    this.init(getClass(), getFXMLName());
}

private void init(Class clazz, String conventionalName) {
    final URL resource = clazz.getResource(conventionalName);
    String bundleName = getBundleName();
    ResourceBundle bundle = getResourceBundle(bundleName);
    this.loader = new FXMLLoader(resource, bundle);
    this.loader.setControllerFactory(new Callback, Object>() {
        @Override
        public Object call(Class p) {
            return InjectionProvider.instantiatePresenter(p);
        }
    });
    try {
        loader.load();
    } catch (Exception ex) {
        throw new IllegalStateException("Cannot load " 
            + conventionalName, ex);
    }
}
The init will load the ResourceBundle and the fxml-file with an instance of the class FXMLLoader. The most importand step is the setting of the ControllerFactory. Inside the instance of the ControllerFactory you will see the methodcall InjectionProvider.instantiatePresenter(p);  This ist the place where the injection will be taken place. One big point to know is, only inside a controller/presenter you will be able to use injection. Inside the Presenter no injection is available.

The InjectionProvider - DI with reflection
The InjectionProvider ist the heart of the framework. The base steps are the following:
- create an instance
- inject the attributes with the annotation Inject
- call the method with annotation Postconstruct
Thats all... but how it is realized?

The first step is quite easy, just call clazz.newInstance().

Step two is a littele bit more complex. You have to instantiate the attributes but the the attributes inside too. Thes means the injection must be done recursive. To do this you will check the attributes if they are annotated with Inject, if so, do the same for this instance.. and so on..
There is a small thig to know. The implementation from Adam will only create one instance of every used class. This means you will get only singletons!! And this for the complete application.

The last step is easy again, call all methods with the annotation Postconstruct per reflection.

static Object instantiateModel(Class clazz) {
    Object product = models.get(clazz);
    if (product == null) {
        try {
            product = injectAndInitialize(clazz.newInstance());
            models.put(clazz, product);
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException(
                "Cannot instantiate view: " + clazz, ex);
        }
    }
    return product;
}

static Object injectAndInitialize(Object product) {
    injectMembers(product);
    initialize(product);
    return product;
}

static void injectMembers(final Object instance) {
    Class aClass = instance.getClass();
    Field[] fields = aClass.getDeclaredFields();
    for (final Field field : fields) {
        if (field.isAnnotationPresent(Inject.class)) {
            Class type = field.getType();
            final Object target = instantiateModel(type);
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    boolean wasAccessible = field.isAccessible();
                    try {
                        field.setAccessible(true);
                        field.set(instance, target);
                        return null; // return nothing...
                    } catch (IllegalArgumentException | 
                                IllegalAccessException ex) {
                        throw new IllegalStateException(
                            "Cannot set field: " + field, ex);
                    } finally {
                        field.setAccessible(wasAccessible);
                    }
                }
            });
        }
    }
}
static void initialize(Object instance) {
    invokeMethodWithAnnotation(instance, PostConstruct.class);
}
Lesson Learned
The framework afterburner.fx from Adam Bien is really small without any configuration. You could use this to inject Instances per annotation Inject. If you want to use this inside your application you have to know the following:


  • There are no Scopes, all instances will have the the lifecycle of the application. The only way to terminate them earlier is to call the method forgettAll(), but this will terminate all instances. The method annotated with PreDestroy will be called before. You could not select the order the instances are destroyed.
  • All instances are singletons
  • No Producers, this means you could not abstract over an Interface layer. Or you can not switch between different implementations like you could do with Qualifiers.
If you could deal with this limitations, this will be good form you. But to use this to learn more about injection works, this is a good project to play with.