程式扎記: [ OSGi In Action ] Introducing OSGi : Mastering modularity (3)

標籤

2011年7月14日 星期四

[ OSGi In Action ] Introducing OSGi : Mastering modularity (3)

Finalizing the paint program design : 
So far, you’ve defined three bundles for the paint program: a shape API bundle, a shape implementation bundle, and a main paint program bundle. Let’s look at the complete metadata for each. The shape API bundle is described by the following manifest metadata : 

Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.foo.shape
Bundle-Version: 2.0.0
Bundle-Name: Paint API
Import-Package: javax.swing
Export-Package: org.foo.shape; version="2.0.0"

The bundle containing the shape implementations is described by the following manifest metadata : 

Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.foo.shape.impl
Bundle-Version: 2.0.0
Bundle-Name: Simple Shape Implementations
Import-Package: javax.swing, org.foo.shape; version="2.0.0"
Export-Package: org.foo.shape.impl; version="2.0.0"

And the main paint program bundle is described by the following manifest metadata : 

Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.foo.paint
Bundle-Version: 2.0.0
Bundle-Name: Simple Paint Program
Import-Package: javax.swing, org.foo.shape; org.foo.shape.impl;
version="2.0.0"

As you can see in figure 2.13, these three bundles directly mirror the logical package structure of the paint program. This approach is reasonable, but can it be improved? To some degree, you can answer this question only if you know more about the intended uses of the paint program; but let’s look more closely at it anyway. 
 
Figure 2.13 Structure of the paint program’s bundles 

- Improving the paint program’s modularization 
In the current design, one aspect that sticks out is the shape-implementation bundle. Is there a downside to keeping all shape implementations in a single package and a single bundle? Perhaps it’s better to reverse the question. Is there any advantage to separating the shape implementations into separate bundles? Imagine use cases where not all shapes are necessary; for example, small devices may not have enough resources to support all shape implementations. If you separate the shape implementations into separate packages and separate bundles, you have more flexibility when it comes to creating different configurations of the application. 

This is a good issue to keep in mind when you’re modularizing applications. Optional components or components with the potential to have multiple alternative implementations are good candidates to be in separate bundles. Breaking your application into multiple bundles gives you more flexibility, because you’re limited to deploying configurations of your application based on the granularity of your defined bundles. Sounds good, right? You may then wonder why you don’t divide your applications into as many bundles as you can. 

You pay a price for the flexibility afforded by dividing an application into multiple bundles. Lots of bundles mean you have lots of artifacts that are versioning independently, creating lots of dependencies and configurations to manage. So it’s probably not a good idea to create a bundle out of each of your project’s packages, for example. You need to analyze and understand your needs for flexibility when deciding how best to divide an application. There is no single rule for every situation. 

Returning to the paint program, let’s assume the ultimate goal is to enable the possibility for creating different configurations of the application with different sets of shapes. To accomplish this, you move each shape implementation into its own package (org.foo.shape.circle, org.foo.shape.square, and org.foo.shape.triangle). You can now bundle each of these shapes separately. The following metadata captures the circle bundle : 

Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.foo.shape.circle
Bundle-Version: 2.0.0
Bundle-Name: Circle Implementation
Import-Package: javax.swing, org.foo.shape; version="2.0.0"
Export-Package: org.foo.shape.circle; version="2.0.0"

The metadata for the square and triangle bundles is nearly identical, except with the correct shape name substituted where appropriate. These changes also require changes to the program’s metadata implementation bundle; you modify its metadata as follows : 

Bundle-ManifestVersion: 2
Bundle-SymbolicName: org.foo.paint
Bundle-Version: 2.0.0
Bundle-Name: Simple Paint Program
Import-Package: javax.swing, org.foo.shape; org.foo.shape.circle;
org.foo.shape.square; org.foo.shape.triangle; version="2.0.0"

The paint program implementation bundle depends on Swing, the public API bundle, and all three shape bundles. Figure 2.14 depicts the new structure of the paint program : 
 
Figure 2.14 Logical structure of the paint program with separate modules for each shape implementation 

