Best practices

This section covers some of the best practices that can be applied when designing modular systems, and in particular, modular applications that are built on either OSGi or using the standard Eclipse extension mechanisms.

Separate API and implementation

It is very common for OSGi applications to have a separate API and implementation. This allows the API to be versioned independently from any implementations that may follow.

To implement this effectively, most APIs are specified in terms of pure Java interfaces. However, it is possible that classes are present as well; exceptions must be represented as classes, as are common POJO data structures.

Eclipse documents the interfaces that are not suitable to implement and the POJOs that are not suitable to subclass with the @noimplement and @noextend JavaDoc tags. These indicate to clients that the interface is not intended to be used outside of the API and that the classes are not designed to be subclassed. The annotations are not binding, but along with being good documentation, they give an idea of whether doing this is supported or not.

One way of separating an interface from implementation is to put all the publicly visible types in one package and have all the implementation details inside another package. Then, using the OSGi Export-Package manifest header, only the implementation package can be exposed. This technique is used by the Java libraries (particularly by AWT) where internal implementation classes are put in a com.sun package and end user APIs are put into java.* or javax.* packages. However, the disadvantage with this approach is that any client can become tightly bound to the implementation bundle and so, it cannot be replaced without refreshing/restarting the client bundle.

The reason this occurs is because a client becomes tightly bound to the APIs when they are resolved. The client bundle has a package dependency to the API classes, and this is not released until the bundle is refreshed (which implies a stop/start cycle). This prevents the API bundle from being replaced on the fly.

The solution to this problem is to put the API classes and interfaces in separate bundles. This way, clients only need to depend on the API bundle and acquire the implementation through another means (for example, looking up an OSGi service). This permits the client to only be tightly bound to the API but permit dynamic replacement of the implementation at runtime.

This technique is used by JDBC drivers, where the client depends on the java.sql package, but the implementation comes from elsewhere. It is also used to access the OSGi framework, where the client depends on the org.osgi.framework.* packages, not on the specific implementation provided by the framework itself.

Tip

For convenience, it may be tempting to provide a factory instance in the API bundle, such as the FeedParserFactory from Chapter 2, Creating Custom Extension Points, and Chapter 3, Using OSGi Services to Dynamically Wire Applications. Care must be taken not to leak implementation details out to the client, as otherwise, the API bundle will end up being wired to the implementation bundle. The typical way to prevent this is to ensure that the implementation bundle depends on the API bundle, then a cyclic reference cannot occur.

The popular logging framework, Simple Logging Facade for Java (SLF4J), provides a separate versioned API to the implementations. This allows clients to depend at compile time only on the API and not on any implementation details, so at runtime, the appropriate implementation can be used and changed where necessary.

Decouple packages

An application that depends only on package dependencies might be seen as modular but might still run into modularity problems. For a start, multiple packages in the same bundle are visible to one another, so it may seem that the packages are loosely coupled but still have dependencies leak between them.

Building a system using multiple separate modules is the only way to enforce separation of implementation. Using OSGi to provide additional filtering/hiding of implementation packages can do this further.

At the extreme end, having one public package per bundle will allow an OSGi runtime to validate the set of dependencies completely. It is likely that this is not appropriate in many cases, but having a smaller number of public packages will allow better granularity than having all packages in one bundle.

Note

One advantage of using an OSGi-aware build system is that it can detect when private implementation classes are leaked, thereby preventing compilation from occurring when it happens. The ability to hide internal implementation packages and prevent their use outside of a bundle is one of OSGi's key strengths, and it aids in maintainability.

Decouple services

A better approach to having a pure package-level separation is to have a service-level separation. This allows an implementation to be substituted at runtime instead of having a fixed lifetime for the bundle itself.

An example of this is the Tomcat engine. When a web application is dropped into the webapps folder, the application is automatically deployed, and when it is removed, the web application is stopped. This allows a web application to be updated by simply dropping in a new version without having to restart the server.

Such decoupling comes from having a separation of API to implementation; in this case, the javax.servlet.Servlet API and the web applications as providers of the javax.servlet.Servlet service.

Tip

