Annotations
A few years ago, the XML config files found in Spring and Java EE deployments gave way to Java Annotations. Originally, it was thought that non-developers ("application assemblers") would find it easier to specify their configuration using plain XML rather than source code compiled into class files. I think the opinion has changed on this in favor of the convenience of associating metadata within the Java classes it's describing and of using plain Java code to represent centrally defined items (AbstractModule subclass versus spring-config.xml).
This application uses a Java Annotation called @SubApp to define a Java FX module to be loaded at runtime. In previous posts I described a need for partitioning a Java FX application into SubApps that could be loaded by the presence of one or more JAR files on the classpath. Any classes flagged with the @SubApp are picked up by the initialization routine of the main workhorse HomeScreenController.java.
Here is the @SubApp annotation. It is a RUNTIME annotation is intended for the class ("TYPE").
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface SubApp { public String buttonId(); public String buttonTitle(); public String stageTitle(); public String fxmlFile(); }
There are 6 pieces of information associated with the SubApp. Four items are covered by the annotation. buttonId and buttonTitle provide information on the control that will launch the SubApp. stageTitle provides information to the Stage for the SubApp title. fxmlFile correletes a lead-off FXML file for the SubApp.
The fifth configuration item is the Controller class that's pointed to in the fxmlFile.
The remaining items is the class itself. @SubApp is put on a Google Guice AbstractModule subclass and the Class object is used with a newInstance() call to create a new module instance for use in an Injector creation call.
Gallery of @SubApp
Here is the AbstractModule subclass used by Screen A. It is a placeholder since it doesn't include any dependencies at this point. Screen A does interact with the Core-provided ServiceObject1, but that is inherited from the kernel's definition.
@SubApp( buttonId = "A", buttonTitle = "Show A", stageTitle = "Screen A", fxmlFile = "subapp_a/screenA.fxml") public class A_Module extends AbstractModule { private Logger logger = LoggerFactory.getLogger(A_Module.class); @Override protected void configure() { if( logger.isInfoEnabled() ) { logger.info("Configuring {}", this.getClass().getName()); } } }
This is the AbstractModule subclass used by Screen B and is similar to Screen C and Screen D.
@SubApp( buttonId = "B", buttonTitle = "Show B", stageTitle = "Screen B", fxmlFile = "subapp_b/screenB.fxml") public class B_Module extends AbstractModule { private Logger logger = LoggerFactory.getLogger(B_Module.class); @Override protected void configure() { if( logger.isInfoEnabled() ) { logger.info("Configuring {}", this.getClass().getName()); } bind( ServiceObject2.class ).
to( ServiceObjectImpl2.class ).
asEagerSingleton();
}
}
B_Module wires up the B-specific ServiceObject2 object.
CoreModule is an AbstractModule subclass, but it does not have the @SubApp annotation. Because it's always around -- it transcends the lifecycle of an individual modules -- it is created with the new operator.
Reflections
Reflections is a project that can scan a classpath, restricted to a performant range, and look for items containing an annotation. It does a lot more than just work with annotations, but that's outside the scope of this article. See this post for more information about this Google Code project: Reflections
This code -- cleverly with a Java 8 stream -- looks for @SubApp AbstractModule subclasses, creates a Button for each found class, adds the annotated class to the Button for later retrieval, and adds the Button to the HomeScreenController's VBox. (Check out the Lambda on the onAction setter.)
The startScreen() event handler uses the userData() Object from the Button to pull in it's metadata stored as an annotated Class Object.private final String DEFAULT_PACKAGE_TO_SCAN = "fms";public void initializeSubApps() { Reflections reflections = new Reflections(DEFAULT_PACKAGE_TO_SCAN); Set<Class<?>> subapps = reflections.getTypesAnnotatedWith(SubApp.class); subapps.stream().sorted(Comparator.comparing(Class::getSimpleName)).forEach(sa -> { SubApp saAnnotation = sa.getAnnotation(SubApp.class); Button btn = new Button(saAnnotation.buttonTitle()); btn.setId( saAnnotation.buttonId() ); btn.setUserData(sa); // holds the metadata btn.setOnAction(evt -> startScreen(evt)); vbox.getChildren().add(btn); }); }
public void startScreen(ActionEvent evt) { if( logger.isDebugEnabled() ) { logger.debug("Starting a screen {}", ((Button)evt.getSource()).getId()); } Class<?> sa = (Class<?>)((Button)evt.getSource()).getUserData(); SubApp saAnnotation = sa.getAnnotation(SubApp.class); try { Callback<Class<?>, Object> gcf = gcfCache.get( (Button)evt.getSource() ); if( gcf == null ) { AbstractModule m = (AbstractModule)sa.newInstance(); Injector inj = injector.createChildInjector( m ); gcf = clazz -> inj.getInstance(clazz); gcfCache.put( evt.getSource(), gcf ); } Stage stageToShow = stageCache.get(evt.getSource()); if (stageToShow == null) { Stage stage = createStage( saAnnotation.fxmlFile(), gcf, saAnnotation.stageTitle(), sa ); stageToShow = stage; stageCache.put(evt.getSource(), stageToShow); } stageToShow.show(); } catch (Exception exc) { logger.error("cannot start a screen", exc); } }
Here is the createStage() method called from startScreen().
private Stage createStage( String fxmlFile, Callback<Class<?>, Object> gcf, String stageTitle, Class<?> sa ) throws IOException { if( logger.isDebugEnabled() ) { logger.debug("[CREATE STAGE] fxmlFile=" + fxmlFile + ", using loader from class=" + sa.getName()); } FXMLLoader aScreenLoader = new FXMLLoader( sa.getResource( "/" + DEFAULT_PACKAGE_TO_SCAN + "/" + fxmlFile ), null, builderFactory, gcf); Parent someScreen = (Parent) aScreenLoader.load(); Stage stage = new Stage(); stage.setTitle( stageTitle ); Scene someScene = new Scene(someScreen); stage.setScene(someScene); return stage; }
Caching
In these Reflections-driven methods, I'm using a pair of caches to save off a ControllerFactory and a Stage. Both caches are keyed off of the Buttons. Although I don't worry about object creation as much as I did in the early days of garbage collection, I don't see a compelling reason not to reuse these objects especially since there might be a performance hit as each SubApp gets more complex. I might change this implementation if RAM became an issue such as if the user were opening many SubApps, possibly working with non-singleton SubApps.
The cache definitions follow.
private Map<Object, Stage> stageCache = new LinkedHashMap<>(); private Map<Object, Callback<Class<?>, Object>> gcfCache = new LinkedHashMap<>();I also provide a "Refresh" button to clear out the caches and try to get the program state back to before any of the modules were called. This might give a user a fighting chance at recovering if they hit a severe memory problem with one of the modules.
This is called from a MenuItem.
@FXMLpublic void refreshScreens() { gcfCache.clear(); stageCache.clear(); }
Annotations are a great way to let your app find out about itself. In this example, an annotation @SubApp is placed on a Google Guice AbstractModule. When the main HomeScreenController object is initialized, the SubApps are found and each one drives the creation of a Button. The annotation metadata is associated with the Button for future use. Lazily, the SubApp will be initialized when the Button is pressed. This will be cached so that subsequent Button presses don't need to recreate everything.
These past two posts presented the foundational technologies of the dynamic FX application modules solutions. The next three posts will cover developing with such as scheme.
Click Dynamic FX Application Modules - Part 4 - Bootstrap for the next part of the series.
No comments:
Post a Comment