Java Modules

2024 03 01 head

Java 9 introduces a new level of abstraction above packages, known as the Java Platform Module System JPMS, or Modules for short [1].

Modules emphasize at compilation time modern software engineering principles such as encapsulation, and modularity [2, 3, 4].

Java 9 was released in September 2017.

Most Java developers are still not using this powerful and elegant construct to modularize their applications.

Modules

A module is a group of closely related Java packages and resources along with a new module descriptor file. A module is distributed as a regular jar file.

Each module is responsible for its resources, like media or configuration files.

Previously, we put all resources into the root level of our project. We manually manage which resources belong to different parts of the application. It works, but it is not really a good way to define modular systems.

With modules, we can ship required images and XML files with the module that needs it, making our projects much easier to manage.

Module Descriptor

When we create a module, we include a descriptor file that defines several aspects of our new module:

Name

the name of our module. Please use the same conventions as for package names.

Dependencies

a list of other modules that this module depends on.

Public Packages

a list of all packages we want accessible from outside the module.

Services Offered

we can provide service implementations that can be consumed by other modules.

Services Consumed

allows the current module to be a consumer of a service.

Reflection Permissions

explicitly allows other classes to use reflection to access the private members of a package. Please minimize the use of reflection.

The module naming rules should be similar to how we name packages. It is common to do Reverse-DNS net.tangly.mymodule style names.

Module Directives

We need to list all packages we want to be public because by default, all packages are module private.

Used Dependencies

We can declare dependencies on other modules.

requires module list

depends on the named module

requires static module list

depends on the named module, but that users of our library will never want to use.

requires transitive module list

depends on the named module and makes it available to users of our module.

Provided Abstractions

By default, a module does not expose any of its API to other modules.

exports package list

We use the exports directive to expose all public members of the named package. The export quantum is a package, not a class or interface.

exports package list to module list

Similar to the exports directive, we declare a package as exported. We additionally list which modules we are allowing to import this package as required. This mechanism is similar to the friend concept in C++.

Services

A service is an implementation of a specific interface or abstract class that can be consumed by other classes.

uses classname

We designate the services our module consumes with this directive.

provides service name with class list

A module can also be a service provider that other modules can consume. The first part of the directive is the provides keyword. Here is where we put the interface or abstract class name. Next, we have the with directive where we provide the implementation class name that either implements the interface or extends the abstract class.

Reflection
open

If we want to continue to allow full reflection as older versions of Java did, we can simply open the entire module up to reflection.

opens package list

If we need to allow reflection of private types, but we do not want all of our code exposed, we can use the opens directive to expose specific packages. But remember, this will open the package up to the entire world, so be careful with this directive.

opens package list to module list

We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive.

module net.tangly.fsm {
    exports net.tangly.fsm;
    exports net.tangly.fsm.dsl;
    exports net.tangly.fsm.utilities;

    requires org.apache.logging.log4j;
    requires static transitive org.jetbrains.annotations;
}

Module Types

There are four types of modules in the new module system:

System Modules

These are the modules listed when we run the list-modules command above. They include the Java SE and JDK modules.

Application Modules

These modules are what we usually want to build when we decide to use Modules. They are named and defined in the compiled module-info.class file included in the assembled JAR.

Automatic Modules

We can include unofficial modules by adding existing JAR files to the module path. The name of the module will be derived from the name of the JAR. Automatic modules will have full read access to every other module loaded by the path.

Unnamed Module

When a class or JAR is loaded onto the classpath, but not the module path, it is automatically added to the unnamed module. It is a catch-all module to maintain backward compatibility with previously written Java code.

Services

Java services are used to do something like a plug-in to the core application.

Service Interface

It is an interface or abstract class that a Service defines.

Service Provider

Has specific implementations of a service interface. A Service could have zero, one, or many service providers.

ServiceLoader

The main class used to discover and load a service implementation.

module ch.hslu.pcp.services {                                        (1)
    exports ch.hslu.pcp.services;
}

module ch.hslu.pcp.serviceSwiss {                                    (2)
    requires ch.hslu.pcp.services;
    provides ch.hslu.pcp.services.Service with ch.hslu.pcp.serviceSwiss.ServiceSwiss;
}

module ch.hslu.pcp.locator {                                         (3)
    requires ch.hslu.pcp.services;
    uses ch.hslu.pcp.services.Service;
    exports ch.hslu.pcp.locator;
}

module ch.hslu.pcp.consumer {                                        (4)
    requires ch.hslu.pcp.services;
    requires ch.hslu.pcp.locator;
}
1 Export the package containing the interface describing the service
2 Implement the service and provide it to interested parties
3 Optional locator responsible for finding all implementations of a specific service interface
4 Consumer calling an implementation of the service. If you do not implement an explicit locator, the consumer is responsible for finding a suitable implementation.

Migration Strategy

A sore point of the Java module is the abysmal slow adoption of modules in Java applications. Projects of the Apache foundation needed years to support at least automatic module names.

You have two migration strategies. Either top-down or bottom-up. In both cases, it can only be nicely completed if the used libraries have an automatic module name or a module descriptor.

Before starting the migration process, you should create the dependency graph for all modules and library jar files used in the application. Beware that the same package name cannot be declared in multiple jar files.

Use the Gradle Build Tool or Maven mechanisms to create the transitive dependency graph of your application.

The module approach does not work with cyclic dependencies. If your dependency graph has cyclic dependencies, you need to refactor your code to remove the cycles.

Top-Down

  1. Place all projects on the module path.

  2. Pick the highest-level project that has not yet been migrated.

  3. Add a module-info file to that project to convert the automatic module into a named module. Again, remember to add any exports or requires directives.
    You can use the automatic module name of other modules when writing the requires directive since most of the projects on the module path do not have names yet.

  4. Repeat with the next-lowest-level project until you are done.

Bottom-Up

  1. Pick the lowest-level project that has not yet been migrated. Do you remember the way we ordered them by dependencies in the previous section? Use that order to identify the lowest-level project.

  2. Add a module-info.java file to that project. Be sure to add any exports to expose any package used by higher-level JAR files. Also, add a requires directive for any modules it depends on.

  3. Move this newly migrated named module from the classpath to the module path.

  4. Ensure any projects that have not yet been migrated stay as unnamed modules on the classpath.

  5. Repeat with the next-lowest-level project until you are done.

References

[1] S. Mak, Java 9 Modularity. O’Reilly Media, 2017 [Online]. Available: https://www.amazon.com/dp/1491954167

[2] D. Farley, Modern Software Engineering. Pearson Education, Limited, 2022 [Online]. Available: https://www.amazon.com/dp/B09GG6XKS4

[3] J. Bloch, Effective Java, Third. Addison-Wesley Professional, 2017 [Online]. Available: https://www.amazon.com/dp/B078H61SCH

[4] N. Ford, R. Parsons, and P. Kua, Building Evolutionary Architectures: Automated Software Governance, Second. O’Reilly Media, 2023 [Online]. Available: https://www.amazon.com/dp/B0BN4T1P27