Chapter 5. Delving deeper into modularity

 

This chapter covers

  • Exploring advanced aspects of exporting packages
  • Importing optional or unknown packages
  • Requiring bundles instead of packages
  • Splitting bundles into fragments
  • Dealing with platform dependencies and native code

 

In the preceding chapters, we covered a myriad of details about the three layers of the OSGi framework. Believe it or not, we didn’t cover everything. Instead, we focused on explaining the common specification features, best practices, and framework behavior necessary to get you started with OSGi technology. Depending on the project, the aforementioned features and best practices may not be sufficient. This can be especially true when it comes to legacy situations, where you’re not able to make sweeping changes to your existing code base. Sometimes the clean theory of modularity conflicts with the messiness of reality, so occasionally compromises are needed to get things moving or to meet objectives.

In this chapter, we’ll investigate more aspects of OSGi’s module layer. We’ll look into simple features, such as making imported packages a little more flexible, and into more complicated ones, such as splitting Java packages across multiple bundles or breaking a single bundle into pieces. You probably won’t need to use most of the features described in this chapter as often as the preceding ones; if you do, you should review your design, because it may not be sufficiently modular. With that said, it’s worthwhile to be aware of these advanced features of the OSGi module layer and the circumstances under which they’re useful. To assist in this endeavor, we’ll introduce use cases and examples to help you understand when and how to apply them.

This chapter isn’t strictly necessary for understanding subsequent chapters, so feel free to postpone reading it until later. Otherwise, let’s dig in.

5.1. Managing your exports

From what you’ve learned so far, exporting a package from a bundle is fairly simple: include it in the Export-Package header, and potentially include some attributes. This doesn’t cover all the details of exporting packages. In the following subsections, we’ll discuss other aspects, like importing exported packages, implicit attributes, mandatory attributes, class filtering, and duplicate exports.

5.1.1. Importing your exports

In chapter 2, you learned how Export-Package exposes internal bundle classes and how Import-Package makes external classes visible to internal bundle classes. This seems to be a nice division of labor between the two. You may even assume the two are mutually exclusive. In other words, you may assume a bundle exporting a given package can’t import it also and vice versa. In many module systems, this would be a reasonable assumption, but for OSGi it’s incorrect. A bundle importing a package it exports is a special case in OSGi, but what exactly does it mean? The answer to this question is both philosophical and technical.

The original vision of the OSGi service platform was to create a lightweight execution environment where dynamically downloaded bundles collaborate. In an effort to meet the “lightweight” aspect of this vision, these bundles collaborated by sharing direct references to service objects. Using direct references means that bundles collaborate via normal method calls, which is lightweight. As a byproduct of using direct references, bundles must share the Java class associated with shared service objects. As you’ve learned, OSGi has code sharing covered in spades with Export-Package and Import-Package. Still, there’s an issue lurking here, so let’s examine a collaboration scenario more closely.

Imagine that bundle A wants to use an instance of class javax.servlet.Servlet from bundle B. As you now understand, in their respective metadata, bundle A will import package javax.servlet, and bundle B will export it. Makes sense. Now imagine that bundle C also wants to share an instance of class javax.servlet.Servlet with bundle A. It has two choices at this point:

  • Don’t include a copy of package javax.servlet in its bundle JAR file, and import it instead.
  • Include a copy of package javax.servlet in its bundle JAR file, and also export it.

If the approach in option 1 is taken (see figure 5.1), bundle C can’t be used unless bundle B is present, because it has a dependency on package javax.servlet and only bundle B provides the package (that is, bundle C isn’t self-contained).

Figure 5.1. If bundle C imports from B, both can share servlet instances with A because there’s only one copy of the Servlet class; but this creates a dependency for C on B.

On the other hand, if the approach in option 2 is taken (see figure 5.2), bundle C is self-contained, and both B and C can be used independently. But what happens if you want bundle A to interact with the javax.servlet.Servlet objects from bundles B and C at the same time? It can’t do so. Why?

Figure 5.2. If B and C each have a copy of the Servlet class, A can only share Servlet instances with either B or C because it can only see one definition of a class.

The answer is technical, so we’ll only briefly explain it. To use a class, Java must load it into the JVM using a class loader. The identity of a class at execution time is not only associated with its fully qualified class name, it’s also scoped by the class loader that loaded it. The exact same class loaded by two different class loaders is loaded twice by the Java VM and is treated as two different and incompatible types. This means if you try to cast an instance of one to the other, you receive a ClassCast-Exception. Combine this knowledge with the fact that the OSGi specification requires each bundle to have its own class loader for loading its classes, and you begin to understand the issue with the second option we described.

If bundles B and C both include and export a copy of the javax.servlet package, then there are two copies of the javax.servlet.Servlet class. Bundle A can’t use both instances, because they come from different class loaders and are incompatible. Due to this incompatibility, the OSGi framework only allows bundle A to see one copy, which means A can’t collaborate with both B and C at the same time.

It’s not important for you to completely understand these arcane details of Java class loaders, especially because OSGi technology tries to relieve you from having to worry about them in the first place. The important point is to understand the issues surrounding the two options: option 1 results in bundle C requiring B to be present, whereas option 2 results in bundle A not being able to see the shared object instances from bundles B and C at the same time. This gets us to the crux of OSGi’s special case for allowing a bundle to import a package it exports.

Neither of the previous two options is satisfactory. The solution devised by the OSGi specification is to allow a bundle to both import and export the same package (see figure 5.3). In this case, the bundle contains the given package and its associated classes, but it may not end up using its copy. A bundle importing and exporting the same package is offering the OSGi framework a choice; it allows the framework to treat it as either an importer or an exporter of the package, whichever seems appropriate at the time the framework makes the decision. Here’s another way to think about this: it defines a substitutable export, where the framework is free to substitute the bundle’s export with an imported package from another bundle. Returning to the example, both bundles B and C can include a copy of package javax.servlet, with both importing and exporting it, knowing they’ll work independently or together.

Figure 5.3. B and C can both export and import the Servlet package, which makes it possible for the framework to choose to substitute packages so all bundles use a single class definition.

Admittedly, this may seem odd; but as the discussion here illustrates, to simplify the OSGi vision of a collaborative environment, it’s necessary to make sure bundles use the same class definitions. Up until the OSGi R4 specification, Export-Package implicitly meant Import-Package, too. The R4 specification removed this implicitness, making it a requirement to have a separate Import-Package to get a substitutable export; but this didn’t lessen the importance of doing so in cases where collaboration is desired. An interesting side effect of this is the possibility of metadata like this:

Export-Package: javax.servlet; version="2.4.0"
Import-Package: javax.servlet; version="2.3.0"

This isn’t a mistake. A bundle may include a copy of a given package at a specific version but may work with a lower range. This can make the bundle useful in a wider range of scenarios, because it can still work in an environment where an older version of the package must be used.

 

When to import your exports

The question on your mind probably is, “With all these benefits, shouldn’t I make all my exports substitutable?” Not necessarily. If the packages in question are somehow coupled to private (non-exported) packages, or all packages are exported, you should only export them. Conversely, if other bundles can reasonably expect to get the packages from a different provider, you may want to import and export them. For example, if you’re embedding and exporting common open source packages, you may want to import them too, because other bundles may reasonably expect to get them from other providers; this is especially necessary if your other exported packages have uses constraints on the common packages.

Importing and exporting a package is also useful when you’re using an interface-based development approach. In interface-based programming, which is the foundation of the OSGi service approach, you assume there are potentially multiple implementations of well-known interfaces. You may want to package the interfaces into their implementations to keep them self-contained. In this case, to ensure interoperability, the bundles should import and export the interface packages. Because the whole point of OSGi services is to foster collaboration among bundles, the choice between importing only or exporting and importing interface packages is fairly common.

You do have an alternative approach: to always package your collaborative interface packages into a separate bundle. By never packaging your interfaces in a bundle that provides implementations, you can be sure all implementations can be used together, because all implementations will import the shared packages. If you follow this approach, none of your implementations will be self-contained, because they’ll all have external dependencies on the shared packages. The trade-off is deciding whether you want more bundles with dependencies among them or fewer self-contained bundles with some content overlap. It’s all about balancing coupling and cohesion.

 