Now you have five bundles (shape API, circle, square, triangle, and paint). Great. But what do you do with these bundles? The initial version of the paint program had a static main() method on PaintFrame to launch it; do you still use it to launch the program? You could use it by putting all the bundle JAR files on the class path, because all the example bundles can function as standard JAR files, but this would defeat the purpose of modularizing the application. There’d be no enforcement of modular boundaries or consistency checking. To get these benefits, you must launch the paint program using the OSGi framework. Let’s look at what you need to do. 

- Launching the new paint program 
The focus of this chapter is on using the module layer, but you can’t launch the application without a little help from the lifecycle layer. Instead of putting the cart before the horse and talking about the lifecycle layer now, we created a generic OSGi bundle launcher to launch the paint program for you. This launcher is simple: you execute it from the command line and specify a path to a directory containing bundles; it creates an OSGi framework and deploys all bundles in the specified directory. The cool part is that this generic launcher hides all the details and OSGi-specific API from you. We’ll discuss the launcher in detail in chapter 13. 

Just deploying the paint bundles into an OSGi framework isn’t sufficient to start the paint program; you still need some way to kick-start it. You can reuse the paint program’s original static main() method to launch the new modular version. To get this to work with the bundle launcher, you need to add the following metadata from the original paint program to the paint program bundle manifest : 

Main-Class: org.foo.paint.PaintFrame

As in the original paint program, this is standard JAR file metadata for specifying the class containing the application’s static main() method. Note that this feature isn’t defined by the OSGi specification but is a feature of the bundle launcher. To build and launch the newly modularized paint program, go into the chapter02/paint-modular/ directory in the companion code and type ant. Doing so compiles all the code and packages the modules. Typing java -jar launcher.jar bundles/ starts the paint program. 

The program starts up as it apparently always has; but underneath, the OSGi framework is resolving the bundles’ dependencies, verifying their consistency, and enforcing their logical boundaries. That’s all there is to it. You’ve now used the OSGi module layer to create a nicely modular application. OSGi’s metadata-based approach didn’t require any code changes to the application, although you did move some classes around to different packages to improve logical and physical modularity. 

The goal of the OSGi framework is to shield you from a lot of the complexities; but sometimes it’s beneficial to peek behind the curtain, such as to help you debug the OSGi-based applications when things go wrong. Next section, we’ll look at some of the work the OSGi framework does for you, to give you a deeper understanding of how everything fits together. Afterward, we’ll close out the chapter by summarizing the benefits of modularizing the paint program. 

OSGi dependency resolution : 
You’ve learned how to describe the internal code composing the bundles with Bundle-ClassPath, expose internal code for sharing with Export-Package, and declare dependencies on external code with Import-Package. Although we hinted at how the OSGi framework uses the exports from one bundle to satisfy the imports of another, we didn’t go into detail. The Export-Package and Import-Package metadata declarations included in bundle manifests form the backbone of the OSGi bundle dependency model, which is predicated on package sharing among bundles. 

In this section, we’ll explain how OSGi resolves bundle package dependencies and ensures package consistency among bundles. After this section, you’ll have a clear understanding of how bundle modularity metadata is used by the OSGi framework. You may wonder why this is necessary, because bundle resolution seems like an OSGi framework implementation detail. Admittedly, this section covers some of the more complex details of the OSGi specification; but it’s helpful when defining bundle metadata if you understand a little of what’s going on behind the scenes. Further, this information can come in handy when you’re debugging OSGi-based applications. Let’s get started. 

- Resolving dependencies automatically 
Adding OSGi metadata to your JAR files represents extra work for you as a developer, so why do it? The main reason is so you can use the OSGi framework to support and enforce the bundles’ inherent modularity. One of the most important tasks performed by the OSGi framework is automating dependency management, which is called bundle dependency resolution. 

A bundle’s dependencies must be resolved by the framework before the bundle can be used, as shown in figure 2.15. The framework’s dependency resolution algorithm is sophisticated; we’ll get into its gory details, but let’s start with a simple definition. 

RESOLVING The process of matching a given bundle’s imported packages to exported packages from other bundles and doing so in a consistent way so any given bundle only has access to a single version of any type.

 
Figure 2.15 Transitive dependencies occur when bundle A depends on packages from bundle B and bundle B in turn depends on packages from bundle C. To use bundle A, you need to resolve the dependencies of both bundle B and bundle C. 

