OSGi Service Tutorial
Contents
- What is a service?
- What is a service factory?
- What can services be used for?
- How are services accessed??
- Releasing services
- Best practices for accessing services
- The white-board model
What is a service?
The OSGi R3 specification, chapter 4.10 is highly recommended reading. Also, the javadoc for BundleContext contains lot of information.
The client of a service is always an OSGi bundle, i.e. a piece of java code possible to start via the BundleActivator interface.
Each bundle may register zero or more services. Each bundle may also use zero or more services. There exists no limit on the number of services, more than the ones given by memory limits or java security permissions.
Both publishing/registering and usage of services can be restricted by using java security permissions.
Registering a very simple object as a serviceLong i = new Long(20); Hashtable props = new Hashtable(); props.put("description", "This an long value"); bc.registerService(Long.class.getName(), i, props);
Note: a service can also be registered as several interfaces. In this case, the object must implement all of the interfaces.
What is a service factory?
Sometimes a service needs to be differently configured depending on which bundle uses the service. For example, the log service needs to be able to print the logging bundle's id, otherwise the log would be hard to read.
A service factory is registered in exactly the same way as a normal service, using registerService, the only difference is an indirection step before the actual service object is handed out.
The client using the service need not, and should not, care if a service is generated by a factory or by a plain object.
A simple service factory exampleclass LongFactory implements ServiceFactory { public Object getService(Bundle bundle, ServiceRegistration reg) { // each bundle gets a Long with it's own id return new Long(bundle.getBundleId()); } void ungetService(Bundle bundle, ServiceRegistration reg, Object service) { // nothing needed in this case } } ServiceFactory factory = new LongFactory(); bc.registerService(Long.class.getName(), factory, new Hashtable());
Note: The framework will cache generated service objects. Thus, at most one service can be generated per client bundle.
What can services be used for?
The service concept is a very general-purpose tool, but some examples are:
- Export functionality from a bundle to other bundles
- Import functionality from other bundles
- Register listeners for events from other bundles
- Expose external devices, such as UPnP devices or even hardware, to other OSGi bundles. See the Device and UPnP APIs
- Expose java code running in OSGI to an external network, e.g. via the UPnP or SOAP protocols.
- Bundle configuration, using the Configuration Manager
Generally, services is the preferred method bundles should use to communicate between each other.
How are services accessed?
- Get a ServiceReference
- Get the service object from the reference, using BundleContext.getService()
- Explicit usage of BundleContext.getService()
This returns either the highest ranked service of the given class, or null - Explicit usage of BundleContext.getServices()
This return the complete set of matching services, or null - Callback from a ServiceListener
- Using the utility class ServiceTracker
All cases, except the first, allow the client to specify a set of properties that the service must fulfill. These properties are specified using LDAP filters. Zero or more service references can match a given filter.
A typical filter contains a class name and a set of properties. The example below matches a Http service having a "port" property equal to 80.
An LDAP filter string(&(objectclass=org.osgi.service.http.HttpService)(port=80))
As soon as a ServiceReference is available, the service object can be accessed using a cast:
HttpService http = (HttpService)bc.getService(sr);
After this, the service object can be accessed as any other java object. Actually, it is a fully normal java object, more exact, it is the same object as the bundle registering the service created.
Releasing services
As soon as the service isn't needed anymore, the client should release the service by calling the BundleContext.ungetService() method.
bc.ungetService(sr);
All services are automatically released when the client bundles stops. No special service cleanup is needed in BundleActivator.stop().
Note that service should only be released when really unused. Some services may keep a state for each using bundle, and releasing the service will release this state, which may, or may not, be the intention. Each service documentation must be consulted. An example of this is the HttpService, which will remove all servlets and resources from itself when a client releases the HttpService.
Some special words are needed when using service listeners. A listener will get a ServiceEvent.UNREGISTERING event when a service is in the process of being unregistering from the framework.
While in this UNREGISTERING listener, the getService() call should not be expected to return a valid service object. In fact, the expected value is null, even if the spec is a bit vague on this. The OSGi reference implementation does however return null in this case.
Note: It's fully possible for a client to hold on to a service object via a variable, even after the exporting bundle has been stopped. The client shouldn't expect the service to work in this case and this also blocks any garbage collection. Thus, don't forget to null variables holding services when the service is released.
Best practices for accessing OSGi services
Suggested practices:
- Always expect RuntimeException when calling a service
A service may fail at any time, similar to RMI based code. The recommended exception an unusable service should throw is IllegalStateException This does not mean that a try/catch block should be made on each service call, rather that the code must be prepared to handle such exceptions on a higher level. - Use the white-board model
If possible, construct your code so users of your bundle functionality can participate by registering services, instead of getting services. It's much easier to register a service, than to get it. Thus, if you want to make life easier for clients, go for the white-board model. - Use listeners
ServiceListener and ServiceTracker provide various levels of automation for getting services. - Use the declarative ServiceBinder
The ServiceBinder by Cervantes/Hall provide a framework for cases where it's possible to declare service dependencies in XML.
01: ServiceReference sr = bc.getServiceReference(HttpService.class.getName()); 02: HttpService http = (HttpService)bc.getService(sr); 03: http.registerServlet(....)Three things can fail, one for each line!
- The ServiceReference can be null if no HttpService is registered, resulting in NullPointerException on line 2.
- The http service object cannot be get, due to missing permissions, possible timing issues if the http unregisters between lines 1 and 2, causing NullPointerException on line 3.
- The http service may have become unusable, resulting in any RuntimeException subclass, most likely IllegalStateException on line 3.
Additionally, the code above does not handle the case where more than one service is registered and actions should be taken on each of them.
The NPE problems can be naively avoided by adding conditionals:
01: ServiceReference sr = bc.getServiceReference(HttpService.class.getName()); 02: if(sr != null) { 03: HttpService http = (HttpService)bc.getService(sr); 04: if(http != null) { 05: http.registerServlet(....) 06: } 07: }
This approach quickly becomes very cumbersome, and also creates an undesirable start order problem, since the HttpService must be available when the code is run.
By using a service listener, the code can avoid the first ServiceReference null problem:
01: ServiceListener sl = new ServiceListener() { 02: public void ServiceChanged(ServiceEvent ev) { 03: ServiceReference sr = ev.getServiceReference(); 04: switch(ev.getType()) { 05: case ServiceEvent.REGISTERED: 06: { 07: HttpService http = (HttpService)bc.getService(sr); 08: http.registerServlet(...); 09: } 10: break; 11: default: 12: break; 13: } 14: } 15: }; 16: 17: String filter = "(objectclass=" + HttpService.class.getName() + ")"; 18: try { 19: bc.addServiceListener(sl, filter); 20: } catch (InvalidSyntaxException e) { 21: e.printStackTrace(); 22: }
The possible RuntimeException when actually calling the service (line 8) is handled by the framework event delivery code, so if no special handling is needed in the client code, nothing needs to be done.
There's still one problem -- if all HttpServices already had been registered, the listener above would not be called until the HttpServices were restarted. A small trick solves this: Manually construct REGISTERED ServiceEvents and call the listener:
18: try { 19: bc.addServiceListener(sl, filter); 20: ServiceReference[] srl = bc.getServiceReferences(null, filter); 21: for(int i = 0; srl != null && i < srl.length; i++) { 22: sl.serviceChanged(new ServiceEvent(ServiceEvent.REGISTRED, 23: srl[i])); 24: } 25: } catch (InvalidSyntaxException e) { 26: e.printStackTrace(); 27: }
Now the number of lines has grown from three to 25+. Which may be OK if this is a one-time operation. But if continuous use of a service is intended, it's easier to use a ServiceTracker:
01: ServiceTracker logTracker = new ServiceTracker(bc, LogService.class.getName(), null); 02: logTracker.open(); 03: ((LogService)logTracker.getService()).doLog(...);
The tracker guarantees to hold all currently available services, but may naturally return null if no services are available. This is often OK, since the code need to be prepared for RuntimeException anyway.
The down-side of this approach is the continuous and annoying usage of casting to get the desired object. Wrapping this in a single utility method can ease usage a bit:
01: ServiceTracker logTracker; 02: 03: void init() { 04: logTracker = new ServiceTracker(bc, LogService.class.getName(), null); 05: } 06: 07: LogService getLog() { 08: return (LogService)logTracker.getService(); 09: } 10: 11: void test() { 12: getLog().doLog(...); 13: }
The white-board model
Consider the HttpService case. All clients must constantly monitor the framework in some way to add its servlet(s) to the web server. This requires at least the amount of code exemplified above, and even this doesn't completely guard against timing problems.
Additionally, the Http service must provide special methods for adding and removing servlets, and maintain an internal list of added servlets. This adds complexity both to the API and to the internal server code.
This is a very common scenario -- some kind of callbacks/listeners are needed and the server needs to keep track of all available listeners.
The OSGi framework already provides exactly this functionality.
Thus, an alternative and better approach is to let the client register its servlets into the framework, and let the http server use this list instead.
Servlet client01: class MyServlet extends HttpServlet { 02: ... 03: } 04: 05: HttpServlet servlet = new MyServlet(); 06: Hashtable props = new Hashtable(); 07: props.put("alias", "/servlets/foo"); // desired http alias 08: ServiceRegistration reg = 09: bc.registerService(HttpServlet.class.getName(), servlet, props);
The client needn't do anything more. If the servlet should be removed, it is simply unregistered from the framework.
10: reg.unregister();
The new, improved http server would monitor the framework for all services being HttpServlets, and use the "alias" property to set the alias.
Below is a minimalistic wrapper for the existing http service: (a complete class doing this can be found in the Http Console example code.
HttpService wrappervoid open() { httpTracker = new ServiceTracker(bc, HttpService.class.getName(), null); httpTracker.open(); ServiceListener sl = new ServiceListener() { public void serviceChanged(ServiceEvent ev) { ServiceReference sr = ev.getServiceReference(); switch(ev.getType()) { case ServiceEvent.REGISTERED: { registerServlet(sr); } break; case ServiceEvent.UNREGISTERING: { unregisterServlet(sr); } break; } } }; String filter = "(objectclass=" + HttpServlet.class.getName() + ")"; try { bc.addServiceListener(sl, filter); ServiceReference[] srl = bc.getServiceReferences(null, filter); for(int i = 0; srl != null && i < srl.length; i++) { sl.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, srl[i])); } } catch (InvalidSyntaxException e) { e.printStackTrace(); } } void registerServlet(ServiceReference sr) { HttpServlet servlet = (HttpServlet)bc.getService(sr); String alias = (String)sr.getProperty("alias"); Object[] httplist = httpTracker.getServices(); for(int i = 0; httplist != null && i < httplist.length; i++) { HttpService http = (HttpService)httplist[i]; try { Hashtable props = new Hashtable(); http.registerServlet(alias, servlet, props, null); } catch (Exception e) { e.printStackTrace(); } } } void unregisterServlet(ServiceReference sr) { String alias = (String)sr.getProperty("alias"); Object[] httplist = httpTracker.getServices(); for(int i = 0; httplist != null && i < httplist.length; i++) { HttpService http = (HttpService)httplist[i]; try { http.unregister(alias); } catch (Exception e) { e.printStackTrace(); } bc.ungetService(sr); } }
$Rev$, $Author$