Next, we’ll look at implicit export attributes. Unlike importing exported packages, which gives the framework resolver more flexibility when resolving imports, implicit export attributes can be used to limit the framework’s options for resolving an import.

5.1.2. Implicit export attributes

Generally speaking, OSGi regards the same package exported by multiple bundles as being completely equivalent if the package versions are the same. This is beneficial when it comes to dependency resolution, because it’s possible for the framework to satisfy an import for a given package from any available matching exporter. In certain situations, you may not wish to have your bundle’s imports satisfied by an arbitrary bundle; instead, you may want to import from a specific bundle. For example, perhaps you patched a bug in a common open source library, and you don’t want to risk using a nonpatched version. OSGi supports this through implicit export attributes.

Consider the following bundle manifest snippet:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: my.javax.servlet
Bundle-Version: 1.2.0
Export-Package: javax.servlet; javax.servlet.http; version="2.4.0"

This metadata exports the packages javax.servlet and javax.servlet.http with a version attribute of the specified value. Additionally, the framework implicitly attaches the bundle’s symbolic name and version to all packages exported by a bundle. Therefore, the previous metadata conceptually looks like this (also shown in figure 5.4):

Figure 5.4. a) Your metadata declares explicit attributes that are attached to your bundle’s exported packages, but b) the framework also implicitly attaches attributes explicitly identifying from which bundle the exports come.

Bundle-ManifestVersion: 2
Bundle-SymbolicName: my.javax.servlet
Bundle-Version: 1.2.0
Export-Package: javax.servlet; javax.servlet.http; version="2.4.0";
 bundle-symbolic-name="my.javax.servlet"; bundle-version="1.2.0"

Although this is conceptually what is happening, don’t try to explicitly specify the bundle-symbolic-name and bundle-version attributes on your exports. These attributes can only be specified by the framework; explicitly specifying them results in an installation exception. With these implicit attributes, it’s possible for you limit the framework’s resolution of an imported package to specific bundles. For example, an importing bundle may contain the following snippet of metadata:

Import-Package: javax.servlet; bundle-symbolic-name="my.javax.servlet";
 bundle-version="[1.2.0,1.2.0]"

In this case, the importer limits its dependency resolution to a specific bundle by specifying its symbolic name with a precise version range. As you can imagine, this makes the dependency a lot more brittle, but under certain circumstances this may be desired.

You may be thinking that implicit export attributes aren’t completely necessary to control how import dependencies are resolved. You’re correct. You can also use good old arbitrary attributes to achieve the same effect—just make sure your attribute name and/or value are sufficiently unique. For example, you can modify your exporting manifest like this:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: javax.servlet
Bundle-Version: 1.2.0
Export-Package: javax.servlet; javax.servlet.http; version="2.4.0";
 my-provider-attribute="my.value.scheme"

In this case, the importer needs to specify the corresponding attribute name and value on its Import-Package declaration. There’s an advantage to using this approach if you’re in a situation where you must have brittle dependencies: it’s not as brittle as implicit attributes. You’re able to refactor your exporting bundle without impacting importing bundles, because these attribute values aren’t tied to the containing bundle. On the downside, arbitrary attributes are easier for other bundles to imitate, even though there are no guarantees either way.

In short, it’s best to avoid brittle dependencies, but at least now you understand how both implicit and arbitrary export attributes allow importing bundles to have a say in how their dependencies are resolved. Thinking about the flip side, it may also occasionally be necessary for exporting bundles to have some control over how importing bundles are resolved. Mandatory attributes can help you here.

5.1.3. Mandatory export attributes

The OSGi framework promotes arbitrary package sharing among bundles. As we discussed in the last subsection, in some situations this isn’t desired.

Up until now, the importing bundle appears to be completely in control of this situation, because it declares the matching constraints for dependency resolution. For example, consider the following metadata snippet for importing a package:

Import-Package: javax.servlet; version="[2.4.0,2.5.0)"

Such an import declaration matches any provider of javax.servlet as long as it’s in the specified version range. Now consider the following metadata snippet for exporting a package in another bundle:

Export-Package: javax.servlet; version="2.4.1"; private="true"

Will the imported package match this exported package? Yes, it will, as shown in figure 5.5. The name of the attribute, private, may have tricked you into thinking otherwise, but it’s just an arbitrary attribute and has no meaning (if it did have meaning to the framework, it would likely be a directive, not an attribute). When it comes to matching an import to an export, only the attributes mentioned on the import declaration are compared against the attributes on the export declaration. In this case, the import mentions the package name and version range, which match the exported package’s name and version. The private attribute isn’t even considered.

Figure 5.5. Only attributes mentioned in the imported package declaration impact dependency resolution matching. Any attributes mentioned only in the exported package declaration are ignored.

In some situations, you may wish to have a little more control in your exporting bundle. For example, maybe you’re exposing a package containing a nonpublic API, or you’ve modified a common open source library in an incompatible way, and you don’t want unaware bundles to inadvertently match your exported packages. The OSGi specification provides this capability using mandatory export attributes, which you declare using the mandatory export package directive of the Export-Package manifest header.

 

Mandatory Directive

The mandatory export package directive specifies a comma-delimited list of attribute names that any importer must match in order to be wired to the exported package.

 

To see how mandatory attributes work, let’s modify the previous snippet to export its package, like this:

Export-Package: javax.servlet; version="2.4.1"; private="true";
 mandatory:="private"

You add the mandatory directive to the exported package to declare the private attribute as mandatory. Any export attribute declared as mandatory places a constraint on importers. If the importers don’t specify a matching value for the attribute, then they don’t match. The export attribute can’t be ignored, as shown in figure 5.6. The need for mandatory attributes doesn’t arise often; you’ll see some other use cases in the coming sections. Until then, let’s look into another more fine-grained mechanism that bundles can use to control what is exposed from their exported packages.

Figure 5.6. If an export attribute is declared as mandatory, importing bundles must declare the attribute and matching value; otherwise, it won’t match when the framework resolves dependencies.

5.1.4. Export filtering

In chapter 1, we discussed the limitations of Java’s rules for code visibility. There’s no way to declare module visibility, so you must use public visibility for classes accessed across packages. This isn’t necessarily problematic if you can keep your public and private APIs in separate packages, because bundles have the ability to hide packages by not exporting them. Unfortunately, this isn’t always possible, and in some cases you end up with a public implementation class inside a package exported by the bundle. To cope with this situation, OSGi provides include and exclude export filtering directives for the Export-Package manifest header to enable fine-grained control over the classes exposed from your bundle’s exported packages.

 

Exclude/Include Directives

The exclude and include export package directives specify a comma-delimited list of class names to exclude or include from the exported package, respectively.

 

To see how you can use these directives, consider a hypothetical bundle containing a package (org.foo.service) with a service interface (public class Service), an implementation of the service (package private class ServiceImpl), and a utility class (public class Util). In this hypothetical example, the utility class is part of the private API. It’s included in this package due to dependencies on the service implementation and is public because it’s used by other packages in the bundle. You need to export the org.foo.service package, but you don’t want to expose the Util class. In general, you should avoid such scenarios, but the following metadata snippet illustrates how you can do this with export filtering:

Export-Package: org.foo.service; version="1.0.0"; exclude:="Util"

This exported package behaves like any normal exported package as far as dependency resolution is concerned; but at execution time, the framework filters the Util class from the package so importers can’t access it, as shown in figure 5.7. A bundle attempting to load the Util class receives a “class not found” exception. The value of the directive specifies only class names; the package name must not be specified, nor should the .class portion of the class file name. The * character is also supported as a wildcard, so it’s possible to exclude all classes with names matching *Impl, for example.

Figure 5.7. Bundle A exports the org.foo.service package but excludes the Util class. When bundle B imports the org.foo.service package from bundle A, it can only access the public Service class.

In some cases, it may be easier to specify which classes are allowed instead of which ones are disallowed. For those situations, the include directive is available. It specifies which classes the exported package should expose. The default value for the include directive is *, and the default value for the exclude directive is an empty string. You can also specify both the include and exclude directive for a given exported package. A class is visible only if it’s matched by an entry in the include directive and not matched by any entry in the exclude directive.