Resolving a bundle may cause the framework to resolve other bundles transitively, if exporting bundles themselves haven’t yet been resolved. The resulting set of resolved bundles are conceptually wired together in such a fashion that any given imported package from a bundle is wired to a matching exported package from another bundle, where a wire implies having access to the exported package. The final result is a graph of all bundles wired together, where all imported package dependencies are satisfied. If any dependency can’t be satisfied, then the resolve fails, and the instigating bundle can’t be used until its dependencies are satisfied. 
This description likely makes you want to ask three questions : 

1. When does the framework resolve a bundle’s dependencies?
2. How does the framework gain access to bundles to resolve them in the first place?
3 What does it mean to wire an importing bundle to an exporting bundle?

The first two questions are related, because they both involve the lifecycle layer, which we’ll discuss in the next chapter. For the first question, it’s sufficient to say that the framework resolves a bundle automatically when another bundle attempts to use it. To answer the second question, we’ll say that all bundles must be installed into the framework in order to be resolved (we’ll discuss bundle installation in more depth in chapter 3). For the discussion in this section, we’ll always be talking about installed bundles. As for the third question, we won’t answer it fully because the technical details of wiring bundles together isn’t important; but for the curious, we’ll explain it briefly before looking into the resolution process in more detail. 

At execution time, each OSGi bundle has a class loader associated with it, which is how the bundle gains access to all the classes to which it should have access (the ones determined by the resolution process). When an importing bundle is wired to an exporting bundle, the importing class loader is given a reference to the exporting class loader so it can delegate requests for classes in the exported package to it. You don’t need to worry about how this happens—relax and let OSGi worry about it for you. Now, let’s look at the resolution process in more detail. 

- SIMPLE CASES 
At first blush, resolving dependencies is fairly straightforward; the framework just needs to match exports to imports. Let’s consider a snippet from the paint program example : 

Bundle-Name: Simple Paint Program
Import-Package: org.foo.shape

From this, you know that the paint program has a single dependency on the org.foo.shape package. If only this bundle were installed in the framework, it wouldn’t be usable, because its dependency wouldn’t be satisfiable. To use the paint program bundle, you must install the shape API bundle, which contains the following metadata : 

Bundle-Name: Paint API
Export-Package: org.foo.shape

When the framework tries to resolve the paint program bundle, it knows it must find a matching export for org.foo.shape. In this case, it finds a candidate in the shape API bundle. When the framework finds a matching candidate, it must determine whether the candidate is resolved. If the candidate is already resolved, the candidate can be chosen to satisfy the dependency. If the candidate isn’t yet resolved, the framework must resolve it first before it can select it; this is the transitive nature of resolving dependencies. If the shape API bundle has no dependencies, it can always be successfully resolved. But you know from the example that it does have some dependencies, namely javax.swing : 

Bundle-Name: Paint API
Import-Package: javax.swing
Export-Package: org.foo.shape

What happens when the framework tries to resolve the paint program? By default, in OSGi it wouldn’t succeed, which means the paint program can’t be used. Why? Because even though the org.foo.shape package from the API bundle satisfies the main program’s import, there’s no bundle to satisfy the shape API’s import of javax.swing. In general, to resolve this situation, you can conceptually install another bundle exporting the required package : 

Bundle-Name: Swing
Export-Package: javax.swing

Now, when the framework tries to resolve the paint program, it succeeds. The main paint program bundle’s dependency is satisfied by the shape API bundle, and its dependency is satisfied by the Swing bundle, which has no dependencies. After resolving the main paint program bundle, all three bundles are marked as resolved, and the framework won’t try to resolve them again (until certain conditions require it, as we’ll describe in the next chapter). The framework ends up wiring the bundles together, as shown in figure 2.16. 
 
Figure 2.16 Transitive bundle-resolution wiring 

You’ve learned that you can have attributes attached to exported and imported packages. At the time, we said it was sufficient to understand that attributes attached to imported packages are matched against attributes attached to exported packages. Now you can more fully understand what this means. Let’s modify the bundle metadata snippets to get a deeper understanding of how attributes factor into the resolution process. Assume you modify the Swing bundle to look like this : 

Bundle-Name: Swing
Export-Package: javax.swing; vendor="Sun"

Here, you modify the Swing bundle to export javax.swing with an attribute vendor with value "Sun". If the other bundles’ metadata aren’t modified and you perform the resolve process from scratch, what impact does this change have? This minor change has no impact at all. Everything resolves as it did before, and the vendor attribute never comes into play. Depending on your perspective, this may or may not seem confusing. As we previously described attributes, imported attributes are matched against exported attributes. In this case, no import declarations mention the vendor attribute, so it’s ignored. Let’s revert the change to the Swing bundle and instead change the API bundle to look like this : 

Bundle-Name: Paint API
Export-Package: org.foo.shape
Import-Package: javax.swing; vendor="Sun"

Attempting to resolve the paint program bundle now fails because no bundle is exporting the package with a matching vendor attribute for the API bundle. Putting the vendor attribute back on the Swing bundle export allows the main paint program bundle to successfully resolve again with the same wiring, as shown earlier in figure 2.16. Attributes on exported packages have an impact only if imported packages specify them, in which case the values must match or the resolve fails. 

Recall that we also talked about the version attribute. Other than the more expressive interval notation for specifying ranges, it works the same way as arbitrary attributes. For example, you can modify the shape API bundle as follows : 

Bundle-Name: Paint API
Export-Package: org.foo.shape; vendor="Manning";
 version="2.0.0"
Import-Package: javax.swing; vendor="Sun"

And you can modify the paint program bundle as follows : 

Bundle-Name: Simple Paint Program
Import-Package: org.foo.shape; vendor="Manning";
 version="[2.0.0,3.0.0)"

In this case, the framework can still resolve everything because the shape API bundle’s export matches the paint program bundle’s import; the vendorattributes match, and 2.0.0 is in the range of 2.0.0 inclusive to 3.0.0 exclusive. This particular example has multiple matching attributes on the import declaration, which is treated like a logical AND by the framework. Therefore, if any of the matching attributes on an import declaration don’t match a given export, the export doesn’t match at all. 

- MULTIPLE MATCHING PACKAGE PROVIDERS 
In the previous section, dependency resolution is fairly straightforward because there’s only one candidate to resolve each dependency. The OSGi framework doesn’t restrict bundles from exporting the same package. Actually, one of the benefits of the OSGi framework is its support for side-by-side versions, meaning it’s possible to use different versions of the same package in the same running JVM. In highly collaborative environments of independently developed bundles, it’s difficult to limit which versions of packages are used. Likewise, in large systems, it’s possible for different teams to use different versions of libraries in their subsystems. 

Let’s consider what happens when multiple candidates are available to resolve the same dependency. Consider a case in which a web application needs to import the javax.servlet package and both a servlet API bundle and a Tomcat bundle provide the package (see figure 2.17) : 
 
When the framework tries to resolve the dependencies of the web application, it sees that the web application requires javax.servlet with a minimum version of 2.4.0 and both the servlet API and Tomcat bundles meet this requirement. Because the web application can be wired to only one version of the package, how does the framework choose between the candidates? As you may intuitively expect, the framework favors the highest matching version, so in this case it selects Tomcat to resolve the web application’s dependency. Sounds simple enough. What happens if both bundles export the same version, say 2.4.0? 

In this case, the framework chooses between candidates based on the order in which they’re installed in the framework. Bundles installed earlier are given priority over bundles installed later; as we mentioned, the next chapter will show you what it means to install a bundle in the framework. If you assume the servlet API was installed before Tomcat, the servlet API will be selected to resolve the web application’s dependency. The framework makes one more consideration when prioritizing matching candidates: maximizing collaboration. 

The framework gives priority to already-resolved exporters, so if it must choose between two matching candidates where one is resolved and one isn’t, it chooses the resolved candidate. Consider again the example with the servlet API exporting version 2.4.0 of the javax.servlet package and Tomcat exporting version 2.5.0. If the servlet API is already resolved, the framework will choose it to resolve the web application’s dependency, even though it isn’t exporting the highest version, as shown in figure 2.18 : 
 

