Modularity - Java advanced (OCP)

Possibility to organize a projects in modules appeared in Java 9. In this post, we’ll have a look at differences between non-modular (“legacy”) projects and modular projects in Java, see how modules are created and how modular applications are run, and highlight gotcha’s for those taking a certification exam covering Java modules.

A Java module is an ensemble of exported and concealed package

At the time I write this article, the topic is pretty fresh for me, so feel free to add anything that comes in your mind and might be useful to know, or to drop any question or point anything that’s unclear. Thanks for helping me and future readers.

This article is part of a series on advanced Java concepts, also corresponding to topics to study for upgrading from level I certification to level II certification (OCP level). The new Oracle Java certification framework (as from Java 11) only offers one certification covering both levels, so you’ll want to go through the level I (“OCA”) topics as well.

Java modular vs non-modular code organization

Non-modular Java project

Before Java 9, and further by default, a Java project is organized in packages. The packages provide a logical grouping but no physic restrictions.

Classes are packaged into jar files and accessed via class path. Common deployment of related classes is not enforced.

The visibility of classes is controlled with the access modifiers (public/”default”), and encapsulation can be bypassed by Reflection. It’s impossible to restrict in which exact packages code can be used: any package can be imported in any class.

To compile a “legacy” (non-modular) java class, we use following command, which create a jar archive file package:

javac -cp /path/to/source/files
      -d /path/to/destination/folder
      -sourcepath /path/to/source

As per Oracle documentation:

-cp or -classpath path
Specify where to find user class files, and (optionally) annotation processors and source files. This class path overrides the user class path in the CLASSPATH environment variable. If neither CLASSPATH, -cp nor -classpath is specified, the user class path consists of the current directory.

If the -sourcepath option is not specified, the user class path is also searched for source files.

If the -processorpath option is not specified, the class path is also searched for annotation processors.

-d directory
Set the destination directory for class files. The directory must already exist; javac will not create it. If a class is part of a package, javac puts the class file in a subdirectory reflecting the package name, creating directories as needed. For example, if you specify -d C:\myclasses and the class is called com.mypackage.MyClass, then the class file is called C:\myclasses\com\mypackage\MyClass.class.

If -d is not specified, javac puts each class files in the same directory as the source file from which it was generated.

Note: The directory specified by -d is not automatically added to your user class path.

-sourcepath sourcepath
Specify the source code path to search for class or interface definitions. As with the user class path, source path entries are separated by semicolons (;) and can be directories, JAR archives, or ZIP archives. If packages are used, the local path name within the directory or archive must reflect the package name.

Note: Classes found through the class path may be subject to automatic recompilation if their sources are also found.

https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html

Optionally, we can add a descriptor file MANIFEST.MF, in which case we’d use the command:

jar --create --file myApp.jar
    --manifest=META-INF/MANIFEST.MF
    -C /path/to/source

That command can also be used without a manifest file, in which case a default manifest will be created (see Oracle documentation).

To execute the jar file, we use command:

java -jar myApp.jar

Modular Java project

Modules offer high-level code aggregation. The relations between the packages and resources are described and in control.

All classes/packages in a module are packaged and deployed together, no splitting can be made inside a module.

In the module root folder, we place a descriptor with the name module-info.class, which defines the relations of the described module:

  • the module is defined by a unique module name
  • it requires module dependencies
  • it exports some or all of his packages (= make them available to others modules; otherwise by default their are unavailable to other modules, hence the improved encapsulation)
  • it gives permissions to open content to other modules with Reflection (by default, it’s not permitted)
  • it offers services to other modules
  • it consumes services from other modules

All fields are not mandatory, it depends on the needs in each case.

The structure of the module-info.class file looks like:

module com.myDomain.myProject.moduleName {
  requires <module names>;
  exports <packages>;
  opens <packages>;
  uses <services from other modules>;
  provides <services in other modules> with <service implementation from current module>;
  version <value>;
}

Advantages of modularity

To start with, it’s important to underline that modularity is a choice, not an obligation! If you feel like you won’t need to design a modular application, you don’t have to!

Deeper encapsulation

Packages in a module are hidden by default. They’ll be made available only explicitly by exposing them in the module specs. Even if some classes are public, they’re hidden unless otherwise specified.

Also use of Reflection API is not allowed unless explicitly specified.

Smaller application deployment footprint

Because each module only exposes and consumes necessary packages.

Separation of concerns

Known as a good practice in OOP (and probably not only using that paradigm).

Reusability

Well conceived modules can be used in several projects, preventing from coding redundant logic, thus gaining time in development.

Java Platform Module System (JPMS)

The Java Platform Module System specifies a distribution format for collections of Java code and associated resources. It also specifies a repository for storing these collections, or modules, and identifies how they can be discovered, loaded and checked for integrity.

Java Platform Module System

Since modules were added to Java capabilities with Java 9, all Java APIs were repackaged as modules. It means that all classes of Java belong to modules. Practically, it doesn’t make any difference. Like previous native Java packages were globally available in Java projects, those modules are globally available without explicit import.

The standard Java modules’ names start with java.* prefix. They are part of the Java SE specification. Some examples: java.sql/.xml/.logging/.base/.desktop/…

java.base exports core packages like java.lang/.io/.net/.util but also includes concealed packages like com.sun.crypto.provider, sun.nio.ch, sun.reflect.annotation, sun.security.provider…

Every module depends on java.base. It doesn’t need an explicit import.

There are other modules, specific to the JDK, whose name start with jdk.* prefix. For example: jdk.shell/.policytool/.httpserver/.jshell/…