You should definitely strive to separate your public and private APIs into different packages so these mechanisms aren’t needed, but they’re here to get you out of a tough spot when you need them. Next, we’ll move on to another mechanism to help you manage your APIs.

5.1.5. Duplicate exports

A given bundle can see only one version of a given class while it executes. In view of this, it’s not surprising to learn that bundles aren’t allowed to import the same package more than once. What you may find surprising is that OSGi allows a bundle to export the same package more than once. For example, the following snippet of metadata is perfectly valid:

Export-Package: javax.servlet; version="2.3.0",
 javax.servlet; version="2.4.0"

How is it possible, you ask? The trick is that the bundle doesn’t contain two separate sets of classes for the two exported packages. The bundle is masquerading the same set of classes as different packages. Why would it do this? Expounding on the previous snippet, perhaps you have unmodifiable third-party bundles with explicit dependencies on javax.servlet version 2.3.0 in your application. Because version 2.4.0 is backward compatible with version 2.3.0, you can use duplicate exports to allow your bundle to stake a backward compatibility claim. In the end, all bundles requiring either version of javax.servlet can resolve, but they’ll all use the same set of classes at execution time, as shown in figure 5.8.

Figure 5.8. A bundle can export the same package multiple times, but this is only a form of masquerading. Only one set of classes exists for the package in the bundle.

As with export filtering, this is another mechanism to manage your APIs. You can take this further and combine it with some of the other mechanisms you’ve learned about in this section for additional API management techniques. For example, you generally don’t want to expose your bundle’s implementation details to everyone, but sometimes you want to expose implementation details to select bundles; this is similar to the friend concept in C++. A friend is allowed to see implementation details, but nonfriends aren’t. To achieve something like this in OSGi, you need to combine mandatory export attributes, export filtering, and duplicate exports.

To illustrate, let’s return to the example from export filtering:

Export-Package: org.foo.service; version="1.0.0"; exclude:="Util"

In this example, you excluded the Util class, because it was an implementation detail. This is the exported package your nonfriends should use. For friends, you need to export the package without filtering:

Export-Package: org.foo.service; version="1.0.0"; exclude:="Util",
 org.foo.service; version="1.0.0"

Now you have one export hiding the Util class and one exposing it. How do you control who gets access to what? That’s right: mandatory export attributes. The following complete export metadata gives you what you need:

Export-Package: org.foo.service; version="1.0.0"; exclude:="Util",
 org.foo.service; version="1.0.0"; friend="true"; mandatory:="friend"

Only bundles that explicitly import your package with the friend attribute and matching value see the Util class. Clearly, this isn’t a strong sense of friendship, because any bundle can specify the friend attribute; but at least it requires an opt-in strategy for using implementation details, signaling that the importer is willing to live with the consequences of potential breaking changes in the future.

Best practice dictates avoiding the friendship concept, because it weakens modularity. If an API is valuable enough to export, you should consider making it a public API.

In this section, we’ve covered additional capabilities that OSGi provides for exporting packages to help you deal with various uncommon use cases. Likewise, OSGi provides some additional capabilities for importing packages. In the next section, you’ll learn how to make importing packages a little more flexible, which again can provide some wiggle room when you’re trying to get legacy Java applications to work in an OSGi environment.

5.2. Loosening your imports

Explicitly declared imports are great, because explicit requirements allow you to more easily reuse code and automate dependency resolution. This gives you the benefit of being able to detect misconfigurations early rather than receiving various class-loading and class-cast exceptions at execution time. On the other hand, explicitly declared imports are somewhat constraining, because the framework uses them to strictly control whether your code can be used; if an imported package can’t be satisfied, the framework doesn’t allow the associated bundle to transition into the RESOLVED state. Additionally, to import a package, you must know the name of a package in advance, but this isn’t always possible.

What can you do in these situations? The OSGi framework provides two different mechanisms for dealing with such situations: optional and dynamic imports. Let’s look into how each of these can help, as well as compare and contrast them.

5.2.1. Optional imports

Sometimes a given package imported by a bundle isn’t strictly necessary for it to function properly. Consider an imported package for a nonfunctional purpose, like logging. The code in a given bundle may have been programmed to function properly regardless of whether a logging facility is available. To express this, OSGi provides the resolution directive for the Import-Package manifest header to mark imported packages as optional.

 

Resolution Directive

The resolution import package directive can have a value of mandatory or optional to indicate whether the imported package is required to successfully resolve the bundle.

 

Consider the following metadata snippet:

Import-Package: javax.servlet; version="2.4.0",
 org.osgi.service.log; version="1.3.0"; resolution:="optional"

This import statement declares dependencies on two packages, javax.servlet and org.osgi.service.log. The dependency on the logging package is optional, as indicated by the use of the resolution directive with the optional value. This means the bundle can be successfully resolved even if there isn’t an org.osgi.service.log package available. Attempts by the bundle to use classes from this package at execution time result in ClassNotFoundExceptions. All imported packages have a resolution associated with them, but the default value is mandatory. We’ll look at how this is used in practice in section 5.2.4, but for now let’s consider the other tool in the box: dynamic imports.

5.2.2. Dynamic imports

Certain Java programming practices make it difficult to know all the packages that a bundle may need at execution time. A prime example is locating a JDBC driver. Often the name of the class implementing the JDBC driver is a configuration property or is supplied by the user at execution time. Because your bundle can only see classes in packages it imports, how can it import an unknown package? This sort of situation arises when you deal with service provider interface (SPI) approaches, where a common interface is known in advance, but not the name of the class implementing it. To capture this, OSGi has a separate DynamicImport-Package manifest header.

 

Dynamicimport-Package

This header is a comma-separated list of packages needed at execution time by internal bundle code from other bundles, but not needed at resolve time. Because the package name may be unknown, you can use a * wildcard (matching all packages) or a trailing .* (matching sub-packages recursively).

 

You may have expected from the previous examples that dynamic imports would be handled using an import directive rather than their own manifest header, but they’re sufficiently different to warrant a separate header. In the most general sense, a dynamic import is expressed in the bundle metadata like this:

DynamicImport-Package: *

This snippet dynamically imports any package needed by the bundle. When you use the wildcard at the end of a package name (for example, org.foo.*), it matches all subpackages recursively but doesn’t match the specified root package.

Given the open-ended nature of dynamic imports, it’s important to understand precisely when in the class search order of a bundle they’re employed. They appear in the class search order as follows:

  1. Requests for classes in java. packages are delegated to the parent class loader; searching stops with either a success or failure (section 2.5.4).
  2. Requests for classes in an imported package are delegated to the exporting bundle; searching stops with either a success or failure (section 2.5.4).
  3. The bundle class path is searched for the class; searching stops if found but continues to the next step with a failure (section 2.5.4).
  4. If the package in question isn’t exported by the bundle, requests matching any dynamically imported package are delegated to an exporting bundle if one is found; searching stops with either a success or failure.

As you can see, dynamic imports are attempted only as a last resort. But when a dynamically imported package is resolved and associated with the importing bundle, it behaves just like a statically imported package. Future requests for classes in the same package are serviced in step 2.

 

Avoid the Siren’s song

Dynamic imports are alluring to new OSGi programmers because they provide behavior similar to that in standard Java programming, where everything available on the class path is visible to the bundle. Unfortunately, this approach isn’t modular and doesn’t allow the OSGi framework to verify whether dependencies are satisfied in advance of using a bundle. As a result, dynamically importing packages should be seen as bad practice, except for explicitly dealing with legacy SPI approaches.

 

You can also use dynamically imported packages in a fashion more similar to optionally imported packages by specifying additional attributes like normal imported packages

DynamicImport-Package: javax.servlet.*; version="2.4.0"

or even

DynamicImport-Package: javax.servlet; javax.servlet.http; version="2.4.0"

In the first case, all subpackages of javax.servlet of version 2.4.0 are dynamically imported, whereas in the second only the explicitly mentioned packages are dynamically imported. More precise declarations such as these often make less sense when you’re using dynamic imports, because the general use case is for unknown package names.

We apparently have two different ways to loosen bundle imports. Let’s compare and contrast them a little more closely.

5.2.3. Optional vs. dynamic imports