framework favors already-resolved packages as a means to minimize the number of different versions of the same package being used. Let’s summarize the priority of dependency resolution candidate selection : 

* Highest priority is given to already-resolved candidates, where multiple matches of resolved candidates are sorted according to version and then installation order.
* Next priority is given to unresolved candidates, where multiple matches of unresolved candidates are sorted according to version and then installation order.

It looks like we have all the bases covered, right? Not quite. Next, we’ll look at how an additional level of constraint checking is necessary to ensure that bundle dependency resolution is consistent. 

Ensuring consistency with uses constraints : 
From the perspective of any given bundle, a set of packages is visible to it, which we’ll call its class space. Given your current understanding, you can define a bundle’s class space as its imported packages combined with the packages accessible from its bundle class path, as shown in figure 2.19 
 
Figure 2.19 Bundle A’s class space is defined as the union of its bundle class path with its imported packages, which are provided by bundle B’s exports. 

A bundle’s class space must be consistent, which means only a single instance of a given package must be visible to the bundle. Here, we define instances of a package as those with the same name, but from different providers. For example, consider the previous example, where both the servlet API and Tomcat bundles exported the javax.servlet package. The OSGi framework strives to ensure that the class spaces of all bundles remain consistent. Prioritizing how exported packages are selected for imported packages, as described in the last section, isn’t sufficient. Why not? Let’s consider the simple API in the following code snippet : 

  1. package org.osgi.service.http;  
  2. import javax.servlet.Servlet;  
  3. public interface HttpService {  
  4.     void registerServlet(Sting alias, Servlet servlet, HttpContext ctx);  
  5. }  
This is a snippet from an API you’ll meet in chapter 15. The details of what it does are unimportant at the moment; for now, you just need to know its method signature. Let’s assume the implementation of this API is packaged as a bundle containing the org.osgi.service.http package but not[i]javax.servlet[/i]. This means it has some metadata in its manifest like this: 

Export-Package: org.osgi.service.http; version="1.0.0"
Import-Package: javax.servlet; version="2.3.0"

Let’s assume the framework has the HTTP service bundle and a servlet library bundle installed, as shown in figure 2.20. Given these two bundles, the framework makes the only choice available, which is to select the version of javax.servlet provided by the Servlet API bundle. 
 

Now, assume you install two more bundles into the framework: the Tomcat bundle exporting version 2.4.0 of javax.servlet and a bundle containing a client for the HTTP service importing version 2.4.0 of javax.servlet. When the framework resolves these two new bundles, it does so as shown in figure 2.21. 
 

The HTTP client bundle imports org.osgi.service.http and version 2.4.0 of javax.servlet, which the framework resolves to the HTTP service bundle and the Tomcat bundle, respectively. It seems that everything is fine: all bundles have their dependencies resolved, right? Not quite. There’s an issue with these choices for dependency resolution! 

Consider the servlet parameter in the HTTPService.registerServlet() method. Which version of javax.servlet is it? Because the HTTP service bundle is wired to the Servlet API bundle, its parameter type is version 2.3.0 of javax.servlet.Servlet. When the HTTP client bundle tries to invoke HTTPService.registerServlet(), which version of javax.servlet.Servlet is the instance it passes? Because it’s wired to the Tomcat bundle, it creates a 2.4.0 instance of javax.servlet.Servlet.The class spaces of the HTTP service and client bundles aren’t consistent; two different versions of javax.servlet are reachable from both. At execution time, this results in class cast exceptions when the HTTP service and client bundles interact. 

The framework made the best choices at the time it resolved the bundle dependencies; but due to the incremental nature of the resolve process, it couldn’t make the best overall choice. If you install all four bundles together, the framework resolves the dependencies in a consistent way using its existing rules. Figure 2.22 shows the dependency resolution when all four bundles are resolved together : 
 