The OSGi HTTP service uses this technique along with the extender pattern to notice when Servlets are published, which are then wired up for incoming requests.

Using Dynamic Services requires a dynamic framework such as OSGi, and it also requires the clients to be aware of the services coming and going. This is typically achieved through the use of helper classes such as ServiceTracker or by extender patterns such as Declarative Services.

The concept of decoupled services has been around in Java for some time; JDBC has the concept of a Driver, which registers an implementation, and a lookup based on a string representation of the driver type. More generally, the ServiceLoader (added to Java in 1.6) provides a generic way of locating one or more implementations based on the name of an interface. This is used to provide a form of decoupling from the consumer to the provider; the consumer needs an implementation of a specific interface, and the provider offers an implementation of that specific interface.

Tip

As covered in Chapter 6, Understanding ClassLoaders, the ServiceLoader class is not well suited from a dynamic perspective though load-time weaving of classes can rewrite references to the ServiceLoader if necessary.

This pattern is often combined with the Factory pattern in which an interface of the desired type is used and in which a third party (such as a Factory, ServiceLoader or the Service registry) is used to obtain and return an instance of this interface. Typically, Factory implementations are configured to instantiate new services, but in OSGi's case, they can be used to return or select an existing instance.

Prefer Import-Package to Require-Bundle

A bundle can declare its dependencies using either Require-Bundle or Import-Package. The former allows the import of all exported packages while the latter allows packages to be imported individually (a more general form, Require-Capability, was added in OSGi R4.3; this allowed non-package dependencies to be expressed).

Tip

Historically, the Eclipse platform used plug-in dependencies that required access to the entire content of the plug-in. When it migrated to OSGi in 3.0, the Require-Bundle was added to the OSGi R4 specification. Today, Eclipse PDE still prefers to generate dependencies with Require-Bundle as a result.

Typically, OSGi applications should prefer using Import-Package for the following reasons:

  • It is possible to version packages independent of the version of the bundle
  • It represents the bundle's requirements more accurately (the code depends on classes in the specified packages and not necessarily on any one named bundle)
  • It permits the package to be moved to a different bundle should it be refactored in the future
  • It allows a bundle to be replaced with a different or mock implementation without changing any consuming bundles or code

On the other hand, it's quite common for Eclipse bundles to use Require-Bundle. For example, the JDT UI bundle expresses a hard dependency on the JDT core bundle:

Bundle-SymbolicName: org.eclipse.jdt.ui; singleton:=true
Require-Bundle: …
 org.eclipse.jdt.core;bundle-version="[3.9.0,4.0.0)",
 …  

Partly this is because the PDE tooling in Eclipse is set up with the expectation of writing the bundle's manifest by hand, and creating automated lists of imports/exports is best left to tooling. It's also historical as most dependencies in Eclipse are set up to use bundle versions instead of package versions, and so the tooling is optimized for the common case of Eclipse development.

It is possible to switch from using Require-Bundle to Import-Package in PDE; there is a somewhat hidden Automated Management of Dependencies section that allows candidate bundles to be specified. Clicking on the add dependencies hyperlink populates the list of dependencies as either a list of Require-Bundle or Import-Package, depending on which setting is used below the list.

To migrate the dependencies for the feeds.ui project, open up the MANIFEST.MF file and switch to the Dependencies tab. Expand the collapsible Automated Management of Dependencies section, and it will show an empty table:

Prefer Import-Package to Require-Bundle

Now move the dependencies down from the Required Plug-ins section to the Automated Management of Dependencies section. To allow the existing packages to be automatically imported, add the com.packtpub.e4.advanced.feeds and org.eclipse.osgi.services bundles to the candidate list and remove them from the Imported Packages section:

Prefer Import-Package to Require-Bundle

Now change the Require-Bundle at the bottom to Import-Package, and then click on the underlined add dependencies link. This will populate the Imported Packages list on the right-hand side with a full list of packages. Packages that have a versioned export are also imported with the version installed in the workspace:

Prefer Import-Package to Require-Bundle

In this example, the org.osgi.framework package is imported with version 1.7.0, which corresponds to the version in Eclipse 4.3. To support running on older versions of Eclipse, changing this to a lower value would be required. Of course, testing the bundle on older versions would still be required to make sure that the package does not take advantage of any newer features.

