OSGi Classloading

Categories: Java, OSGi

(back to Osgi Introduction)

About This Article : OSGi and Classloading

OSGi is a Java-specific framework that improves the way that Java classes interact within a single JVM. It provides the following features:

  1. a modified Java classloader which provides fine-grained control over symbolic linking with other code in the same JVM;
  2. a central service registry for decoupling callers of an interface from the interface implementation;
  3. an enhanced version of the java.lang.SecurityManager (ConditionalPermissionAdmin);
  4. a large set of standardized optional services for things like loading configuration-files, publishing events, exposing Java servlets, etc.

This article provides some details about how item (1) is implemented; it is (IMHO) the most useful and best-designed part of OSGi. It can be used without the rest of OSGi, and is very light-weight. Among other features, it:

  • allows internal implementation classes to be hidden from other jarfiles (mostly);
  • allows concurrent loading of jarfiles whose implementation happens to depend on different versions of the same library;
  • reduces the chance that the dreaded ClassNotFoundException will occur at runtime (missing dependencies are much more obvious);
  • speeds up application startup (class resolution) a little;
  • allows code in a jarfile to be executed when the jarfile is loaded;
  • allows jarfiles to take actions when other jarfiles are loaded (self-extensible framework);
  • allows jarfiles to be unloaded at runtime (under some conditions);
  • forces bundles to include metadata about their identity and version (similar to maven pom declarations);
  • treats much of the JDK library like an OSGi bundle, forcing jarfiles to declare their dependencies on JDK features explicitly.

There has been ongoing discussion within the core Java developer teams about building a similar system into the Java JDK itself; there was some attempt to design something for Java 1.7; it was postponed to 1.8 and has then been postponed again. The need for a “module” system in Java is well recognised, and hopefully (when/if something ever finally gets released) it will bear some resemblance to the OSGi one.

This article assumes that you are familiar with traditional Java classloaders: ie that a classloader can have a “parent”, and that when classes are loaded at runtime, the loading classloader and its parents are responsible for finding other classes that the loaded one references.

For further information about OSGi in general, see the OSGi Introduction.

OSGi Manifests

The OSGi unit of code-packaging is the jarfile (just like traditional Java). If a jarfile has some extra OSGi-specific properties in its /META-INF/MANIFEST.MF file then it is an “OSGi Bundle”.

There are a dozen or so properties defined by the OSGi specification, of which only a few are relevant for OSGi’s enhanced classloading abilities. The relevant properties are:

  • Bundle-SymbolicName -> the name of this bundle; normally the FQDN (fully qualified domain name) of the primary Java package within this jarfile
  • Bundle-Version -> major.minor.patch version of this release of the bundle
  • Import-Package: list of (Java package, version-range) that classes in this jarfile expect other bundles to provide.
  • Export-Package: list of Java packages within this jarfile that external code is allowed to access.
  • Require-Bundle: list of other bundles (specified via symbolic-name and version-range) that must also be installed

Require-bundle declares a dependency on a specific bundle (which might export multiple packages), while “import-package” declares that some bundle in the current environment must provide the specified code, but doesn’t care which. The “require-bundle” declaration is similar to having an “import-package” declaration for each “export-package” declaration in the target bundle, but is not so flexible as it constrains the solution to a specific bundle (implementation) rather than a package requirement (interface).

Multiple loaded bundles can export the same Java package with different versions; having two bundles export the same package with the same version is probably an error.

The jarfiles containing OSGi bundles must not be on the normal Java classpath; instead they are loaded by creating an OSGi org.osgi.framework.launch.Framework object, and using Framework.getBundleContext().installBundle(path) to load bundle jarfiles. See the example code referenced below. If you are using an OSGi container such as Apache Servicemix then you don’t have to worry about these things; the OSGi container will provide its own main method, and config parameters can be used to specify which jarfiles (bundles) should be loaded.

OSGi environments and the Bundle Registry

To start up an OSGi “environment”, the OSGi core jarfile is placed on the normal java classpath and bootstrap code creates an instance of the OSGi Framework class. A method on the Framework instance is then used to tell the Framework to load various jarfiles (bundles).

The framework maintains a “bundle registry”, consisting of an instance of class org.osgi.framework.Bundle representing each loaded jarfile. The bundle instance in turn contains a custom Classloader instance which is used to load classes from its jarfile (ie there is a separate classloader for each jarfile). The bundle instance holds information extracted from the MANIFEST.MF file within the jarfile, in particular the bundle version and list of imported and exported packages.

When initially created a bundle will check to see if its “imports” are satisfied - that is, whether there exist within the same bundle registry other “resolved” bundle objects which export all of the packages that its jarfile wants to import - in the version that is needed.

If these imports cannot (yet) be satisfied, then the bundle is marked as “installed” (but not resolved), and the check will be repeated again later (eg after other bundles have been installed); while in the “installed” state, no other bundle can import packages from it. A bundle in “installed” state does not yet have an associated classloader - classes from this jarfile cannot yet be loaded.

If the constraints can be satisfied, however, then the bundle builds a map of (imported-package-name => bundle.classloader) for each imported package, creates a classloader instance which is initialised with that map, and then marks itself as “resolved”. At this point it is now possible to actually load classes from the jarfile that this bundle represents; class resolution (ie finding external classes referenced from a class in this bundle) should always be possible - assuming the “import-packages” declaration was correct. And because the map entries point directly to a classloader that can supply that package, class resolution is actually more efficient than a J2EE-like system which builds a tree of classloaders and always has to pass class-lookups to the parent classloader first. And unlike a standard “tree” of classloaders where there is a single parent, the bundle classloader can point to many other bundles - potentially a different one for each package it imports.

When a bundle moves to the “resolved” state (ie classes can be loaded from it), other bundles that require classes which the bundle exports may now be able to themselves move from “installed” to “resolved” state. The OSGi environment automatically tries resolving such dependent bundles.

Bundles can also potentially be “uninstalled” while an OSGi environment is running, in which case other bundles that currently use code from the uninstalled bundle need to be correctly handled; see the section later which addresses unloading/reloading bundles.

Bundle Events

As the framework loads bundles (state=installed), and as bundles transition from “installed” to “resolved”, corresponding events are broadcast to any registered listeners. This mechanism makes it possible for user code to customise many aspects of bundle loading. Interestingly, such code is usually written as a bundle itself, and simply installed early in the application startup sequence so that it can register and then receive events about other bundle state transitions occurring after it has itself been activated.

Classloading

As a result of the framework startup procedure described above, a large number of classloaders (one per bundle) are created. The classloader hierarchy that exists at runtime is typically something like:

bootstrap classloader (includes Java standard libraries from jre/lib/rt.jar etc)
   ^
extension classloader
   ^
system classloader (ie stuff on $CLASSPATH, including OSGi core code)
   ^
OSGi environment classloader
   ^    (** Note: OSGi classloaders forward lookups to parent classloader only for some packages, eg java.*)
   \ 
    \   |-- OSGi classloader for "system bundle"  -> (map of imported-package->classloader)
     \--|-- OSGi classloader for bundle1    -> (map of imported-package->classloader)
        |-- OSGi classloader for bundle2    -> (map of imported-package->classloader)
        |-- OSGi classloader for bundle3    -> (map of imported-package->classloader)
                                     /
                                    /
      /========================================================================================\
      |  shared bundle registry, holding info about all bundles and their exported-packages  |
      \========================================================================================/

Each OSGi classloader instance (ie OSGi bundle) has its own private mapping table that tells it which classloader is responsible for providing classes from a particular package to the current bundle. This is usually populated when the bundle is first loaded (“resolved”), although “dynamic imports” look things up via the “bundle registry” as needed.

When the jarfile for a bundle is loaded, OSGi’s Framework class first creates a Bundle object to represent it, and that in turn creates a classloader which is a child of an “osgi environment classloader”. The bunde-specific classloaders pass requests for classes in packages under java.* up to the parent classloader as is standard in java, ie classes from the very core of java are available automatically to all bundles and are loaded from the bootstrap classloader as normal. However for all other packages, requests are not passed up the chain to the parent; instead the bundle’s classloader resolves classes using the bundle-specific map of package-to-classloader. As a result, classes in the new bundle can’t by default see any classes from the Java JDK which are in namespaces like javax.* or org.*, nor classes on the normal Java application classpath ($CLASSPATH). To see those “standard but not core” packages, a bundle should simply use an Import-Package declaration, which tells OSGi to copy the appropriate entry from the shared map of all possible packages into the bundle-specific map of imported packages.

Note that because OSGi classloaders do not pass requests up to their parent (other than java.*), it is actually fairly irrelevant what their parent is. However current versions (as of end 2012) of Felix and Equinox both use the above classloader hierarchy layout.

Processing Import-Package declarations

A new OSGi bundle instance reads its jarfile’s META-INF/MANIFEST and for each “Import-Package: {package};{version}” declaration:

  • locates a suitably matching entry exported by a bundle in the bundle registry. If this fails, then processing stops and the bundle is marked as “installed but not resolved”, and cannot be used for loading any classes yet, as its dependencies are not yet available.
  • adds an entry to a local map of (packagename=>classloader)

If all imports can be resolved, then the classloader marks itself as being in RESOLVED state, and continues to the next step.

When a bundle gets left in INSTALLED state due to currently-missing dependencies then it is retried again each time some other bundle becomes RESOLVED, in case the missing dependencies have now been fulfilled.

Processing Export-Package declarations

Unlike “import-package” declarations, these are more passive; they simply tell other bundles that they can use this bundle’s classloader as a source of the specified package, ie that a reference to this bundle’s classloader can be added into the (package->classloader) map of some other bundle.

Resolving Classes

When some OSGi classloader has to load a class from its own jarfile, and finds that the class depends on some type example.ui.Gadget which is not in the local jarfile, then it simply does a lookup in its local mapping table to find the classloader responsible for package example.ui, and asks that classloader to return the Class object for example.ui.Gadget.

If a class tries to reference some other class from a package that was not explicitly imported, then there will be no corresponding entry in the (package->classloader) mapping, so that lookup will immediately fail (ClassNotFoundException or NoClassDefFoundException). The only exceptions are classes in the ‘java.*’ package (and the configurable “bootclass delegation” list), where the request is forwarded to the parent classloader.

This process can actually be faster than the normal Java classloading algorithm, which requires classloaders to first ask their parent classloader, and only if that fails to try to resolve the class themselves. The OSGi approach does a little more work when loading a bundle, but then normally resolves any needed class with a single table lookup followed by invocation of getClass on exactly the right classloader instance.

A ClassNotFoundException can still occur in an OSGi environment, in the following circumstances:

  • a class in the bundle statically references some other type, and that package is not in the Import-Packages list, ie the bundle’s import-packages declaration is wrong
  • a class is looked-up dynamically via the Class.forName(String name) API or similar, and the package is not in the Imported-Packages list.
  • code references some class “example.ui.Gadget”, and package “example.ui” has successfully been imported - but does not contain a “Gadget” class.

There are tools which can scan Java code and correctly compute the necessary Import-Package declaration for static references (eg bndtools.org). However uses of Class.forName will usually have to be added to the import-packages declaration manually.

Resolving OSGi core classes

The OSGi core jarfile is on the normal classpath, and so can see both the JDK and potentially any other jarfiles on the normal classpath.

When an API from this jarfile is used to create an OSGi “environment”, it automatically creates a dummy “system” bundle whose classloader can see the OSGi standard classes. To other bundles, the “system” bundle looks and behaves just like any other OSGi bundle.

Resolving classes from the Java standard libraries

As noted above, an OSGi classloader will pass requests for classes in any package matching java.* up to the parent classloader in the traditional Java manner. The request will continue up to the top bootstrap classloader which will then return the global class instance. There is a configuration option that tells OSGi what other classes to perform the same behaviour for; some com.sun.* classes for example malfunction if not loaded from the bootstrap classloader. However there are only a handful of such packages.

