Whilst performing some code-cleanups on Eclipse, Lars made the observation “This looks unnecessarily hard – why isn’t there a simple API for this?”.
The code in question was acquiring an instance of DebugOptions
, which is
used liberally throughout Eclipse to determine if an option is present to
enable debugging. Actually the use of DebugOptions
itself wasn’t that
much of an issue (though the getBooleanOption
is unable to determine
whether or not a boolean value is present or is false
– but that’s
another bug). The problem is that looking up an OSGi service is more than
a single-liner, and as such, is the type of thing that probably causes
more pain than it needs.
There’s three ways of getting services in OSGi, in reverse order of ease of use:
- Acquire the
BundleContext
(such as via theBundleActivator
or through a handler likeFrameworkUtil
) and then usegetService()
directly - Use a
ServiceTracker
to keep a cache of the services available for quick return - Use Declarative Services to instantiate your component and have the services injected in directly
The problem with using Declarative Services is that you yield not only the acquisition of the service but also the component lifetime. It also precludes the use of static methods or integration with other APIs that expect to manage object creation (or indeed, use object creation with constructors or builder patterns).
The other two use a non-trivial amount of code to get running, all for the sake of satisfying a ‘Please give me an instance’ request.
Fortunately, Java 8 provides a simple way of abstracting this; using the
Supplier
interface. A supplier is something that, when asked, returns
an instance of a particular request. It’s also used in a number of different
collections to defer object acquisition until it’s required. This fits in
with what we’re trying to do – get an instance of a service. So how might
it look in OSGi?
public class OSGiTracker<T> implements AutoClosable, Supplier<T> {
private final ServiceTracker<T,T> serviceTracker;
private boolean closed = true;
private OSGiTracker(Class<T> target, Class<?> source) {
if (target == null) {
throw new IllegalArgumentException("Target cannot be null");
}
if (source == null) {
throw new IllegalArgumentException("Source cannot be null");
}
Bundle bundle = FrameworkUtil.getBundle(source);
BundleContext context = bundle == null ? null : bundle.getBundleContext();
if (context == null) {
throw new IllegalArgumentException(
"Unable to acquire bundle context for " + source.getCanonicalName());
}
this.serviceTracker = new ServiceTracker<T,T>(context,target,null);
}
public static <T> OSGiTracker<T> supply(Class<T> target, Class<?> source) {
return new OSGiTracker<>(target, source);
}
@Override
public T get() {
if(closed) {
serviceTracker.open();
closed = false;
}
return serviceTracker.getService();
}
protected void finalize() throws Throwable {
close();
super.finalize();
}
public void close() throws Exception {
if(serviceTracker != null && !closed) {
serviceTracker.close();
}
}
}
This provides a Supplier
for a given service, and the only two parameters
that are required are the generic target required (e.g. DebugOptions
) and
the calling class (so that the BundleContext
can be resolved). The API
doesn’t get much simpler than that. This is how it looks when using it:
private final Supplier<DebugOptions> options =
OSGiTracker.supply(DebugOptions.class, getClass());
private final boolean DEBUG = options.get().getBooleanOption(...)
No worrying about dependencies other than the OSGiTracker
class, and
the client side API is trivial.
However, in most cases there’s no need to keep a ServiceTracker
hanging
on since you are using it in a one-shot basis. If the service isn’t there,
you want to use a default without changing anything else. As a result,
there’s an alternative implementation that can be used:
public class OSGiSupplier<T> implements Supplier<T> {
private final BundleContext context;
private final ServiceReference<T> serviceReference;
private OSGiSupplier(Class<T> target, Class<?> source) {
if (target == null) {
throw new IllegalArgumentException("Target cannot be null");
}
if (source == null) {
throw new IllegalArgumentException("Source cannot be null");
}
Bundle bundle = FrameworkUtil.getBundle(source);
BundleContext context = bundle == null ? null : bundle.getBundleContext();
if (context == null) {
throw new IllegalArgumentException(
"Unable to acquire bundle context for " + source.getCanonicalName());
}
this.context = context;
this.serviceReference = context.getServiceReference(target);
}
public static <T> OSGiSupplier<T> supply(Class<T> target, Class<?> source) {
return new OSGiSupplier<>(target, source);
}
@Override
public T get() {
try {
T service = context.getService(serviceReference);
if (service != null) {
context.ungetService(serviceReference);
}
return service;
} catch (Throwable t) {
return null;
}
}
}
The API for using this is almost identical:
private final Supplier<DebugOptions> options =
OSGiSupplier.supply(DebugOptions.class, getClass());
private final boolean DEBUG = options.get().getBooleanOption(...)
See the difference? It’s the type of supplier we are using. Otherwise the
field type and use case is identical. That makes it easy to switch between
the two; in fact, we could wrap this in another supplier if we wanted to
default to everything being false
or null
:
public class DebugOptionsWrapper implements Supplier<DebugOptions> {
private final Supplier<DebugOptions> delegate;
public DebugOptionsWrapper(Supplier<DebugOptions> delegate) {
this.delegate = delegate;
}
@Override
public DebugOptions get() {
DebugOptions options = delegate.get();
if(options == null) {
return new DebugOptions() { ... } // Empty implementation
} else {
return options;
}
}
}
This could be wrapped to prevent any NullPointerException
being thrown if the
service isn’t present:
private final Supplier<DebugOptions> options =
new DebugOptionsWrapper(OSGiSupplier.supply(DebugOptions.class, getClass()));
The move to Java 8 facilitates these kinds of improvements, and should be
part of Eclipse. The question is, where should the above go? Some shared
package would make sense, but does this belong in org.eclipse.core.runtime
or org.eclipse.equinox.util
? Does this even make sense to add to Eclipse?
Your thoughts are welcome; reach out to me @alblue.