There are intended use cases for both optional and dynamic imports, but the functionality they provide overlaps. To better understand each, we’ll look into their similarities and differences. Let’s start with the similarities.

Similarities

Both are used to express dependencies on packages that may or may not be available at execution time. Although this is the specific use case for optional imports, it’s a byproduct of dynamic imports. Either way, this has the following impact:

  • Optional/dynamic imports never cause a bundle to be unable to resolve.
  • Your bundle code must be prepared to catch ClassNotFoundExceptions for the optionally/dynamically imported packages.

Only normally imported packages (that is, mandatory imported packages) impact bundle dependency resolution. If a mandatory imported package can’t be satisfied, the bundle can’t be resolved or used. Neither optionally nor dynamically imported packages are required to be present when resolving dependencies. For optional imports, this is the whole point: they’re optional. For dynamic imports, they aren’t necessarily optional; but because they aren’t known in advance, it’s not possible for the framework to enforce that they exist.

Because the packages may not exist in either case, the logical consequence is that the code in any bundle employing either mechanism must be prepared to catch ClassNotFoundExceptions when attempting to access classes in the optionally or dynamically imported packages. This is typically the sort of issue the OSGi framework tries to help you avoid with explicit dependencies; we shouldn’t be dealing with class-loading issues as developers.

Differences

By now, you must be wondering what the difference is between optional and dynamic imports. It has to do with when the framework tries to resolve the dependencies.

The framework attempts to resolve an optionally imported package once when the associated bundle is resolved. If the import is satisfied, the bundle has access to the package. If not, the bundle doesn’t and will never have access to the package unless it’s re-resolved. For a dynamically imported package, the framework attempts to resolve it at execution time when the bundle’s executing code tries to use a class from the package.

Further, the framework keeps trying to resolve the dynamically imported package each time the bundle’s executing code tries to use classes from it until it’s successfully resolved. If a bundle providing the dynamically imported package is ever deployed into the executing framework, the framework eventually will be able to resolve it. After the resolve is successful, the bundle is wired to the provider of the package; it behaves like a normal import from that point forward.

Let’s look at how you can use these mechanisms in a logging example, which is often an optional activity for bundles.

5.2.4. Logging example

The OSGi specification defines a simple logging service that you may want to use in your bundles, but you can’t be certain it will always be available. One way to deal with this uncertainty is to create a simple proxy logger that uses the logging service if available or prints to standard output if not.

Our first example uses an optional import for the org.osgi.service.log package. The simple proxy logger code is shown here.

Listing 5.1. Simple proxy logger using optional import

The proxy logger has a constructor that takes the BundleContext object to track log services, an init() method to create a ServiceTracker for log services, a close() method to stop tracking log services, and a log() method for logging messages. Looking more closely at the init() method, you try to use the logging package to create a ServiceTracker . Because you’re optionally importing the logging package, you surround it in a try-catch block. If an exception is thrown, you set your tracker to null; otherwise, you end up with a valid tracker.

When a message is logged, you check if you have a valid tracker . If so, you try to log to a log service. Even if you have a valid tracker, that doesn’t mean you have a log service, which is why you verify it . If you have a log service, you use it; otherwise, you log to standard output. The important point is that you attempt to probe for the log package only once, with a single call to init() from the constructor, because an optional import will never be satisfied later if it’s not satisfied already.

The bundle activator is shown in the following listing.

Listing 5.2. Bundle activator creating the proxy logger
public class Activator implements BundleActivator {
  private volatile Logger m_logger = null;
  public void start(BundleContext context) throws Exception {
    m_logger = new Logger(context);
    m_logger.log(4, "Started");
    ...
  }
  public void stop(BundleContext context) {
    m_logger.close();
  }
}

When the bundle is started, you create an instance of your proxy logger that’s used throughout the bundle for logging. Although not shown here, the bundle passes a reference or somehow provides access to the logger instance to any internal code needing a logger at execution time. When the bundle is stopped, you invoke close() on the proxy logger, which stops its internal service tracker, if necessary. The manifest for your logging bundle is

Bundle-ManifestVersion: 2
Bundle-SymbolicName: example.logger
Bundle-Activator: example.logger.Activator
Import-Package: org.osgi.framework, org.osgi.util.tracker,
 org.osgi.service.log; resolution:=optional

How would this example change if you wanted to treat the logging package as a dynamic import? The impact to the Logger class is as follows.

Listing 5.3. Simple proxy logger using dynamic import

You can no longer make your ServiceTracker member variable final, because you don’t know when it will be created. To make your proxy logger thread safe and avoid creating more than one ServiceTracker instance, you need to synchronize your entry methods . Because the logging package can appear at any time during execution, you try to create the ServiceTracker instance each time you log a message until successful. As before, if all else fails, you log to standard output. The manifest metadata is pretty much the same as before, except you use DynamicImport-Package to import the logging package:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: example.logger
Bundle-Activator: example.logger.Activator
Import-Package: org.osgi.framework, org.osgi.util.tracker
DynamicImport-Package: org.osgi.service.log

These two examples illustrate the differences between these two mechanisms. As you can see, if you plan to take advantage of the full, dynamic nature of dynamically imported packages, there’s added complexity with respect to threading and concurrency. There’s also potential overhead associated with dynamic imports, not only because of the synchronization, but also because it can be costly for the framework to try to find a matching package at execution time. For logging, which happens frequently, this cost can be great.

 

Optional imports are optional

We should point out that you can use dynamic imports in a fashion similar to optional imports. In this sense, the use of the optional import package mechanism is itself optional. For example, you can modify the metadata of the optional logger example to be a dynamic import instead, but keep the code exactly the same. If you did this, the two solutions would behave equivalently.

If this is the case, then why choose one over the other? There’s no real reason or recommendation for doing so. These two concepts overlap for historical reasons. Dynamic imports have existed since the R2 release of the OSGi specification, whereas optional imports have only existed since the R4 release. Even though optional imports overlapped dynamic imports, they were added for consistency with bundle dependencies, which were also added in R4 and can also be declared as optional.

 

We’ve finished covering the advanced capabilities for importing packages, but there’s still a related concept provided by OSGi for declaring dependencies. In some situations, such as legacy situations where modules are tightly coupled or contain a given package split across modules, importing a specific package isn’t sufficient. For these situations, OSGi allows you to declare dependencies on specific bundles. We’ll look at how this works next.

5.3. Requiring bundles

In section 5.1.2, we discussed how implicit export attributes allow bundles to import packages from a specific bundle. The OSGi specification also supports a module-level dependency concept called a required bundle that provides a similar capability. In chapter 2, we discussed a host of reasons why package-level dependencies are preferred over module-level dependencies, such as them being more flexible and fine-grained. We won’t rehash those general issues. But there is one particular use case where requiring bundles may be necessary in OSGi: if you must deal with split packages.

 

Split Package

A split package is a Java package whose classes aren’t contained in a single JAR but are split across multiple JAR files. In OSGi terms, it’s a package split across multiple bundles.

 

In standard Java programming, packages are generally treated as split; the Java class path approach merges all packages from different JAR files on the class path into one big soup. This is anathema to OSGi’s modularization model, where packages are treated as atomic (that is, they can’t be split).

When migrating to OSGi from a world where split packages are common, we’re often forced to confront ugly situations. But even in the OSGi world, over time a package may grow too large and reach a point where you can logically divide it into disjoint functionality for different clients. Unfortunately, if you break up the existing package and assign new disjoint package names, you break all existing clients. Splitting the package allows its disjoint functionality to be used independently; but for existing clients, you still need an aggregated view of the package.

This gives you an idea of what a split package is, but how does this relate to requiring bundles? This will become clearer after we discuss what it means to require a bundle and introduce a use case for doing so.

5.3.1. Declaring bundle dependencies

The big difference between importing a package and requiring a bundle is the scope of the dependency. Whereas an imported package defines a dependency from a bundle to a specific package, a required bundle defines a dependency from a bundle to every package exported by a specific bundle. To require a bundle, you use the Require-Bundle manifest header in the requiring bundle’s manifest file.

 

Require-Bundle

This header consists of a comma-separated list of target bundle symbolic names on which a bundle depends, indicating the need to access all packages exported by the specifically mentioned target bundles.

 

