In AEM we tend to develop custom OSGi services that do small units of work. For example, an OSGi service to send emails. Or a service that executes search queries. Sometimes we have to deal with many service instances at once. Factory configurations can create many instances of the same class. Or an interface can have many implementations.
Open the services console at http://localhost:4502/system/console/services. Use the filter (service.factoryPid=org.apache.sling.jcr.repoinit.RepositoryInitializer). There are several instances of this service each bound to a different configuration.
Use the filter (&(objectClass=javax.servlet.Servlet)(sling.servlet.paths=/bin/wcm/*)). These are all the Servlet interface implementations configured with a path prefix. See any you like?
What I See Most of the Time
This is typical of the sort of code I run into most of the time
final var context = FrameworkUtil.getBundle(this.getClass())
.getBundleContext();
final var serviceReferences = context.getServiceReferences(MyService.class, null);
for (final var serviceReference : serviceReferences) {
final var service = context.getService(serviceReference);
final var serviceId = serviceReference.getProperty("service.id");
// do something...
log.debug("{} ({})", service.getSomeProperty(), serviceId);
context.ungetService(serviceReference);
}
There is a lot of overhead to get the services. Unit testing may be a challenge. And I'm not sure it's thread-safe. But hey, it works!
Additionally, it will be up to you to sort the services. Usually by the service.ranking property.
The Whiteboard Pattern
OSGi is a specification, Apache Felix is the implementation. And Felix is the basis for AEM. Let's take advantage of the service registry that implements the whiteboard design pattern. All you need to do is create a service that will track the services you need
@Component(service = MyServices.class)
public class MyServices {
@Reference(cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
private volatile List<MyService> myServices;
public void doSomething() {
for (var myService : myServices) {
// do something...
}
}
}
According to the spec, because the policy is DYNAMIC, whenever there is a change to the set of bound services
SCR must replace the field value with a new mutable collection that must contain the updated set of bound services sorted using the same ordering as ServiceReference.compareTo based upon service ranking and service id.
A cardinality of MULTIPLE means {0..1}. This service does not depend on the services it is tracking to be present. It can proceed with activation and sit there until a MyService instance activates. And as a bonus, service ranking works.
Interact With Bind & Unbind
There may be instances in which you want to hook into the bind & unbind methods. Here is another piece of code I see all the time.
@Component(service = MyServices.class)
public class MyServices {
private final List<MyService> myServices = new CopyOnWriteArrayList<>();
@Reference(cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
protected final void bindMyService(final MyService myService) {
this.myServices.add(myService);
}
protected final void unbindMyService(final MyService myService) {
this.myServices.remove(myService);
}
public void doSomething() {
for (var myService : myServices) {
// do something...
}
}
}
CopyOnWriteArrayList is thread-safe and the service binding works. Instead of the collection getting replaced, it gets updated as services come and go. If this satisfies your need then you can stop reading here.
However, the service ranking is lost!
You can add a Map<Object, String> properties parameter to the methods. Then write all the plumbing code to sort based on the service.ranking in that map. Or you can make MyService aware of the service.ranking during its activation. Then implement the Comparable interface.
The RankedServices Class
Luckily there is a data structure for this use case. The RankedServices class.
@Component(service = MyServices.class)
public class MyServices {
private final RankedServices<MyService> myServices = new RankedServices<>(Order.ASCENDING);
@Reference(cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
protected final void bindMyService(final MyService myService, final Map<String, Object> properties) {
this.myServices.bind(myService, properties);
}
protected final void unbindMyService(final MyService myService, final Map<String, Object> properties) {
this.myServices.unbind(myService, properties);
}
public void doSomething() {
for (var myService : myServices) {
// do something...
}
}
}
And like before the collection gets updated as services come and go. Except that it is using the service.ranking and service.id to do the sorting for you based on the spec.
Setting the Service Rank
There are 3 ways to set the service rank. With the @ServiceRanking annotation
@Component
@ServiceRanking(100)
public class MyFirstService implements MyService {
By adding it to the @Component properties
@Component(property = { "service.ranking:Integer=100" })
public class MyFirstService implements MyService {
Or through a config file com.mysite.core.services.MyFirstService.config
service.ranking=I"100"
⚠️ In all three cases, an Integer is used. If you use a sling:OsgiConfig or JSON config file, the service.ranking in the properties map will be Long. The spec calls for Integer. Otherwise, it will default to zero. |
Conclusion
Let the framework do all the work for you. Don't get service references from a bundle context. Let the references come to you. And don't waste time writing complex sorting code. That is already baked in.
Comments