Pages

Sunday, November 30, 2014

Making JavaFX better with OSGi

Why you should run JavaFX on OSGi

OSGi makes JavaFX better for three reasons:
  1. Hot code reload during development
  2. A services based architecture keeps UI features nicely isolated
  3. 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:
  1. The method fails with a ClassNotFoundException
  2.  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 Map 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());
   }
  });
 }
}

A bundle that adds a new tab could contain the following code:


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 :-)

7 comments:

  1. GitHub repo with the example code: https://github.com/paulbakker/javafx-osgi-example

    ReplyDelete
  2. Hello Paul,

    Nice introduction to OSGI with JavaFX.

    But i am having having issues trying to run this code. I cloned the same from github. I am using java version 1.8.0_25 with eclipse Luna SR1 4.4.1. Installed BND tools from the market place. Downloaded the latest dependencies from apache felix site. I was able to compile the code. But the UI would simply not show up. I put in a few println() messages to debug and figured that the starteBundle() in the App class is getting invoked. The Stage instance is getting set in the StageServiceImpl instance, also verified that the Stage instance is not null. But the start function in the UI class was not getting invoked. I also listed all the bundles in the Gogo shell and it shows the same list as shown in your video post. Am i missing something here. ?

    Thanks

    ReplyDelete
    Replies
    1. The service depency is not getting invoked.

      Delete
  3. Hi Paul,

    Thanks for the nice example. I am still very new to bndtools and osgi though and I am having trouble getting your example to compile. I hate to bother you with such a basic question, but it appears as thought the dependency manager annotation jar file is not getting added to my build path or is otherwise inaccessible. I can see the plugin in the the cnf/plugins direction in the bndtool perspective, and the build.bnd looks fine. However in the osgi module I can not use annotations such as @Start and so the module is not compiling.

    Am I supposed to add the plugin to the eclipse build path? I thought that bndtools was suppose to take care of stuff like this but I am pulling my hair out trying to get this to work. I have tried adding the jar file to the build path in the bnd.bnd file for the module, but bndtools says that the jar file is not a valid bundle and won't let me do that. Right now I am pining for a pom or build.xml file...any help or pointers would be appreciated. TIA

    - chooks

    ReplyDelete
  4. Hi, this is a really useful article, however I am having one specific issue that I don't understand. The line:

    BundleContext ctx = FrameworkUtil.getBundle(this.getClass()).getBundleContext();

    gives me a compile error as follows:

    The method getBundle(Class) is undefined for the type FrameworkUtil ... which clearly seems wrong according to the docs

    ... any clue? I have seen this code in many OSGi articles/tutorials and I can never get it to compile. I'm using Java 8 and Felix framework 4.4.1

    ReplyDelete
  5. Thanks for this example Paul. Really appreciate it.

    Though there are some more steps to be considered:

    In order to use felix-dm-annotations I had to edit my cnf/ext/pluginpaths.bnd and copy the dependecymanager-annotation-jar to the build-repository

    This is the content of cnf/ext/pluginpaths.bnd
    -pluginpath: \
    ${plugin-dir}/biz.aQute.repository/biz.aQute.repository.jar,\
    ${workspace}/cnf\buildrepo/org.apache.felix.dependencymanager.annotation/org.apache.felix.dependencymanager.annotation-4.0.0.jar

    Furthermore I needed to add the annotation-plugin to the bnd-files in each project
    -plugin org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin;log=debug

    The steps are documented here: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager/tutorials/working-with-annotations.html

    @Andrew: make sure your buildpath is using at lease OSGI Version 5.0 (BndTools will default to 4.something).

    ReplyDelete
  6. Nice post it is very useful for me.
    Each and every year we are providing Cheap and best students project at Madurai.

    ReplyDelete