You use the Require-Bundle header to specify a bundle dependency in a manifest, like this:

Require-Bundle: A; bundle-version="[1.0.0,2.0.0)"

Resolving required bundles is similar to imported packages. The framework tries to satisfy each required bundle; if it’s unable to do so, the bundle can’t be used. The framework resolves the dependency by searching the installed bundles for ones matching the specified symbolic name and version range. Figure 5.9 shows a resolved bundle dependency.

Figure 5.9. Requiring a bundle is similar to explicitly importing every package exported by the target bundle.

To a large degree, requiring bundles is just a brittle way to import packages, because it specifies who instead of what. The significant difference is how it fits into the overall class search order for the bundle, which is as follows:

  1. Requests for classes in java. packages are delegated to the parent class loader; searching stops with either a success or failure (section 2.5.4).
  2. Requests for classes in an imported package are delegated to the exporting bundle; searching stops with either a success or failure (section 2.5.4).
  3. Requests for classes in a package from a required bundle are delegated to the exporting bundle; searching stops if found but continues with the next required bundle or the next step with a failure.
  4. The bundle class path is searched for the class; searching stops if found but continues to the next step with a failure (section 2.5.4).
  5. If the package in question isn’t exported or required, requests matching any dynamically import package are delegated to an exporting bundle if one is found; searching stops with either a success or failure (section 5.2.2).

Packages from required bundles are searched only if the class wasn’t found in an imported package, which means imported packages override packages from required bundles. Did you notice another important difference between imported packages and packages from required bundles in the search order? If a class in a package from a required bundle can’t be found, the search continues to the next required bundle in declared order or the bundle’s local class path. This is how Require-Bundle supports split packages, which we’ll discuss in more detail in the next subsection. First, let’s look at the remaining details of requiring bundles.

As we briefly mentioned in section 5.2.4, it’s also possible to optionally require a bundle using the resolution directive:

Require-Bundle: A; bundle-version="[1.0.0,2.0.0)"; resolution:="optional"

The meaning is the same as when you optionally import packages, such as not impacting dependency resolution and the need to catch ClassNotFoundExceptions when your bundle attempts to use potentially missing classes. It’s also possible to control downstream visibility of packages from a required bundle using the visibility directive, which can be specified as private by default or as reexport. For example:

Require-Bundle: A; bundle-version="[1.0.0,2.0.0)"; visibility:="reexport"

This makes the required bundle dependency transitive. If a bundle contains this, any bundle requiring it also sees the packages from bundle A (they’re re-exported). Figure 5.10 provide a pictorial example.

Figure 5.10. When bundle B requires bundle A with reexport visibility, any packages exported from A become visible to any bundles requiring B.

 

Warning

There are few, if any, good reasons to use Require-Bundle with reexport visibility. This mechanism isn’t very modular, and using it results in brittle systems with high coupling.

 

Now let’s return our attention to how Require-Bundle supports aggregating split packages.

5.3.2. Aggregating split packages

Avoiding split packages is the recommended approach in OSGi, but occasionally you may run into a situation where you need to split a package across bundles. Require-Bundle makes such situations possible. Because class searching doesn’t stop when a class isn’t found for required bundles, you can use Require-Bundle to search for a class across a split package by requiring multiple bundles containing its different parts.

For example, assume you have a package org.foo.bar that’s split across bundles A and B. Here’s a manifest snippet from bundle A:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: A
Bundle-Version: 2.0.0
Export-Package: org.foo.bar; version="2.0.0"

Here is a manifest snippet from bundle B:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: B
Bundle-Version: 2.0.0
Export-Package: org.foo.bar; version="2.0.0"

Both bundles claim to export org.foo.bar, even though they each offer only half of it. (Yes, this is problematic, but we’ll ignore that for now and come back to it shortly.) Now, if you have another bundle that wants to use the entire org.foo.bar package, it has to require both bundles. The bundle metadata may look something like this:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: C
Bundle-Version: 1.0.0
Require-Bundle: A; version="[2.0.0,2.1.0)", B; version="[2.0.0,2.1.0)"

When code from bundle C attempts to load a class from the org.foo.bar package, it follows these steps:

1.  It delegates to bundle A. If the request succeeds, the class is returned; but if it fails, the code goes to the next step.

2.  It delegates to bundle B. If the request succeeds, the class is returned; but if it fails, the code goes to the next step.

3.  It tries to load the class from bundle C’s local class path.

The last step allows org.foo.bar to be split across the required bundles as well as the requiring bundle. Because searching continues across all required bundles, bundle C is able to use the whole package.

What about a bundle wanting to use only one half of the package? Instead of requiring both bundles, it can require just the bundle containing the portion it needs. Sounds reasonable; but does this mean that after you split a package, you’re stuck with using bundle-level dependencies and can no longer use package-level dependencies? No, it doesn’t, but it does require some best practice recommendations.

Handling Split Packages with Import-Package

If another bundle wants to use Import-Package to access the portion of the package contained in bundle B, it can do something like this:

Import-Package: org.foo.bar; version="2.0.0"; bundle-symbolic-name="B"

This is similar to using Require-Bundle for the specific bundle. If you add an arbitrary attribute to each exported split package—called split, for example—you can use it to indicate a part name instead. Assume you set split equal to part1 for bundle A and part2 for bundle B. You can import the portion from B as follows:

Import-Package: org.foo.bar; version="2.0.0"; split="part2"

This has the benefit of being a little more flexible, because if you later change which bundle contains which portion of the split package, it won’t break existing clients. What about existing clients that were using Import-Package to access the entire org.foo.bar package? Is it still possible? It’s likely that existing client bundles are doing the following:

Import-Package: org.foo.bar; version="2.0.0"

Will they see the entire package if it’s now split across multiple bundles? No. How can the framework resolve this dependency? The framework has no understanding of split packages as far as dependency resolution is concerned. If bundles A and B are installed and another bundle comes along with the above import declaration, the framework treats A and B as both being candidates to resolve dependency. It chooses one following the normal rules of priority for multiple matching candidates. Clearly, no matter which candidate it chooses, the resulting solution will be incorrect.

To avoid such situations, you need to ensure that your split package portions aren’t accidentally used by the framework to resolve an import for the entire package. But how? Mandatory attributes can help. You can rewrite bundle A’s metadata like so:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: A
Bundle-Version: 2.0.0
Export-Package: org.foo.bar; version="2.0.0"; split="part1";
 mandatory:="split"

Likewise for bundle B, but with split equal to part2. Now for a bundle to import either part of the split package, they must explicitly mention the part they wish to use. But what about an existing client bundle wanting to import the whole package? Because its import doesn’t specify the mandatory attribute, it can’t be resolved. You need some way to reconstruct the whole package and make it available for importing; OSGi allows you to create a facade bundle for such a purpose. To make bundle C a facade bundle, you change its metadata to be

Bundle-ManifestVersion: 2
Bundle-SymbolicName: C
Bundle-Version: 1.0.0
Require-Bundle: A; version="[2.0.0,2.1.0)", B; version="[2.0.0,2.1.0)"
Export-Package: org.foo.bar; version="2.0.0"

The only change is the last line where bundle C exports org.foo.bar, which is another form of re-exporting a package. In this case, it aggregates the split package by requiring the bundles containing the different parts, and it re-exports the package without the mandatory attribute. Now any client importing org.foo.bar will be able to resolve to bundle C and have access to the whole package.

 

Summarizing split package best practices

In short, if you must use a split package, make sure you follow these steps:

  • Always export split packages with a mandatory attribute to avoid unsuspecting bundles from using them.
  • Use either Require-Bundle or Import-Package plus the mandatory attribute to access parts of the split packages.
  • To provide access to the whole package, create a facade bundle that requires the bundles containing the package parts and exports the package in question.

 

Admittedly, this isn’t the most intuitive or straightforward way to deal with split packages. This approach wasn’t intended to make them easy to use, because they’re best avoided; but it does make it possible in those situations where you have no choice.

Despite these dire-sounding warnings, OSGi provides another way of dealing with split packages, called bundle fragments. We’ll talk about those shortly, but first we’ll discuss some of the issues surrounding bundle dependencies and split packages.

5.3.3. Issues with bundle dependencies

Using Import-Package and Export-Package is the preferred way to share code because they couple the importer and exporter to a lesser degree. Using Require-Bundle entails much higher coupling and lower cohesion between the importer and the exporter and suffers from other issues, such as the following:

  • Mutable exports —Requiring bundles are impacted by changes to the exports of the required bundle, which introduce another form of breaking change to consider. Such changes aren’t always easily apparent because the use of reexport visibility can result in chains of required bundles where removal of an export in upstream required bundles breaks all downstream requiring bundles.
  • Shadowing —Because class searching continues across all required bundles and the requiring bundle’s class path, it’s possible for content in some required bundles to shadow other required bundle content and the content of the requiring bundle itself. The implications of this aren’t always obvious, especially if some bundles are optionally required.
  • Ordering —If a package is split across multiple bundles, but they contain overlapping classes, the declared order of the Require-Bundle header is significant. All bundles requiring the bundles with overlapping content must declare them in the same order, or their view of the package will be different. This is similar to traditional class path ordering issues.
  • Completeness —Even though it’s possible to aggregate split packages using a facade bundle, the framework has no way to verify whether an aggregated package is complete. This becomes the responsibility of the bundle developer.
  • Restricted access —An aggregated split package isn’t completely equivalent to the unsplit package. Each portion of the split package is loaded by its containing bundle’s class loader. In Java, classes loaded by different class loaders can’t access package-private members and types, even if they’re in the same package.

This is by no means an exhaustive list of issues, but it gives you some ideas of what to look out for when using Require-Bundle and (we hope) dissuades you from using it too much.

Enough of the scare tactics. So far, we’ve introduced you to some of the more advanced features of managing OSGi dependencies, including importing exports, implicit export attributes, mandatory export attributes, export filtering, duplicate exports, optional and dynamic imports, and requiring bundles. These tools allow you to solve some of the more complex edge cases found when migrating a classic Java application to an OSGi environment. That must be it—we must have covered every possible mechanism of specifying dependencies, right? Not quite. There’s one more curve ball to be thrown into the mix: bundle fragments. Fragments are another way to deal with split packages by allowing the content of a bundle to be split across multiple, subordinate bundle JAR files.

5.4. Dividing bundles into fragments

Although splitting packages isn’t a good idea, occasionally it does make sense, such as with Java localization. Java handles localization by using java.util.ResourceBundles (which have nothing to do with OSGi bundles) as a container to help you turn locale-neutral keys into locale-specific objects. When a program wants to convert information into the user’s preferred locale, it uses a resource bundle to do so. A Resource-Bundle is created by loading a class or resource from a class loader using a base name, which ultimately defines the package containing the class or resource for the ResourceBundle. This approach means you typically package many localizations for different locales into the same Java package.

If you have lots of localizations or lots of information to localize, packaging all your localizations into the same OSGi bundle can result in a large deployment unit. Additionally, you can’t introduce new localizations or fix mistakes in existing ones without releasing a new version of the bundle. It would be nice to keep localizations separate; but unlike the split package support of Require-Bundle, these split packages generally aren’t useful without the bundle to which they belong. OSGi provides another approach to managing these sorts of dependencies through bundle fragments. We’ll come back to localization shortly when we present a more in-depth example, but first we’ll discuss what fragments are and what you can do with them.

5.4.1. Understanding fragments

If you recall the modularity discussion in chapter 2, you know there’s a difference between logical modularity and physical modularity. Normally, in OSGi, a logical module and a physical module are treated as the same thing; a bundle is a physical module as a JAR file, but it’s also the logical module at execution time forming an explicit visibility encapsulation boundary. Through fragments, OSGi allows you to break a single logical module across multiple physical modules. This means you can split a single logical bundle across multiple bundle JAR files.

Breaking a bundle into pieces doesn’t result in a handful of peer bundles; instead, you define one host bundle and one or more subordinate fragment bundles. A host bundle is technically usable without fragments, but the fragments aren’t usable without a host. Fragments are treated as optional host-bundle dependencies by the OSGi framework. But the host bundle isn’t aware of its fragments, because it’s the fragments that declare a dependency on the host using the Fragment-Host manifest header.

 

Fragment-Host

This header specifies the single symbolic name of the host bundle on which the fragment depends, along with an optional bundle version range.

 

A fragment bundle uses the Fragment-Host manifest header like this:

Fragment-Host: org.foo.hostbundle; bundle-version="[1.0.0,1.1.0)"

The Fragment-Host header is somewhat confusingly named, because it seems to be declaring the bundle as a host; it should be read as “require fragment host.” Although this header value follows the common OSGi syntax, you can’t specify multiple symbolic names. A fragment is limited to belonging to one host bundle, although it may be applicable to a range of host versions. Note that you don’t need to do anything special to define a bundle as a host; any bundle without a Fragment-Host header is a potential host bundle. Likewise, any bundle with a Fragment-Host header is a fragment.

You now understand the relationship between a host and its fragments, but how do they work together? When the framework resolves a bundle, it searches the installed bundles to see if there are any fragments for the bundle being resolved. If so, it merges the fragments into the host bundle. This merging happens in two different ways:

  • Physically —The content and metadata from the fragments are conceptually merged with the host’s content and metadata.
  • Logically —Rather than giving each fragment its own class loader, the framework attaches the fragment content to the host’s class loader.

The first form of merging recombines the split physical pieces of the logical bundle, and the second form creates a single logical bundle because OSGi uses a single class loader per logical bundle to achieve encapsulation.

 

Fragments and package-private access

As a technical side note, Java only allows package-private access to classes loaded by the same class loader. Two classes in the same package, but loaded by two different class loaders, can’t access each others’ package-private members. By loading fragment classes with the host’s class loader, the fragment classes are properly recombined to avoid this issue. This isn’t true for split packages accessed through Require-Bundle. This isn’t always important, but the distinction between these two forms of support for split packages is worth understanding.

 

Returning to the discussion about resolving a bundle, if the bundle being resolved has fragments, the framework merges their metadata with the host’s and resolves the bundle as normal. Figure 5.11 illustrates the before- and after effects of the merging process.

Figure 5.11. a) The host and fragment bundles are deployed as independent bundles in the framework. b) When the framework resolves the host, it effectively merges the fragment’s content and metadata into the host bundle.

In addition to merging the exported and imported packages and required bundles, the bundle class paths are also merged. This impacts the overall class search order for the bundle, like this:

  1. Requests for classes in java. packages are delegated to the parent class loader; searching stops (section 2.5.4).
  2. Requests for classes in an imported package are delegated to the exporting bundle; searching stops (section 2.5.4).
  3. Requests for classes in a package from a required bundle are delegated to the exporting bundle; searching continues with a failure (section 5.3.1).
  4. The host bundle class path is searched for the class; searching continues with a failure (section 2.5.4).
  5. Fragment bundle class paths are searched for the class in the order in which the fragments were installed. Searching stops if found but continues through all fragment class paths and then to the next step with a failure.
  6. If the package in question isn’t exported or required, requests matching any dynamically import package are delegated to an exporting bundle if one is found. Searching stops with either a success or a failure (section 5.2.2).

This is the complete bundle class search order, so you may want to mark this page for future reference! This search order makes it clear how fragments support split packages, because the host and all fragment class paths are searched until the class is found.

 

Fragments and the Bundle-ClassPath

The bundle class path search order seems fairly linear, but fragments do introduce one exception. When specifying a bundle class path, you can specify embedded JAR files, such as

Bundle-ClassPath: .,specialized.jar,default.jar

Typically, you’d expect both of these JAR files to be contained in the JAR file of the bundle declaring them, but this need not be the case. If fragments aren’t involved, the framework ignores a non-existent JAR file on the bundle class path. But if the bundle has fragments attached to it, the framework searches the fragments for the specified JAR files if they don’t exist in the host bundle.

In the example, imagine that the host contains default.jar but doesn’t contain specialized.jar, which is contained in an attached fragment. The effect this has on the class search order is that the specified fragment content is searched before some of the host bundle content.

Sometimes this is useful if you want to provide default functionality in the host bundle but be able to override it on platforms where you have specialized classes (for example, using native code). You can also use this approach to provide a means for issuing patches to bundles after the fact, but in general it’s better to update the whole bundle.

 

