Sonntag, 24. März 2013

JavaFX FXMLLoader with OSGi

Using FXMLLoader in OSGi is not completely trivial. This article explains, how to do it. The osgisnippets project contains a project bundle_set5fxml, which is used for that.

Loading an fxml file is done like that:

URL url = this.getClass().getResource("clickme.fxml"); Pane pane = FXMLLoader.load(url);

So let's just try that and see what happens:

18:05:57.927 [JavaFX Application Thread] ERROR - Error loading FXML file javafx.fxml.LoadException: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:726) ~[jfxrt.jar:na] ... at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2685) ~[jfxrt.jar:na] at example.set5fxml.impl.Activator$1.run(Activator.java:63) ~[na:na] ... Caused by: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_17] ... at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ~[na:1.7.0_17] at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:724) ~[jfxrt.jar:na] ... 17 common frames omitted

The fxml file references the class ClickmeController, which is inside the set5fxml bundle. Obviously FXMLLoader doesn't use a bundle class loader and thus cannot load the controller class. There's a method FXMLLoader.setDefaultClassLoader():

FXMLLoader.setDefaultClassLoader(Activator.class.getClassLoader()); URL url = this.getClass().getResource("clickme.fxml"); Pane pane = FXMLLoader.load(url);

The class loader passed into setDefaultClassLoader must be a bundle class loader, so it should be one from a class which is loaded from this bundle. (See below for more information.) So that worked well enough:

There are a few other things, which must done to make it work. Apache Felix does not export the JavaFX packages by default, so the relevant packages must be added to the list of extra system packages. This is done in the class OSGiFwLoader:

private void prepareConfigMap(Map<Object, Object> configMap) { configMap.put( Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, "example.set2appapi.api, " + "example.set2cmdapi.api, " + "javafx.application, " + "javafx.fxml, " + "javafx.event, " + "javafx.scene, " + "javafx.scene.control, " + "javafx.scene.layout, " + "javafx.scene.paint, " + "javafx.scene.text, " + "javafx.stage, " + "org.slf4j"); ... }

Also the bundle must import the required packages. In the osgisnippets project this is done in the file build.default.properties:

prj.bundle.name=Set5_FXML prj.bundle.import-package=org.osgi.framework, org.slf4j, \ javafx.application, \ javafx.fxml, \ javafx.event, \ javafx.stage, \ javafx.scene, \ javafx.scene.control, \ javafx.scene.layout, \ javafx.scene.paint, \ javafx.scene.text #prj.bundle.export-package=

Coming back to the class loader given to FXMLLoader.setDefaultClassLoader: In the sample bundle, the ClassLoader of the Activator class is used. This works, because the Activator class is part of the bundle and thus loaded by the bundle class loader. What happens, if the class loader of the BundleActivator class is used? An error is logged and the underlying exception with stack trace is this one:

Caused by: java.lang.ClassNotFoundException: example.set5fxml.impl.ClickmeController at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_17] at java.net.URLClassLoader$1.run(URLClassLoader.java:355) ~[na:1.7.0_17] at java.security.AccessController.doPrivileged(Native Method) [na:1.7.0_17] at java.net.URLClassLoader.findClass(URLClassLoader.java:354) ~[na:1.7.0_17] at java.lang.ClassLoader.loadClass(ClassLoader.java:423) ~[na:1.7.0_17] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) ~[na:1.7.0_17] at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ~[na:1.7.0_17] at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:724) ~[jfxrt.jar:na]

In osgisnippets and its guitool application, the classes of the OSGi framework (Apache Felix) are loaded by the system class loader. The felix jar is in the classpath of the guitool application. The BundleActivator interface is part of the felix jar and its class loader is the system class loader. However, the system class loader doesn't know about the bundles and it can't find the ClickmeController class. To summarize: Make sure, the class loader passed into FXMLLoader.setDefaultClassLoader() is actually a bundle class loader.

Resources

1 Kommentar: