BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Java 9, OSGi and the Future of Modularity (Part 1)

Java 9, OSGi and the Future of Modularity (Part 1)

Bookmarks

Key takeaways

  • Java 9 is being released in 2017 and a flagship feature will be the new module system called Java Platform Module System (JPMS). The article explores how this relates to, and what impact it has, on the existing Java module standard, OSGi.
  • Java has grown by 20 times since version 1.0, and there is a need to modularise the platform. There have been many unsuccessful attempts to solve this problem. Meanwhile OSGi has been offering application modularity for 16 years.
  • OSGi and JPMS differ substantially in their implementation details. There appear to be significant flaws and missing features if JPMS is to be considered a general solution for modularity.
  • JPMS aims to be simpler and easier to use than OSGi. However, making an existing non-modular product modular is the principal source of complexity, and JPMS does not appear to have succeeded in this goal.
  • JPMS has done a very good job of modularising the Java platform itself, meaning we can construct small runtimes containing only relevant parts of the Java platform for a specific workload. For application modularity OSGi has many advantages. We have shown the two can be combined and by doing so it looks set to be a winning formula.

This is the first article of the "Java 9, OSGi and the Future of Modularity” series. Please also see Part 2: Java 9, OSGi and the Future of Modularity (Part 2).

When Java 9 is released next year, the flagship feature will be the new module system: the Java Platform Module System (JPMS). Although the details of JPMS are not entirely fixed, we already know quite a lot about the shape it will take.

Java does have a pre-existing module system, which has been around in various forms since 2000. This is the module system known as OSGi, which is a vendor-independent industry standard. It is published by the OSGi Alliance, a consortium of leading software vendors, telecoms companies and other organisations, including Adobe, Bosch, Huawei, IBM, Liferay, NTT, Oracle, Paremus and Software AG. It powers almost all Java EE application servers, the most popular IDE, web applications like eBay, Salesforce.com and Liferay, and is used by governments and militaries such as the U.S. Air Force and Federal Aviation Administration.

OSGi was made for IoT – from the start OSGi was designed for embedded devices, years ago when memory and CPU resources were significantly constrained. Devices today have more power. This has provided the opportunity to build sophisticated applications and solutions, and has spawned thriving ecosystems of organisations and individuals contributing software and hardware elements to an overall solution. Such ecosystems can be found in markets as diverse as connected homes, connected cars, smart cities and Industry 4.0 (IIoT). A gateway is typically used to connect sensors and devices to each other and to back-end systems. Applications and services can run both locally on the gateway and/or in the cloud.

OSGi also provides many specifications to enable essential features for building open IoT ecosystems. These features include device management, software provisioning, and device abstraction which generalises devices from their underlying communication protocols. Companies like AT&T, Bosch, NTT, Deutsche Telekom, General Electric, Hitachi, Miele, Schneider Electric all benefit from using OSGi to build IoT solutions today, and have done for many years. With OSGi and IoT there are already millions of devices connected today.

Naturally, OSGi users are curious how the new module system in Java 9 will affect OSGi, both in the short and long terms.

There are technical, political and commercial reasons why two module systems in the Java ecosystem will soon exist. In this article we steer clear of the politics and compare the two from a technical perspective. We conclude with a vision of how JPMS and OSGi can work together, what we think their respective domains are, and what opportunities will exist in the new world.

Note that in this article we have used information publicly available in August 2016. Some details may change before the specification becomes final.

Background

The Java platform has grown significantly since its birth in the late 1990's. Looking at download sizes, the JDK 1.1 was under 10Mb in size, whereas the Mac OS X download for JDK 8u77 is a massive 227Mb. There has been a corresponding increase in installed footprint and memory requirements. This increase has been driven by the addition of new features, most of which have been welcome and useful. However, every new feature in the platform created bloat for users who didn't need it... and nobody uses all of the platform. Also all existing features are retained even after they have become obsolete, because the stewards of Java have shown an admirable dedication to backwards compatibility.