Things to know about modules in Java

  • Circular module dependencies are not allowed.
  • Classes of modular applications are loading using module-path, not class-path.
  • Missing modules are detected at startup.
  • Module java.base doesn’t need explicit import, but writing explicitly “requires java.base” is not raising any error or problem. It contains Java core classes.
  • When configuring a list of modules or packages, with keywords exports and requires for example, you can use a comma separated list on the same line. But you might also write one line per import/export.

Configuring modules with module-info.class

Next sections will go through the configurations of the module info file. The keywords used in the module-info.java file are called “directives”. These keywords are restricted keywords, only in the module-info class context.

Module dependencies – “requires”

module com.myDomain.myProject.myModule {
  requires java.logging;
  requires transitive org.bar;
  requires static com.foo;
}

You can require modules (which have to make their or some of their packages available to the requiring module in their definition). As in usual applications, the packages have to be imported by the classes using them.

transitive means any module requiring the current module will require that module too. This is kind of a cascade require. Taking an example, if a module B requires myModule, B will implicitly be able to access bar.

static means that the module is required at compile time but not necessarily at runtime.

Allow use of packages – “exports”, “exports … to”

module com.myDomain.myProject.myModule {
  exports foo.a;
  exports foo.be to otherModule;
}

Exports makes one or several packages available to all other modules. If you wish to make them available to a specific (list of) module(s), you can specify it with “to” (qualified export).

Beware that only public classes (and their public members) will be made available!

Open module content – “opens”


module com.myDomain.myProject.myModule {
  opens foo.a;
  opens foo.be to otherModule;
}

Opening a package means the same as export BUT also allows the use of Reflection. As a reminder, Reflection allows on-fly access to all classes and can bypass encapsulation!

Then why would you open classes, as this is obviously risky? Well, for example, classes that contain injectable code should use “opens” directive because injections work via Reflection!

Open entire module to all – “open”

open module com.myDomain.myProject.myModule {
  ...
}

Keyword “open” (and not “opens”) in front of the module word makes your module work almost as in non-modular applications. It creates runtime only access to all content of the module being open.

Consuming and producing services – “uses”, “provides”

It’s possible to expose only services instead of all public classes.

module bar.service {
  exports bar.service.a;
}

// defines the interface or abstract class somewhere in the module
module nan.provider {
  requires bar.service;
  provides bar.service.X with nan.provider.b.Y;
}

// implementation in that module
package nan.provider.b;
public class Y implements bar.service.a.X {
  ...
}
module foo.app {
  requires bar.service;
  uses bar.service.a.X;
}

bar.service is a module, a is a package, X is an interface or an abstract class. Thanks to the abstract type, no direct reference to the provider is made in the code.

// example of use in package foo.app
public static void main (String [] args) {
  ServiceLoader<X> loader = ServiceLoader.load(X.class); // loads a Stream of all modules providing implementation for type X
  ...
}

Versions – “version”

Version configuration allows multi-release module archives. Only one copy of a module can be placed into a module-path. Multi-release jar can be used to support different versions for different versions of the JVM (Java versions).

Module root directory may contain either default version of the module or non-modularized version for Java before 9.

Multi-release require version descriptors in the manifest file.

META-INF
    MANIFEST.MF
    versions
        11
            com
        9
            com
    ...

Compiling a module

Compiled module includes the packages exported by the module to other modules, location of the jars for automatic modules, and the module-info file.

javac --module-path /paths/
      -d /path/to/destination/folder
      -sourcepath /path/to/source

Packaging to jar

One module jar is produced for every module. The module is the unit of release and reuse.

jar --create -f /path/myJar.jar
    --main-class thePackage.theMainClass
    -C /path/to/compiled/module/code

You can verify the jar (get description) with following command:

java --module-path /path/to/compiled/module
     --describe-module theModuleName

Running a Java modular application

java -p /path/to/modules
     -m moduleName</packageName.mainClassName>* <args>

-p can be replaced by –module-path; -m by –module

*If main class wasn’t defined when packaging the module

When running a non-modular project:

java -cp /path/to/jars/and/modularized/jars

-cp can be replaced by –class-path

The modular jars in class path are treated as non-modular ones for backward compatibility.

From Java 9, classes accessed via class path are assigned to the “unnamed module”.

Class dependency analysis with jdeps

jdeps utility allows to analyze dependencies.

jdeps myApp.jar

The output would look like this, where the first part shows modules dependencies, and the second package dependencies and their containing module.

myApp.jar -> java.base
myApp.jar -> java.logging
myApp.jar -> java.sql
...
   y.z.foo -> com.bar.some   myApp.jar
   y.z.foo -> java.io        java.base

Migrating legacy Java apps with automatic modules

Legacy jar files placed into the module path are treated as automatic modules.

They can be referenced with “requires” as other modules.

By default, the jar file name is used instead as a module name for referencing it. The module name can be specified (recommended) in the MANIFEST.MF file placed into the legacy jar (Automatic-Module-Name : MyModule.data).

To complete migration properly, you can create a module-info descriptor and repackage the legacy jar as a module.

jlink utility allows to create self-contained applications including Java runtime, thus create an executable jar file. It only contains necessary elements for running the app.

jlink --module-path
      --add-modules
      --bind-services
      --launcher 
      --output

The jar is platform specific, thus optimized for space and speed.


See other OCP topics on the blog and subscribe to receive future articles in your mailbox.

0 thoughts on “Modularity – Java advanced (OCP)

Leave a Reply

Your email address will not be published. Required fields are marked *

Don’t miss out!

Receive every new article in your mailbox automatically.


Skip to content