To register a service, an instance of the implementation class needs to be created and registered with the framework. Interactions with the framework are performed with an instance of BundleContext
—typically provided in the BundleActivator.start
method and stored for later use. The *FeedParser
classes from the previous chapter will be extended to support registration as a service instead of the Equinox extension registry.
A bundle's activator is a class that is instantiated and coupled to the lifetime of the bundle. When a bundle is started, if a manifest entry Bundle-Activator
exists, then the corresponding class is instantiated. As long as it implements the BundleActivator
interface, the start
method will be called. This method is passed as an instance of BundleContext
, which is the bundle's connection to the hosting OSGi framework.
Create a class in the com.packtpub.e4.advanced.feeds
project called com.packtpub.e4.advanced.feeds.internal.FeedsActivator
, which implements the org.osgi.framework.BundleActivator
interface.
The quick fix may suggest adding org.osgi.framework
as an imported package. Accept this, and modify the META-INF/MANIFEST.MF
file as follows:
Import-Package: org.osgi.framework Bundle-Activator: com.packtpub.e4.advanced.feeds.internal.FeedsActivator
The framework will automatically invoke the start
method of the FeedsActivator
when the bundle is started, and correspondingly, the stop
method when the bundle is stopped. Test this by inserting a pair of println
calls:
public class FeedsActivator implements BundleActivator { public void start(BundleContext context) throws Exception { System.out.println("Bundle started"); } public void stop(BundleContext context) throws Exception { System.out.println("Bundle stopped"); } }
Now run the project as an OSGi framework with the feeds
bundle, the Equinox console, and the Gogo shell. The required dependencies can be added by clicking on Add Required Bundles, although the Include optional dependencies checkbox does not need to be selected. Ensure that the other workspace and target bundles are deselected with the Deselect all button, as shown in the following screenshot:
The required bundles are as follows:
com.packtpub.e4.advanced.feeds
org.apache.felix.gogo.command
org.apache.felix.gogo.runtime
org.apache.felix.gogo.shell
org.eclipse.equinox.console
org.eclipse.osgi
On the console, when the bundle is started (which happens automatically if the Default Auto-Start is set to true
), the Bundle started message should be seen.
Once the FeedsActivator
instance is created, a BundleContext
instance will be available for interaction with the framework. This can be persisted for subsequent use in an instance field and can also be used directly to register a service.
The BundleContext
class provides a registerService
method, which takes an interface, an instance, and an optional Dictionary
instance of key/value pairs. This can be used to register instances of the feed parser at runtime. Modify the start
method as follows:
public void start(BundleContext context) throws Exception { context.registerService(IFeedParser.class, new RSSFeedParser(), null); context.registerService(IFeedParser.class, new AtomFeedParser(), null); context.registerService(IFeedParser.class, new MockFeedParser(), null); }
Now start the framework again. In the console that is launched, look for the bundle corresponding to the feeds
bundle:
osgi> bundles | grep feeds com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=56} {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=57} {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=58}
This shows that bundle 4
has started three services, using the interface com.packtpub.e4.advanced.feeds.IFeedParser
, and with service IDs 56
, 57
, and 58
.
It is also possible to query the runtime framework for services of a known interface type directly using the services
command and an LDAP style filter:
osgi> services (objectClass=com.packtpub.e4.advanced.feeds.IFeedParser) {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=56} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service." {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=57} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service." {com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=58} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service."
The results displayed represent the three services instantiated. They can be introspected using the service
command passing the service.id
:
osgi> service 56 com.packtpub.e4.advanced.feeds.internal.RSSFeedParser@52ba638e osgi> service 57 com.packtpub.e4.advanced.feeds.internal.AtomFeedParser@3e64c3a osgi> service 58 com.packtpub.e4.advanced.feeds.internal.MockFeedParser@49d5e6da
Services have an implicit order, based on the order in which they were instantiated. Each time a service is registered, a global service.id
is incremented.
It is possible to define an explicit service ranking with an integer property. This is used to ensure relative priority between services, regardless of the order in which they are registered. For services with equal service.ranking
values, the service.id
values are compared.
To pass a priority into the service registration, create a helper method called priority
, which takes an int
value and stores it in a Hashtable
with the key service.ranking
. This can be used to pass a priority to the service registration methods. The following code illustrates this:
private Dictionary<String,Object> priority(int priority) { Hashtable<String, Object> dict = new Hashtable<String,Object>(); dict.put("service.ranking", new Integer(priority)); return dict; } public void start(BundleContext context) throws Exception { context.registerService(IFeedParser.class, new RSSFeedParser(), priority(1)); context.registerService(IFeedParser.class, new MockFeedParser(), priority(-1)); context.registerService(IFeedParser.class, new AtomFeedParser(), priority(2)); }
Now when the framework starts, the services are displayed in order of priority:
osgi> services | (objectClass=com.packtpub.e4.advanced.feeds.IFeedParser) {com.packtpub.e4.advanced.feeds.IFeedParser}={service.ranking=2, service.id=58} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service." {com.packtpub.e4.advanced.feeds.IFeedParser}={service.ranking=1, service.id=56} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service." {com.packtpub.e4.advanced.feeds.IFeedParser}={service.ranking=-1, service.id=57} "Registered by bundle:" com.packtpub.e4.advanced.feeds_1.0.0.qualifier [4] "No bundles using service."
Dictionary
was the original Java Map
interface, and Hashtable
the original HashMap
implementation. They fell out of favor in Java 1.2 when Map
and HashMap
were introduced (mainly because they weren't synchronized by default) but OSGi was developed to run on early releases of Java (JSR 8 proposed adding OSGi as a standard for the Java platform). Not only that, early low-powered Java mobile devices didn't support the full Java platform, instead exposing the original Java 1.1 data structures. Because of this history, many APIs in OSGi refer to only Java 1.1 data structures so that low-powered devices can still run OSGi systems.
The BundleContext
instance can be used to acquire services as well as register them. FeedParserFactory
, which originally used the extension registry, can be upgraded to refer to services instead.
To obtain an instance of BundleContext
, store it in the FeedsActivator.start
method as a static
variable. That way, classes elsewhere in the bundle will be able to acquire the context. An accessor method provides an easy way to do this:
public class FeedsActivator implements BundleActivator { private static BundleContext bundleContext; public static BundleContext getContext() { return bundleContext; } public void start(BundleContext context) throws Exception { // register methods as before bundleContext = context; } public void stop(BundleContext context) throws Exception { bundleContext = null; } }
Now the FeedParserFactory
class can be updated to acquire the services. OSGi services are represented via a ServiceReference
instance (which is a sharable object representing a handle to the service) and can be used to acquire a service instance:
public class FeedParserFactory { public List<IFeedParser> getFeedParsers() { List<IFeedParser> parsers = new ArrayList<IFeedParser>(); BundleContext context = FeedsActivator.getContext(); try { Collection<ServiceReference<IFeedParser>> references = context.getServiceReferences(IFeedParser.class, null); for (ServiceReference<IFeedParser> reference : references) { parsers.add(context.getService(reference)); context.ungetService(reference); } } catch (InvalidSyntaxException e) { // ignore } return parsers; } }
In this case, the service references are obtained from the bundle context with a call to context.getServiceReferences(IFeedParser.class,null)
. The service references can be used to access the service's properties, and to acquire the service.
The service instance is acquired with the context.getService(ServiceReference)
call. The contract is that the caller "borrows" the service, and when finished, should return it with an ungetService(ServiceReference)
call. Technically, the service is only supposed to be used between the getService
and ungetService
calls as its lifetime may be invalid afterwards; instead of returning an array of service references, the common pattern is to pass in a unit of work that accepts the service and then call ungetService
afterwards. However, to fit in with the existing API, the service is acquired, added to the list, and then released immediately afterwards.
Now run the project as an Eclipse application, with the feeds
and feeds.ui
bundles installed. When a new feed is created by navigating to File | New | Other | Feeds | Feed, and a feed such as http://alblue.bandlem.com/atom.xml
is entered, the feeds will be shown in the navigator view. When drilling down, a NullPointerException
may be seen in the logs, as shown in the following:
!MESSAGE An exception occurred invoking extension: com.packtpub.e4.advanced.feeds.ui.feedNavigatorContent for object com.packtpub.e4.advanced.feeds.Feed@770def59 !STACK 0 java.lang.NullPointerException at com.packtpub.e4.advanced.feeds.FeedParserFactory. getFeedParsers(FeedParserFactory.java:31) at com.packtpub.e4.advanced.feeds.ui.FeedContentProvider. getChildren(FeedContentProvider.java:80) at org.eclipse.ui.internal.navigator.extensions. SafeDelegateTreeContentProvider. getChildren(SafeDelegateTreeContentProvider.java:96)
Tracing through the code indicates that the bundleContext
is null
, which implies that the feeds
bundle has not yet been started. This can be seen in the console of the running Eclipse application by executing the following code:
osgi> ss | grep feeds 866 ACTIVE com.packtpub.e4.advanced.feeds.ui_1.0.0.qualifier 992 RESOLVED com.packtpub.e4.advanced.feeds_1.0.0.qualifier
While the feeds.ui
bundle is active, the feeds
bundle is not. Therefore, the services haven't been instantiated, and bundleContext
has not been cached.
By default, bundles are not started when they are accessed for the first time. If the bundle needs its activator to be called prior to using any of the classes in the package, it needs to be marked as having an activation policy of lazy. This is done by adding the following entry to the MANIFEST.MF
file:
Bundle-ActivationPolicy: lazy
The manifest editor can be used to add this configuration line by selecting Activate this plug-in when one of its classes is loaded, as shown in the following screenshot:
Now, when the application is run, the feeds will resolve appropriately.
Both mechanisms (using the extension registry and using the services) allow for a list of feed parsers to be contributed and used by the application. What are the differences between them, and are there any advantages to one or the other?
Both the registry and services approaches can be used outside of an Eclipse runtime. They work the same way when used in other OSGi implementations (such as Felix) and can be used interchangeably. The registry approach can also be used outside of OSGi, although that is far less common.
The registry encodes its information in the plugin.xml
file by default, which means that it is typically edited as part of a bundle's install (it is possible to create registry entries from alternative implementations if desired, but this rarely happens). The registry has a notification system, which can listen to contributions being added and removed.
The services approach uses the OSGi framework to store and maintain a list of services. These services don't have an explicit configuration file and, in fact, can be contributed by code (such as the registerService
calls previously covered) or by declarative representations (which are covered in the next section).
The separation of how the service is created versus how the service is registered is a key difference between the service and the registry approach. Like the registry, the OSGi services system can generate notifications when services come and go.
One key difference in an OSGi runtime is that bundles depending on the Eclipse registry must be declared as
singletons; that is, they have to use the ;singleton:=true
directive on Bundle-SymbolicName
. This means that there can only be one version of a bundle that exposes registry entries in a runtime, as opposed to multiple versions in the case of general services.
While the registry does provide mechanisms to be able to instantiate extensions from factories, these typically involve simple configurations and/or properties that are hard-coded in the plugin.xml
files themselves. They would not be appropriate to store sensitive details such as passwords. On the other hand, a service can be instantiated from whatever external configuration information is necessary and then registered, such as a JDBC connection for a database.
Finally, extensions in the registry are declarative by default and are activated on demand. This allows Eclipse to start quickly because it does not need to build the full set of class loader objects or run code, and then bring up services on demand. Although the approach previously didn't use declarative services, it is possible to do this as covered in the next section.