Till the recent past, I was developing OSGi applications (bundles services) using either the blueprint way or the java DSL way.
I came across few articles about the power of DS (Declarative Services), introduced starting OSGi 4.0 spec release. Note that 4.2 specifications make DS more efficient and effective.
I wish to share my experience here that could help someone looking for a direction in this way and I would stick to my way of explaining with a hands on approach.
Prerequisites:
Assuming you are familiar with the following prerequisites before we jump into creating and using DS.
- Understanding OSGi life cycle - Here I mean understanding roles of a container like karaf, a DI framework like the Aries (Refer here)
- Understanding usage of BnD tools - A good pointer or a primer would be here (Courtesy Muruga)
- Brief understanding of what an SCR is (Service Component Runtime)
- Understanding on OCD (Object Class Definitions)
- Understanding on AD (Attribute Definitions)
- Also the OSGi runtime configurations (typical persistence-IDs and the associated command usages)
Note: The BnD DS (Annotations) are a specification implementation defined to help assist OSGi component developers to reduce the pluming code and need not propagate to the deployable code.
The approach and structure of the example I want to explain here is to take a beginner simple complex example of an API with more than one implementation (Providers) for same API interface and a consumer distinguishing between the kind of implementation it wants to invoke.
Approach
We will first build and deploy an API, Provider and Consumer, simply marking them with DS annotations.
API (BnD OSGi project)
package com.dix.osgi.api;
/**
*
* @author Dixie.Sebastianappan
* A simple API Service interface
*/
public interface APIInterface {
String callImplementer(String implementor);
}
Provider (BnD OSGi project)
package com.dix.osgi.impl;
import com.dix.osgi.api.APIInterface;
@Component
public class CarTypeImpl implements APIInterface {
private static final Logger log = Logger.getLogger(CarTypeImpl.class.getName());
public static final String IMPL_NAME = "CarTypeImpl";
@Override
public String callImplementer(String implementorText) {
log.info("Car Implementor");
return "CAR : " + implementorText;
}
}
import com.dix.osgi.api.APIInterface;
@Component
public class CycleTypeImpl implements APIInterface {
private static final Logger log = Logger.getLogger(CycleTypeImpl.class.getName());
public static final String IMPL_NAME = "CycleTypeImpl";
@Override
public String callImplementer(String implementorText) {
log.info("Cycle Implementor");
return "CYCLE : " + implementorText;
}
}
Consumer (BnD OSGi Project)
@Component
public class Consumer {
private static final Logger log = Logger.getLogger(Consumer.class.getName());
APIInterface car;
APIInterface cycle;
@Reference
public void setCar(APIInterface car) {
this.car = car;
log.info(car.callImplementer("CarType"));
}
@Reference
public void setCycle(APIInterface cycle) {
this.cycle = cycle;
log.info(cycle.callImplementer("CycleType"));
}
}
If you try to deploy these as bundles and verify the SCR list, you would see
karaf@root > scr:list ID State Component Name [5 ] [ACTIVE ] CarTypeImpl [6 ] [ACTIVE ] CycleTypeImpl [7 ] [ACTIVE ] com.dix.osgi.consumer.Consumer karaf@root >
Here the states of the components are ACTIVE.
But when you look at the logs, all the consumer APIInterface references point to the CarTypeImpl, since the consumer couldn't identify which instance of the implementer was injected to resolve.
But when you look at the logs, all the consumer APIInterface references point to the CarTypeImpl, since the consumer couldn't identify which instance of the implementer was injected to resolve.
To solve this ambiguity, DS provides a metatype mechanism to resolve the right version and service type binding.
The specification says that, a component can register itself using an unique identifier (component.name) and can host properties that can be used to resolve when a specific version or type of implementation is being looked up for. The specification also dictates policy modes to organize the way a component can register itself.
More information on @Component and @Reference metatype can be found here.
Hence rewriting the Provider classes' code with added meta information inside the @Component annotation would look like this.
@Component(name = CarTypeImpl.IMPL_NAME, configurationPolicy = ConfigurationPolicy.require, immediate = true)
Note: The immediate value is set to true, just for the testing purpose. Usually not adviced, since an instance will be created only when requested for.
After making a similar change to the other implementation (CycleTypeImpl) and a deployment will result with an unsatisfied state.
karaf@root > scr:list ID State Component Name [5 ] [UNSTATISFIED ] CarTypeImpl [6 ] [UNSTATISFIED ] CycleTypeImpl [7 ] [UNSTATISFIED ] com.dix.osgi.consumer.Consumer karaf@root >
The reason is, the component information, indicates that the component should register only when a configuration policy is met. Which means a persistence id with the component name should exists, so that the components could register along with the configuration. In our case, configuration files with names CycleTypeImpl and CarTypeImpl and with extensions .cfg should exist under the etc folder.
Now, there are two ways to do this,
1. Using the config commands
2. Directly creating the .cfg files under etc folder of the container
Once done, the configuration list should yield the following indicating the properties being set and the scr:list should show all the components with an ACTIVE status.
karaf@root > config:list | grep Impl Pid: CycleTypeImpl service.pid = CycleTypeImpl felix.fileinstall.filename = file:/D:/Installed/apache-karaf-2.3.5/etc/CycleTypeImpl.cfg ImplType = Cycle Pid: CarTypeImpl service.pid = CarTypeImpl felix.fileinstall.filename = file:/D:/Installed/apache-karaf-2.3.5/etc/CarTypeImpl.cfg ImplType = Car karaf@root >
The next step is to update the consumer code to resolve the right Implementation instance of the API, by adding more properties to the @Reference annotation.
@Reference(target = "(ImplType=Car)")
public void setCar(APIInterface car) {
this.car = car;
log.info(car.callImplementer("CarType"));
}
@Reference(target = "(ImplType=Cycle)")
public void setCycle(APIInterface cycle) {
this.cycle = cycle;
log.info(cycle.callImplementer("CycleType"));
}
Now building and deploying the applications, will have all the statuses active and working.
To ensure the properties were really being used to resolve the right type of Implementation instances, the Bundle activator methods (@Activate) can be used.
In this case for the CarTypeImpl provider, the following method can be added to verify if the CarTypeImpl is bound only to the CarTypleImpl instance reference.
To ensure the properties were really being used to resolve the right type of Implementation instances, the Bundle activator methods (@Activate) can be used.
In this case for the CarTypeImpl provider, the following method can be added to verify if the CarTypeImpl is bound only to the CarTypleImpl instance reference.
@Activate public void activate(final Mapproperties) { log.info("Car Implementor : Activating Car Implementor"); if (properties.containsKey("ImplType")) { log.info("Car Implementor : " + (String) properties.get("ImplType")); } else { log.info("Car Implementor : Cannot read property ImplType"); } }
~Peace.
Hey Dixie,
ReplyDeleteThat was a beautiful explanation on OSGi along with nice example, it was indeed really helpful to kick start.
And yes, I am looking forward for more :-)
Cheers,
Nikhil