Version packages and bundles

In the previous section, the Require-Bundle was replaced with an Import-Package. This increased the number of dependencies added to the bundle; however, since these were automatically added, managing them becomes easier.

Another change was that the package was versioned instead of the bundle. A versioned package is declared by appending a version attribute to the package export:

Export-Package: org.osgi.framework;version="1.7"

In order to use this package, it must be imported by specifying the version as well:

Import-Package: org.osgi.framework;version="1.7.0"

This ensures that the bundle will bind to a minimum of version 1.7. Note that the strings 1.7 and 1.7.0 are equivalent, as a version number component defaults to 0 if not specified.

The advantage of versioning specific packages is that they provide a finer level of granularity than the version level of a bundle. If a bundle exports many packages (such as the Eclipse Equinox kernel, org.eclipse.osgi), then it can only have a single version number at the aggregate level. In the case of Eclipse 4.3.2, the bundle version is 3.9.2. In the case of Eclipse 4.4.0, the bundle version is 3.10.0.

However, both versions of Eclipse export org.osgi.framework.hooks.bundle with version 1.1.0. Clients that need to use this package only need to import it and don't have to worry that in Equinox, it comes from the org.eclipse.osgi bundle. In the case of Apache Felix, this package comes from the felix bundle and exports the same version.

If new functionality is required, such as depending on CollisionHook that was added in version 1.1.0, then it would be an error to install it in an environment that did not provide a minimum of version 1.1.0. If this functionality is installed in Eclipse in the org.eclipse.osgi bundle but in Felix as the felix bundle, then there is no consistent name or version of the bundle that could be used. If the bundle developer used Require-Bundle: org.eclipse.osgi;bundle-version="3.9.0", then the bundle would not resolve in other OSGi frameworks such as Felix.

Using a versioned Import-Package allows the bundle to depend on the appropriate level of service, regardless of which bundles are installed in the system. In general, all OSGi framework packages are versioned, as are many of the Apache Felix bundles.

Tip

Note that most Eclipse-based OSGi bundles do not export package versions.

Avoid split packages

In OSGi terminology, a split package is one that is exported by several bundles. This leads to a more complex view of the environment as a whole and might lead to incorrect behavior if only part of the bundle is imported. When an Import-Package dependency is wired for a bundle, it is only wired to a single provider of that package. If there is more than one version of a package, the framework can choose to wire it to one or the other but not both.

Avoid split packages

As a result, if a package is split between two or more bundles, then it cannot be imported by a single client with Import-Package.

Tip

Note that JARs have the same concept of a single package coming from a single bundle, using sealed packages. This is achieved for plain Java with entries in the manifest such as the following:

Name: com/example/pack/age/Sealed: true

Typically, split packages evolve by accident or through refactoring or evolution of a package where some functionality has been exported and made available elsewhere.

To allow packages with content to be imported from two or more split packages, an intermediary aggregator bundle needs to be used. This uses Require-Bundle to wire together the packages by a symbolic name while using Export-Package with the common package.

Using Require-Bundle to merge the dependencies together is easy, but it is also necessary to prevent bundles from depending on the original exported packages. To do this, add a mandatory directive to the split packages, which prevents the package from being imported unless it has a Require-Bundle or Import-Package with the appropriate attribute:

Bundle-SymbolicName: foo.logger
Export-Package: log;mandatory:=foo;foo=bar

This will prevent bundles from importing the log package unless they also add a foo attribute with a bar value:

Import-Package: log;foo=bar

Another way to get the log package is to use Require-Bundle:

Require-Bundle: foo.logger

The solution will look like the following MANIFEST.MF entries:

# logger.jar
Bundle-SymbolicName: com.packtpub.e4.advanced.log.logger
Export-Package: log;mandatory:=by;by=logger

# other.jar
Bundle-SymbolicName: com.packtpub.e4.advanced.log.other
Export-Package: log;mandatory:=by;by=other

# merge.jar
Bundle-SymbolicName: com.packtpub.e4.advanced.log.merge
Export-Package: log
Require-Bundle:
 com.packtpub.e4.advanced.log.logger,
 com.packtpub.e4.advanced.log.other

