Java Platform Module System: How

Your First Module

To create a module, the only special thing you need to do is add a module-info.java-file to your source folder and define your module. Formally, follow these steps:

  1. Project Structure: Organise your project’s directory structure appropriately. Typically, your project will consist of multiple packages, each containing related classes.
    • If you’ve been organising your code cleanly and properly then this is already a habit for you.
  2. Module Descriptor (module-info.java): Create a file named module-info.java within the source folder. This file is where you define the module and its properties.

    In this example:
    • module com.examplemy.module: Declares the name of the module.
      • You could have called it MickeyMouse, but it’s best practice to follow the naming you used for the package.
    • exports com.example.mymodule;: Exports a package (here, com.example.mymodule) to allow other modules to access its content.
    • requires anothermodule;: Specifies any modules that your module depends on.
  3. Compilation: Compile your module’s classes, including the module-info.java file. Use the -d option to specify the output directory for the compiled bytecode (we’ll be doing this in the next topic).
  4. JAR Packaging: Package the compiled classes and resources into a JAR file. This JAR file will represent your module and can be used by other modules (we’ll be doing this in an upcoming topic).

Creating a Module

  • To create a module, you start with a module-info.java file in the root directory of your module source code.
  • This file defines the module’s dependencies, what it exports, and other configurations.
Example
module com.example.myapp {
    requires java.sql;
    exports com.example.myapp.api;
}

This simple module named com.example.myapp:

  • Requires the java.sql module to function.
  • Exports its com.example.myapp.api package, making it available to other modules.

👩‍💻 Hands-on Demo: Creating a Module

🔎 Click to expand

Project Structure:

hello-modules   
└── lib-core
    └── src
        ├── module-info.java
        └── be
            └── multimedi
                └── library
                    └── core
                        ├── credentials
                        │   └── HardcodedPasswords.java
                        ├── cui
                        │   └── LibApp.java
                        ├── model
                        │   ├── Author.java
                        │   ├── Book.java
                        │   ├── Catalogue.java
                        │   ├── Library.java
                        │   ├── Loan.java
                        │   ├── Reservation.java
                        │   └── User.java
                        └── util
                            └── IsbnUtils.java
  1. Create a new project folder named “hello-modules”.
  2. This project is multi-modular, meaning it will consist of many Java modules in upcoming demos, each with its own “src” and “out” folders: create the first module’s folder “lib-core” inside “hello-modules”.
  3. Create folders and .java files to build up the project structure shown.
    • For now, you don’t need to actually fill out the insides of the classes, just the package declarations and class declarations.
  1. In the main-method of the LibApp-class, add a print-statement that prints “Welcome to the Library!” to the console output.
  2. To the module-info.java file, add the necessary code to give the module a name, and to export the model and util packages (not the cui or credentials packages).

Solutions

🕵️‍♂️ Click here to reveal the solutions
module be.multimedi.library.core {
    exports be.multimedi.library.core.model;
    exports be.multimedi.library.core.util;
}

Understanding the Module Descriptor

The module descriptor, defined in the module-info.java file, is a critical component of your module. It outlines the module’s characteristics, relationships, and access control. Here’s a breakdown of the key directives within the module descriptor:

DirectiveDescription
moduleDeclares the name of the module.
requiresSpecifies required modules that your module depends on. The java.base module by default is always implicitly required, so we can (but don’t need to) include it.
exportsMakes a package accessible to other modules. Any packages you do not “export” remain encapsulated (i.e. included in the module but inaccessible to other developers using the module).
opensSimilar to exports, but allows reflection access.
providesDeclares a service implementation.
usesIndicates a service used by your module.
requires transitiveExtends required modules to dependents.
We’ll be exploring most of these in upcoming topics.

The requires Directive

  • The requires directive declares your module’s dependence on other modules.
  • These other modules can be your own modules within your application, or external third-party modules.
  • By specifying these required modules, you establish clear relationships between parts of your application.
  • Facilitates dependency management.
module <module-name> {
    requires <dependency-module>;
    // Other directives and code
}
  • requires indicates that your module requires another module to be available during compilation and runtime.
  • The required module must exist in the module path or classpath.
  • The module name in the requires directive must match the declared name of the required module.

Example: Our Multi-modular Library Application

To illustrate requires, we’re going to make our application multi-modular:

Example: Organising a Library System

ModulePurposeDependencies
be.multimedi.library.coreCore functionalities of the libraryNone
be.multimedi.library.booksBook-related featuresbe.multimedi.library.core
java.sql
be.multimedi.library.usersUser management and authenticationbe.multimedi.library.core
be.multimedi.library.uiUser interface componentsbe.multimedi.library.core,
be.multimedi.library.books,
be.multimedi.library.users

As you can see, all our other modules depend on our core-module:


👩‍💻 Hands-on Demo: Using requires

🔎 Click to expand
  1. In the “hello-modules” project folder, make a new “lib-books” directory.
  2. Build up the be.multimedi.library.books module as shown in the example:
    • BookRepository: must have a method called “fetchAllBooks“, which uses JDBC to return a List<Book>, which means you need to import the model Book-class from the core module (just do a simple import statement with Book‘s full name).
    • BookService: You don’t need to implement this for the sake of this demo.
    • module-info:
      • Give this module its name (see module overview).
      • Make the service package available to the outside world but hide the rest.
      • As one of our classes here uses a class from the core module, declare this module’s requirement of the core module.
      • As one of our classes here uses JDBC, which is not included in the implicit java.base, declare this module’s requirement of the java.sql module.

Module overview:

Source code project structure:

hello-modules   
├── lib-core
│   └── ...
├── lib-books
│   └── src
│       ├── module-info.java
│       └── be
│           └── multimedi
│               └── library
│                   └── books
│                       ├── service
│                       │   └── BookService.java
│                       └── repository
│                           └── BookRepository.java
└── ...
    └── ...
  1. To experiment with the encapsulation of modules, go to your BookRepository-class and attempt to use the HardcodedPasswords-class from the be.multimedi.library.core-module.

Solutions

🕵️‍♂️ Click here to reveal the solutions
module be.multimedi.library.books {
    exports be.multimedi.library.books.service;
    requires be.multimedi.library.core;
    requires java.sql;
}
package be.multimedi.library.books.repository;

import be.multimedi.library.core.model.Book;
import java.sql.*;
import java.util.List;

public class BookRepository {
    public List<Book> fetchAllBooks() {
        try (
                Connection conn = DriverManager.getConnection("foo", "bar", "sausages");
                PreparedStatement ps = conn.prepareStatement("SELECT * FROM book");
        ) {
            // Code to populate list of books and return it
            return null;
        } catch (SQLException e) {
            System.out.println("Failed to fetch all books.");
            throw new RuntimeException(e);
        }
    }
}
public class BookRepository {
    HardcodedPasswords passwords = new HardcodedPasswords(); // ❌

The requires transitive Directive

  • With requires we define our module’s dependencies explicitly.
  • Alongside this, exists the requires transitive directive which can simplify our module’s definition by removing redundant dependency declarations.
  • For example:
    • Notice how ui relies on books and users and core.
    • But both books and users already rely on core.
    • Is it then necessary for us to explicitly declare a reliance on core in the ui module definition?
  • In the example above, ui’s dependency on core is a transitive dependency, because the books module (and the users module) already provide this dependency.
  • Is there a way to remove the redundant, explicit dependency on the core module in our ui module’s module descriptor? Yes! By using requires transitive:
  • The requires transitive directive extends the reach of dependencies to dependent modules.
  • When a module uses requires transitive, it exposes that dependency to any module that depends on it.
  • Meaning: the transitive dependency relationships are automatically inherited by the dependent modules.
  • Eliminates the need for them to explicitly declare those same dependencies again.
  • Simplifies dependency declaration, reduces boilerplate code, and promotes flexibility.

Extra: For the Interested Reader

🔎 Click to expand

Module Types: Named, Automatic and Unnamed

These three types help facilitate a smoother transition from classpath-based organisation to the module-path-based one, enhancing backwards compatibility while encouraging migration.

Named Modules

These are modules with a module-info.java file that explicitly declares the module’s dependencies, exported packages, and other configuration details. They are fully-fledged modules that participate in the module system and are therefore migrated.

Automatic Modules

These modules are JAR files placed on the module path that do not have a module-info.java. Their name is derived from the JAR file name, and they automatically require all other modules. They can access all other automatic modules and named modules but export all their packages, offering a bridge between named modules and the classpath.

Unnamed Modules

Any JARs or classes placed on the classpath (not the module path) are part of the unnamed module. This module can require any automatic modules and other unnamed modules but is not visible to named modules. It is essentially the classpath as a module.

How Module Types Facilitate Easy Migration

