Service Trackers

I was recently fishing for topics to build potential sessions for Devcon 2019 (you're going, right? ;-) ) and one community member suggested the topic of Service Trackers.

While I'm not sure I could fill a whole session on service trackers, I think I can certainly fill a blog entry about them...

What Are Service Trackers

The best way I can describe service trackers, well they are programmatic ways to deal with dependency resolution, a programmatic option to the @Reference annotation.

Service trackers are used to track the addition, modification or removal of DS components, and the programmatic aspects give you a lot of control over that tracking.

Simple Service Tracker Example

The simplest examples of a service tracker can be found in the XxxLocalServiceUtil classes that Service Builder 7.x generates for you:

public static GuestbookEntryLocalService getService() {
  return _serviceTracker.getService();
}

private static ServiceTracker<GuestbookEntryLocalService, GuestbookEntryLocalService> _serviceTracker;

static {
  Bundle bundle = FrameworkUtil.getBundle(GuestbookEntryLocalService.class);

  ServiceTracker<GuestbookEntryLocalService, GuestbookEntryLocalService> serviceTracker = 
    new ServiceTracker<GuestbookEntryLocalService, GuestbookEntryLocalService>(bundle.getBundleContext(),
      GuestbookEntryLocalService.class, null);

  serviceTracker.open();

  _serviceTracker = serviceTracker;
}

In this simple example, the bundle (always going to be the bundle the ServiceTracker is being being created in, not the bundle w/ the interface the ServiceTracker is tracking) is used to get the BundleContext and create an org.osgi.util.tracker.ServiceTracker instance.

The code declares it is building a ServiceTracker over a specific component service type, in this case the GuestbookEntryLocalService interface. The constructor shown takes a BundleContext (for OSGi access), the service class, and null in place of a ServiceTrackerCustomizer instance (to filter available services).

When the service tracker is created, it needs to be open()ed, afterwards the _serviceTracker.getService() method can be used to return the current instance the tracker has available.

If there are no service instances available, getService() will return null.

If there are multiple service instances available, getService() will return only one of those, but there is no guarantee which instance will be returned.

Service Tracker Customizers

A ServiceTrackerCustomizer is used to provide touchpoints for event handling for services (adds, modifies and removes) and can also filter service instances.

Say you have a Shape interface, and you're interested in getting new shapes, but you only want to track shapes that have an even number of sides (squares, hexagons, octagons, etc.). Normally a ServiceTracker is notified of all shape instances that are added and removed, but through your ServiceTrackerCustomizer you can filter the services that are tracked.

Here's our ServiceTrackerCustomizer for the even-number-sided shape services:

private static class ShapeServiceTrackerCustomizer implements ServiceTrackerCustomizer<Shape, Shape> {

  @Override
  public Shape addingService(ServiceReference<Shape> serviceReference) {
    Map<String, Object> properties = serviceReference.getProperties();

    if ((GetterUtil.getInteger(properties.get("sides")) % 2) == 1) {
      // odd number of sides
      return null;
    }
  
    // has even sides
    Shape service = _bundleContext.getService(serviceReference);
  
    // check the shape for sides
    if ((shape.getSides() % 2) == 1) {
      // odd number of sides, destroy the instance before returning.
      service.destroy();
      return null;
    }

    return service;
  }

  @Override
  public void modifiedService(ServiceReference<Shape> serviceReference, Shape service) {
  }

  @Override
  public void removedService(ServiceReference<Shape> serviceReference, Shape service) {
    service.destroy();
  }
}

Those properties you assign inside of the @Component annotation? Those are the properties we're getting from the service reference. We can then get the number of sides defined in the properties and decide if we want to track the service or not.

Or we can poll the shape directly for the sides as long as the api supports it.

Either way, we keep the even number of sides shapes but discard the odd sides.

Service Tracker Lists

Tracking single services is not all that interesting. I mean, we could do a lot of what we've seen so far with an @Reference along with a target filter.

Imagine though that we are building a paint portlet and we use the shape implementations to draw on the canvas. We don't know in advance what shapes we might get, but as new shapes are added we want to be able to include and use them.

The Service Tracker itself can give us support to access a list of Shapes, but Liferay also provides the ServiceTrackerList utility class.

The ServiceTrackerListFactory can create ServiceTrackerList instances for tracking a list of services.

We can create a ServiceTrackerList for our Shapes using the following example:

private ServiceTrackerList<Shape,Shape> shapesTrackerList = 
  ServiceTrackerListFactory.open(bundle.getBundleContext(), Shape.class, 
    new ShapeServiceTrackerCustomizer());

This convenience method creates the service tracker plus it opens the tracker.

The ServiceTrackerList interface provides methods to get the current size (number of available services) and an iterator over services.

Now our paint portlet can use the ServiceTrackerList and it will automatically be notified of new shapes being added and removed from the system. The paint portlet doesn't need to do anything special, it can just iterate over the list of shapes and make them available to the user.

Service Tracker Maps

Lists are interesting, but another useful collection are ServiceTrackerMaps.

Service tracker maps use a <key,service> sort of mapping where the key often comes from the service component properties.

Liferay's DDM facilities offer a practical example. The DDM types implement an interface, but the implementation has a name such as "Text", "Radio", etc. Using a ServiceTrackerMap, you can still have all of the different service implementations, but in a map form you can access a specific instance from the name property.

If our shapes have a Name property so we can track Circles, Triangles, Squares, etc and we want to be able to find the Shape implementation using the name, we can construct our ServiceTrackerMap as follows:

private ServiceTrackerMap<String,Shape> shapesTrackerMap = 
  ServiceTrackerMapFactory.openSingleValueMap(bundle.getBundleContext(), Shape.class, "name");

Shape square = shapesTrackerMap.getService("Square");

Now not only is our map completely dynamic as bundles are deployed and undeployed, but we are populating a map from the component properties and we can use our key value to locate a specific service instance.

The ServiceTrackerMapFactory also has openMultipleValueMap() methods that use a <key,List<Service>> map so you can have multiple services for given keys.

Conclusion

This has been a whirlwind introduction to the Service Trackers including the ServiceTrackerCustomizer. We also saw the service tracker lists and maps. Interestingly, both lists and maps support the ServiceTrackerCustomizers too, so you have filtering control over the lists and maps too.

If you're wondering about practical uses for ServiceTrackers, ServiceTrackerCustomizers, ServiceTrackerLists and/or ServiceTrackerMaps, I'd point you to the Liferay source code. Liferay relies heavily on all of these classes and you can find some great examples of each.