OSGI – Modularizing your application
I believe that this technology is a breakthrough in how we create applications using the Java platform. With OSGi, it is very simple to create highly extensible applications, see for example the Eclipse IDE. My goal here is not to show in depth how the technology works, but to demonstrate a small example of some of its advantages. The sample consists of a system for sending messages. The user types a message into a TextField and this message can be sent in several ways, such as Email or SMS. However, in this example, we have four modules. The graphical user interface, domain, sender of email messages and the sender via SMS.
Following the nomenclature of OSGi, each module is a Bundle. A Bundle is nothing more than a “jar” with some additional information from MANIFEST.MF. This information is used by the OSGi framework. Like almost everything in Java, OSGi technology is a specification and therefore have different implementations to choose from. Among them are the most famous Equinox (Eclipse Project), Felix (Apache) and Knopflerfish. In this article we will use the Equinox.
Download the Equinox. For this article we only need the jar. Run the jar to access the console of the Equinox.
C:\osgi>java -jar org.eclipse.osgi_3.5.1.R35x_v20090827.jar –console
To view Bundles installed, simply type the command ss.
C:\osgi>java -jar org.eclipse.osgi_3.5.1.R35x_v20090827.jar – console osgi> ss
Framework is launched.
id State Bundle< 0 ACTIVE org.eclipse.osgi_3.5.1.R35x_v20090827 osgi> _
As we can see at this point we only have one bundle installed. The bundle of Equinox.
Now we’ll create our bundle and add it to Equinox. Create a bundle is very simple.
Create a simple project with the following class:
package br.com.luiscm.helloworld; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { System.out.println("Hello World!"); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { System.out.println("Good Bye World!"); } }
This class is the Activator of our bundle. The Activator is used by the OSGi framework to start or stop a bundle. In this first example, the Activator will only print messages when started and stopped. Now we need to modify the MANIFEST of the jar to make it an OSGi bundle.
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: LuisCM Plug-in Bundle-SymbolicName: br.com.luiscm.helloworld Bundle-Version: 1.0.0 Bundle-Activator: br.com.luiscm.helloworld.Activator Bundle-ActivationPolicy: lazy Bundle-RequiredExcutionEnvironment: JavaSE-1.6 Import-Package: org.osgi.framework;version=”1.3.0?
See the MANIFEST passed to the OSGi bundle some of our information. Among them the name of the bundle (SymbolicName) and the Activator class. Now let’s install this bundle in Equinox. Generate a jar of the project and to install it in Equinox is simple:
install file:.jar osgi> install file:bundle.jar Bundle id is 1 osgi>
To verify that the bundle was properly installed, simply run the command ss:
osgi> ss Framework is launched. id State Bundle 0 ACTIVE org.eclipse.osgi_3.5.1.R35x_v20090827 1 INSTALLED br.com.luiscm.helloworld_1.0.0 osgi> _
The bundle is properly installed, you just start it now:
start osgi> start 1 Hello World! osgi>
To stop the bundle:
osgi> stop 1 Goodbye World! osgi>
Now that we know how to create a bundle, let’s start our example. In the example, we have four bundles.
* Domain: As the name says, it stores the domain classes in our example. We will have two classes: Message and IMessageSender.
* SenderSMS: implementation of IMessageSender that sends messages via SMS.
* SenderEmail: implementation of IMessageSender that sends messages by email.
* UI: GUI example
Bundle UI
We’ll start with the UI bundle. The activator will just build the frame for the user to enter the message.
package br.com.luiscm.helloworld; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JTextField; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import br.com.luiscm.helloworld.core.service.Message; public class Activator implements BundleActivator { private Message message; private JFrame frame; /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { buildInterface(); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { destroyInterface(); } private void destroyInterface() { frame.setVisible(false); frame.dispose(); } private void buildInterface() { frame = new JFrame("Hello"); frame.setSize(200, 80); frame.getContentPane().setLayout(new BorderLayout()); final JTextField textField = new JTextField(); final JButton button = new JButton("Send"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { message.send(textField.getText()); } }); frame.getContentPane().add(textField, BorderLayout.NORTH); frame.getContentPane().add(button, BorderLayout.SOUTH); frame.setVisible(true); } }
Note that the bundle depends on a class called Message. This class is our domain, so it is not part of this bundle. Here comes another detail of OSGi. The communication is done through bundles of services. We can consider this model as an SOA within the VM. The services bundle UI will use the bundle core. Let’s look at the MANIFEST bundle UI.
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: UI Plug-in Bundle-SymbolicName: br.com.luiscm.helloworld.ui< Bundle-Version: 1.0.0 Bundle-Activator: br.com.luiscm.helloworld.ui.Activator Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: br.com.luiscm.helloworld.core.service, javax.swing, org.osgi.framework;version=”1.3.0?, org.osgi.util.tracker;version=”1.3.6?
See the Import-Package statement. We are importing a package bundle core. In this package are the services that our field is providing. Also import the javax.swing package.
Now we need to create the service.
Bundle Core
The Core Bundle has two domain classes. The interface of the senders and the Message field.
Interface:
package br.com.luiscm.helloworld.core.service; public interface IMessageSender { void send(String message); }
Domain:
package br.com.luiscm.helloworld.core.service; import java.util.ArrayList; import java.util.List; public class Message { private final List services = new ArrayList(); public void addService(final IMessageSender messageSender) { services.add(messageSender); } public void removeService(final IMessageSender messageSender) { services.remove(messageSender); } public void send(final String message) { for (final IMessageSender messageSender : services) { messageSender.send(message); } } }
See the Message class is comprised of a list of services. These services are the senders of messages to be used. Note that the send method only interact on the mailing list message. So far everything is very simple. Now we need to export the Message class as a core service bundle. The UI module will interact directly with this service to send messages.
First we need to tell it to export OSGi bundle to other bundles. See the MANIFEST:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Helloworld Plugin Bundle-SymbolicName: br.com.luiscm.helloworld.core Bundle-Version: 1.0.0 Bundle-Activator: br.com.luiscm.helloworld.core.Activator Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: org.osgi.framework;version=”1.3.0?, org.osgi.util.tracker;version=”1.3.6? Export-Package: br.com.luiscm.helloworld.core.service
See information Export-Package. For a class to be visible for another bundle, it must be exported within a package. In our case, the UI needs bundle of the Message class, so we need to export the package where the class is. Remember that the UI imported the bundle package.
Message To register the component as a service, we need to directly interact with the OSGi API. When the core bundle is started, we will register the service in the context of OSGi. The code is simple:
package br.com.luiscm.helloworld.core; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { context.registerService(Message.class.getName(), messageService, null); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { messageService = null; } }
The method registerService expects parameter as the service name (for recommendation is the class name), the service itself and some additional settings.
Now we need to change the UI to use the bundle Message service. In the bundle activator UI, just do the lookup service using your name (class name):
private Message message; private JFrame frame; private ServiceTracker serviceTracker; /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { serviceTracker = new ServiceTracker(context, Message.class.getName(), null); serviceTracker.open(); message = (Message)serviceTracker.getService(); buildInterface(); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { destroyInterface(); serviceTracker.close(); } }
If we add the two bundles in the Equinox, we see that the two bundles are communicating. Now we need to create bundles that actually send the messages.
Bundle Sender Email and SMS
Shipping services via email and SMS will be new services from our system. Therefore, we create a bundle for each. This way we can control them separately. For example, we can stop the service by sending SMS and leave only the Email without affecting system operation. The two bundles have virtually the same structure, so I’ll save some lines here.
The sender will have only one bundle class that implements the interface and class IMessageSender Activator. This interface is at the core bundle, so we need to import the package in the same way we did in the bundle UI.
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-name: SMS Plug-in Bundle-SymbolicName: br.com.luiscm.helloworld.sms.Activator< Bundle-Version: 1.0.0 Bundle-Activator: br.com.luiscm.helloworld.sms.Activator Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: br.com.luiscm.helloworld.core.service, org.osgi.framework;version=”1.3.0?
The only class Sender implements our interface:
package br.com.luiscm.helloworld.sms; import br.com.luiscm.helloworld.core.service.IMessageSender; public class MessageSenderSMS implements IMessageSender { @Override public void send(final String message) { System.out.println("Sending by SMS : " + message); } }
Send by SMS is a service of our system. Therefore, we must register it in the OSGi context:
public class Activator implements BundleActivator { private IMessageSender service; /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { service = new MessageSenderSMS(); context.registerService(IMessageSender.class.getName(), service, null); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { service = null; } }
The bundle of mail is virtually the same code. The only difference is the message on System.out.
Note that registered the service with the name of the interface. So now we have two services with the same name. Whenever we ask for the service context with the interface name, he will perform a logical priority to return only one implementation.
Now we have two services for sending messages, we need to change our bundle core to use them. To achieve this objective, a ServiceTrackerCustomizer.
ServiceTrackerCustomizer e ServiceTracker
As we saw, we used to do Servicetrack lookup service. However, in the case of senders, we need to know when a new sender service is available or when a sender is removed. This information is important to feed the list of services within the Message object.
To access this information, we use a ServiceTrackerCustomizer. The code is simple:
package br.com.luiscm.helloworld.core; import org.osgi.framework.BundleContext; public class MessageSenderServiceTracker implements ServiceTrackerCustomizer { private final BundleContext context; private final Message message; public MessageSenderServiceTracker(final BundleContext context, final Message message) { this.context = context; this.message = message; } @Override public Object addingService(final ServiceReference serviceReference) { final IMessageSender sender = (IMessageSender)context.getService(serviceReference); message.addService(sender); System.out.println("tracker : " + sender.getClass().getName()); return sender; } @Override public void removedService(final ServiceReference serviceReference, Object service) { final IMessageSender sender = (IMessageSender)context.getService(serviceReference); message.removeService(sender); } }
Just implement the interface and code ServiceTrackerCustomizer what you want when a service is added, modified or removed. Simple!
In our case we will add or remove the service from the list of services on our Message object. Also has a message of “log”to help us with testing.
Now we need to make one more minor change in the bundle core activator. We must register our ServiceTrackerCustomizer as a listener for services such IMessageSender.
public class Activator implements BundleActivator { public Message messageService = new Message(); private ServiceTracker messageSenderServiceTracker; /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { context.registerService(Message.class.getName(), messageService, null); final MessageSenderServiceTracker serviceTracker = new MessageSenderServiceTracker(context, messageService); messageSenderServiceTracker = new ServiceTracker(context, IMessageSender.class.getName(), serviceTracker); messageSenderServiceTracker.open(); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { messageSenderServiceTracker.close(); messageService = null; } }
We use ServiceTrackerCustomizer along with ServiceTtracker. Where a service is added, modified or removed, our component will be called.
Testing the application
Now that we code, we test the application.
Create four jars:
* bundleCore.jar
* bundleUI.jar
* bundleSenderEmail.jar
* bundleSenderSMS.jar
Install the four bundles in the Equinox:
Framework is launched.
id State Bundle 0 ACTIVE org.eclipse.osgi_3.5.1.R35x_20090827.jar osgi> install file:bundleCore.jar Bundle id is 5 osgi> install file:bundleUI.jar Bundle id is 6 osgi> install file:bundleSenderEmail.jar Bundle id is 7 osgi> install file:bundleSenderSMS.jar Bundle id is 8 osgi> ss
Framework is launched.
0 ACTIVE org.eclipse.osgi._3.5.1.R35x_v20090827.jar 5 INSTALLED br.com.luiscm.helloworld.core_1.0.0 6 INSTALLED br.com.luiscm.helloworld.ui_1.0.0 7 INSTALLED br.com.luiscm.helloworld.email_1.0.0 8 INSTALLED br.com.luiscm.helloworld.sms_1.0.0
Start the bundles and test the application.
C:\osgi>java -jar org.eclipse.osgi._3.5.1.R35x_v20090827.jar -console osgi> tracker: br.com.luiscm.heloworld.sms.SenderSMS tracker: br.com.luiscm.helloworld.email.SenderEmail
See which messages are sent by email and SMS. In console Equinox, pause the service email:
stop
Try again send a message. Because the service is no longer available, the message was sent only by SMS.
Stopping power application modules without suffering side effects is sensational. Imagine that you discover a critical error in the SMS module. You need not take all the air is applied to correct this problem. Just pause the SMS module. The rest of the system will continue normal operation. Take the test with this small example. Pause and Start services. This will not affect the core, much less the UI.
I managed to explain a little of what is OSGi. It is noteworthy that has much more detail on control and classpath configuration bundles that do not pay attention here. It is the task for those interested take a look at other features.
It’s worth looking at the Spring-DM project. The spring makes it very easy to set up services in addition to providing an excellent IoC container.
Reference: OSGI – Modularizing your application from our JCG partner Luis Carlos Moreira da Costa at the Eclipse Brazil blog.
Luis,
Thanks for publishing this article!
Your use of multiple packages that mirror a real world application (UI, Domain, multiple services) really helped me see the right patterns!
Hi Tim
Thanks for comments!
Going by this example, what changes would need to be made, if you needed to have 2 instances of the SMS sender service?