Why you should run JavaFX on OSGi
OSGi makes JavaFX better for three reasons:
- Hot code reload during development
- A services based architecture keeps UI features nicely isolated
- Provisioning to devices
Let's take a look at all of them in detail before we see how to make JavaFX run on OSGi.
Hot code reload
Waiting for a build and restarting your application to see the effect of code changes is annoying. Even more so with UI related work, because it's less easy to work test-driven and you often need a bit of experimentation to get the UI to look the way you intended.
OSGi is designed to be dynamic; in a running framework bundles can be added, removed and updated. Combined with an IDE that understands this, we can make updates to a running application without ever waiting for builds or restarts. See the following video for an example.
Services based architecture
In OSGi it's trivial to create plugin systems based on the so called "whiteboard pattern". Each part of the UI can be provided by a different bundle, which makes the code loosely coupled. This makes it easier to add features or change existing features without impacting the rest of the code. The same mechanism can be implemented using other Dependency Injection frameworks, but it comes most natural in OSGi.
Provisioning
JavaFX is gaining popularity in the IoT space; it's very well suited to run on all kind of devices. If we have a lot of devices, it becomes a question how we install and update our application to those devices. Manually copying files to a devices is ok if we have one or two of them, but what if you roll out your software to many devices? Also, in the IoT space there might be limited bandwidth, so rolling out updates should preferably be efficient in this aspect as well. Because an OSGi framework can be updated, it's possible to create provisioning systems; a system that takes care of installing and updating the software running on a target (such as a device). Apache ACE is a great example of this. At JFokus, Sander Mak and me will present about exactly this topic in a lot more detail.
Running JavaFX on OSGi
Out of the box it seems a bit problematic to run JavaFX on OSGi. The reason for this is that starting a JavaFX application requires the use of a launcher, which wasn't designed to be used in a modular or dynamic environment.
Typically the launch method is used to start a JavaFX application. Running this method from an OSGi bundle (e.g. from an Activator) produces two issues:
- The method fails with a ClassNotFoundException
- The method may only be called once (this is explicitly checked), so updating/restarting the bundle that invokes the method is not possible.
Importing JavaFX packages
Besides these two problems there is something else to straighten out. When using the javafx packages, most OSGi frameworks will give resolver issues such as: "Unable to resolve 11.0: missing requirement [11.0] osgi.wiring.package; (osgi.wiring.package=javafx.application)".
In OSGi the packages made available from the JRE are explicitly exported by the "system bundle". The list of packages needs to be configured however, and most launchers available today don't export the javafx packages yet. This is no problem at all, we can do this manually with just a bit of configuration. When using Bndtools you can add the following to a .bndrun file: "-runsystempackages: javafx.application,javafx.scene". Just add other packages when you need them.
Fixing the ClassNotFoundException
Looking at the source code of the launch method, it uses the ContextClassLoader of the current thread to load the class that you are trying to launch. A common problem with an easy fix: Setting the ConextClassLoader to the bundle's classloader before invoking the method.
Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader()); launch();
Working around the single invoke launch limitation
When we setup the UI in the same bundle as where the launch method is invoked, we run into a practical problem. Each time we edit our code, the bundle is updated in the running app (like we want it to). This fails because the launch will throw an exception. Not good. We can easily work around this problem by moving the code that actually sets up the UI to a separate bundle (or bundles).
In the following example I register the Stage created by the launcher as a service. Another bundle can depend on this service to get the Stage and add UI elements to it. This way the launching code and the actual UI are separated, and we can happily restart and update the UI bundle without problem.
Code in the launcher bundle.
import java.util.concurrent.Executors; import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; import org.apache.felix.dm.DependencyManager; import org.apache.felix.dm.annotation.api.Component; import org.apache.felix.dm.annotation.api.Start; import org.apache.felix.dm.annotation.api.Stop; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import example.javafx.launcher.StageService; @Component public class App extends Application { @Start public void startBundle() { Executors.defaultThreadFactory().newThread(() -> { Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader()); launch(); }).start(); } @Override public void start(Stage primaryStage) throws Exception { BundleContext bc = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); DependencyManager dm = new DependencyManager(bc); dm.add(dm.createComponent() .setInterface(StageService.class.getName(), null) .setImplementation(new StageServiceImpl(primaryStage))); } @Stop public void stopBundle() { Platform.exit(); } }
Code in the UI bundle.
@Component public class UI { @ServiceDependency private volatile StageService m_stageService; @Start public void start() { Platform.runLater(() -> { Stage primaryStage = m_stageService.getStage(); primaryStage.setTitle("Hello World!"); Button btn = new Button(); btn.setText("Say 'Hello'"); btn.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) { System.out.println("Hello World!"); } }); StackPane root = new StackPane(); root.getChildren().add(btn); primaryStage.setScene(new Scene(root, 300, 250)); primaryStage.show(); }); } }
That's it! JavaFX now runs successfully in an OSGi container.
A pluggable architecture
Now that we are running on OSGi, we can make use of services to make the whole architecture more modular. As an example we take a UI that has multiple screens, divided by tabs. Let's try to create a mechanism that let you provide new tabs from different bundles, so that new features can be added to the UI without even changing anything in the main UI code.
The main UI bundle just sets up the TabPane. It will than listen for any AppScreen services that are registered (this is an example of the whiteboard pattern). Each AppScreen represents a tab, and has a title and a Node that represent that tab. When a new AppScreen is found, it is added to the TabPane, and when an AppScreen is removed it is removed from the TabPane. Now we can add new parts to the UI by just installing new bundles.
import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.stage.Stage; import org.apache.felix.dm.annotation.api.Component; import org.apache.felix.dm.annotation.api.ServiceDependency; import org.apache.felix.dm.annotation.api.Start; import org.osgi.framework.ServiceReference; import example.javafx.launcher.StageService; import example.javafx.ui.AppScreen; @Component public class UI { @ServiceDependency private volatile StageService m_stageService; private volatile TabPane tabPane; private final MapA bundle that adds a new tab could contain the following code:screens = new ConcurrentHashMap<>(); @Start public void start() { Platform.runLater(() -> { Stage primaryStage = m_stageService.getStage(); primaryStage.setTitle("Tabs example!"); tabPane = new TabPane(); screens.values().forEach(this::createTab); primaryStage.setScene(new Scene(tabPane, 300, 250)); primaryStage.show(); }); } private void createTab(AppScreen s) { Tab tab = new Tab(s.getName()); tab.setContent(s.getContent()); tabPane.getTabs().add(s.getPosition(), tab); tabPane.getSelectionModel().select(tabPane.getTabs().size()-1); } @ServiceDependency(removed = "removeScreen") public void addScreen(ServiceReference sr, AppScreen screen) { if (tabPane != null) { Platform.runLater(() -> { createTab(screen); }); } screens.put(sr, screen); } public void removeScreen(ServiceReference sr) { Platform.runLater(() -> { AppScreen remove = screens.remove(sr); Optional findAny = tabPane.getTabs().stream() .filter(t -> t.getText().equals(remove.getName())) .findAny(); if (findAny.isPresent()) { tabPane.getTabs().remove(findAny.get()); } }); } }
import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.layout.VBox; import org.apache.felix.dm.annotation.api.Component; import example.javafx.ui.AppScreen; @Component public class OtherScreen implements AppScreen{ @Override public String getName() { return "Other screen"; } @Override public Node getContent() { VBox vbox = new VBox(); Button button = new Button("Other screen"); vbox.getChildren().add(button); return vbox; } @Override public int getPosition() { return 1; } }
Tooling
To get the most out of OSGi we need an IDE that "understands" updating bundles in a running framework. This makes build tools like Maven and Gradle a less than optimal choice, although they can definitely be used for building bundles. A much better choice is Bndtools; an Eclipse plugin that makes OSGi development easy. There is also an open issue for support in Intellij, please vote! For JavaFX support in Eclipse I used e(fx)clipse.
Deploying
Just like any OSGi application built with Bndtools we can export the application as an executable JAR, as also shown in this video. Simply click the export button in the .bndrun configuration screen, or run gradle export with the out of the box available Gradle build.
For a more advanced IoT setup we would use Apache ACE for provisioning. Come see us at JFokus for more about this :-)
For a more advanced IoT setup we would use Apache ACE for provisioning. Come see us at JFokus for more about this :-)