Starting with OSGI with Maven and Spring, and understanding OSGI dependency problems
In this article, we first cover the Open Services Gateway Initiative(OSGI). Then we setup an OSGI project with Maven. We make sure Maven adds the correct manifest when building a bundle and we automatize testing our bundles inside an OSGI environment. After that we look into understanding OSGI dependency problems.
OSGI
The Open Services Gateway Initiative (OSGI) makes it possible to break your application into multiple modules which can be independently deployed from each other, making it easier to manage cross-dependencies.
The different modules (coming in the form of bundles/jars for deployment) can be remotely installed, started, stopped, updated, and uninstalled without requiring a reboot.
OSGI Bundles
Each bundle is a tightly-coupled, dynamically loadable collection of classes, jars and configuration files that explicitly declares its external dependencies (if any).
Each bundle contains a detailed manifest MANIFEST.MF file, which includes different OSGI tags, and which is what physically makes an OSGI bundle different from a non-OSGI Java jar.
An example of such a manifest file:
Bundle-Name: Integrating Stuff OSGI test Bundle-SymbolicName: com.integratingstuff.osgi Bundle-Description: A Test OSGI bundle Bundle-ManifestVersion: 2 Bundle-Version: 1.0.0 Bundle-Activator: com.integratingstuff.osgi.BundleActivator Export-Package: com.integratingstuff.osgi;version="1.0.0" Import-Package: org.osgi.framework;version="1.3.0"
Hence, building an OSGI jar instead of a regular jar comes down to setting the values in this manifest file correctly.
The first 5 tags in the example are obvious.
The 3 last ones are the more interesting ones:
Bundle-Activator: Indicates the class name to be invoked once a bundle is activated.
Export-Package: Expresses what Java packages contained in a bundle will be made available to the outside world.
Import-Package: Indicates what Java packages will be required from the outside world, in order to fulfill the dependencies needed in a bundle.
Activator
The Bundle-Activator BundleActivator class, declared in the manifest file, will be invoked once a bundle is activated. A typical OSGI jar, which contains services that should be registered with the OSGI container, contains such a Bundle-Activator declaration.
However, the declaration of a Bundle-Activator is optional. Some jars will only export certain packages and will not need a bundle activator/not register any services. The OSGI jar for the Spring framework, for example, does not require a Bundle Activator. All it does is exporting packages for other OSGI bundles to use.
An example of a typical BundleActivator:
public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { System.out.println("Starting: Integrating OSGI stuff"); context.registerService(AccountService.class.getName(), new AccountServiceImpl(), null); } public void stop(BundleContext context) throws Exception { System.out.println("Stopping with integrating OSGI stuff"); ServiceReference serviceReference = context. getServiceReference(AccountService.class.getName()); if (serviceReference != null){ context.ungetService(serviceReference); }; } }
For brevity, we do not include the AccountService class here. To compile this, you will also need the osgi-core lib on your classpath. If you want to experiment with the code, please download the Maven project made available in the last section of this article.
OSGI Lifecycle
The OSGI life cycle layer introduces dynamics that are normally not part of an application.
Every bundle within the OSGI container can be in one of the following states thanks to this mechanism:
Installed: The bundle has been successfully installed.
Resolved: All Java classes that the bundle needs are available. This state indicates that the bundle is either ready to be started or has stopped.
Starting: The bundle is being started, the BundleActivator.start method will be called, and this method has not yet returned. When the bundle has an activation policy, the bundle will remain in the Starting state until the bundle is activated according to its activation policy..
Active: The bundle has been successfully activated and is running; its Bundle Activator start method has been called and returned.
Stopping: The bundle is being stopped. The BundleActivator.stop method has been called but the stop method has not yet returned.
Uninstalled: The bundle has been uninstalled. It cannot move into another state.
OSGI containers
There are 3 popular OSGI container implementations: Knopflerfish, Equinox, and Apache Felix. Concierge seems to be receiving a lot of adaptation on mobile and embedded devices too, due to its limited file footprint of about 80 kBytes.
Adding the correct manifest to your bundle with Maven
To make sure Maven adds the correct manifest to our bundle, we use the org.apache.felix maven-bundle-plugin. This plugin generates the correct manifest for us, based on the configuration we pass, configured through Maven properties.
We also use the maven-dependency-plugin to be able to embed libraries and we configure the maven-jar-plugin to use the generated manifest file.
This is how our pom.xml looks like:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.integratingstuff</groupId> <artifactId>com.integratingstuff.osgi</artifactId> <version>1.0.0</version> <name>com.integratingstuff.osgi</name> <build> <plugins> <!-- this is needed to enable reference in the Manifest file to Embedded libraries --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>compile</phase> <goals> <goal>copy-dependencies</goal> </goals> </execution> </executions> </plugin> <plugin> <artifactId>maven-jar-plugin</artifactId> <version>2.3.2</version> <configuration> <archive> <manifestFile>${manifestFile}</manifestFile> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>2.3.6</version> <extensions>true</extensions> <configuration> <manifestLocation>${manifestFolder}</manifestLocation> <excludeDependencies>${maven.bundleplugin.exclude.dependencies}</excludeDependencies> <instructions> <Bundle-SymbolicName>${project.build.bundle.symbolicname}</Bundle-SymbolicName> <Bundle-Name>${project.build.bundle.name}</Bundle-Name> <Bundle-Version>${project.build.bundle.version}</Bundle-Version> <Bundle-Activator>${project.build.bundle.activator}</Bundle-Activator> <Import-Package>${project.build.import.package}</Import-Package> <Export-Package>${project.build.export.package}</Export-Package> <Private-Package>${project.build.private.package}</Private-Package> <Web-ContextPath>${project.build.web.contextpath}</Web-ContextPath> <Webapp-Context>${project.build.web.contextpath}</Webapp-Context> <Bundle-ClassPath>${project.build.bundle.classpath}</Bundle-ClassPath> <Embed-Dependency>${project.build.embed.dependency}</Embed-Dependency> <Embed-Directory>${project.build.embed.directory}</Embed-Directory> <Embed-Transitive>${project.build.embed.transitive}</Embed-Transitive> </instructions> <supportedProjectTypes> <supportedProjectType>jar</supportedProjectType> <supportedProjectType>bundle</supportedProjectType> <supportedProjectType>war</supportedProjectType> </supportedProjectTypes> </configuration> <executions> <execution> <id>bundle-manifest</id> <phase>process-classes</phase> <goals> <goal>manifest</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <properties> <project.build.bundle.symbolicname>${project.artifactId}</project.build.bundle.symbolicname> <project.build.bundle.name>${project.name}</project.build.bundle.name> <project.build.bundle.version>${project.version}</project.build.bundle.version> <project.build.bundle.activator></project.build.bundle.activator> <project.build.import.package>*</project.build.import.package> <project.build.export.package>*</project.build.export.package> <project.build.private.package></project.build.private.package> <project.build.bundle.classpath></project.build.bundle.classpath> <project.build.web.contextpath></project.build.web.contextpath> <project.build.embed.dependency></project.build.embed.dependency> <project.build.embed.directory></project.build.embed.directory> <project.build.embed.transitive></project.build.embed.transitive> <manifestFolder>src/main/resources/META-INF</manifestFolder> <manifestFile>${manifestFolder}/MANIFEST.MF</manifestFile> </properties> </project>
Testing OSGI bundles with Spring
When building OSGI bundles, it is probably a good idea to check whether the built bundles are valid. We are going to check this by writing unit tests that extend from AbstractConfigurableBundleCreatorTests, from the spring-osgi-mock jar. This test will start an OSGI container and run the test in it. Tests like these can catch OSGI dependency problems automatically, requiring no manual deploy, allowing easy OSGI integration testing.
To start, add the following dependencies section to the Maven pom.xml:
<dependencies> <!-- test dependencies --> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>spring-osgi-core</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>spring-osgi-mock</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>spring-osgi-test</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>spring-osgi-annotation</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>spring-osgi-extender</artifactId> <version>1.2.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.osgi.core</artifactId> <version>1.4.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.framework</artifactId> <version>1.7.0-private</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.main</artifactId> <version>1.7.0-private</version> <scope>test</scope> </dependency> <!-- from http://dev.anyframejava.org/maven/repo/org/springframework/osgi/log4j.osgi/1.2.15-SNAPSHOT/ --> <dependency> <groupId>org.springframework.osgi</groupId> <artifactId>log4j.osgi</artifactId> <version>1.2.15-SNAPSHOT</version> <scope>test</scope> </dependency> </dependencies>
This will add the spring osgi dependencies we need.
This will also add all the dependencies needed to run the Felix OSGI container, which will be the OSGI container we will run our tests in.
Note: You might need to download and install the log4j.osgi manually from http://dev.anyframejava.org/maven/repo/org/springframework/osgi/log4j.osgi/1.2.15-SNAPSHOT into your own nexus or local maven repo.
Then add the following test:
public class LoadOSGITestCase extends AbstractConfigurableBundleCreatorTests{ @Override protected String[] getTestBundlesNames() { return new String[] { "com.integratingstuff, com.integratingstuff.osgi, 1.0.0" }; } public void testOSGIPlatformStarts() throws Exception { System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VENDOR)); System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VERSION)); System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT)); } public void testOSGIEnvironment() throws Exception { Bundle[] bundles = bundleContext.getBundles(); for (int i = 0; i < bundles.length; i++) { System.out.print(OsgiStringUtils.nullSafeName(bundles[i])); System.out.print(", "); } } public void testRetrieveService() throws Exception { Properties props = new Properties(); props.put("Language", "English"); ServiceTracker accountServiceTracker = new ServiceTracker(bundleContext, AccountService.class.getName(), null); accountServiceTracker.open(); AccountService accountService = (AccountService) accountServiceTracker.getService(); accountService.createAccount(1000l); } @Override protected String getPlatformName() { return Platforms.FELIX; } }
And run it.
These tests will run within the Felix OSGI container. The two first tests just write out some info about the OSGI environment itself. The last test fetches a reference to this service and then calls a method on it.
Troubleshooting OSGI dependency problems
When starting with OSGI, you need to get used to not having a global classpath, but instead, take into account which packages a bundle exports or imports.
You might run into problems because of this. In what follows, we cover some common import/export package mistakes. Different OSGI containers can have different implementations, so the error messages you see because of these mistakes might differ a bit. One OSGI container might fail to resolve a bundle because of a conflict, while another one might just load one of two conflicting bundles and report a ClassNotFoundException when a class residing in a discarded or unknown bundle is used. In certain containers, it is possible to get away with making one of the mistakes covered here, in certain circumstances. Still, they are errors against the specification and should always be avoided.
If you want to experiment with these errors to better understand them, please download this OSGI test project. It is in a correct starting state, but in what follows, we will indicate how to produce every covered error using this project.
Any of these errors will cause the unit test in the com.integratingstuff.osgi.test module to fail.
Failing to export a package needed by another bundle
This error can be produced by changing the project.build.export.package Maven property in the pom.xml of the com.integratingstuff.osgi.services1 module to “com.test.model” instead of “*”. Then com.test.service of the com.integratingstuff.osgi.services1 module will not be exported then(not declared in the Export-Package section of the manifest), and the AccountService class will not be available to other modules that need it, such as the com.integratingstuff.osgi.services2 module.
Failing to import a required package
This error can be produced by changing the project.build.import.package Maven property in the pom.xml of the com.integratingstuff.osgi.services2 module to “com.test.model” instead of “*”. Then com.test.service of the com.integratingstuff.osgi.services1 module will not be imported then(not declared in the Import-Package section of the manifest), and the AccountService class will be unknown to the com.integratingstuff.osgi.services2 module.
Exporting conflicting packages
This error can be produced by renaming the com.test.person.service package in the com.integratingstuff.osgi.services2 module to com.test.service, so it conflicts with the com.test.service package of the com.integratingstuff.osgi.services1 module.
Depending on the container, bundles importing the conflicting package will not resolve(“package uses conflict” errors) or only one of the two conflicting packages will be known by the container, resulting in ClassNotFoundExceptions.
In an OSGI environment, two bundles should not export a package with the same name.
Well, nice tips and tricks, but it would be even better if you would supply a working example.
Was this answer helpful?
LikeDislikedisregard my last comment,I did not read properly the article (you provided the source code). The article is really good then.
Was this answer helpful?
LikeDislike