# client.jar
Bundle-SymbolicName: com.packtpub.e4.advanced.log.client
Import-Package: log

The relationships between the bundles are shown graphically in the following diagram:

Avoid split packages

The mandatory directive is used to state that the package can only be imported using the by attribute, which allows a selection of a specific variant of the package. These can be merged with multiple Require-Bundle bindings. This is then transparent to the clients that can use the Import-Package to be wired to the merged bundle.

Tip

Using split packages can cause problems at implementation and should generally be avoided. The only reason split packages should be used is when refactoring existing packages has meant that there is no alternative to having classes in more than one place. If split bundles are required, then it will be necessary to provide a combination bundle that knows of the components and has imported them; this is optional in the case of a component that is no longer required.

Import and export packages

In OSGi Release 3, an Export-Package implied an Import-Package of the package. In OSGi Release 4 (which is the version Eclipse started using), an Export-Package no longer implies an Import-Package.

Tools that use bnd to build bundles typically automatically add an Import-Package to every Export-Package that is generated. This includes tools such as Bndtools, Maven Felix, and the Gradle OSGi plug-in. As a result, a foo package that is exported will have the following in the MANIFEST.MF file:

Export-Package: foo
Import-Package: foo

However, bundles that are written by hand (such as used by PDE) tend to not do this and instead just export the package:

Export-Package: foo

What's the difference between both approaches and which is preferred? The OSGi specification (section 3.6.6) suggests that it is best practice to import packages that are exported, as long as the package does not use private packages and a private package uses an exported package. For example, it should also import it in a bundle that exports an API, as shown in the following diagram:

Import and export packages

The reason to import a package that is exported is to allow the bundle to substitute the local package with one from a different bundle. For example, if two versions of org.slf4j API packages are installed and the package is imported from two bundles, the framework can upgrade the API package by importing the package from the more recent bundle. This allows a package to be substituted for a newer version if it becomes available. The following diagram illustrates this:

Import and export packages

Substitutability only works if the exported package does not expose dependencies on internal packages. If the exported package has a dependency on an internal package (for example, a return type, argument type, annotation, or exception), then it cannot be substituted. Similarly, the bundle must internally depend on the package in order for it to be replaceable; if it doesn't depend on the package, then there is no need to import it.

Avoid start ordering requirements

The OSGi framework has a concept of a start level, which mirrors start levels in Unix. This is a positive integer that can be increased or decreased at runtime. The framework begins and ends at start level 0 and then progressively increments the start level until it reaches the initial start level (which is generally 4 for Eclipse and 1 for Felix).

Each time the start level increases, bundles that are defined to start at that level are started automatically. Similarly, each time the start level decreases, bundles that no longer meet the start level are stopped. Upon framework shutdown, the start level is progressively decreased until all bundles are stopped, and the start level goes to 0.

Relying on a particular start ordering generally indicates a fragile relationship between bundles. However, sometimes start ordering is necessary. It is used by Eclipse and Equinox to ensure that Declarative Services are started early so that any bundles subsequently started are able to take advantage of Declarative Services. Similarly, some weaving hooks, which are used to process class files, can only be used if the hook is installed prior to subsequent class files being loaded.

There are several ways to avoid start level-ordering problems:

  • The first is to use a component model such as Declarative Services (see Chapter 3, Using OSGi Services to Dynamically Wire Applications, for more information). This will ensure that the services are registered and available for use but only instantiated once the services' dependencies are available. This will ensure that the system will wait until it is needed, and the services will be instantiated on demand.
  • The second is to listen to bundle events (see Chapter 8, Event-driven Applications with EventAdmin). It is possible to listen to bundles coming and going and taking appropriate action when bundles are installed. This is used by bundles that implement the extender pattern (see the The extender pattern section earlier in this chapter) to process bundles as they are installed and started. Generally, the processing involves iterating through all the currently installed bundles followed by switching to a listener-based mechanism to pick up newer bundles. The ServiceTracker performs something similar to listen to services that are coming and going.
  • The final option is to design the bundle or service in such a way that if dependent services are not available, the correct operation still occurs. For example, if a bundle depends on a logging service and it is not present, then the bundle could decide not to log information or substitute a default null logging system instead. Generally, in a dynamic framework such as OSGi, services may come and go at any time, and the client should be prepared to handle such cases.