For other packages that are in the $JRE_HOME/lib/*.jar files (and are therefore usually also available from the bootstrap classloader), the OSGi startup code simply makes them “exports” of the OSGi system bundle. Bundles can then use normal Import-Package statements which causes lookups of such classes to be delegated to the OSGi system bundle’s classloader which is able to resolve them. Examples of packages which need to be explicitly imported are javax.swing, javax.sql, org.xml.sax.

Not automatically making the whole JDK available to bundles has the following benefits:

  • certain Java environments (eg JavaME) only have subsets of the full Java libraries that come with the desktop JDK; because bundles declare their requirements, it is easy to see which bundles are compatible with restricted Java environments.
  • older versions of Java have fewer packages in their libraries than newer versions. However declaring a constraint on a Java version is not the solution because sometimes third-party libraries can be installed to provide functionality that is only in later JDKs.
  • the full JDK is huge; it is just good design for bundles to declare their requirements at a finer-grained level

Optional Imports and Dynamic Imports

An Import-Package statement can be marked optional, meaning the bundle will be resolved (and therefore be useable) even when the imported package cannot be found at the time the bundle is being resolved.

The DynamicImport-Package: statement declares that packages will be looked up at runtime; as with optional imports, the bundle will resolve even when the imported package cannot be found at the time the bundle is being resolved.

Both Import-Package and DynamicImport-Package can take a list of package-names; dynamic import can also take a ‘*’ wildcard.

Import-Package is always processed at bundle resolution time. Dependencies can be marked ‘optional’, but if the bundle successfully resolves all of its non-optional imports without finding a provider of an optional package, that optional package cannot be found later. Using Import-Package with the ‘optional’ flag is appropriate when it is known that a bundle will definitely be providing a particular package and might be providing an associated one, ie where the mandatory and optional packages are visible as an “atomic” operation, and the importing bundle already explicitly imports the mandatory one. This may be the case where a bundle has an optional “bundle fragment”; in this situation the expected package and the optional package will be made visible together as bundle-fragments are loaded before the host bundle is resolved. Using the ‘optional’ flag in other circumstances is an invitation to “startup race conditions”, where the optional package might or might not be visible depending upon whether the bundle providing the optional package is started first. Otherwise, if a bundle with optional imports is resolved before the optional import is available, then an explicit “refresh” of that bundle will be needed before it sees those now-available optional imports.

DynamicImport-Package is instead processed when needed, eg when Class.forName is used. When a match is found at runtime, the classloader which provided the needed package is cached in a similar way to the Import-Package behaviour, so that later lookups of classes in the same package will go direct to the appropriate classloader. The performance impact is therefore similar, except that the resolving performance hit is taken for Import-Package at “bundle resolve” time, while for DynamicImport-Package it happens unpredictably whenever the package is actually needed. There is a significant performance hit for lookups that fail, as each lookup of a missing class by name will search over all bundles each time. As with Import-Package, there are potential “startup race conditions” where Class.forName is invoked before the bundle providing the needed package has been resolved. However in this case, a later lookup will repeat the search and find the bundle whereas once Import-Package has decided the package is not available (at resolution time) it will never be found unless the searching bundle is uninstalled and reinstalled/re-resolved.

Using DynamicImport-Package with a wildcard * prevents using a version-specifier, ie if multiple versions of the package are available it is not defined which version will get used.

The classic use-case for DynamicImport-Package is a bundle which implements deserialization of arbitrary objects (eg from binary form, or from xml). If the bundle cannot know which classes it may encounter, then dynamic imports is a possible solution. An alternative solution for this use-case may be to iterate over a set of Bundle objects calling Bundle.loadClass to see which (if any) bundle can provide the required class; this is effectively what OSGi will do anyway, but the application may have better information about which bundles to look in.

Bundle Fragments

A jarfile with the entry Fragment-Host: {bundle-name} in its manifest.mf file is an OSGi fragment. Fragments are not bundles themselves, but instead “additional resources” for the bundle specified by {bundle-name}. In effect, the fragment is “merged into” the specified bundle at runtime (ie content is accessed via the host’s classloader). Any classes inside the fragment are effectively part of the host bundle, as are any non-class resource files.

A fragment has no effect at all if there is no installed bundle matching the {bundle-name}.

One use-case for fragments is to contribute localisation configuration files to a core bundle. The core bundle can be shipped to all customers, and different localisation fragments to corresponding customers. At runtime, there is just one bundle which appears to have both the code and the relevant localisation files.

Fragment bundles must be installed before their host bundles.

Bundles with Embedded Jarfiles

A bundle can include one or more jarfiles within it, and use a Bundle-Classpath manifest.mf entry to point to them. The classes in those jarfiles effectively become part of the bundle’s code. Bundle fragments can also include jarfiles and point to them via a Bundle-Classpath entry in which case classes and resources in those jars also become part of whatever “host” bundle they are merged with.

Importing an Exported Package

When a bundle includes some classes and exports these to other users, it can also optionally choose to add an import statement for the same package. This is useful if multiple bundles (particularly different versions of the same bundle) contain copies of the same classes.

When code is just used for internal purposes (ie is not exported), then the fact that there are multiple copies does no harm. However if those classes become part of a public API (ie are exported) then communication between bundles can fail because classes (even when identical) with different classloaders are not compatible (cannot be cast to each other).

When a bundle imports a package it also declares internally, and the import-statement matches some other bundle then the classes are imported from the other bundle and override (hide) the internal implementation. The result is that (as long as the imported code really is compatible with the internal one, which the import-statement can ensure by specifying the right version range), the incompatible-class-in-api issue is resolved.

When “Import-Package” is used, and the package is not found externally but is found internally then resolution succeeds anyway.

Unloading/reloading bundles

OSGi provides an API to “unload” a bundle (ie a jarfile) at runtime. This is possible because each OSGi bundle has its own Classloader (unlike in traditional Java where unloading a jarfile is not possible because all classes are loaded via a single classloader responsible for everything on $CLASSPATH).

However bundles that “import” packages from an unloaded bundle still have references to the bundle’s classloader and therefore to the bundle classes. In addition, all classes which inherit from or have members with types from the the referenced bundle have indirect references to the original bundle classloader. The original classloader for the unloaded bundle therefore cannot yet be garbage-collected (nor any class the classloader ever loaded). If an unloaded bundle is replaced by another which provides the same packages (eg has been upgraded to a new version with bugfixes) then class-cast-exceptions can also occur when communicating between bundles that have bound to different (old vs new) versions of those packages; in addition, bundles resolved against the old uninstalled bundle version will still be using old/obsolete code.

OSGi therefore supports “refreshing” a bundle, forcing OSGi to re-resolve its imported packages, and if any change has occurred in the providing classloaders then the refreshed bundle’s classloader is discarded (ie as if the refreshed bundle had itself been unloaded/reloaded). Bundles that then use exports from the refreshed bundle should also be refreshed.

This cascading refresh process doesn’t work well if any bundle is using static variables, as their value is lost when the bundle is refreshed. And if a static variable in a non-refreshed bundle references a class from another bundle then that blocks garbage-collection of that bundle’s classloader including all classes it has previously loaded.

However the refresh process does work very well when some “api” bundle defines an interface and then an “implementation” bundle defines a private implementation of that interface and exposes instances of the type to users rather than the raw implementation class. This is in fact exactly the design pattern used for OSGi services.

Supporting Incompatible Libraries

Suppose bundle A requires package org.utils version 2, and bundle B requires package org.utils version 3.

Both versions of org.utils need to be packaged as OSGi bundles, with a manifest that declares “Export-Package: org.utils; version=…”, and loaded as OSGi bundles rather than being on the normal classpath. Each will then get its own OSGi classloader. When bundle A is resolved, its internal map will have an entry for “org.utils” that points to the bundle-loader for the org.utils(v2) jarfile. Any classes in A which reference types in package org.utils then have their dependencies resolved via that bundle-classloader. When bundle B is resolved, its map points to a different bundle-classloader, and both A and B are happy.

This is not possible if either A or B export classes whose apis expose classes from org.utils, as typecasts will fail when A and B try to communicate. See the section on “importing exported packages” for further information on this.

Possible Complications of incompatible library versions.

Static variables need to be handled with some care. If one bundle A exports code that directly or indirectly uses a static variable, then other bundles which use the same version of A will effectively be sharing that static variable. However if multiple different versions of A are available, then other bundles might not share the static variable depending on which version of A they bind to. This is particularly interesting for static vars that are used to implement the singleton pattern.

When a bundle internally uses a dependency-injection framework (such as Guice) then injection of objects whose types are taken from other bundles becomes interesting.

Defining AOP rules in one bundle which are to be applied to types from other bundles may be tricky.

Initialising an OSGi environment

To take advantage of OSGi’s classloading model without necessarily using the rest of OSGi, a simple Main class is needed which initialises OSGi and loads any required jarfiles. You can find an example program here.

If you still want to develop in a “top down” approach rather than an event-driven one, then perhaps the best way to “retake control” after starting OSGi is to define an “activator class” in one of your bundles pointing at the class that should be invoked after OSGi has been started. This class must implement the org.osgi.framework.BundleActivator interface. Alternately you can add a Declarative Services xml file to the jarfile containing an entry pointing at a plain POJO to be executed on startup (although it is then necessary to load/start the optional declarative-services bundle first).

The Export-Package qualifiers

The Export-Package declaration can specify uses {package}, include {classname} or exclude {classname} to ensure that other bundles can only successfully import this package if they can also:

  • see the specified packages in the same version the exporter uses;
  • see the specified included-classes; and
  • not see the specified excluded-classes.

In effect, uses/include are declaring “transitive dependencies” while exclude is declaring a “conflicts-with” issue. The requirement that the importer sees exactly the same package ensures that a ClassCastException does not occur. The referenced packages/classes should be types used in the api of classes in the exported package.

Provisioning

An OSGi “application” is composed of a set of cooperating bundles. However OSGi deliberately does not specify how to group sets of bundles that should be deployed together. This is done by higher-level management/provisioning software.

One example is Apache Karaf. Karaf defines the concept of a “features file” which is an xml file containing a list of bundles under a “feature name”. In the Karaf config files or user interface it is then possible to “install a feature” which simply uses the OSGi api to load each of the bundles in the feature. Interestingly, an entry in the feature file may use maven syntax to specify the location of the bundle, ie the bundle will be pulled from a maven repository (which may be remote).

There are many other “management” applications for OSGi.

Bundle Activators

Usually, one or more bundles also specifies a Bundle-Activator property in its MANIFEST which points to a class inside the jarfile; that activator class is eventually instantiated and invoked which results in an active thread now running “inside” the OSGi environment rather than the initial main method which was running “outside” it.

Other Notes

Require-bundle declarations are also processed at load-time; they are basically equivalent to having an “import-package” declaration for each “export-package” declaration in the target bundle.

The fact that import-package/requires-bundle declarations have been satisfied is enough to move a bundle to RESOLVED state. However this doesn’t mean that every class needed by the bundle exists; it is still possible when loading a class from the bundle to fail to find a type it uses; this triggers a ClassNotFoundException for that required type - which then results in a NoClassDefFound exception due to failed class resolution. There is a Maven plugin that checks for referential correctness at compile-time, and some IDEs do too. However if the bundle with the ‘Export-Package’ declaration properly applies uses, include and exclude options to the export-package declaration then the change of a ClassNotFound/NoClassDefFound exception at runtime is significantly reduced or eliminated.

A single JVM instance can potentially have multiple OSGi “environments” active at the same time. This is useful for webservers for example, where each webapp can run in its own OSGi environment totally decoupled from other webapps (including having its own copy of static variables)

Although it is not possible to directly obtain references to Class objects from bundles that don’t explicitly export them, it is possible to do so via reflection. If a bundle exposes classes in package example.util, and those classes have member fields of types from the (unexported) example.util.internal package, then code in other bundles can load a class in the exported package and then use reflection APIs to follow the links to the types in the internal package. Therefore ‘internal’ (unexported) packages prevent accidental use of internal classes, and prevent accidental name clashes, but are not a security mechanism. In fact, the Class.loadClass(classname, resolve, classloader) method can be used to load internal classes from any bundle; the OSGi bundle classloader implementations make no effort to separate “in-bundle callers” from “external callers”. It is of course trivial to obtain access to the ClassLoader for any bundle. In fact, loading internal classes is even easier than that : the Bundle.loadClass method can be used to load any class (including internal classes) from a bundle, and references to any Bundle can be obtained via BundleContext.getBundles(). Again, this just points out that not exporting a class is not a security measure; importing/exporting packages avoids only accidental coupling between bundles.

It is technically possible for multiple bundles to contribute different classes to the same java package, eg one bundle provides “example.ui.Widget” and a different bundle provides “example.ui.Gadget”; this is called “split packages”. It is highly discouraged, and the only good reason to do this is if you are refactoring an existing jarfile into multiple jarfiles and do not wish to break users of the old code. See the official OSGi documentation on “split packages” for more details.

Summary

OSGi has some very neat features at a very low overhead; I strongly recommend that any project which is composed of more than half-a-dozen libraries use the OSGi framework to launch itself, and take advantage of the OSGi classloader - even if it doesn’t use the rest of OSGi (service registration, standard service providers). The clean declaration of dependencies and the hiding of internal implementation details such as reliance on specific versions of other libraries are very tempting features for medium-to-large application development.

References

comments powered by Disqus