Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Acquiring OSGi Services in Java 8

2015, eclipse, java, osgi

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 the BundleActivator or through a handler like FrameworkUtil) and then use getService() 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?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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:

1
2
3
4
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
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.