Avoid long Activator start methods

At the start of the framework, the start level is increased (see the Avoid start ordering requirements section for more information). All the bundles in a given start level are started before moving to the next start level.

As a result, it's possible for a bundle to delay further bundles by taking a long time to return from a start method in a BundleActivator. Generally, the start method should return as quickly as possible and move processing to a different thread if computation will take a long time.

This is especially true if the bundle is started not through the framework start-up but through an automated start caused by a lazy bundle activation. If a bundle has a Bundle-ActivationPolicy: lazy manifest entry, then as soon as a class is requested from this bundle, it will be started automatically. If the bundle start-up takes some time (for example, interacting with an external resource or dealing with I/O), then it will delay the class being handed back to the caller.

Sometimes, it may be desirable to have a bundle start-up delayed until a particular resource is available. For example, if a bundle requires database connectivity, it might be the case that the activator verifies that the database is available before being started. An alternative is to let the bundle come up but only present the service after the database connection has been verified. Instead of treating the bundle as the atomic unit of start-up, using services is more flexible, and it means that the service can be implemented by a fallback or another bundle subsequently.

Use configuration admin for configuration

Configuration Admin, also known as Config Admin, is a standard OSGi service that can be used to supply configuration data to a bundle at start-up. Management agents can supply the configuration from a number of sources such as a property file in the case of Felix FileInstall or custom configuration bundles if desired.

By separating how the configuration information is passed (by defining a generic API) and how it is sourced, a bundle can be configured without needing to change anything about the system or code.

Reading configuration information directly from a properties file (a technique used by many Java libraries) can be problematic as it will require a filesystem and a hard-coded list of properties. If any configuration information needs to be changed (such as increasing the logging level for a particular component), typically, the file needs to be edited and the process restarted. Similarly, hard-coding system properties at start-up of the JVM suffers from the same problem; any changes made are not visible to the bundle at runtime.

Using Config Admin solves both of these problems. Config Admin uses a push mechanism—when either initial or new configuration is found, it is pushed to the component for configuration. As the component can already handle having configuration data set in this way, it will be able to dynamically react to configuration changes in addition to an initial static set of data.

Additionally, OSGi permits bundles and services to be instantiated multiple times within a JVM. Config Admin can configure each of these individually, whereas a system property or single file would not be able to distinguish between the two.

The only requirement to be usable by Config Admin is to either implement the ManagedService interface, which provides an updated method that takes a Dictionary of key/value pairs, or to use a component model such as Declarative Services, which is already integrated with Config Admin. As such, it is very easy to implement generic services that consume this configuration information.

Tip

Why does ManagedService use an updated method and not use JavaBeans properties? Essentially, if many configuration values change at one time, it is desirable to commit this set of changes atomically and then restart whatever is required once all the changes are in place. If a set of changes were drip-fed into a service, one JavaBean setter at a time, it would not be possible to hook in the completion of all the changes in a standardized way.

Share services, not implementation

As OSGi is a dynamic environment, services can come and go after the system is started. Bundles that depend solely on implementation classes will not be dynamic; bundles that depend on services can be.

The corollary is that it makes sense to share behavior by exporting/registering services rather than exporting code. It is possible to provide helper methods in the API that perform the service lookup (similar to SLF4J's Logger.getLogger method), as the returned instance can be part of a well-known interface. It also allows the acquisition of that data object to be encapsulated inside a small but standard implementation that does not need to concern the caller.

To take full advantage of the dynamism that OSGi provides, these services should be registered and deregistered dynamically. The easiest way to achieve this is to use a component model such as Declarative Services or Blueprint, as both of these allow components to be defined externally to the code and then instantiated on demand. In this way, when the component is created (through access, configuration admin, or some other mechanism), it will be automatically registered as a service for other bundles to consume. If there are any required services that are not available, the creation of the component will be delayed until a time where the services are available.

Services also make it much easier to provide mock implementations. The bundle under test can be installed into a test runtime, along with a test bundle that provides a mock service for use by the main bundle. In this way, the service can be tested in isolation without requiring any additional data.

Loosely coupled and highly cohesive

Bundles should be designed so that they are loosely coupled and highly cohesive. Loosely coupled means that the bundle has limited dependencies on API classes (never implementation) and will dynamically acquire services when needed. Highly cohesive means that the bundle forms a united whole, and the contents cannot be split into logical subgroups. (If a bundle can be split into logical subgroups, then it indicates that a better approach would be to split the bundle into two or more bundles; one per subgroup and optionally, one overarching group).

Loose coupling is desirable as a function of modules in a complex system, because it means that it is unlikely that this module will depend on any specific set or emergent behaviors in the rest of the system. Instead, if the dependencies are cleanly exposed, then any refactoring required will be limited to the dependencies present.

Loose coupling is harder than it sounds in a large system. If a project in an IDE is loosely structured (such as having many logical packages) but is compiled with a project-wide classpath, then it's easy for unintended dependencies to creep in.

Note

For example, in the Jetty project in version 6, the client implementation had an unintended dependency on the server implementation; this meant that all client code had to be shipped with a copy of the server code at the same time. This unintentional dependency crept in because the individual units were not separated on the filesystem, and the compiler transparently compiled the server classes at compile time.

Similarly, the JDK has unintended dependencies by virtue of the Java packages being compiled as a monolithic unit; the java.beans package depends on the java.applet package, which depends on the java.awt package. So, despite the fact that the beans package has no direct need for a GUI, any project that uses java.beans is implicitly drawing in GUI dependencies. Modularizing is hard; modularizing the JDK doubly so, although JEP 200 plans to split the JDK into separate modules and JEP 201 plans to enforce modularity at compile-time for the JDK..

One way of achieving loose coupling is to break down modules into smaller and smaller units, until they become indivisible units both logically and from an implementation perspective. By doing this, loose coupling will be an emergent property of the modules after the translation is complete. It will also highlight where there may be some dependencies that are not appropriate, and attention can be given to how to rectify the situation (such as deprecating the java.beans.Beans.instantiate method that takes an AppletInitializer). Often, the act of doing the modularization will be enough to highlight where these APIs require remediation. Separating the modules into their own classpaths (for example, separate Maven modules or Eclipse projects) will often immediately highlight where assumptions about a global classpath have been made.

Highly cohesive bundles have packages tightly grouped together. In other words, the packages form a tight knit group and don't expect to be split from each other. Typical anti-cohesive bundles are ones with a util in the name; they contain all manner of unrelated classes. The problem with such bundles is that they tend to accrete contents and make refactoring subsequently more difficult. Secondly, the accretion of contents often implies an accretion of dependencies; this makes the bundle a join in a many-to-many dependency wiring between bundles.

Note

In most cases, the solution is to minimize dependencies and maximize cohesion. This is often a journey instead of a destination, as a bundle can keep being split until all the units meet the cohesion requirements.

Compile with the lowest level execution environment

OSGi bundles can specify a Bundle-RequiredExecutionEnvironment, which is the set of Java platforms that the bundle will work on. These include CDC-1.0/Foundation-1.0, JavaSE-1.7 and JavaSE-1.8.

When installing, building, or running a bundle, the OSGi framework will ensure that the required execution environment is met. The bundle will not resolve unless the running environment is at least the minimum requirement. However, it is possible for a bundle to run on a higher version than it is expecting both at runtime and at compile time.

There is a risk when compiling on JavaSE-1.8 but declaring a required execution environment of JavaSE-1.7 that methods available in 1.8 but not in 1.7 are used. The compiler might not be able to tell whether new methods (such as List.sort) are present in older versions of Java and let the bytecode be compiled and work on an OSGi platform with a JavaSE-1.8 runtime. If the bundle is subsequently run on a JavaSE-1.7 platform, then silent runtime failures might occur.

The way to avoid this is to ensure that the bundles are built only using the specified version of Java declared in the Bundle-RequiredExecutionEnvironment. This is often done by server-side builds so that even if a single IDE does not have the correct JDK, then the server-side builds will pick up the error.

Note

OSGi supports low-powered devices such as embedded routers and home automation systems. The APIs in OSGi therefore limit themselves to OSGi/Minimum-1.2, which might not include generics or the newer collections APIs added since Java 1.2.

As embedded systems become more powerful, the use of generics has started to creep in to OSGi runtimes. With Java 8 bringing lambdas and default methods, it is likely that bundles will transition to supporting a minimum of Java 8 in the near future, even if the traditional APIs do not.

Avoid Class.forName

Traditionally, Java applications have used Class.forName to dynamically load a class from a given name. This is often used in conjunction with externalized configuration such as a JDBC database driver or the name of a codec or charset encoding.

There are two problems with Class.forName, which means that it should be avoided wherever possible in an OSGi application.

First, it assumes a global visibility of classes such that any class will be available from any other class or ClassLoader. This works for monolithic Java applications (which only have a single application ClassLoader) or ones where class loaders and therefore classes are not shared (such as individual applications in a Tomcat container). As this may not always be the case, a lookup of a class becomes dependent upon where it was looked up from instead of being globally available.

Second, the result of the class is pinned both in the ClassLoader of the providing bundle and in the bundle of the requesting class. This would be fine if it were the interface or API class (as this will be pinned in the lifetime of the bundle anyway), but the implementation class is specifically supposed to be replaceable dynamically. Instead, once a Class.forName has been invoked, then the implementation class is permanently wired to the client bundle until the client bundle is refreshed (when it gets a new ClassLoader object).

The solution to both of these problems is not to call Class.forName. How then can bundles work with APIs where the classes are not known in advance? The obvious solution is to use services instead and have the bundle locate the service using the standard OSGi mechanisms. For situations where this is not possible, the BundleContext can be used to get the Bundle, which provides a getResourceAsStream method (for loading resources), and a loadClass, which does the right resolution for a specific implementation class.

If the Bundle isn't known, it is possible to use ClassLoader.loadClass instead by using the ClassLoader of the current class. This will then delegate to the right ClassLoader to find the implementation.

The best approach is not to use string names at all but pass Class instances instead, or if a string must be used, then pass it with the appropriate ClassLoader as well. Many database mapping tools will take a Class instance to perform mapping instead of a class name.

Avoid DynamicImport-Package

Along with using the Require-Bundle and Import-Package to wire dependencies, OSGi also provides a more general dynamic one called DynamicImport-Package. This can be used to provide a last attempt to find a class that is being requested from the bundle if it has not been found any other way.

The format of DynamicImport-Package is to specify a package name, optionally with a wildcard, that can be used to create an Import-Package wire on demand if a class is requested. Using a generic * for everything will result in dependencies being found automatically when they are looked up and not found in any other way:

DynamicImport-Package: com.example.*

When a class in the bundle attempts to find com.example.One or com.example.two.Three, then the framework will attempt to create a wiring for the com.example and com.example.two packages. If there are bundles in the system that export these packages, then wires will be added to the bundle, and the resolution will work as expected. If not, then the load attempt fails, as with other failed lookups, and an exception will be thrown.

Tip

When debugging failed lookups, the following two types of failures might occur:

  • ClassNotFoundException: This is raised when the direct type cannot be found, for example, you have a typo in the class name (such as omc.example.One).
  • NoClassDefFoundError: This is raised when an indirect type cannot be found; in other words, the JVM has found the class requested (com.example.One), but this class depends on another that cannot be loaded or found (com.example.Two). Exceptions thrown in a static initializer might also result in a NoClassDefFoundError being reported.

The problem with the use of DynamicImport-Package is that it will pin the dependency permanently for the lifetime of the requesting bundle. As such, even if this dependent bundle were to be stopped, the ClassLoader would be pinned to the wired dependency bundle. The only way the wire will be removed is if the requesting bundle is refreshed or stopped.

Generally, the use of DynamicImport-Package is a code smell in OSGi, and the underlying problem should be resolved (such as replacing lookups with services or with the explicit dependencies as required). It can be a useful diagnostic tool in the right use cases, but it should not be generally relied upon.

Avoid BundleActivator

BundleActivator is used in many cases where it is not necessary. Typically, the BundleActivator will store a static reference to the BundleContext and/or provide helper methods that look up platform services. However, this ends up being stored as an effective singleton with a static accessor to return the instance:

public class Activator implements BundleActivator {
  private static Activator instance;
  public static Activator getInstance() {
    return instance;
  }
  private BundleContext context;
  public BundleContext getContext() {
    return context;
  }
  public void start(BundleContext context) throws Exception {
    this.context = context;
    instance = this;
  }
  public void stop(BundleContext context) throws Exception {
  }
}

This is not necessary because the Bundle (and therefore, the BundleContext) can be acquired from any class in an OSGi runtime using FrameworkUtil and from that, any service of a given type.

Tip

The BundleContext only exists for bundles that are ACTIVE (have been started). If a bundle has not been started, then its bundle context will be null. Code should handle this and fail gracefully. Alternatively, the Bundle-ActivationPolicy: lazy manifest header can be added to the manifest, which will automatically start the bundle on access.

If a service is required, this method can be used to return an instance of a service:

public static <S> S getService(Class<S> type, Class<?> caller) {
  Bundle b = FrameworkUtil.getBundle(caller);
  if (b.getState() != Bundle.ACTIVE) {
   try {
      b.start(Bundle.START_TRANSIENT);
    } catch (BundleException e) {
    }
  }
  BundleContext bc = b.getBundleContext();
  if(bc != null) {
    ServiceReference<S> sr = bc.getServiceReference(type);
    S s = null;
    if(sr != null) {
      s = bc.getService(sr);
      bc.ungetService(sr);
    }
  }
  return s;
}

As it is possible to obtain an instance of a service given a caller class and a desired type, the majority of the use cases in a BundleActivator are no longer required. The only remaining use case is to start or register a service dynamically at bundle start-up, but this can be achieved through the use of Declarative Services or another component model instead.

Consider thread safety

Other than the BundleActivator and the start and stop methods, it cannot be guaranteed that any particular thread will be calling the bundle's code or, indeed, might be calling it at the same time. In particular, when processing events from Event Admin (covered in Chapter 8, Event-driven Applications with EventAdmin), the events might be delivered from different threads from the one they first posted.

If there are UI operations that are required and they need to obtain a lock or run on a particular thread, then care must be taken that when the UI is invoked, it is done from the correct thread. Generally, OSGi frameworks will use multiple threads, and so an incoming event or service call might not be on an appropriate thread for the service to process.

Thread safety should also be considered with regard to mutable data structures. For data structures that are highly volatile or might be mutated and read at the same time, suitable synchronization guards should be implemented to prevent unknown problems from occurring at runtime.

Test in different frameworks

Although an OSGi bundle might work correctly in the framework environment that the developer tested in, it might have problems if run in a different framework or a different environment.

One common cause for failure is not having the right dependent bundles in the target environment, such as having Declarative Services installed and running. In general, any extender pattern is only going to work if the extender is configured and running in the system.

Another problem is the different frameworks and their use of the boot delegation options. When a class is looked up from a bundle, if the package begins with java. or is listed as an entry in the org.osgi.framework.bootdelegation system property, it is delegated to the parent (JVM) ClassLoader. Otherwise, the package is selected from the Import-Package wiring (if present) or Require-Bundle (if present), followed by the embedded Bundle-ClassPath contents.

This presents a problem with testing bundles, particularly ones that look up classes in the sun.* or javax.* spaces. Code that appears to work correctly in Equinox will fail to work in Felix because the former is looser by default. To ensure that Equinox behaves in the same way as Felix, set osgi.compatibility.bootdelegation=false as a system property on the Equinox JVM. Although it talks about OSGi compatibility, in fact this is an Eclipse-specific behavior, and the default action of Eclipse is to use non-standard boot delegation. As this has not changed in over a decade, it is likely that it will not happen any time soon, and there might be misconfigured bundles that never show up in test failures because of this issue.

Note

Generally, if a bundle works correctly in Felix, then it will work in other runtime engines. The reverse is not always true.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset