![]() |
![]() |
|
![]() |
![]() |
Home Contact Links Deutsch |
|
Introduction to the OSGi R4 Service PlatformBasicsThe OSGi R4 Service Platform is a specification of a service-oriented and component-based application framework. It is specified by the OSGi alliance which consists of several expert groups:
Layer L0 specifies a minimum Java execution environment. This could be a J2SE or one of the Java ME profiles for devices (e.g. mobiles). The next layer L1 introduces the concept of isolated modules/services/components called "bundles" that can use classes from each other in a controlled way. This is done by sharing Java packages and includes a version management. Layer L2 takes care about the life cycle of these bundles in a bundle repository without requiring the VM to be restarted. On top of this L3 builts a service model to decouple bundles and provides a notification system that publishes information about started and stopped bundles. Bundles (L1)A bundle is deployed as a single jar file and has its own namespace. The framework provides a seperate "bundle classloader" for each bundle and so its classes are isolated from the classes of other bundles. To allow communication through common objects, it's possible to share Java packages between bundles. For this aim, each bundle has to declare the shared Java packages in its own manifest file. The manifest is stored inside the jar at path "/META-INF/MANIFEST.MF" and provides statements "Export-Package:", "Import-Package:" and "Require-Bundle:". Im- and export of Java packages from or to other bundles mainly is controlled by these statements. To export packages from the current bundle to any other bundle, a list of these packages must be declared at "Export-Package:". Once these packages are exported they become available as shared packages and might be used by other bundles. In the manifest of other bundles a list of packages at "Import-Package:" defines, which of these packages are imported. This allows to declare imports independently from specific bundles - the belonging bundles don't have to be specified in the list of the required ones. Only if you're sure that all packages of a bundle must be imported, then add this bundle to "Require-Bundle:"-statement. It's important to understand, that each bundle creates its own space of classes by using a distinct classloader. At the beginning this class space only consists of the java.*-classes and the classes the bundle itself internally has got, but any imported package adds classes to it. Exports can be made only explicitly and only out of this space. All imported classes are loaded not by bundles' own classloader, but by the classloader of the exporter that provides these classes. The following simplifying1 picture shows the basic search concept that is used, if a class (or any other resource) must be loaded: First of all the bundle classloader checks, if the searched class belongs to a java.*-package. If yes, then the searching request will be forwarded to its parent classloader (which normally is the bootstrap classloader of the JVM). In contrast to the J2SE environment, the bundle classloader won't continue searching, if its parent classloader couldn't find the class2. If the class doesn't belong to a java.*-package then the bundle classloader checks, if it belongs to a package imported by the "Import-Package:" statement. If yes, then the searching request is delegated to the classloader of that bundle that exports this package. If not, then the next check is, if the class being searched belongs to an exported package of one of the bundles specified at "Require-Bundle:". If yes, then the searching request is delegated to the classloader of the appropriate bundle that exports this package. If not, then the class is searched in the bundle-classpath of the current bundle specified in its manifest-statement "Bundle-ClassPath:". OSGi offers a way to add features to a bundle later on without the need to build a new bundle version. For this aim bundles can be enhanced by so called "bundle fragments". All fragments of a bundle run in the context of it. This means, that they use the classloader of the bundle they belong to. Bundle manifestYou already know, that dependencies of bundles are declared in their manifest files.
To give you a simple first impression of how a manifest could look like, please have a look at the following extract of a manifest file
being built by graphical manifest-editor of Eclipse V3.3.0 (only the OSGi-specific statements are shown):
The "Bundle-ManifestVersion: 2" indicates R4 semantics and syntax. The "Bundle-Name:" specifies the name of the bundle in a human-readable form, but the OSGi framework uses the "Bundle-SymbolicName:" together with its version specified by "Bundle-Version:" as a globally unique identifier and namespace for this bundle - it's called "bundle ID" and in this example it's "de.luers_net.test.MyBundle1 (1.0.0)". As a consequence no other bundle can be identical in both symbolic name and version. To ensure this, the symbolic name has to begin (according to the usual practise for package names in Java) with the reversed domain name of the manufacturer - the one that is decribed in a human-readable form by "Bundle-Vendor:", too. In this example the "Require-Bundle:"-statement specifies a second bundle with a global unique identifier of "de.luers_net.test.MyBundle2". A version range is also specified for it and so this dependency is valid only for MyBundle2-versions from "1.0.0" (including!) to "2.0.0" (excluding!). On demand the current bundle is able to load each class which is exported by the second bundle, but only if it doesn't belong to package "de.luers_net.test.mybundle3.interf", which could be exported by any other bundle, too. In this example the only package that is exported by current bundle "de.luers_net.test.MyBundle1 (1.0.0)" is "de.luers_net.test.mybundle1.interf". The last statement "Bundle-ClassPath:" specifies the classpath of the current bundle. If it's specified, then a single "." should be added to find classes being in the root of the jar. This classpath here is extended by "library.jar" and the content of the folder "./lib/" within the jar of the current bundle. Bundle life cycle management (L2)An OSGi bundle defines an explicit boundary for a module and the bundles' manifest explicitly declares versioned dependencies to other bundles. The framework enforces the compliance with these rules by automatically managing bundles and their dependencies. The following picture shows the life cycle states a bundle can have: After a bundle is installed its dependencies will be resolved automatically. This means, that its required package-imports declared in its manifest will be checked against the exported packages of other bundles. The framework also checks, if all required bundles are successfully resolved, too. If all checks are successful, then the results are stored in a so called "bundle repository" and the bundle enters state "resolved". Now the bundle can be used without any further state change, but this doesn't mean, that itself is active in any way - it can be invoked or is invoked by others like a library. Later, when discussing Eclipse RCP environment you'll see, that this suffices for a RCP-plug-in. Bundles representing a service must be started, because they have to prepare and wait for incoming requests. For this aim a bundle can provide a so called bundle activator which has to implement interface "org.osgi.framework.BundleActivator". If a bundle is started then it enters state "starting" and its bundle activator method "void start(BundleContext context)" is called. In this method a service normally will prepare its transportation layer for incoming requests. As soon as this call successfully returns (meaning no exceptions are thrown) the bundle reaches state "active". Of course it's possible to stop a running service. If this happens, then bundle enters state "stopping" and its bundle activator method "void stop(BundleContext context)" will be called. For a service this is the signal to block new incoming requests, to finish currently running short term requests, to stop all longer term activities (e.g. its threads) and to free all allocated resources. Other services that depend on the stopped service also have to react, but this will be discussed later on. After stopping has finished, the bundle is in state "resolved" again. Advanced dependency controlAs you've seen, most of OSGi framework is about managing dependencies. Something we've not discussed until now is, what happens if an import fits to multiple exports. For all resolving issues in case of doubt the following priority list will be invoked:
Multi-version support for packages and optional packagesThe upper text has shown, that bundles are having a symbolic name and that it's possible to refer to a special bundle-version.
The im- and exported packages had no versions and so only the package name was analysed for dependency calculation,
but there's also a way to im- and export special versions of a package and to import packages only, if they are present:
The three manifest-lines above show an example of this. The bundle tries to import package "de.luers_net.test.mybundle2.interf" with a minimal version of "1.2.3" (open range for higher versions), but resolution of this dependency is optional. In other words, the bundle rechons on the fact, that the package might be not available. Additively to this the package "de.luers_net.test.mybundle3.interf" is required, but only versions from "1.0.0" (excluding!) to "1.0.5" (including!) will fit. If no such version is found, then resolving won't be successful and the bundle won't be usable. Besides this the bundle exports package "de.luers_net.test.mybundle1.interf" in version "1.5.0". Explicit package versions allow to have multiple versions of a shared package in memory at the same time. This is needed to implement backwards compatibility. By the way ... Please notice usage of ":=" versus "="! Values to attributes (like version) are assigned by using "=". Assigments to directives (like resolution) are differentiated by using a ":=". Another point is specification of version inclusion and exclusion: "(" and ")" mean exclusion while "[" and "]" mean inclusion. Reexport of bundles and optional bundlesPackages imported by "Import-Package:" can be (re-)exported by "Export-Package:" and it was shown,
that optional package imports are possible by adding a "resolution:=optional"-directive.
The following line shows how the same is achieved for a bundle:
The current bundle tries to import all packages of bundle "de.luers_net.test.MyBundle2", but the resolution of this dependency isn't mandatory. By specifying directive visibility with a value of reexport, the imported packages of MyBundle2 will be exported by the current bundle. Dependency problemsIf you get a ClassNotFoundException or a ClassCastException (although the involved class types seem to fit), then you probably have a problem with bundle or package dependencies. In such moments you should carefully think about the dependency design of the bundles being involved. Please have a look at the picture below: If bundle A searches a class X of package p, then it will check bundles B, D and C for that - in this order, because B has a lower bundle ID then C - and D is required by C. If the class X is found in B, then this one will be used. This also implies that X is loaded by the classloader of B. But what happens, if bundle A makes a call to a class of bundle C and this call returns an object of class X, too? This class X was loaded by classloader of bundle D and so both classes X will not fit! This situation often occurs, if common libraries are used - e.g. a logging- or XML-library or a library for database access. To avoid these problems, you should wrap these libraries always by an own bundle (see below). Here the library that implements package p is wrapped by bundle X. If bundle A searches a class of package p and accesses classloader of B, then it will delegate the request to the classloader of bundle X. Almost the same occurs, if bundle C wants to access the same class and so bundle A will never have a problem again with classes that come from package p. Now please figure out what happens if bundle B would have a backward dependency to bundle A (see upper picture). If bundle A loads class X of package p then its classloader will delegate to classloader of B which in turn delegates to A and so on, right? Not at all! OSGi specifies that each classloader is accessed only once for each search of a class. Service registry (L3)With layers L0 to L2 a framework is available that supports isolated startable and stoppable services. What's missing is a registry were potential clients can query all available services for a specific topic. What's also missing is an eventing mechanism that informs clients about shutdown of services or if services become available. That's realised by layer L3 of OSGi framework. Service sideWhen starting a service bundle its activator method "start(BundleContext)" is called. Through the context passed to this method the service can register itself at service registry. This can be done through method "BundleContext.registerService(String, Object, Dictionary)": (A)
When stopping a service bundle its activator method "stop(BundleContext)" is called. Besides stopping all service activities and cleaning up all allocated resources, the service has to unregister itself. This can be done by calling "ServiceRegistration.unregister()". A service should minimize possibly negative effects of eventually wrong behaviour of clients, especially if a client forgets to cleanup its references to a service that becomes unavailable. For this intent only a proxy object should be passed to registerService-method. If the service is stopped, then all references to it in all proxy objects should be set to null by the service. Client sideA client bundle always has to reckon with the fact that a service stops or restarts. It's very important to correctly handle all the events for that, because elsewhere the client could keep "stale references" to a service or a part of it. This would hinder the garbage collector from clearing up all the memory and it could be impossible to update or restart the service. To simplify using services correctly, OSGi provides the class "org.osgi.util.tracker.ServiceTracker". A client can create an instance of it by using constructor "ServiceTracker(BundleContext, String, ServiceTrackerCustomizer)":
To access the services, method "Object[] ServiceTracker.getServices()" must be called. (D) The returned array consists of all the service objects of all services being tracked. If at least one object is returned in the array, then the client can cast it to the appropriate interface that is supported by the service and then can call the service through it. (E/F) If no service object is returned this means that all tracked services have become unavailable. If the client doesn't want to use the tracked services anymore, it has to call "ServiceTracker.close()". This must be done at latest in the stop-method of its activator. It should be ensured, that this method is called even under exceptional cases. 1 Normally knowledge about additive class searching steps isn't needed.
Nevertheless the missing steps in the picture are ... 2 The exact behaviour of class searching depends on configuration parameters of the OSGi environment. Copyright (c) 2016: Juergen Luers |