  • Backward Compatibility: Allows legacy applications to run without modification on the Java Platform Module System by treating classpath entries as an unnamed module.
  • Gradual Adoption: Applications can be migrated incrementally by converting libraries to automatic modules before moving to fully named modules, easing the transition.
  • Flexibility: The module system is designed to integrate seamlessly with existing codebases and third-party libraries, regardless of whether they are modularized.

Java Tools for Managing Modules and Dependencies

  • Java provides several tools designed to analyze and handle module dependencies.
  • One of the most useful tools in this context is jdeps.
    • Dependency Analysis: It analyzes Java bytecode to find dependencies among classes, and between classes and packages. This is crucial for understanding how existing applications or libraries are structured before modularizing them.
    • Identify JDK Internal API Dependencies: jdeps can identify dependencies on internal JDK APIs, which is vital since these APIs may not be accessible in JPMS due to encapsulation.
    • Generate module-info.java Recommendations: For projects migrating to JPMS, jdeps can suggest a module-info.java content based on the current usage of Java APIs and other library dependencies.

Reality Check: JPMS Adoption and Framework Support

The Java Platform Module System (JPMS) introduces significant advances in Java’s ability to manage dependencies and improve application security through encapsulation. However, its adoption is limited in several key frameworks even as of JDK 21. Here’s a quick overview of the JPMS adoption landscape:

Key Points:

  • Framework Support: Major frameworks like Spring Boot, Micronaut, and Quarkus have limited support for JPMS. Most Spring components, for instance, are treated as automatic modules, which restricts the full use of JPMS features like creating minimal runtime environments with jlink.
  • Challenges with JPMS:
    • Reflection and Dynamic Code Generation: JPMS’s strong encapsulation can interfere with frameworks that utilize reflection and runtime code generation, often breaking their functionality unless modules are explicitly opened.
    • Automatic Modules: While easing migration by allowing legacy code on the module path, automatic modules cannot leverage full modularity benefits, such as reduced runtime images.
  • Consider Gradual Adoption: Assess the compatibility and benefits of JPMS for your specific situation. Frameworks that fully support JPMS can leverage its advantages, but many existing applications may encounter complexities during migration.

Summary

  • Module Basics: Introduces a system where related classes are grouped in modules, enhancing code organization and dependency management.
  • Module Descriptor: Create a module-info.java in the source folder to define module names, exported packages, and required modules.
  • Essential Directives:
    • requires: Specifies dependencies on other modules.
    • exports: Allows other modules access to specified packages.
    • requires transitive: Extends dependencies to consuming modules, reducing redundancy.
  • Compilation and Packaging: Compile module classes and package into JAR files using tools like jlink for optimized runtime images.
  • Adoption Challenges: JPMS adoption is limited by its compatibility with frameworks that use reflection and dynamic code generation.
  • Practical Implications: Provides clear dependency paths and better security through encapsulation but requires careful integration with existing frameworks and libraries.
  • Gradual Adoption Strategy: Assess JPMS benefits versus potential migration complexities, especially in projects using frameworks with limited JPMS support.