程式扎記: [ OSGi In Action ] Introducing OSGi : Delving deeper into modularity (2)

標籤

2011年8月3日 星期三

[ OSGi In Action ] Introducing OSGi : Delving deeper into modularity (2)

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. 

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 : 
 

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 useRequire-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 importingorg.foo.bar will be able to resolve to bundle C and have access to the whole package. 
 

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. 

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. UsingRequire-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. 

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 usingjava.util.ResourceBundles (which have nothing to do with OSGi bundles) as a container to help you turn localeneutral 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. 

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 fragmentbundles. 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)"

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 aFragment-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. 
 

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. 
 

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. 

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 ShapeTrackerclass to track SimpleShape services in the OSGi service registry and inject them into the PaintFrame instance. ShapeTracker injects services into the PaintFrameusing its addShape() method, as shown in the following listing. 
- Listing 5.4 Method used to inject shapes into PaintFrame object
  1. public void addShape(String name, Icon icon, SimpleShape shape) {  
  2.     m_shapes.put(name, new ShapeInfo(name, icon, shape));  
  3.     JButton button = new JButton(icon);  
  4.     button.setActionCommand(name);  
  5.     button.setToolTipText(name);  
  6.     button.addActionListener(m_reusableActionListener);  
  7.     if (m_selected == null) {  
  8.         button.doClick();  
  9.     }  
  10.     m_toolbar.add(button);  
  11.     m_toolbar.validate();  
  12.     repaint();  
  13. }  

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
  1. public class Activator implements BundleActivator {  
  2.     public void start(BundleContext context) {  
  3.         Hashtable dict = new Hashtable();  
  4.         dict.put(SimpleShape.NAME_PROPERTY, "Circle");  
  5.         dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass()  
  6.                 .getResource("circle.png")));  
  7.         context.registerService(SimpleShape.class.getName(), new Circle(), dict);  
  8.     }  
  9.   
  10.     public void stop(BundleContext context) {  
  11.     }  
  12. }  

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
  1. public class Activator implements BundleActivator {  
  2.     public static final String CIRCLE_NAME = "CIRCLE_NAME";  
  3.   
  4.     public void start(BundleContext context) {  
  5.         ResourceBundle rb = ResourceBundle  
  6.                 .getBundle("org.foo.shape.circle.resource.Localize");  
  7.         Hashtable dict = new Hashtable();  
  8.         dict.put(SimpleShape.NAME_PROPERTY, rb.getString(CIRCLE_NAME));  
  9.         dict.put(SimpleShape.ICON_PROPERTY, new ImageIcon(this.getClass()  
  10.                 .getResource("circle.png")));  
  11.         context.registerService(SimpleShape.class.getName(), new Circle(), dict);  
  12.     }  
  13.   
  14.     public void stop(BundleContext context) {  
  15.     }  
  16. }  

You modify the activator to look up the shape name using the key constant "CIRCLE_NAME" in a ResourceBundle you create C, 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/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 

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. 

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!