Wednesday, January 22, 2020

To Embed or Not To Embed Your API Packages

For many years the OSGi Alliance, like other Java Standards organisations, has been creating APIs as part of its specifications. Multiple communities have then been involved in providing implementations of those APIs and specifications (see https://en.wikipedia.org/wiki/OSGi_Specification_Implementations).

In Java, not just OSGi, to use a specification you need both the API and the code which provides the API implementation. As a convenience many OSGi bundles implementing an API also provide the API package(s) from the bundle via ‘Substitutable Exports’ (see https://docs.osgi.org/whitepaper/semantic-versioning/050-exporter-policy.html). This means that the API package is both exported as well as imported by the implementation bundle. Because the OSGi framework controls the classloading on the package level, when multiple bundles export the API package(s), the OSGi framework can decide which exporting bundle ultimately provides the API package(s) to each bundle importing the API, that is, to each API consumer. This is not possible in “vanilla” Java, where the first API package(s) found on the classpath will be used, potentially causing problems if multiple versions of the API are present.

The use of substitutable exports had historical advantages because it made the job of a management agent easier when trying to find a set of functional bundles to install into an OSGi framework. By providing the API packages that they implement, implementations make themselves easier to deploy. The management agent (or a human) does not also have to find a bundle providing the API to make an implementation resolve. As the OSGi specifications continued to evolve the process to  search for dependencies was replaced by a solution that uses the standard Resolver and Repository services. Due to the limited metadata available, the use of substitutable exports also had advantages here. If a client bundle imports an API package and that API package is provided by another bundle which also provides an implementation of the API then a resolver will automatically pull in the implementation bundle along with the API when resolving the set of bundles to install.  If the API is provided by a separate bundle from the implementation then the resolver needed some proprietary way to discover the requirement for the implementation, and then to discover the bundles that provide the implementation.  In modern OSGi the “resolution problem” can now be completely solved by using the generic capability and requirements functionality provided by the OSGi core specification.  Not only can bundles declare requirements on the API using the Import-Package header, but they can also declare requirements on an implementation using the Require-Capability header.  Similarly bundles can declare that they provide an implementation using the Provide-Capability header.

In the past there was another benefit to substitutably exporting your API, getting API package(s) for individual OSGi specifications used to be tricky. The main Jar file that had them available was typically the osgi-cmpn.jar which contained all the API packages for all the Compendium specs and this Jar was certainly not designed or intended to be used at runtime.  Fortunately this last problem is now resolved since OSGi Release 6 as individual specification API jars are now available in Maven Central for each specification and these jars can be used at runtime.

So while the approach for embedding API packages has served the OSGi community well for many years, it requires a runtime supporting type visibility encapsulation, such as an OSGi Framework, that is in control of the class loading to select the appropriate package provider at runtime. Recently, work has started at the OSGi Alliance to support operating in environments where the class loader may not be provided by an OSGi Framework, or where there is no class loader at all. See RFC 243 OSGi Connect and RFC 245 Resource Encoding For Java Modules. This could be when running with the Java Platform Module System (JPMS), running as part of another framework such as Spring Boot, or even in a AOT-compiled environment such as SubstrateVM. Many existing OSGi bundles can work in these environments, providing the dynamic OSGi service model and allowing users to continue using popular OSGi technologies such as Config Admin, Declarative Services, the Converter and many others. However the embedding of API packages defined in specifications becomes a problem if there are multiple bundles that contain and provide the packages, since in these contexts there is no OSGi classloader to select the proper packages and hide, through type visibility encapsulation, the unused copies of the packages.

Going Forward

Going forward, to support all environments for your bundles, you should consider not embedding API packages that are defined by OSGi specifications. The better approach is to declare them as dependencies using Import-Package manifest headers. Most build tools will do this automatically for you. At runtime, simply use the API runtime bundles provided by OSGi. If you do embed the API packages in an implementation bundle then be sure to only embed the API packages you directly implement.  In most all cases you should not embed the APIs that you use but do not implement.  For example, if you use the OSGi Log Service you should avoid embedding the org.osgi.service.log package unless your bundle is actually providing the implementation of the Log Service.