Invoke Ivan continues with his OSGi best practices in the second half of series 4 in the OSGi Demystified series. In this post, we learn best practices from converting to OSGi bundle, to separating API from implementation. Check out the first half of series 4 here.
- Convert to OSGi bundle? – or use Bundle-ClassPath?
- Avoid Require-Bundle
- Don’t split your packages
- Know your Execution Environment
- Avoid circular/cyclic dependencies
- Separate API from implementation
Convert to OSGi bundle – or use Bundle-ClassPath?
Migrating to OSGi isn’t always straightforward and there are design/practicality trade-offs. For example, should you place existing JARs on an OSGi bundle-classpath and wrap them inside the OSGi bundle? or should you convert those JARs to OSGi bundles in their own right?
As explained earlier in this blog series, each OSGi bundle has it’s own classloader. By using the
Bundle-ClassPath header in the MANIFEST.MF of an OSGi bundle you can add other JARs and classes to that bundle’s class path. The JARs and classes become part of the bundle. One potential drawback of this approach is that those JARs lose version independence and granularity and they are no longer eligible to be shared libraries. However, there are situations where this approach is the prudent choice. Ultimately it is a design choice. Our rule of thumb is this – for lightweight libraries with few dependencies, where the library is unlikely to be used by other OSGi bundles in the system, Bundle-ClassPath is fine. You can even chose to export its packages through your own OSGi bundle should you desire. Where a library is large, has other dependencies, many consumers, or might be versioned within a system, installing it as an OSGi bundle in its own right is the way to go. This can reduce the overall footprint of the system, promote reuse, and reduce both complexity and duplication.
Using the OSGi bundle MANIFEST.MF header Require-Bundle is to be avoided where possible, it ties you to a specific bundle implementation. You also lose package granularity because you are forced to consume other packages from the same “required” bundle. If those packages are not your preferred versions of an implementation then very quickly your system can become constrained and inflexible. A more fine-grained approach with Import-Package is recommended because it allows you to pick and choose each package/version individually.
Require-Bundle: com.ibm.multiple.packages Import-Package: com.ibm.specific.package;version="[1.0.0, 2.0.0)"
Don’t split your packages
A split package is the situation where classes from the same package are provided by more than one JAR. In the normal Java world, where you are dealing with a single hierarchical class loader there is no issue. Even when accessing package-private classes everything works quite merrily. However, if you bring this concept to the OSGi world, you are begging for trouble. In OSGi, each OSGi bundle has its own class loader. If you put two parts of a package in different OSGi bundles, those classes end up on different class loaders and are considered incompatible. This is because Java uses the class loader/package/class combination as the unique identifier. Split packages in OSGi often result in a
java.lang.IllegalAccessError exception at runtime.
In OSGi, a package is considered an atomic unit and so it should be loaded by the same class loader to ensure consistency. If you really need split packages, and re-factoring isn’t an option, a workaround is to make one of the OSGi bundles an OSGi bundle fragment instead. A fragment always lives within a host OSGi bundle so applying this technique ensures both are loaded by the same class loader.
Know your Execution Environment
The OSGi bundle MANIFEST.MF header Bundle-RequiredExecutionEnvironment is often misunderstood. The value of this header is the minimum level of Java runtime in which the OSGi bundle can execute. It is often incorrectly set to an arbitrarily high level, for example JavaSE-1.8 when a value of JavaSE-1.5 might be the more correct. Setting it correctly gives your OSGi bundle greater flexibility to install into multiple environments.
Avoid circular dependencies
Circular dependencies usually indicate sub-optimal packaging or application architecture problems. It is important to ensure these dependencies are designed out of the structure of your applications. A number of approaches claim to remove these cycles like Inversion of control (Dependency Injection), Interfaces, and creation of an intermediate object. Although these techniques can “fix” the problem, it is often better to look at the design between the modules and question why the cyclic dependency exists. Can the application can be better designed to avoid them? Sometimes simply moving classes to a more appropriate package is enough. If you are developing in an IDE such as Eclipse this type of error is highlighted with an error message similar to the following:
A cycle was detected in the build path of project 'com.company.package'.
The following articles provide useful information on how to tackle those issues along with some best practices for OSGi development.
- OSGi and cyclic dependencies
- Best practices for developing and working with OSGi applications
- How to solve circular package dependencies
Separate API from implementation
By putting your Java API in a separate OSGi bundle to the implementation of that API, you gain stability and flexibility. An implementation will generally require regular refresh due to bug fixes and improvements, while the API tends to remain stable. Keeping the two apart means that consumers of the API will not be required to refresh their dependencies each time a minor change comes along. A clear separation between API and implementation can also help avoid circular dependency problems.
In our roundup of best-practices we’ve covered many different aspects of OSGi development from guidance with common bundles, avoiding split packages, preventing cyclic dependencies, configuration property files, whether Bundle-Classpath or full OSGi conversion is best for you. Our hope is that our experience can help you avoid difficulties and embrace the benefits.