Java 9 was the first release making the JPMS available for general use. For many developers the impact of JPMS might not be clear, since its not a mandatory thing and the benefits are often overlooked.

Why JPMS was desperately needed

One might think - Java strict coupling of the package to filesystem structure allows to neatly organize the code. For dependency management of other packages/modules there are proven tools available like maven or gradle.

Organizing the code is only half of the picture. Once the javac compiler get hands on the code all that well intended structure gets blown out of the window. The compiled application does not follow the packaging structure layed out and maybe put up in a nice graph. In fact every time an unknown class is referenced the JVM looks through all packages starting with the main class and takes the first matching class. It does not have the concept of artifacts and their dependencies. Means, there is a misconception about what we design and what ends up in runtime.

This has major implications to which everyone who worked with a slightly larger and maybe legacy application can relate to.

NoClassDefFound

If the JVM can’t find a class that was available during development it simply crashes during runtime sooner or later - unexpectedly. Reasons for this could be difficult to find.

Two versions of the same class (shadowed class)

Different dependencies might pull in the same library in different versions (i.e. for logging). The JVM is going to use only one version. Which one is pure luck same as the time when this surfaces during runtime. In the worst case the new/old version of a library doesn’t change the API but only its behavior.

Multipe class loaders

With the intend to solve either of the mentioned problems one might introduce multiple class loaders to handle transitive dependencies of libraries in different versions. Class loading becomes a complex delegation problem that is challenging to test and understand.

Weak (no real) encapsulation

While package only might suggest a safe whay to encapsulate implementations no external party should use/call directly - this only applies on the surface.

Whith the serious attempt to use an effectivly internal part of your module one might simply use reflection to access it(break in). You can’t avoid this. Don’t even think of making part of your module available to internal module only.

There is a famous example for this. The JDK internal class Unsafe used by popular projects like Netty or Neo4j. It was never intended to be used outside of the JDK - but its functionality was to jucy so folks worked around it. It became a critical part of the JDK, difficult to change without breaking lots of unwanted dependencies.

Slow startup

Before Quarkus had become popular Java was stamped to be super slow, impossible to use for many cloud projects. Part of the reason was the linear code scan the JVM had to do to check classes needed for startup - which can be quite a bunch.

Inflexible runtime

One wished to be able to use a trimmed down version of the JRE for applications with small memory footprint requirements. Before Java 8 this was not supported. You had to use the entire JRE with Swing UI support and XML parsing.

Explicit security

Instead of modelling boundaries with carefully exposed APIs security had to be explicitly tested with SecurityManager::checkPackageAccess to verify malicious code is not accessing sensitive class functionalities during runtime.

Most of the mentioned problems have in common that they surface unexpectedly during runtime. This is what makes them so tricky.

JPMS - Everything is a module

A software is a module that has clear responsibilities and uses other dependecies through APIs. If used by other modules it would export an API on its own. Only then can it be replaced and modified without impacting other modules.

With the Jigsaw project the OpenJDK brought this idea directly into the core of Java. Every part of the JDK was turned into a module. In result a small java.base module remained covering the core java. All additional functionality like java.xml, java.rmi have been moved into their own module. Aggregator modules exist to simplify the handling. An example for this is java.se for the Java SE API that act as a boundle for various modules. Everything is a module!

JPMS has certain goals:

  • reliable configuration by identifying violations to the dependency/module graph latest during launch time.
  • improve security & maintainability by enforcing API boundaries
  • improve performance by avoiding classpath scans
  • scalable platform by only using functionalities that are needed

Verification

JPMS runs verifications during compilation and runtime.

  • check dependency hierarchy for direct and transitive dependencies
  • verify there is only one version of a module. If not a result is a compilation error, Java itself cannot resolve this situation automatically.
  • verify there are no module cycles during compile time
  • ensure uniqueness of packages. Two different modules must not declare the same package.

Rule Enforcing

JPMS introduces the module path in addition to the classpath. Rules are only enforced when a Jar is on the module path. This allows for migration to the module path. In short - Jars participating in the module system by providing a module descriptor module-info.class need to obey the rules and only see other odules they need. Jars not participating in the module system see the whole JDK. The JDK itself always runs in module mode.

Example module declaration module-info.java

The module org.my.app.service (this name must be globally unique), uses another module external.dependency.

module org.my.app.service {
    requires external.dependency;
    exports org.my.app.service.alpha;
}

Hence, the external.dependency must export requires external.dependency.

module external.dependency {
    exports external.dependency.a;
}

The shown example only shows the very basics. There are additional concepts like transitive dependencies and service provider (just to name a few).

Summary

JPMS transformed the JDK into a set of modules(hundreds). It allows the JVM to understand the concept of dependencies and artifacts. This allows for a much improved runtime behavior with better security (enforced module relationships) faster execution (avoding class path scan) and less errors(explicit declaration of dependencies through a module descriptor in the JAR lead to compile or startup errors for duplicate or missing modules).

Links