How to improve modularity and extensibility of web-apps using OSGi
Please note that this blog assumes that the reader has basic knowledge of OSGi and how to create bundles. If you are a beginner you may like to read the Getting Started with OSGi.
Sometimes in an existing application you may want to use the capabilities of the OSGi framework. For example, in my recent project we had a already built, live web application. A module of the application was offering some services and user could choose from the available list. Each service in this module was in turn a separate product with its own pricing and license. As part of enhancement we had following requirements
- When a client procures this application (product) he should have the flexibility to procure some or all associated services.
- If the client has ready made implementation of some related services which he wants to incorporate in the application then that integration should be possible with minimum effort.
On deliberation and analysis we decided to use OSGi framework to solve the technical problem in hand.
Reasons to choose OSGi
- On use of OSGi the application would not contain any logic related to any services. Application would only know how to discover the different categories of bundles and make them availble to the user.
- The logic of each service will get encapsulated in bundles. This would make the design very modular, flexible, extensible and clean.
- It would be very easy to remove unwanted services or add desired services without any change in the application.
- For integrating of the client's application we would need to wrap the client's implementation in a bundle and deploy it, that's all. Application code would remain untouched.
Advantages of using OSGi
By using the capabilities of OSGi the application could be moved to next level of flexibility. The application would only know how to interact with the user and the bundles. All the core logic of services would now move to the bundle. Which meant the new requirements had led to a design which would make our application light, easily testable (less to test), easily extensible and maintainable (an issue in a service would mean check the respective bundle and not the whole application).
Get your hands dirty
Since the background is set in support of using OSGi as part of web application, its time to make our hands dirty and learn how we can go about doing it. Let us take a simple problem statement and try to solve it. Suppose xyz comany has 'n' number of stores. xyz company is IT equipped and they have a website where people can come and see the available stores. They can also see the list of items available at store along with their prices etc. Due to popularity of the website the other store owners started contacting xyz company to enlist their store in the same website. To solve the issue techies decided to use OSGi. It was decided that every store will be an OSGi bundle.
1. Identify the common interface/service declaration
The first task is to create a set of common interface and classes which would facilitate communication between web application and OSGi bundles.
a. Interface Store.java
package com.osgi.learn.ext;
import java.util.List;
import com.osgi.learn.ext.entities.Item;
public interface Store {
public String getStoreName();
public List<Item> getItems();
}
b. Class Item.java
package com.osgi.learn.ext.entities;
public class Item {
private String name;
private float price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
}
This glue code can be packaged as a Jar and injected as dependency to both application and implementing bundles. The bundles would provide the implementation for Store.java and export the implementation as service.
To download the Jar file for the common interface click here.
Common communication interface is in place. The bundles can also be created with little effort (to download the bundle jar click here). The next task is to enable communication between web application and bundles. To do that we need to do the following:
2. Embed OSGi container
Embed OSGi container in the web-application - Apache Felix in our case. To programmatically start a Felix container we need to do the following (to see the complete code download the HostApplication.java and StoreServiceTracker.java).
try {
felix = new Felix(getConfigMap());
felix.start();
logger.info("OSGI container started");
} catch (BundleException ex) {
logger.error("Could not create framework: ", ex);
throw new RuntimeException(ex);
}
In the above code one thing that needs special attention is 'getConfigMap()'. getConfigMap() will return an Object of java.util.Map which contains special configuration info which we want to pass to the container during start up. The configMap argument in the Felix container can be passed as null. The framework is supposed to have intelligent defaults which it will use to get started. But in case you want to override the defaults then you can do something like this:
public Map getConfigMap(){
// Create a configuration property map.
Map configMap = new HashMap();
// Export the host provided service interface package.
configMap.put(FelixConstants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, - 1
"com.learn.osgi.ext; version=1.0.0");
//specify the file system path to a directory which will be used by framework. One of the many uses is to store the cached bundle here.
configMap.put(FelixConstants.FRAMEWORK_STORAGE, cacheDirectory); - 2
return configMap;
}
-1 FelixConstants.FRAMEWORK_SYSTEMPACKAGES_EXTRA - This attribute tells OSGi framework to add the specified package as part of system package so that it gets available to all bundles by default. It also enable communication between application and the bundles. The other option could be to also deploy the jar with the package com.learn.osgi.ext as bundle.
-2 FelixConstants.FRAMEWORK_STORAGE - This attribute tells the application that the location in file system specified (cacheDirectory) can be used by Framework for all purposes like storing cached bundles etc.
3. Create service trackers
There may be number of bundles deployed in the container, some of them may be system bundles and others may be supporting bundles. To identify the Store bundles we will have to create a service tracker
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import com.osgi.learn.ext.Store;
public class StoreServiceTracker extends ServiceTracker {
/**
* Constructor
* @param bundleContext
*/
public StoreServiceTracker(BundleContext bundleContext) {
super(bundleContext, Store.class.getName(), null);
}
/**
* Opens this tracker
*/
@Override
public void open() {
super.open();
}
}
Just after the start up of felix we can execute the following code to instantiate the tracker
storeServiceTracker = new storeServiceTracker(felix.getBundleContext()); storeServiceTracker.open();
Now to get all bundles that are registered as Store bundles just call the following method
public Set<Bundle> getStoreBundles() {
ServiceReference[] refs = getTrackedStoreServiceReferences();
if (refs == null) {
return Collections.emptySet();
}
Set<Bundle> bundles = new HashSet<Bundle>();
for (ServiceReference ref : refs) {
bundles.add(ref.getBundle());
}
return bundles;
}
public ServiceReference[] getTrackedStoreServiceReferences() {
return storeServiceTracker.getServiceReferences();
}
4. Interact with the bundle service
The last step is to interact with the bundle
Store service = bundle.getService(Store.class); service.getStoreName(); service.getItems();
This solves the complete cycle.
Filed under: Java
