Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Sunday, February 15, 2015

Dynamic FX Application Modules - Part 6 - Module Development

This is the last part of a six part series on dynamic Java FX application modules.  This post describes how to create such modules and where to put them in terms of source code and the target deployment.

SubApps

Extending the demo application involves adding modules called SubApps to the codebase.  SubApp is a term I'm using to describe an FX Stage / Google Guice AbstractModule pair.  A SubApp is defined in an @SubApp annotation placed on an AbstractModule subclass.  See SubApp B defined in the fms.subapp_b.B_Module class.

@SubApp(
        buttonId = "B",
        buttonTitle = "Show B",
        stageTitle = "Screen B",
        fxmlFile = "subapp_b/screenB.fxml")
public class B_Module extends AbstractModule {

    @Override    protected void configure() {
        bind( ServiceObject2.class ).to( ServiceObjectImpl2.class ).asEagerSingleton();
    }
}

The SubApp is launched from a Button with a title "Show B".  "Show B" creates a Stage titled "Show B" with a single Scene containing screenB.fxml as its root.  screenB.fxml has a controller, ScreenBController which can access ServiceObject1 from the CoreModule services (as all SubApps can) and it's own ServiceObject2 as defined in B_Module.

This package diagram is reposted to show the logical dependencies of the SubApp to the main module called app_core.  I steer away from inter-SubApp dependencies as this might make wiring buggy, especially if the dependencies are bi-directional.

SubApps Depend on app_core and Everyone Depends on framework
Although I've provided just a single ServiceObject class and a single FXML/Controller pair, a SubApp can be substantially more complex.  The @SubApp metadata serves as an entry point where common module initialization can take place in the referenced Controller.  In subapp_b's case, the @FXML initialize() method in ScreenBController will be the main initialization routine for the module.

Component and Deployment Views

In the package diagram, notice that subapp_d is set off by itself in a boarder.  That's because it is placed into a separate JAR and distributed optionally with the rest of the app.  When running my main(), I am loading the generated JAR of subapp_d rather than making an IntelliJ reference to the project.  This gives me good deployment flexibility.  If I spot a problem with subapp_d, I can simply yank it.  Or, I can swap it out with a hotfix without affecting the rest of the app.