Some final issues regarding fragments: Fragments are allowed to have any metadata a normal bundle can have except Bundle-Activator. This makes sense because fragments can’t be started or stopped and can only be used when combined with the host bundle. Attaching a fragment to a host creates a dependency between the two, which is similar to the dependencies created between two bundles via Import-Package or Require-Bundle. This means if either bundle is updated or uninstalled, the other bundle is impacted, and any refreshing of the one will likely lead to refreshing of the other.

We started this foray into fragments discussing localization, because it’s the main use case for them. Next, we’ll look at an example of how to use fragments for this purpose.

5.4.2. Using fragments for localization

To see how you can use fragments to localize an application, let’s return to the service-based paint program from chapter 4. The main application window is implemented by the PaintFrame class. Recall its design: PaintFrame doesn’t have any direct dependencies on the OSGi API. Instead, it uses a ShapeTracker class to track SimpleShape services in the OSGi service registry and inject them into the PaintFrame instance. ShapeTracker injects services into the PaintFrame using its addShape() method, as shown in the following listing.

Listing 5.4. Method used to inject shapes into PaintFrame object
public void addShape(String name, Icon icon, SimpleShape shape) {
  m_shapes.put(name, new ShapeInfo(name, icon, shape));
  JButton button = new JButton(icon);
  button.setActionCommand(name);
  button.setToolTipText(name);
  button.addActionListener(m_reusableActionListener);

  if (m_selected == null) {
    button.doClick();
  }

  m_toolbar.add(button);
  m_toolbar.validate();
  repaint();
}

The addShape() method is invoked with the name, icon, and service object of the SimpleShape implementation. The exact details aren’t important, but the shape is recorded in a data structure, a button is created for it, its name is set as the button’s tool tip, and, after a few other steps, the associated button is added to the toolbar. The tool tip is textual information displayed to users when they hover the mouse over the shape’s toolbar icon. It would be nice if this information could be localized.

You can take different approaches to localize the shape name. One approach is to define a new service property that defines the ResourceBundle base name. This way, shape implementations can define their localization base name, much as they use service properties to indicate the name and icon. In such an approach, the PaintFrame.addShape() must be injected with the base name property so it can perform the localization lookup. This probably isn’t ideal, because it exposes implementation details.

Another approach is to focus on where the shape’s name is set in the first place: in the shape implementation’s bundle activator. The following listing shows the activator of the circle implementation.

Listing 5.5. Original circle bundle activator with hardcoded name
public class Activator implements BundleActivator {
  public void start(BundleContext context) {
    Hashtable dict = new Hashtable();
    dict.put(SimpleShape.NAME_PROPERTY, "Circle");
    dict.put(SimpleShape.ICON_PROPERTY,
      new ImageIcon(this.getClass().getResource("circle.png")));
    context.registerService(
      SimpleShape.class.getName(), new Circle(), dict);
  }

  public void stop(BundleContext context) {}
}

The hardcoded shape name is assigned to the service property dictionary, and the shape service is registered. The first thing you need to do is change the hardcoded name into a lookup from a ResourceBundle. This code shows the necessary changes.

Listing 5.6. Modified circle bundle activator with ResourceBundle name lookup

You modify the activator to look up the shape name using the key constant defined at from a ResourceBundle you create , whose resulting value is assigned to the service properties. Even though we won’t go into the complete details of using ResourceBundle objects, the important part in this example is when you define it. You specify the base name of org.foo.shape.circle.resource.Localize. By default, this refers to a Localize.properties file in the org.foo.shape.circle.resource package, which contains a default mapping for your name key. You need to modify the circle implementation to have this additional package, and you add the Localize.properties file to it with the following content:

CIRCLE_NAME=Circle

This is the default mapping for the shape name. If the example was more complicated, you’d have many more default mappings for other terms. To provide other mappings to other languages, you need to include them in this same package, but in separate property files named after the locales’ country codes. For example, the country code for Germany is DE, so for its localization you create a file called Localize_de.properties with the following content:

CIRCLE_NAME=Kreis

You do this for each locale you want to support. Then, at execution time, when you create your ResourceBundle, the correct property file is automatically selected based on the locale of the user’s computer.

This all sounds nice; but if you have a lot of information to localize, you need to include all this information in your bundle, which can greatly increase its size. Further, you have no way of adding support for new locales without releasing a new version of your bundle. This is where fragments can help, because you can split the resource package into different fragments. You keep the default localization in your circle implementation, but all other localizations are put into separate fragments. You don’t need to change the metadata of your circle bundle, because it’s unaware of fragments, but the content of your circle bundle becomes

META-INF/MANIFEST.MF
META-INF/
org/
org/foo/
org/foo/shape/
org/foo/shape/circle/
org/foo/shape/circle/Activator.class
org/foo/shape/circle/Circle.class
org/foo/shape/circle/circle.png
org/foo/shape/circle/resource/
org/foo/shape/circle/resource/Localize.properties

For this example, you’ll create a German localization fragment bundle for the circle using the property file shown earlier. The metadata for this fragment bundle is

Bundle-ManifestVersion: 2
Bundle-Name: circle.resource-de
Bundle-SymbolicName: org.foo.shape.circle.resource-de
Bundle-Version: 5.0
Fragment-Host: org.foo.shape.circle; bundle-version="[5.0,6.0)"

The important part of this metadata is the last line, which declares it as a fragment of the circle bundle. The content of the fragment bundle is simple:

META-INF/MANIFEST.MF
META-INF/
org/
org/foo/
org/foo/shape/
org/foo/shape/circle/
org/foo/shape/circle/resource/
org/foo/shape/circle/resource/Localize_de.properties

It only contains a resource file for the German translation, which you can see is a split package with the host bundle. You can create any number of localization fragments following this same pattern for your other shapes (square and triangle). Figure 5.12 shows the paint program with the German localization fragments installed.

Figure 5.12. Paint program with installed German localization fragments

To run this example, go into the chapter05/paint-example/ directory of the companion code and type ant to build and java –Duser.language=de -jar launcher.jar bundles/ to run it using a German locale. With this approach, you only need to deploy the required localization fragments along with your shape implementations, and you can create new localizations or update existing ones without releasing new versions of the shape bundles.

We’ve now covered all major aspects of the OSGi module layer! As you can see, tools are available to help you deal with virtually any scenario the Java language can throw at you. But we have one more trick up our sleeves: the OSGi specification does a pretty good job of dealing with native code that runs outside of the Java environment. We’ll look at this and how to deal with general factors relating to the JVM environment in the next and final section of this chapter.

5.5. Dealing with your environment

Although Java has been fairly successful at attaining its original vision of “write once, run everywhere,” there are still situations where it’s not entirely able to achieve this goal. One such situation is the myriad of Java platforms, such as Java ME and the different versions of Java SE. If you develop a bundle with requirements for a specific Java platform—for example, if you use classes from the java.util.concurrent package—you need a Java 5.0 JVM or above. Another situation is if you need to natively integrate with the underlying operating system, as may be necessary if you must communicate directly with underlying hardware.

As you may expect, in both these situations the OSGi specification provides mechanisms to explicitly declare these scenarios in your bundles to allow an OSGi framework to do whatever is necessary at execution time. In this section, we’ll cover both of these topics, starting with the former.

5.5.1. Requiring execution environments

If you develop a bundle with a dependency on specific Java execution environments, what happens if this bundle executes in an unintended environment? Most likely, you’ll get various exceptions for missing classes or methods and/or faulty results. If you have a bundle with specific execution environment requirements, you must explicitly declare these requirements to avoid people unknowingly trying to use your bundle in invalid environments. The OSGi specification defines an execution environment concept for just this purpose. Like all bundle metadata, you use a manifest header to define it. In this case, it’s a manifest header with a long name: Bundle-RequiredExecutionEnvironment.

 

Bundle-Requiredexecutionenvironment

This header specifies a comma-delimited list of supported execution environments.

 

The OSGi specification defines standard values for the common execution environments; table 5.1 lists them.

Table 5.1. OSGi defined standard execution environment names

Name

Description