- INTER- VS. INTRA-BUNDLE DEPENDENCIES 
The difficulty is that Export-Package and Import-Package only capture inter-bundle dependencies, but class-space consistency conflicts result from intra-bundle dependencies. Recall the org.osgi.service.http.HttpService interface; its register-Servlet() method takes a parameter of type javax.servlet.Servlet, which meansorg.osgi.service.http uses javax.servlet. Figure 2.23 shows this intra-bundle uses relationship between the HTTP service bundle’s exported and imported packages : 
 

How do these uses relationships arise? The example shows the typical way, which is when the method signatures of classes in an exported package expose classes from other packages. This seems obvious, because the used types are visible, but it isn’t always the case. You can also expose a type via a base class that’s downcast by the consumer. Because these types of uses relationships are important, how do you capture them in the bundle metadata? 

Directives are additional metadata to alter how the framework interprets the metadata to which the directives are attached. The syntax for capturing directives is similar to arbitrary attributes. For example, the following modified metadata for the HTTP service example shows how to use the uses directive : 

Export-Package: org.osgi.service.http;
uses:="javax.servlet"; version="1.0.0"
Import-Package: javax.servlet; version="2.3.0"

Notice that directives use the := assignment syntax, but the ordering of the directives and the attributes isn’t important. This particular example indicates that org.osgi. service.http uses javax.servlet. How exactly does the framework use this information? uses relationships among packages act like grouping constraints for the packages. In this example, the framework ensures that importers of org.osgi.service.http also use the same javax.servlet used by the HTTP service implementation. 

This captures the previously missing intra-bundle package dependency. In this specific case, the exported package expresses a uses relationship with an imported package, but it could use other exported packages. These sorts of uses relationships constrain which choices the framework can make when resolving dependencies, which is why they’re also referred to as constraints. Abstractly, if package foo uses package bar, importers of foo are constrained to the same bar if they use bar at all. Figure 2.24 depicts how this would impact the original incremental dependency resolutions. 
 

For the incremental case, the framework can now detect inconsistencies in the class spaces, and resolution fails when you try to use the client bundle. Early detection is better than errors at execution time, because it alerts you to inconsistencies in the deployed set of bundles. In the next chapter, you’ll learn how to cause the framework to re-resolve the bundle dependencies to remedy this situation. 

You can further modify the example, to illustrate how uses constraints help find proper dependency resolutions. Assume the HTTP service bundle imports precisely version 2.3.0 of javax.servlet, but the client imports version 2.3.0 or greater. Typically, the framework tries to select the highest version of a package to resolve a dependency; but due to the uses constraint, the framework ends up selecting a lower version instead, as shown in figure 2.25 : 
 

If you look at the class space of the HTTP client, you can see how the framework ends up with this solution. The HTTP client’s class space contains bothjavax.servlet and org.osgi.service.http, because it imports these packages. From the perspective of the HTTP client bundle, it can use either version 2.4.0 or 2.3.0 of javax.servlet, but the framework has only one choice for org.osgi.service.http. Because org.osgi.service.http from the HTTP service bundle usesjavax.servlet, the framework must choose the same javax.servlet package for any clients. Because the HTTP service bundle can only use version 2.3.0 ofjavax.servlet, this eliminates the Tomcat bundle as a possibility for the client bundle. The end result is a consistent class space where a lower version of a needed package is correctly selected even though a higher version is available. 

- ODDS AND ENDS OF USES CONSTRAINTS 
Let’s finish the discussion of uses constraints by touching on some final points. First, uses constraints are transitive, which means that if a given bundle exports package foo that uses imported package bar, and the selected exporter of bar uses package baz, then the associated class space for a bundle importing foo is constrained to have the same providers for both bar and baz, if they’re used at all. 

Also, even though uses constraints are important to capture, you don’t want to create blanket uses constraints, because doing so overly constrains dependency resolution. For example, in larger applications, it is common for independently developed subsystems to use different versions of the same XML parser. If you specify uses constraints too broadly, this isn’t possible. 


Supplement :
[ OSGi In Action ] Introducing OSGi : Mastering modularity (1)
[ OSGi In Action ] Introducing OSGi : Mastering modularity (2)
[ OSGi In Action ] Introducing OSGi : Mastering modularity (3)
Source code for osgi-in-action

沒有留言:

張貼留言

網誌存檔

關於我自己

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