This separation appears in the component view too.  subapp_d is a totally separate IntelliJ project (actually, a module but I've used that term enough).  See the following screenshot from my project browser.

subapp_d is Totally Separate Project and Deployment
This means that subapp_d can be put in a separate Git repository and built with it's artifacts deployed outside of the main development.  Speaking in non-technical terms, we can develop subapp_d without giving access to any more of the application than the CoreModule service interfaces.

Inside Job

subapp_a, subapp_b, and subapp_c are all inside the main project.  While they don't get the benefit of the a la carte deployment of the broken-out JAR, they still benefit from the consistency of this approach.  These modules can use the same type of metadata and initialization which can help with testing, performance tuning, and progress reporting.  If the SubApp development is done by the mainline group and there is no valid use case for the SubApps every not being deployed, then it may be worthwhile to bundle them together.

Wrap-Up

Look for an update with links to the project.  I may put this in a public GitHub.

It's easy to get started in FX.  Subclass an application. Load an FXML file with a Controller. Create a Scene.  Press Run.  And you can go this route for a while, but if there are a lot of developers working on the project, you'll find excessive coupling and an initialization routine that gets bogged down.  This post presented a structured technique for building an app based on the dynamic self-discovery of components.  If you find problems like those described in the series with your own development effort, consider taking a step backward and applying some of these principles.





18 comments:

  1. thanks Carl, this tutorial is great.
    can you please, publish the code?
    I'd like to check how the controllers are set

    ReplyDelete
    Replies
    1. Hi,

      I don't have the code handy, but this repo has some of the snippets described here: https://bitbucket.org/bekwam/jfxbop-repos-1.

      Where possible, I like to keep the controllers as isolated as possible. For example, if 2 controllers need to talk it's probably because they're sharing data. In this case, bind UI elements from one or more controllers to the same JavaFX property-based model. Share the model, but not references to each other.

      I usually use new FXMLLoader(url).load() from a calling class when working with FXML. That way, I can use the getController() method to wire up my main navigation.

      A lot of people put the FXMLLoader.load() call in a constructor. Then, when you create a new object, you can pass around a reference. I do this with custom components to make the API cleaner. I generally prefer the load() call from a calling class because I use the 470k jar Google Guice to create the instances via dependency injection.

      Another option is to work entirely in the dependency injection framework, injecting controllers into each other based on Singleton, prototype, or a custom scope.

      If you have 2 controllers that need to talk to each other, make one of the directions a WeakReference. That way, if the garbage collector can't release one without releasing the other, you'll have provided information about which one should back off.

      Good luck

      Delete
    2. Thanks Carl,
      how do you use getController()?
      Because I'm trying to use your function startScreen(ActionEvent evt)
      but if in the plugin's fxml the controller is set, I receive java.lang.ClassNotFoundException: path.to.ScreenAController
      pressing the new Button.
      instead if it is not, pressing that Button the view shows up (but then I can't use that view without a controller).

      Delete
    3. Are you calling getController() after this line in the createStage() method?

      Parent someScreen = (Parent) aScreenLoader.load();

      Delete
    4. yes, I think so:
      private Stage createStage( String fxmlFile, Callback, Object> gcf, String stageTitle, Class sa ) throws IOException {
      FXMLLoader aScreenLoader = new FXMLLoader(
      sa.getResource( fxmlFile ),
      null,
      builderFactory,
      gcf);

      Parent someScreen = (Parent) aScreenLoader.load();
      aScreenLoader.getController();
      Stage stage = new Stage();
      stage.setTitle( stageTitle );
      Scene someScene = new Scene(someScreen);
      stage.setScene(someScene);

      return stage;
      }

      Delete
    5. Hi,

      I found the code and put it into Maven. It's on GitHub.

      https://github.com/bekwam/examples-javafx-repos1/tree/master/examples-javafx-parent/examples-javafx-dynamic

      Delete
    6. Hi Carl,
      I tested your code and found the same problem, it's not in the code but in the library.
      is it correct to add the new plugin not only in the directory "plugins" but also as a library ?
      with your code, if I add it only in the "plugins" dir, Reflections discovers it and add the new button but if you press it, the error is
      java.lang.ClassNotFoundException: com.bekwam.examples.javafx.dynamic.subapp_a.ScreenAController

      if the plugin is also in the library everything works fine.

      Delete
    7. My problem is that I'm trying to add new plugin.jar after the core is released.
      so I can't add this jar to the library
      here a part of my code
      https://gist.github.com/Filoz/74d02ccd4570ed127f65

      Delete
    8. Hi,

      I think this technique can still help you. It sounds like you need a little more functionality in establishing the classpath by loading a new plugin JAR. Take a look at this code which uses URLClassLoader to load a JAR file. Once this ClassLoader is setup, I think you can work with the annotation processing to discover the plugin implementations.

      https://github.com/bekwam/examples-javafx-repos1/tree/master/examples-javafx-parent/examples-javafx-plugin-parent

      If the plugins become tangled with the running app or other plugins, you might consider restarting the app with an updated classpath. While the app is running, download the JAR to something like ~/.yourapp/plugins. Capture the JVM args and restart the app, killing the previous instance's PID as it starts.

      Delete
    9. thanks carl,
      actually our method for URLClassLoader works, and the mainApp can find all the plugin.jar.
      but if the fxml is loaded correctly through sa.getResource( "fxmlFileName" ),
      when it tries to load the controller set in the fxml, the MainApp starts looking in its packages, but the plugin controller is in the plugin jar, so it gives my ClassNotFound.
      Is there a way to get the correct position of the controller class like getResorces() ?
      I think the GuiceControllerFactory should find it, but it is not.
      Also following your other tutorial http://bekwam.blogspot.it/2015/06/configuring-google-guice.html and adding bind(BuilderFactory.class).to(JavaFXBuilderFactory.class); in the module, it doesn't work.
      maybe I'm forgetting to set something in the ControllerFactory

      Delete
    10. I'll try to post something that integrates the two.

      Double-check the URLs sent to your URLClassLoader to make sure that they're finding the appropriate packages. Use an anonymous inner class to extend and create a printPackages() method and output getPackages(). Make sure you see the package containing your class.

      Delete
    11. Hi,

      I'm still working on this. I broke the project I had into GitHub into several Maven modules to simulate the different plugins.

      I'm working on 3 things.

      1. Download plugin JARs to a local folder. When the app starts up, it will look into this folder to build the classpath.
      2. Load the plugins after they are downloaded without a restart. I think this is where you're stuck.
      3. For completed or dependent plugins, add a restart of the java.exe process to reconsult downloaded JARs (see Step 1).

      If you pull this, you'll see the new projects. There's a lot of extra stuff.

      https://github.com/bekwam/examples-javafx-repos1/tree/master/examples-javafx-parent

      examples-javafx-dynamic is the main app and core module

      examples-javafx-dynamic-subapp_a, -subapp_b, -subapp_c - JARs that will be loaded into the main app if available

      examples-javafx-dynamic-framework - annotations and common services shared between plugins and the main app

      examples-javafx-parent - groups modules and factors dependencies

      Delete
    12. The current state of the Git repos is a work-in-progress. Everything compiles and runs, but I don't quite have the class loading right yet. - Feb 5

      Delete
    13. I have this working in the current repos. The UI needs a little work (the import function's uninstall doesn't do anything yet, etc). However, the basics are there. To test this,

      1. Run the app
      2. Verify that the main canvas is empty (no JavaFX Buttons)
      3. Go to File > Preferences
      4. For each of the subapp jars (_a, _b, _c) in your Maven repository, browse to it
      5. Select the item and press Import
      6. Verify in your $HOME/.examples-javafx-dynamic/subapps folder that the 3 items are there
      7. Restart the app
      8. Verify that the main canvas has JavaFX Buttons (A, B, C)
      9. Click on each of the JavaFX Buttons and verify that there are no NPEs

      I'll be adding a new post on this, but the difference between the working program and your program is probably the following

      * Add URLs and ClassLoader to Reflections call

      Reflections reflections = new Reflections(DEFAULT_PACKAGE_TO_SCAN, cl, urls);

      * Thread.currentThread.setContextClassLoader() - This is the class loader used by FXMLLoader to find the controller classes (not the controller objects)

      Delete
  2. Hey Carl,

    Thanks for the approach and great job!

    I'm trying to make your latest sources work on Java 7,
    Subapp_A works well, but when I try Subapp_B, then Guice actually throws the following -

    Guice configuration errors:

    1) No implementation for com.bekwam.examples.javafx.dynamic.subapp_b.ServiceObject2 was bound.

    Any ideas if it's any classloaders conflicts or the issue in the Guice bindings (on Java 8 everything is working fine)?

    Thanks

    ReplyDelete
    Replies
    1. Hi,

      I'll test this on Java 7 (JavaFX 2.2) when I get a chance. I suspect FX more than Guice since you should be running against the same Guice 4.0 in both cases.

      What I learned from the FX code is that FXMLLoader is using the setContextClassLoader() on the Thread to find classes. I'm not sure what JavaFX 2.2 is using. I heard that the source is only partially open sourced so I'm not sure if I will be able to find an FXMLLoader.java that can give me insight into this. It seems like a lot of code -- not just Guice or FX -- uses Class.loadClass() which can mess up the ClassLoaders.

      Be sure to check out the latest post on this topic

      http://bekwam.blogspot.com/2016/02/dynamic-application-modules-part-7.html

      Delete
    2. Will check the post, thanks!

      From what I can see, in FXMLLoader 7 uses same Thread.currentThread().getContextClassLoader();
      that's why couldn't figure out the difference.

      In general, do you think this approach can be a full alternative to OSGi, i.e. for plugin development? Or in the end it gets too tricky with the all the classloading manipulations?

      Delete
    3. I think OSGI is really solid technology. It's much better in terms of preventing collisions than what I've laid out here. I was using it for some Talend ESB stuff. However, OSGI still feels a little heavy to me especially if you're not running in an OSGI container.

      This series also was intended to solve some problems not met by OSGI. Specifically, being able to add parts to a bootstrapped JavaFX application. You can still use the @SubApp annotation even if everything is packed in the same JAR. My particular use case was to chop a project up into JARs and sort it out in JavaWS or in an installer. I'm trying to solve the problem where a huge program (>3k files) starts to buckle when tons of development is added. I'd like to find and focus on any problem pieces through an addition/removal of the suspect component.

      So, I'm pinning my hopes on classpath issues being solved in Java 9 with modularization. These posts and what I do on my projects might be a stopgap that will work better with Java 9 but won't necessarily be incompatible. As you can see in the "Part 7" post, we're really talking about a minimum of extra code (3 lines) and if all the developers are the same team, then collisions can be kept to a minimum (continuous integration, etc). Then the extra protection of OSGI isn't as compelling given the learning curve and build and deployment differences.

      Delete