For many years, Java's increasing weight was not a big problem. It was most popular as an enterprise platform, and its main competitor was Microsoft's .NET that followed a broadly similar trajectory. In the modern world, Java is facing different challenges. IoT is driving a renewed focus on footprint, and newer agile platforms and languages like Node.js and Go are serious competitors.

Security is also a big problem: attacks on Java prompted security-conscious organisations to remove it entirely from user desktops. If there were better isolation between the internals of the JVM and user-space application code, many of these attacks would not have been possible.

It has been clear for a long time that something needed to be done to modularise the platform. There were a series of aborted efforts in the mid 2000's – JSR 294 and its "superpackages", JSR 277's "Java Module System" – then eventually a prototype project called Jigsaw emerged. This was originally meant to be delivered in 2011 with Java 7, but was deferred to Java 8 and then again to Java 9. As a prototype project, Jigsaw informs and provides the reference implementation for the JPMS specification.

Meanwhile, OSGi has been evolving and improving for 16 years. OSGi is a standard for application modularity: since it is not directly part of the Java platform, it cannot affect modularity of the platform itself. But many applications have benefited from the modularity model that it provides above the level of the JVM.

High-Level Comparison

There are many small differences between JPMS and OSGi, but there is one very big difference, and that is in the implementation of isolation.

Isolation is the most fundamental feature of any module system. There must be some means of protecting each module from interference by other modules running in the same application. Isolation is a continuum rather than a binary concept: neither OSGi nor JPMS do anything to protect against a badly-behaved module that grabs all available memory in the JVM, spins up thousands of threads, or hogs the CPU with a busy loop. Running modules as separate processes on the OS can provide some protection of this kind, but even then it is not perfect; somebody could still crash the OS or wipe the disk.

Both OSGi and JPMS offer code-level isolation, which means that a module cannot access the internal types of another module except with the explicit consent of that module.

OSGi implements its isolation using class loaders. Each module – or “bundle” in OSGi terms – has a class loader that knows how to load the types within the bundle. It can also delegate class loading requests to the loaders of other bundles that it depends on. This system is highly optimised, for example OSGi does not create a class loader for a bundle until the last possible moment, and the fact that each loader sees a much smaller set of types means that each type can be loaded marginally faster.

The big advantage of this system is that bundles can contain overlapping packages and types, and these do not interfere. As a practical consequence, it's possible to have multiple versions of certain packages and libraries running at the same time in the same JVM. This is a boon when dealing with complex transitive dependency graphs that often result from using build tools like Maven. In many enterprise Java applications it is nearly impossible to come up with a set of dependencies that contains only one version of every library.

For example, let's take a look at the JitWatch library1. JitWatch depends on slf4j-api 1.7.7 and logback-classic 1.1.2... but logback-classic 1.1.2 depends on slf4j-api 1.7.6, clashing with the direct requirement from JitWatch. JitWatch also depends transitively on both version 1.6 and version 1.9 of jansi, and if we include test-scoped dependencies we get yet another version of slf4j-api, 1.6. This kind of mess is very common, and traditional Java has no real solution for it except to gradually add “excludes” to our dependency tree until we magically get a set of libraries that work. Unfortunately JPMS also has no answer for this problem, as we will soon see.

Using class loaders for isolation does have one drawback: it breaks the assumption that every type can be found in at most one location. This is a natural consequence of modularity. If a module can use its own types free from interference by other modules, then it's inevitable that a single type name may be found in more than one module. Sadly this creates a problem for a lot of legacy Java code that is not written with modularity in mind. In particular, calls to Class.forName(String) to find a type from its name will not always work correctly in a truly modular environment, because there are multiple types that it could return.

It is this drawback that prevented OSGi from being used to modularise the JDK itself. Many parts of the JDK have an implicit assumption that any JDK type can be loaded from any other part of the JDK, and so many things would break under an OSGi-like model. In order to solve this problem -- and also to ease migration for code that uses Class.forName – the JPMS elected not to use class loaders at all for isolation. When you launch an application using a set of modules on the “modulepath”, all of these modules will be loaded by the same class loader. Instead, JPMS introduces new accessibility rules to achieve isolation.

The isolation barrier in OSGi is visibility. In OSGi we cannot load internal classes of a module because they are not visible from the outside. That is, my module's class loader can only see the types inside my module, plus the types explicitly imported from other modules. If I try to load an internal class from your module, my class loader cannot see that type. It is as if the type did not exist at all. If we try to go ahead and load the class anyway then we get a NoClassDefFoundError or ClassNotFoundException.

In JPMS, every type has visibility of every other type because they exist in the same class loader. But JPMS adds a secondary check to make sure the loading class has the right to access the type it is trying to load. Internal types from other modules are effectively private, even if they are declared as public. If we try to go ahead and load them anyway then we get an IllegalAccessError or IllegalAccessException. This is the same error we would get if we tried to load a private or default-access type from another package, and it does not help to call setAccessible on the type. This changes the semantics of the public modifier in Java, which formerly meant universally accessible and now means only accessible within a module and its requirers.

The drawback of the JPMS approach is that it is not possible to have modules with overlapping contents. That is, if two modules each contain a private (non-exported) package named org.example.util then those modules cannot both be loaded at the same time on the module path – this would result in a LayerInstantiationException. It is possible to work around this limitation by instantiating class loaders from our application… but this is exactly what OSGi already does for us!

Again this is entirely by design, to allow JPMS to modularise the internals of the JDK. But the consequence is that you can have modules that do not work together purely because their internal implementation details conflict.

Complexity

One of the most common complaints about OSGi is that it can increase complexity for a developer. There is a grain of truth here, but people who make this complaint are mistaking the medicine for the disease.

Modularity is not a magic dust that can be sprinkled onto an application just before release. It is a discipline that must be followed throughout all phases of design and development. Developers who adopt OSGi early and apply modular thinking before writing a line of code realise enormous gains, and they discover that OSGi is in fact exceedingly simple, especially when using a modern OSGi toolchain that automates generation of metadata and does a lot of consistency checking to catch errors before they reach the runtime.

On the other hand, developers who tried to bring OSGi to an existing large code base suffered because that code was rarely modular enough to make migration easy. Without the discipline of enforced modularity, it’s too easy to take shortcuts that break encapsulation. As a developer on BEA WebLogic told me, before BEA was acquired by Oracle: “we thought we were being modular, until we started using OSGi.”

In addition to non-modular applications, OSGi adoption has been hampered by non-modular libraries. Some of the most popular Java libraries are filled with assumptions about class loading and global visibility that are broken in a modular architecture. OSGi has done a lot of work to make it possible to use such libraries, and this is the source of the apparent complexity of the OSGi specifications. We need a certain amount of complexity to deal with the messy, complex real world.

The same problems will arise just as much under JPMS – probably more so, as we will soon see. If your organisation previously tried to adopt OSGi and gave up because of the amount of migration work involved, then you should expect at least as much work if you choose to migrate to JPMS. Just look at the experience of Oracle in modularising the JDK: it was so much work that it caused Jigsaw to slip from Java 7 to Java 8, and then to Java 9, and even Java 9 has been delayed by a year (so far).

Project Jigsaw started with a goal of being simple, but the JPMS specification has increased enormously in complexity: the interplay of modules with class loaders; hierarchical layers and configurations; re-exporting requirements; weak modules; static requirements; qualified exports; dynamic exports; inherited readability across layers; multi-module JARs; automatic modules; unnamed modules… all these features have been added as the need for them became clear. A similar process happened in OSGi, just with a 16-year head start.

Dependencies: Packages vs. Whole Modules

Isolation is only one half of the modularity puzzle: modules still need to work together and communicate. After building walls between modules we need to reintroduce connections, but in a controlled way. A module system must define the ways in which modules access functionality from other modules. This can be done statically, at the level of types, or dynamically with objects.

Static dependencies are those that can be known and controlled at build time. If a type refers to another type across a module boundary then the module system needs a way to make that type visible and accessible. There are two aspects to this: modules need to selectively expose some of their enclosed types, and they need to specify which types they may use from other modules.

Exports

In both OSGi and JPMS, exposing types is done at the level of Java packages. In OSGi we use the Export-Package statement, which declares that certain named packages can be made visible to other bundles. It looks like this:

Export-Package: org.example.foo; version=1.0.1,
	  org.example.bar; version=2.1.0

This statement appears in the META-INF/MANIFEST.MF file. In the early days of OSGi most developers would specify statements like this manually; but increasingly we prefer to use build tooling to generate them. The most popular pattern now is to use annotations on the Java source code, and in Java 5 the package-info.java file was introduced to allow package-level annotations and documentation, so in OSGi we can do the following:

@org.osgi.annotation.versioning.Version("1.0.1")
	package org.example.foo;

This is a useful pattern because the intent to export a package can be indicated directly in that package. The version can also be indicated here, and is close at hand to be updated when the package content changes2.

In JPMS a package is exported in the module-info.java as follows:

	module A {
		exports org.example.foo;
		exports org.example.bar;
	}

Note the lack of a version, as neither modules nor packages can be versioned in JPMS; we will revisit this point later.

Imports/Requires

Whereas OSGi and JPMS are similar with respect to exports, they differ significantly with respect to importing or depending on other modules.

In OSGi, the complement of exporting a package is importing a package. We import packages with an Import-Package statement, for example:


	Import-Package: org.example.foo; version='[1,2)',
	  org.example.bar; version='[2.0,2.1)'

The rule is that an OSGi bundle must import every package that it depends on, with the exception of packages beginning with java.*, e.g. java.util. For example if the code in your bundle depends on the type org.slf4j.Logger (and your bundle does not actually contain the package org.slf4j) then that package must be listed as an import. Likewise if you depend on org.w3c.dom.Element then you must import org.w3c.dom. However if you depend on java.math.BigInteger you do not import java.math because the java.* packages are loaded by the bootstrap class loader of the JVM.

OSGi also has a parallel mechanism for requiring whole bundles, called Require-Bundle, but this is deprecated in the OSGi specification and only exists to support very slim edge cases. The big advantage of Import-Package is that it allows modules to be refactored and renamed without affecting downstream modules. This is illustrated in Figures 1 and 2.

In Figure 1, module A is refactored into two new modules, A and A’, but the module B is unaffected by the operation because it depends on the provided packages. In Figure 2 we perform exactly the same refactoring on A, but now B is probably broken… because it probably used packages that are no longer present in A (we have to say "probably" here because we can't tell what B is using from A... which is exactly the issue!).

Figure 1: Refactoring Modules with Imported Packages

 

Figure 2: Refactoring Modules with Requires

The Import-Package statement would be cumbersome to write manually... so we don't do that. OSGi tooling generates it by inspecting the dependencies that are baked into the compiled types inside the bundle. This is very reliable – much more reliable than developers declaring runtime dependencies for themselves. Of course developers still need to manage their build dependencies, which is done in the normal way with Maven (or your build tool of choice). At build time it hardly matters if you put too many dependencies on your classpath: the worst that can happen is a compilation failure, which affects only the original developer and can easily be fixed. On the other hand, having too many runtime dependencies can kill the portability of a module, since all those dependencies must be dragged along and may conflict with the dependencies of another module.

This leads to another key philosophical difference between OSGi and JPMS. In OSGi we have always recognised that build dependencies and runtime dependencies can and often do differ. For example it's standard practice to build against APIs and run against implementations of those APIs. Furthermore, developers typically build against the oldest version of an API that we can possibly be compatible with, but at runtime we choose the newest version of the implementation we can find. Even non-OSGi developers are familiar with this approach: you normally build against the lowest version of the JDK you are prepared to support, while encouraging users to run with the highest version including all of its security patches and performance enhancements.

JPMS on the other hand takes a different tack. JPMS aims to achieve “fidelity across all phases” so that “the module system should… work in exactly the same way at compile time, at run time, and in every other phase of development or deployment” (from the JPMS Requirements). Therefore dependencies are defined in terms of whole modules at runtime because this is how they are defined at compile time. For example:

module B {
		require A;
	}

This require statement has the same effect as OSGi's deprecated Require-Bundle: all of the exported packages of module A are accessible to module B. It therefore has the same issues as Require-Bundle: there is no way to determine from the module declarations whether it is safe to refactor the contents of module A, so in general it is never safe to do this.

We have found that dependency trees using requirements rather than imports have a much higher degree of fanout: each module carries around many more dependencies than it really needs. These problems are real and significant. Eclipse plug-in authors have suffered from them in particular, because for historical reasons Eclipse bundles tend to use requires rather than imports. We feel it is very unfortunate that JPMS has followed this route.

Interestingly, though compile/runtime fidelity is a fundamental goal of JPMS, recent changes have significantly weakened fidelity. The current Early Access release allows for a requirement to be declared with static modifier, which means that the dependency is mandatory at compile time but optional at runtime. Conversely an export can be declared with the dynamic modifier which makes the exported package inaccessible at compile time but accessible (using reflection) at runtime3. With these new features it's possible to create modules which successfully compile and link but throw IllegalAccessError/Exception at runtime.

Reflection and Services

The Java ecosystem is huge, and contains a wide variety of frameworks for various purposes: from dependency injection to mocking, remote invocation, O/R mapping and so on. Many of these framework use reflection to instantiate and manage objects from within user-supplied code. For example take the Java Persistence Architecture (JPA), which is part of the Java EE suite of specifications: as an O/R mapper it must load and instantiate domain classes from user code in order to map them to records loaded from a database. In another example, the Spring Framework loads and instantiates "bean" classes as implementations of interfaces.

This can present problems for module systems, including both OSGi and JPMS. A domain or a bean class is something that ideally should be hidden inside a module: if it is exported then it becomes public API that can cause consumers to break if they depend on it, but we want the flexibility to change our internal classes at will. On the other hand it can be useful to access non-exported types via reflection to support frameworks, as described.

Due to OSGi's ClassLoader-based design, modules can obtain visibility of the non-exported packages and types of other modules... so long as they know the fully-qualified name of the type and which module to ask (remembering that several modules could contain any given type name). This is a pragmatic reduction of isolation in the spirit of Java's long-standing use of reflection, where even so-called private fields can be opened up by calling the setAccessible method.

Using this capability it is common practice in OSGi to provide implementation modules that have no exports at all! Instead they may contain declarations that refer to internal types that may be loaded by a framework. For example, a module that uses JPA for persistence can refer to domain types in a persistence.xml file, and the JPA implementation module will load the referred types when they are needed.

The biggest use-case is in implementing service components. The OSGi specification includes a chapter called Declarative Services (DS) that defines how a module can declare components: classes whose lifecycle is managed by the framework. Components can be bound to services in the OSGi Service Registry and optionally provide services of their own. For example:

@Component
    public class CartMgrComponent implements CartManager {

    	@Reference
    	UserAdmin users;

        @Override
        public Cart getOrCreateCart(String user) {
            // ...
        }
    }

In this example, CartMgrComponent is a component that provides a service CartManager. It references a service, UserAdmin, and the lifecycle of the class is managed by the DS framework. When a UserAdmin service is available, CartMgrComponent is created and it publishes a CartManager service that can be similarly referenced by other components in other modules.

This framework works because it can load the CartMgrComponent class, which has been flagged as a component with the @Component annotation. Defining components and services is the principal way in which OSGi applications are designed and programmed.

In JPMS, only types in exported packages can be accessed, even with reflection. Although the types in non-exported packages are visible – you can call Class.forName and obtain a Class object – they are not accessible from outside the module. As soon as a framework tries to call newInstance to instantiate an object, an IllegalAccessException is thrown. This appears to cut off many possibilities for frameworks, but there are some ways forward.

One way is to provide individual types as services that can be loaded via java.util.ServiceLoader. ServiceLoader has been a part of the standard platform since Java 6, and it has been updated in Java 9 to work across modules. ServiceLoader can access types from non-exported packages, so long as the providing module includes a provides statement. Unfortunately ServiceLoader is primitive and doesn't offer the flexibility needed for modern frameworks such as DS or Spring.

A second possibility is to use a "qualified" export of the package. This is an export that is only made accessible to a single named module, rather than generally to all modules. For example you could export your beans package to the Spring Framework module. However this would fail for something like JPA, because JPA is a specification rather than a single module, and it can be implemented by different modules such as Hibernate, EclipseLink, etc.

A third possibility is a "dynamic" export, where the package is accessible to anybody but by reflection only, not at compile time. This is a very new feature of JPMS and it is still controversial on the mailing lists. It is closest to OSGi's permissive approach, but it still requires the module author to explicitly add exports dynamic for every package that might contain types that need to be reflectively loaded. As an OSGi user this feels like an unnecessary complication.

Until the next instalment

That’s it for part 1 of our article. Look out for part 2 coming in two weeks, in which we examine the topics of versioning, dynamic loading and the potential for future interoperability of OSGi and JPMS.

About the Authors

Neil Bartlett is a principal engineer, consultant, trainer and developer with Paremus. Neil has been working with Java since 1998 and OSGi since 2003 and specialises in Java, OSGi, Eclipse and Haskell. He is the founder of the Bndtools eclipse plugin, the leading IDE for OSGi. He can often be found on twitter (@nbartlett) tweeting on all things #OSGi and answering questions on Stack Overflow where he is the only holder of a gold OSGi badge. Neil regularly contributes to the Paremus Blogs and is also writing his second book "Effective OSGi” which will show developers how to quickly accelerate their productivity with OSGi using the latest techniques and tools.

Kai Hackbarth is an Evangelist at Bosch Software Innovations. He has been deeply involved in the technical standardization activities of the OSGi Alliance for more than 15 years. Kai is a member of the OSGi Alliance Board of Directors and has been co-chair of the OSGi Residential Expert Group since 2008. Kai is coordinating several research project activities in various IoT domains. His key focus areas are smart homes, automotive, and the Internet of Things in general, where he actively supports the current developments and strategic positioning of the product portfolio.

References

1 Thanks to Alex Blewitt for this analysis.
2 The @Version annotation is used to imply export, since only exported packages need a version. In the next OSGi release a more explicit @Export annotation is planned.
3 This area changed again on Sept 12, 2016, just before this article went to press. Dynamic exports are now replaced with a concept of “weak modules”. We are still evaluating the impact of this fundamental change, and note that it has caused a further slip of 4 months in the release schedule of Java 9.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Complete and very clear article

    by Charlie Mordant,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Love you guys, OSGI rules forever.
    Are the OSGI impls J9 compliant? I heard about a POC has been done a long time ago...

  • Re: Complete and very clear article

    by Neil Bartlett,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    I tested this a while ago. OSGi uses only standard, supported Java APIs so it *has* to work.

  • Re: Complete and very clear article

    by richard nicholson,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    "Pressure and Time"

    Anything can be achieved if enough us get involved with the OSGi Alliance and pull together....

  • Re: Complete and very clear article

    by Simon Gwerder,

    Your message is awaiting moderation. Thank you for participating in the discussion.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT