Another part of the story here is that it can be useful to add aspects to an implementation. This can also be done using the branch by abstraction pattern and in OSGi you can add these aspects to existing services without the need to modify them, and also without the need for bytecode manipulation. In OSGi you can proxy services by hiding the original and placing a proxy service in the service registry that consumers will use instead. This makes it possible to add a chain of aspects to the service invocations without the need to either change the clients nor the services themselves.
To illustrate, I started a little project called service-spector that allows you to add service aspects to any kind of service. The aspects themselves are also implemented as services. Here's what a ServiceAspect looks like:
public interface ServiceAspect {
String announce();
void preServiceInvoke(ServiceReference service, Method method, Object[] args);
void postServiceInvoke(ServiceReference service, Method method, Object[] args, Object result);
String report();
}
Aspects are called before and after a service invocation is done via preServiceInvoke()/postServiceInvoke(). They are called with a reference to the service being invoked and with the parameters that are provided to the service. The post call is also provided with the result of the invocation. The aspect API allows you to do things like obtaining metrics for services, add logging to existing service calls or anything else really that an aspect could do. Additionally it contains annouce() and report() to present information to the user.
The service-spector is configured using Configuration Admin with a list of filters for services that need have the aspects applied in the service.filters property. There is an additional configuration item that states whether the original services need to be hidden. The proxies are automatically registered with a higher service ranking than the original service. If the consumer of the original only uses a single service the original does not need to be hidden as the one with the higher ranking will be selected. However, if a consumer uses all the services of a certain type you do want to hide the originals. In general it's safer to hide the original service, but if you want to run without the HidingHooks you can switch this off.
I am using the Felix File Install to provide the configuration and have it stored in a file in load/org.coderthoughts.service.spector.impl.ServiceSpector.config A little known feature of File Install is that is supports typed configuration data in .config files, so the following file contains a Boolean hide.services property and a String[] service.filters property.
hide.services=B"true"
service.filters=["(objectClass\=org.coderthoughts.primes.service.PrimeNumberService)",
"(objectClass\=org.apache.felix.bundlerepository.RepositoryAdmin)"]
In the ServiceSpector main component I can easily consume this configuration using the new Declarative Services annotation based approach:
@Component(immediate=true)
public class ServiceSpector implements ServiceListener {
// This API is populated by Config Admin with configuration information
@interface Config {
String [] service_filters();
boolean hide_services() default false;
}
...
@Activate
private synchronized void activate(BundleContext bc, Config cfg) {
...
System.out.println("Service filters: " +
Arrays.toString(cfg.service_filters()));
System.out.println("Hide services: " + cfg.hide_services());
}
The Config annotation is automatically populated with the configuration information by matching the property keys with the annotation methods, which I can easily read out using its typed API (the dots in configuration keys are mangled to underscores).
Service-spector has a Service Listener that looks for services appearing and checks if they need to have aspects applied. If so they will be proxied. Then, when the proxied service is invoked, all the ServiceAspect services are called to let them do their thing. For example the CountingServiceAspect simply counts how often a certain API is invoked:
public class CountingServiceAspect implements ServiceAspect {
Map<String, LongAdder> invocationCounts = new ConcurrentHashMap<>();
@Override
public String announce() {
return "Invocation counting started.\n";
}
@Override
public void preServiceInvoke(ServiceReference service, Method method, Object[] args) {
Class declaringClass = method.getDeclaringClass();
String key = declaringClass.getSimpleName() + "#" + method.getName();
invocationCounts.computeIfAbsent(key, k -> new LongAdder()).increment();
}
@Override
public void postServiceInvoke(ServiceReference service, Method method, Object[] args, Object result){
// nothing to do
}
@Override
public String report() {
StringBuilder sb = new StringBuilder();
sb.append("Invocation counts\n");
sb.append("=================\n");
for (Map.Entry<String, LongAdder> entry : invocationCounts.entrySet()) {
sb.append(entry.getKey() + ": " + entry.getValue() + "\n");
}
return sb.toString();
}
}
g! lb
START LEVEL 1
ID|State |Level|Name
0|Active | 0|System Bundle (5.4.0)|5.4.0
1|Active | 1|Apache Felix Bundle Repository (2.0.6)|2.0.6
2|Active | 1|Apache Felix Gogo Command (0.16.0)|0.16.0
3|Active | 1|Apache Felix Gogo Runtime (0.16.2)|0.16.2
4|Active | 1|Apache Felix Gogo Shell (0.10.0)|0.10.0
5|Active | 1|Apache Felix Declarative Services (2.0.2)|2.0.2
6|Active | 1|service (1.0.0.SNAPSHOT)|1.0.0.SNAPSHOT
7|Active | 1|impl (1.0.1.SNAPSHOT)|1.0.1.SNAPSHOT
8|Installed | 1|client (1.0.0.SNAPSHOT)|1.0.0.SNAPSHOT
10|Active | 1|Apache Felix Configuration Admin Service (1.8.8)|1.8.8
11|Active | 1|Apache Felix File Install (3.5.2)|3.5.2
12|Active | 1|service-spector (1.0.0.SNAPSHOT)|1.0.0.SNAPSHOT
g! start 12
Invocation counting started.
Service Spector
===============
Service filters: [(objectClass=org.coderthoughts.primes.service.PrimeNumberService), (objectClass=org.apache.felix.bundlerepository.RepositoryAdmin)]
Hide services: true
g! start 8
Services now: $Proxy6(PrimeNumbers)
First 10 primes: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
First 10 primes: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
First 10 primes: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
g! stop 8
As our filters state that we are also proxying the Repository Admin which comes with the Felix implementation we can also exercise that, for example via the shell commands:
... some output ...
g! obr:list
... some output
Invocation counts
=================
PrimeNumberService#getServiceName: 1
PrimeNumberService#nextPrime: 30
RepositoryAdmin#listRepositories: 1
RepositoryAdmin#discoverResources: 2
Contributing more aspects
Aspects are contributed as services themselves so they don't need to be part of the service-spector bundle itself. The service-spector-method-timer bundle is a separate bundle that contributes a ServiceAspect that provides average method invocations times.
Get the binaries
method-timer: https://github.com/coderthoughts/service-spector-method-timer/releases