CDC-1.1/Foundation-1.1 Equivalent to J2ME Foundation Profile
CDC-1.1/PersonalBasis-1.1 J2ME Personal Basis Profile
CDC-1.1/PersonalJava-1.1 J2ME Personal Java Profile
J2SE-1.2 Java 2 SE 1.2.x
J2SE-1.3 Java 2 SE 1.3.x
J2SE-1.4 Java 2 SE 1.4.x
J2SE-1.5 Java 2 SE 1.5.x
JavaSE-1.6 Java SE 1.6.x
OSGi/Minimum-1.2 Minimal required set of Java API that allows an OSGi framework implementation

Bundles should list all known execution environments on which they can run, which may look something like this:

Bundle-RequiredExecutionEnvironment: J2SE-1.4,J2SE-1.5,JavaSE-1.6

This specific example indicates that the bundle runs only on modern Java platforms. If a bundle lists a limited execution environment, such as CDC-1.1/Foundation-1.1, it shouldn’t use classes or methods that don’t exist in the declared execution environment. The framework doesn’t verify this claim; it only ensures that the bundle isn’t resolvable on incompatible execution environments.

 

Resolve-time, not install-time enforcement

Pay special attention to the previous sentence. It’s possible to install a bundle on a given execution environment even if it’s not compatible with it, but you won’t be able to resolve it unless its required execution environment matches the current one. This is tied to the bundle’s resolved state because it’s possible for the execution environment to change over time. For example, you may switch between different versions of Java on subsequent executions of the framework. This way, any cached bundles not matching the current execution environment will essentially be ignored.

 

A given framework implementation can claim to provide more than one execution environment, because in most cases the Java platform versions are backward compatible. It’s possible to determine the framework’s supported execution environments by retrieving the org.osgi.framework.executionenvironment property from Bundle-Context.getProperty().

Now that you understand how to declare your bundles’ required execution environments, let’s look at how to handle bundles with native code.

5.5.2. Bundling native libraries

Java provides a nice platform and language combination, but it’s not always possible to stay purely in Java. In some situations, you need to create native code for the platform on which Java is running. Java defined Java Native Interface (JNI) precisely for this purpose; JNI is how Java code calls code written in other programming languages (such as C or C++) for specific operating systems (such as Windows or Linux). A complete discussion of how JNI works is outside the scope of this book, but the following list provides the highlights:

  • Native code is integrated into Java as a special type of method implementation. A Java class can declare a method as having a native implementation using the native method modifier.
  • Classes with native code are compiled normally. But after compilation, the javah command is used to generate C header and stub files, which are used to create the native method implementations.
  • The native code is compiled into a library in a platform-specific way for its target operating system.
  • The original Java class with the native method includes code to invoke System. loadLibrary(), typically in a static initializer, to load the native library when the class is loaded in the Java VM.
  • Other classes can invoke the native method as if it were a normal method, and the Java platform takes care of the native invocation details.

Although it’s fairly straightforward to use native code in Java, it’s best to avoid it if possible. Native code doesn’t benefit from the garbage collector and suffers from the typical pointer issues associated with all native code. Additionally, it hinders Java’s “write once, run everywhere” goal, because it ties the class to a specific platform. Still, in those cases where it’s absolutely necessary, it’s nice to know that OSGi supports it. OSGi even simplifies it a little.

One of the downsides of native code is the fact that you end up with an additional artifact to deploy along with your classes. To make matters worse, what you need to do with the native library differs among operating systems; for example, you typically need to put native libraries in specific locations in the file system so they can be found at execution time (for example, somewhere on the binary search path). OSGi native code support simplifies these issues by

  • Allowing you to embed your native library directly into your bundle JAR file
  • Allowing you to embed multiple native libraries for different target platforms
  • Automatically handling execution-time discovery of native code libraries

When you embed a native library into your bundle, you must tell the OSGi framework about it. As with all other modularity aspects, you do so in the bundle metadata using the Bundle-NativeCode manifest header. With this header, you can specify the set of contained native libraries for each platform your bundle supports. The grammar is as follows:

Bundle-NativeCode ::= nativecode (',' nativecode)* (',' optional)?
nativecode        ::= path (';' path)* (';' parameter)+
optional          ::= '*'

The parameter is one of the following:

  • osname—Name of the operating system
  • osversion—Operating system version range
  • processor—Processor architecture
  • language—ISO code for a language
  • selection-filter—LDAP selection filter

For example, if you have a bundle with native libraries for Windows XP, you may have a native code declaration like this one:

Bundle-NativeCode: lib/math.dll; lib/md5.dll; osname=WindowsXP;
 processor=x86

This is a semicolon-delimited list, where the leading entries not containing an = character are interpreted as file entries in the bundle JAR file and the remaining entries with an = character are used to determine if the native library clause matches the current platform. In this case, you state the bundle has two native libraries for Windows XP on the x86 architecture.

If Bundle-NativeCode is specified, there must be a matching header for the platform on which the bundle is executing; otherwise, the framework won’t allow the bundle to resolve. In other words, if a bundle with the previous native code header was installed on a Linux box, the framework won’t allow the bundle to be used.

If these same libraries also work on Vista, you can specify this as follows:

Bundle-NativeCode: lib/math.dll; lib/md5.dll; osname=WindowsXP;
 osname=WindowsVista; processor=x86

In cases where the parameter is repeated, the framework treats this like a logical OR when matching, so these native libraries match Windows XP or Windows Vista.

If your bundle also has native libraries for Linux, you can specify that as follows:

Bundle-NativeCode: lib/math.dll; lib/md5.dll; osname=WindowsXP;
 osname=WindowsVista; processor=x86, lib/libmath.so; osname=Linux;
 osprocessor=x86

You separate different platforms using a comma instead of a semicolon. Notice also that the native libraries don’t need to be parallel. In this example, you have two native libraries for the Windows platform but only one for Linux. This bundle is now usable on Windows XP, Windows Vista, and Linux on the x86 architecture, but on any other platform the framework won’t resolve it.

In some cases, you may have either optional native libraries or a non-optimized Java implementation for unsupported platforms. You can denote this using the optional clause, like this:

Bundle-NativeCode: lib/math.dll; lib/md5.dll; osname=WindowsXP;
 osname=WindowsVista; processor=x86, lib/libmath.so; osname=Linux;
 osprocessor=x86, *

The * at the end acts as a separate platform clause that can match any platform, so this bundle is usable on any platform.

The process of how native libraries are made available when the classes containing native methods perform System.loadLibrary() is handled automatically by the framework, so you don’t need to worry about it. Even though it isn’t often necessary, it’s fairly easy to use this mechanism to create bundles with native code.

That’s it! You’ve now been introduced to many of the specialized features of the OSGi module layer. Let’s review what you’ve learned in this chapter.

5.6. Summary

You learned that the OSGi module layer provides many additional mechanisms to deal with collaborative, dynamic, and legacy situations, such as the following:

  • A bundle may import a package it exports to make its export substitutable for purposes of broadening collaboration among bundles.
  • Exported packages have bundle symbolic name and version attributes implicitly attached to them, which can be useful if you need to import a package from a specific bundle.
  • Exported packages may have mandatory attributes associated with them, which must be specified by an importer for it to be wired to the exported package.
  • It’s possible to export the same package more than once with different attributes, which is sometimes helpful if a bundle wishes to masquerade as more than one version of a package.
  • The framework ignores optionally imported packages if no exporters are present when the importing bundle is resolved.
  • The framework only attempts to resolve dynamically imported packages when the importing bundle tries to use a class in the dynamically imported package. Repeated attempts to use a class in the dynamically imported package will result in repeated attempts to resolve the package until successful.
  • It’s possible to require a bundle, rather than importing specific packages, which wires you to everything the target bundle exports. This is typically useful when aggregating split packages.
  • Bundle fragments support splitting a bundle into multiple optional JAR files, which is helpful in such situations as localization.
  • Bundles with dependencies on specific Java platforms can declare these dependencies with required execution environments.
  • Bundles can include native libraries to integrate platform-specific functionality.

Most of these mechanisms are intended for specific use cases and shouldn’t be overused to avoid less modular solutions.

We’ve now covered all the major functionality of OSGi specification, which closes out this part of the book. In the next section, we’ll look into more practical matters that crop up while trying to use and build applications on top of OSGi technology.

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

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