Chapter THIRTEEN
The Java Platform Module System


Exam Objectives

Define modules and expose module content, including that by reflection, and declare module dependencies, define services, providers, and consumers.
Compile Java code, create modular and non-modular jars, runtime images, and implement migration to modules using unnamed and automatic modules.

Chapter Content


Introduction

One of the most significant changes introduced in Java 9 was the Java Platform Module System (JPMS). But what exactly is JPMS, and why should we care about it?

Let’s start by understanding what a module is in this context.

A module in Java is like a section in a well-organized library. Each module has a clear label (its name) and contains specific books (Java packages). However, you can’t access any book without a library card (a dependency declaration) for that specific section.

In this example:

module com.myapp.core {
    requires java.base;
    exports com.myapp.core.api;
}

We’re declaring a module named com.myapp.core. It requires the java.base module (like having a library card for the basic Java section) and exports the com.myapp.core.api package (making some of its books available to other modules).

While packages group related classes, modules take this concept further by grouping related packages and explicitly declaring their dependencies and exposed APIs.

Consider the benefits of using JPMS:

JPMS includes:

Here’s a more complex sample module-info.java file:

module com.myapp.core {
    requires java.base;
    requires java.sql;
    
    exports com.myapp.core.api;
    exports com.myapp.core.util to com.myapp.plugin;
    
    opens com.myapp.core.model;
    
    uses com.myapp.core.spi.Plugin;
    provides com.myapp.core.spi.Logger 
        with com.myapp.core.logging.FileLogger;
}

This module declaration shows:

While you can still use the classpath, you’d miss out on the benefits of JPMS. The classpath is like a big, unorganized pile of books, while the module path is a well-organized library with controlled access and clear dependencies.

Even in small projects, modules can improve encapsulation and maintainability. Consider this small example:

// In module com.myapp.core
module com.myapp.core {
    exports com.myapp.core.api;
}

package com.myapp.core.api;
public interface UserService {
    User getUser(String id);
}

package com.myapp.core.internal;
class UserServiceImpl implements UserService {
    public User getUser(String id) {
        // Implementation
    }
}

// In module com.myapp.web
module com.myapp.web {
    requires com.myapp.core;
}

package com.myapp.web;
import com.myapp.core.api.UserService;
// import com.myapp.core.internal.UserServiceImpl; // This would cause a compile-time error

public class UserController {
    private UserService userService;
    // ...
}

In this example, the web module can only access the api package of the core module, not its internal implementation.

Modules provide tools to enforce and express the architecture of your system at the language and JVM level.

Consider this: Would you rather have a big box of unsorted LEGO bricks or neatly organized sets with clear instructions? Both approaches can build amazing things, but one makes the process much smoother and less error-prone.

Types of Modules

Now that we’ve got a grasp on what modules are and why they’re useful, let’s dive into the different types of modules in JPMS. Just like how not all books in a library are created equal, not all modules are the same either. JPMS introduces three types of modules:

Named Modules

Named modules are like the properly cataloged books in our library, with a clear title, author information, and a spot on the shelf. In Java terms, a named module is defined by a module-info.java file at the root of your module.

Here’s an example of the content of this file:

module com.myapp.core {
    requires java.base;
    exports com.myapp.core.api;
}

This module-info.java file is the ID card of your module. It gives your module a name (com.myapp.core in this case), lists its dependencies (requires java.base), and declares what parts of itself it’s willing to share with other modules (exports com.myapp.core.api).

Named modules are the most powerful and flexible type of module. They give you full control over your module’s dependencies and what it exposes to the outside world. If you’re starting a new project or refactoring an existing one to use JPMS, named modules are what you’ll be working with most of the time.

Automatic Modules

But what about all those third-party libraries that haven’t been modularized? This is where automatic modules come in. They’re like the books in our library that don’t have a proper catalog entry yet, but we still want to be able to check them out.

When you put a non-modular JAR file on the module path, the Java runtime automatically treats it as a module. This module is called an automatic module.

In this example:

module com.myapp.core {
    requires java.base;
    requires commons.lang; // This is an automatic module
    exports com.myapp.core.api;
}

commons.lang is an automatic module. We can require it just like we would a named module, even though it doesn’t have a module-info.java file.

But how does Java determine the name of an automatic module? Well, the process goes something like this:

  1. First, it looks for the Automatic-Module-Name entry in the JAR’s MANIFEST.MF file. If it’s there, that’s the module name.
  2. If that’s not present, it derives the name from the JAR filename. It removes the file extension and version number, and replaces non-alphanumeric characters with dots.

For example:

You can see this in action using the jar command:

$ jar --describe-module --file=guava-28.0-jre.jar
No module descriptor found. Derived automatic module.

Automatic module name: guava
...

Unnamed Modules

Last but not least, we have unnamed modules. These are like a miscellaneous box in the library where all loose papers and bookmarks that don’t fit anywhere else are stored.

When you run your application on the classpath (not the module path), all the code that is not part of a named module or automatic module ends up in one big unnamed module. This unnamed module reads all other modules, which means it can access all packages exported by all other modules.

In this command:

java -cp app.jar:lib/* com.myapp.Main

app.jar and everything in the lib directory will be part of the unnamed module.

The unnamed module is important for backward compatibility, allowing existing Java code to run without modification on Java 9 and later versions. However, code in the unnamed module doesn’t get the benefits of strong encapsulation that named modules provide.

Here’s a quick comparison:

Module Type Has module-info.java On Module Path On Classpath
Named Yes Yes No
Automatic No Yes No
Unnamed No No Yes

Understanding these different types of modules is key to working effectively with JPMS. Named modules give you the most control and benefits, automatic modules help you integrate non-modular libraries, and unnamed modules ensure your existing code keeps running.

Here’s a diagram that summarizes the types of modules:

┌─────────────────────────────────────────────────────────┐
│                   Java Module Types                     │
│                                                         │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │   Named     │    │  Automatic  │    │   Unnamed   │  │
│  │   Module    │    │   Module    │    │   Module    │  │
│  ├─────────────┤    ├─────────────┤    ├─────────────┤  │
│  │ - Explicit  │    │ - No module-│    │ - No module-│  │
│  │   module-   │    │   info.java │    │   info.java │  │
│  │   info.java │    │ - On module │    │ - Not on    │  │
│  │ - Defined   │    │   path      │    │   module    │  │
│  │   exports   │    │ - Name      │    │   path      │  │
│  │ - Defined   │    │   derived   │    │ - Implicitly│  │
│  │   requires  │    │   from JAR  │    │   exports   │  │
│  │             │    │   filename  │    │   all pkgs  │  │
│  └─────────────┘    └─────────────┘    └─────────────┘  │
│                                                         │
│  Use for:           Use for:           Use for:         │
│  - New Java 9+      - Legacy JARs      - Class path     │
│    projects         - Transition       - Compatibility  │
│  - Full module      - Third-party      - Non-modular    │
│    control            libraries          code           │
│                                                         │
└─────────────────────────────────────────────────────────┘

Key Points:
- Named modules offer full control over exports and requires
- Automatic modules bridge between modular and non-modular code
- Unnamed modules provide backwards compatibility

Creating a Module

Now that we’ve explored the types of modules, let’s create one ourselves. Creating a module is like setting up a new section in your library. We need to decide on its structure, what books (classes) it will contain, and what rules (module-info.java) will govern its use.

Directory Structure

The directory structure for a module is straightforward, but it’s important to get it right. Here’s a typical layout:

mymodule/
├── src/
│   ├── module-info.java
│   ├── com/
│   │   └── mycompany/
│   │       └── mymodule/
│   │           ├── MyClass.java
│   │           └── AnotherClass.java
│   └── resources/
│       └── config.properties

Let’s break it down:

This structure might look familiar, it’s very similar to how we organized non-modular Java projects. The key difference is the presence of the module-info.java file.

The Class Files

Now, let’s look at what goes inside our Java files. Here’s an example of what MyClass.java might look like:

package com.mycompany.mymodule;

public class MyClass {
    public void doSomething() {
        System.out.println("MyClass is doing something!");
    }
}

This is just a regular Java class. However, the package declaration at the top is important, as it determines where this class fits in our module’s structure.

Here’s AnotherClass.java:

package com.mycompany.mymodule;

public class AnotherClass {
    private MyClass myClass = new MyClass();

    public void doSomethingElse() {
        System.out.println("AnotherClass is doing something else!");
        myClass.doSomething();
    }
}

Again, this is a standard Java class. Notice how it can use MyClass without any special import because they’re in the same package.

The module-info.java File

This file is what turns our collection of packages into a proper module. It’s like the card catalog for our library section, defining what’s available and what’s needed.

Here’s what a basic module-info.java might look like:

module com.mycompany.mymodule {
    exports com.mycompany.mymodule;
    requires java.base;
}

Let’s break this down:

But we can get more sophisticated. Let’s say we want to use a logging framework and provide a service:

module com.mycompany.mymodule {
    exports com.mycompany.mymodule;
    requires java.base;
    requires org.apache.logging.log4j;
    
    provides com.mycompany.service.MyService 
        with com.mycompany.mymodule.MyServiceImpl;
    
    uses com.mycompany.service.AnotherService;
}

Here, we’re requiring the Log4j module, providing an implementation of MyService, and declaring that we’ll be using AnotherService (which will be provided by some other module).

Isn’t this module-info.java file a bit like the nutrition label on a food package? It tells you what’s inside (exports), what it needs (requires), what it can do for you (provides), and what it expects to use (uses).

One thing to watch out for: if you’re using an IDE, make sure it’s set up to work with Java modules. Some IDEs might create a module-info.java file automatically when you create a new module, while others might require you to create it manually.

For example, assuming there’s a Main class like this:

package com.mycompany.mymodule;

public class Main {
    public static void main(String[] args) {
        System.out.println("Main class is running!");
        
        MyClass myClass = new MyClass();
        myClass.doSomething();
        
        AnotherClass anotherClass = new AnotherClass();
        anotherClass.doSomethingElse();
    }
}

Here’s how you might compile and run this module from the command line:

javac -d mods/com.mycompany.mymodule 
    src/module-info.java 
    src/com/mycompany/mymodule/*.java

java --module-path mods -m com.mycompany.mymodule/com.mycompany.mymodule.Main

The first command compiles our module, and the second runs it. Notice how we specify the module path (--module-path mods) and the main class (-m com.mycompany.mymodule/com.mycompany.mymodule.Main). Also, for both javac and java commands, you can use the shorter -p option instead of --module-path.

Module Declaration

Now that we’ve set up our module’s structure, let’s review the module declaration itself in the module-info.java file.

Exporting a Package

The exports keyword is used to make our module’s packages accessible to other modules. Here’s an example:

module com.mycompany.mymodule {
    exports com.mycompany.mymodule.api;
}

In this example, we’re making the com.mycompany.mymodule.api package available for other modules to use. Any public types in this package can now be accessed by other modules that require it.

But what if we want to be more selective? Java modules allow for that too:

module com.mycompany.mymodule {
    exports com.mycompany.mymodule.api to com.mycompany.anothermodule, com.mycompany.yetanothermodule;
}

This declaration exports the package, but only to the specified modules. It’s a way to control access to your module’s internals.

Access Control with Modules

Modules add an extra layer of access control on top of Java’s existing public, protected, package-private, and private modifiers. Here’s how it works:

  1. Public types in exported packages are accessible to other modules.
  2. Public types in non-exported packages are only accessible within the module.
  3. Protected members in exported packages are accessible in subclasses within other modules.
  4. All other access rules (protected, package-private, private) still apply as usual.

Let’s see this in action:

// In module com.mycompany.mymodule
module com.mycompany.mymodule {
    exports com.mycompany.mymodule.api;
}

// In package com.mycompany.mymodule.api
public class PublicAPI {
    public void doSomething() { ... }
}

// In package com.mycompany.mymodule.internal
public class InternalClass {
    public void doSomethingElse() { ... }
}

// In another module
import com.mycompany.mymodule.api.PublicAPI; // This works
import com.mycompany.mymodule.internal.InternalClass; // This fails!

Even though InternalClass is public, it can’t be accessed from outside the module because its package is not exported. It’s like having a public reading room that’s only accessible to staff members.

Requiring a Module

The requires keyword is how we declare dependencies on other modules. Consider this example:

module com.mycompany.mymodule {
    requires java.sql;
}

This tells the Java runtime that our module depends on the java.sql module.

But what if we’re building on top of another module and want to expose its functionality through our module? That’s where requires transitive comes in:

module com.mycompany.mymodule {
    requires transitive java.sql;
}

Now, any module that requires our module will automatically require java.sql too. It’s like saying “if you’re checking out books from our section, you’ll also get a library card for the SQL section.”

This is particularly useful when you’re creating an API that builds on another module. Your users don’t need to know about the underlying dependencies, they just require your module, and everything else comes along.

Opening a Package

Sometimes, we need to allow reflective access to a package at runtime, even if it’s not exported. This is where the opens keyword comes in handy. Consider this example:

module com.mycompany.mymodule {
    opens com.mycompany.mymodule.internal;
}

This allows reflective access to all types of the package at runtime, but doesn’t allow compile-time access from other modules.

You can also open a package to specific modules:

module com.mycompany.mymodule {
    opens com.mycompany.mymodule.internal to com.mycompany.testmodule;
}

This is particularly useful for testing frameworks or dependency injection libraries that need to access your module’s internals.

If you need to open all packages in your module for reflection, you can use the open keyword on the module declaration itself:

open module com.mycompany.mymodule {
    // module declarations
}

Isn’t this module system a bit like setting up security clearances in a classified library? You have public sections (exported packages), restricted sections (non-exported packages), special access privileges (opens), and even transitive security clearances (requires transitive). It gives you fine-grained control over who can access what in your codebase.

Here’s a more complex example putting it all together:

module com.mycompany.mymodule {
    exports com.mycompany.mymodule.api;
    exports com.mycompany.mymodule.util to com.mycompany.partnermodule;
    
    requires java.base; // This is implicit
    requires transitive com.mycompany.commonmodule;
    requires org.apache.logging.log4j;
    
    opens com.mycompany.mymodule.internal to org.junit.jupiter.api;
}

This module exports one package globally and another to a specific module, requires several modules (one transitively), and opens a package for testing.

Built-in Modules

Now that we’ve explored creating our own modules and services, let’s look at the modules that come built into the Java platform. These built-in modules provide essential services and resources for everything else to build upon.

Core Java Modules

Modules that start with java are the core modules of the Java SE Platform. These modules contain the fundamental APIs that most Java applications rely on.

Here are some of the most commonly used java modules:

  1. java.base: This is the foundational module of the Java SE Platform. It’s automatically required by all other modules, just like how every section of a library relies on basic organizational principles.

    // You don't need to explicitly require java.base
    module com.mycompany.app {
        // java.base is implicitly required
    }
    
  2. java.sql: Provides the API for accessing and processing data stored in a data source (usually a relational database) using the Java programming language.

    module com.mycompany.app {
        requires java.sql;
    }
    
  3. java.xml: Contains the APIs for processing XML.

    module com.mycompany.app {
        requires java.xml;
    }
    
  4. java.desktop: Defines the APIs for creating rich desktop applications, including AWT and Swing.

    module com.mycompany.app {
        requires java.desktop;
    }
    
  5. java.logging: Provides the classes and interfaces of the Java Logging API.

    module com.mycompany.app {
        requires java.logging;
    }
    

These java modules provide the core functionality that most Java applications rely on. They’re stable, well-documented, and form the backbone of the Java ecosystem.

JDK Modules

Modules that start with jdk are also part of the Java Development Kit, but they’re not considered part of the core Java SE Platform specification. They provide additional tools and APIs that are useful for certain types of systems but aren’t necessary for every application.

Here are some examples of jdk modules:

  1. jdk.httpserver: Provides a simple HTTP server API.

    module com.mycompany.app {
        requires jdk.httpserver;
    }
    
  2. jdk.jshell: Contains the JShell API, which allows you to create an interactive Java shell.

    module com.mycompany.app {
        requires jdk.jshell;
    }
    
  3. jdk.security.auth: Provides implementations of the javax.security.auth.* interfaces.

    module com.mycompany.app {
        requires jdk.security.auth;
    }
    

It’s important to note that while java modules are guaranteed to be available in all Java SE implementations, jdk modules might not be available. They’re part of the JDK but not part of the Java SE specification. This means that if you’re using a jdk module, your code might not be portable across all Java SE implementations.

Here’s an example of how you might use both types of modules:

module com.mycompany.app {
    requires java.base;  // This is implicit
    requires java.sql;   // For database operations
    requires java.logging;  // For logging
    requires jdk.httpserver;  // To create a simple HTTP server
    
    exports com.mycompany.app.api;
}

In this module declaration, we’re using both java and jdk modules. We’re relying on core Java functionality for database operations and logging, but we’re also using the JDK’s simple HTTP server for some additional functionality.

Using the Command Line

While IDEs are great for productivity, understanding how to use javac and java commands is important. It’s like knowing how to cook a meal from scratch instead of just reheating pre-made dishes.

There are several good reasons to learn how to compile and run Java code from the command line:

Think of it as learning to change a tire. You might not need to do it often, but when you do, you’ll be glad you know how.

Compiling Classes with javac

Let’s start with the basics. Here’s how you compile a simple Java file:

javac MyClass.java

This compiles MyClass.java in the default package. But what about when you have packages?

javac com/mycompany/myapp/MyClass.java

This compiles MyClass.java in the com.mycompany.myapp package.

Typing out every file name can get tedious. Thankfully, you can use wildcards:

javac com/mycompany/myapp/*.java

This compiles all .java files in the com/mycompany/myapp directory.

Often, we want to keep our source files separate from our compiled classes. The -d option lets us specify an output directory:

javac -d bin com/mycompany/myapp/*.java

This compiles all .java files and puts the resulting .class files in the bin directory, maintaining the package structure.

When our code depends on external libraries, we need to tell the compiler where to find them. That’s where the classpath option comes in:

javac -cp lib/dependency.jar com/mycompany/myapp/*.java

This tells the compiler to look for classes in dependency.jar while compiling our code. You can specify multiple JAR files or directories by separating them with a colon (:) on Unix-like systems or a semicolon (;) on Windows.

Speaking of JAR files, here’s how you compile against multiple JARs:

javac -cp lib/dependency1.jar:lib/dependency2.jar com/mycompany/myapp/*.java

This compiles our code using classes from both dependency1.jar and dependency2.jar.

When working with modules, we need to specify the module path:

javac --module-path mods -d out src/module-info.java src/com/mycompany/myapp/*.java

This compiles our module, looking for dependencies in the mods directory and outputting to the out directory.

Running Classes with java

Once you’ve compiled a class, you can run it with:

java com.mycompany.myapp.MyClass

This runs MyClass in the com.mycompany.myapp package. Note that we don’t include the .class extension.

Just like with compilation, we might need to specify a classpath when running our code:

java -cp bin:lib/dependency.jar com.mycompany.myapp.MyClass

This runs MyClass, looking for classes in both the bin directory and dependency.jar.

To run a modular application, we use the --module-path and -m options:

java --module-path out:mods -m com.mycompany.myapp/com.mycompany.myapp.MyClass

This runs MyClass from the com.mycompany.myapp module, looking for modules in the out and mods directories.

Packaging with jar

Often, you’ll want to package your application into a JAR file. The jar command helps you do this:

jar -cvf myapp.jar -C bin .

Let’s break this down:

With a modular application, you can package your module into a modular JAR:

jar --create --file mods/com.mycompany.myapp.jar --main-class com.mycompany.myapp.MyClass -C out .

This creates a modular JAR file, optionally specifying MyClass as the main class.

Here’s a more complex example that ties it all together:

# Compile the module
javac --module-path mods -d out \
    src/module-info.java \
    src/com/mycompany/myapp/*.java

# Package the module
jar --create --file mods/com.mycompany.myapp.jar \
    --main-class com.mycompany.myapp.MyClass \
    -C out .

# Run the module
java --module-path mods \
    -m com.mycompany.myapp/com.mycompany.myapp.MyClass

This sequence compiles the module, packages it into a JAR, and then runs it.

By convention, we store the compiled modules in a mods directory. When we use --module-path mods in our Java commands, we’re telling Java to look for modules in this mods directory.

Multiple Modules

Up until now, we’ve been working with a single module, which is like organizing a single shelf in our library. But real-world applications often require multiple modules working together, more akin to organizing an entire library with multiple sections.

Designing a Multi-Module Application

Designing a multi-module application is like planning the layout of a large library. You need to think about how different sections (modules) will interact, what resources they’ll share, and how to organize them for easy navigation and maintenance.

Here’s a simple example of a multi-module application structure:

myapp/
├── core/
│   └── src/
│       ├── main/
│       │   └── java/
│       │       ├── module-info.java
│       │       └── com/mycompany/core/
│       └── test/
├── api/
│   └── src/
│       ├── main/
│       │   └── java/
│       │       ├── module-info.java
│       │       └── com/mycompany/api/
│       └── test/
└── app/
    └── src/
        ├── main/
        │   └── java/
        │       ├── module-info.java
        │       └── com/mycompany/app/
        └── test/

In this structure:

When working with multiple modules, it’s important to understand the dependencies between them.

Let’s look at how we might define the dependencies for our example:

// core/src/main/java/module-info.java
module com.mycompany.core {
    exports com.mycompany.core;
}

// api/src/main/java/module-info.java
module com.mycompany.api {
    requires com.mycompany.core;
    exports com.mycompany.api;
}

// app/src/main/java/module-info.java
module com.mycompany.app {
    requires com.mycompany.core;
    requires com.mycompany.api;
}

In this setup, both api and app depend on core, and app also depends on api. This creates a hierarchy of dependencies that impacts how you develop and maintain your application:

However, this dependency structure helps enforce a clean architecture, preventing lower-level modules from depending on higher-level ones.

When organizing code across modules, think about separation of concerns and information hiding. Each module should have a clear, focused purpose, and should only expose what’s necessary for other modules to use.

Here’s an example of how you might organize some classes:

// In core module
public class User { ... }
public class UserService { ... }

// In api module
public interface UserAPI { ... }

// In app module
public class UserController { ... }

The core module defines the fundamental domain objects and services. The api module defines the public interfaces that other parts of the application (or external systems) will use. The app module contains the application-specific logic that ties everything together.

This organization allows you to change the internals of the core module without affecting clients of the api, as long as the api remains stable.

That said, deciding on the right level of granularity for your modules can be challenging. Too few modules and you lose the benefits of modularization; too many and you introduce unnecessary complexity. Here are some best practices:

  1. Single Responsibility Principle: Each module should have one, and only one, reason to change.

  2. Encapsulation: Modules should hide their internals and expose only what’s necessary.

  3. Stable Dependencies: Modules should depend on modules that are more stable than they are.

  4. Reusability: If a set of functionality might be useful in other contexts, consider making it a separate module.

  5. Size: While there’s no hard and fast rule, modules that are too large become unwieldy, while modules that are too small can lead to dependency hell. Aim for modules that can be reasonably understood and maintained by a small team.

Here’s an example of refactoring our earlier structure, using another approach to improve granularity:

myapp/
├── core/
│   ├── domain/
│   └── services/
├── api/
│   ├── internal/
│   └── public/
├── infrastructure/
│   ├── persistence/
│   └── messaging/
└── app/
    ├── web/
    └── cli/

In this refactored structure:

This granularity allows for more focused modules, each with a clear responsibility, while still maintaining a manageable overall structure.

Inter-module Communication

When working with multiple modules, communication between them becomes important. In Java, inter-module communication typically happens through well-defined APIs.

Here’s how you might set this up:

// In api module
module com.mycompany.api {
    exports com.mycompany.api;
}

public interface UserService {
    User getUser(String id);
    void updateUser(User user);
}

// In core module
module com.mycompany.core {
    requires com.mycompany.api;
    provides com.mycompany.api.UserService 
        with com.mycompany.core.UserServiceImpl;
}

public class UserServiceImpl implements UserService {
    public User getUser(String id) { ... }
    public void updateUser(User user) { ... }
}

// In app module
module com.mycompany.app {
    requires com.mycompany.api;
    uses com.mycompany.api.UserService;
}

public class UserController {
    @Inject
    private UserService userService;

    public void handleUserUpdate(String id, UserUpdateRequest request) {
        User user = userService.getUser(id);
        // Update user based on request
        userService.updateUser(user);
    }
}

In this setup:

This approach allows modules to communicate through well-defined interfaces, promoting loose coupling and making it easier to change implementations without affecting other modules.

Resolving Conflicts Between Modules

As your application grows, you might encounter conflicts between modules. Here are some common conflicts and how to resolve them:

  1. Version Conflicts: When two modules require different versions of the same dependency. Solution: Use the requires directive with a specific version, or use build tools like Maven or Gradle to manage versions.

    module com.mycompany.moduleA {
        requires com.fasterxml.jackson.databind;
    }
    
    module com.mycompany.moduleB {
        requires com.fasterxml.jackson.databind@2.11.0;
    }
    
  2. Split Packages: When classes in the same package are spread across multiple modules. Solution: Refactor your code to ensure each package is contained within a single module.

  3. Naming Conflicts: When two modules export the same package name. Solution: Rename one of the packages to ensure uniqueness across your application.

  4. Cyclic Dependencies: When modules depend on each other in a circular manner. Solution: Introduce a new module that both can depend on, or use the Service Provider Interface (SPI) pattern.

    // Before (cyclic dependency)
    module com.mycompany.moduleA {
        requires com.mycompany.moduleB;
    }
    module com.mycompany.moduleB {
        requires com.mycompany.moduleA;
    }
    
    // After (using SPI)
    module com.mycompany.api {
        exports com.mycompany.api;
    }
    module com.mycompany.moduleA {
        requires com.mycompany.api;
        provides com.mycompany.api.ServiceA with com.mycompany.moduleA.ServiceAImpl;
    }
    module com.mycompany.moduleB {
        requires com.mycompany.api;
        uses com.mycompany.api.ServiceA;
    }
    

To better understand the solution, let’s review services in more detail.

Creating a Service

Let’s dive into one of the most powerful features of the Java Module System: services. Services allow us to create flexible, extensible applications by decoupling interfaces from their implementations.

In the context of the Java Module System, a service is a well-defined set of programming interfaces and classes that provide access to some specific application functionality or feature. It’s like a specialized department in our library that provides a specific service, say, book restoration.

The service model consists of three main components:

  1. The Service Provider Interface (SPI): This is the contract that defines what the service does.
  2. The Service Provider: This is the implementation of the SPI.
  3. The Service Consumer: This is the code that uses the service.

This separation allows for loose coupling between modules. The consumer doesn’t need to know about the specific implementation of the service, just the interface it uses.

Let’s start by declaring our Service Provider Interface. We’ll use the UserService from the previous section as our sample service:

// In the api module
module com.mycompany.api {
    exports com.mycompany.api;
}

package com.mycompany.api;

public interface UserService {
    User getUser(String id);
    void updateUser(User user);
}

This UserService interface defines the contract for the user management service. Any module that implements this interface can provide user management functionality.

Now that we have our Service Provider Interface, we need a way to discover and load implementations of this service. This is where a Service Locator comes in. In Java 9 and above, we can use the ServiceLoader class for this purpose.

Here’s how we might create a UserServiceLocator:

// In the app module
module com.mycompany.app {
    requires com.mycompany.api;
    uses com.mycompany.api.UserService;
}

package com.mycompany.app;

import com.mycompany.api.UserService;
import java.util.ServiceLoader;

public class UserServiceLocator {
    private static final ServiceLoader<UserService> loader = ServiceLoader.load(UserService.class);

    public static UserService getUserService() {
        return loader.findFirst().orElseThrow(() -> new IllegalStateException("No UserService implementation found"));
    }
}

Let’s break this down:

Note the uses directive in the module declaration. This tells the module system that this module will be using the UserService service.

This approach provides several benefits:

Here’s how we might use this in our UserController:

package com.mycompany.app;

public class UserController {
    private final UserService userService;

    public UserController() {
        this.userService = UserServiceLocator.getUserService();
    }

    public void handleUserUpdate(String id, UserUpdateRequest request) {
        User user = userService.getUser(id);
        // Update user based on request
        userService.updateUser(user);
    }
}

In this setup, UserController doesn’t need to know anything about how UserService is implemented or where it comes from. It just uses the service locator to get an instance and then uses it.

However, we haven’t actually provided an implementation yet. Let’s do that now.

First, we’ll create an implementation of our UserService:

// In the core module
package com.mycompany.core;

import com.mycompany.api.User;
import com.mycompany.api.UserService;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class UserServiceImpl implements UserService {
    private final Map<String, User> users = new HashMap<>();

    @Override
    public User getUser(String id) {
        return users.get(id);
    }

    @Override
    public void updateUser(User user) {
        if (user.getId() == null) {
            user.setId(UUID.randomUUID().toString());
        }
        users.put(user.getId(), user);
    }
}

Now, we need to tell the module system that this implementation provides the UserService. We do this in the module-info.java file of the core module:

module com.mycompany.core {
    requires com.mycompany.api;
    provides com.mycompany.api.UserService with com.mycompany.core.UserServiceImpl;
}

The provides ... with clause tells the module system that this module provides an implementation of UserService using the UserServiceImpl class.

Now, when the ServiceLoader in our UserServiceLocator looks for implementations of UserService, it will find and use this UserServiceImpl.

This separation of interface and implementation gives us incredible flexibility. We could easily swap out our UserServiceImpl for a different implementation without having to change any of the consuming code. Maybe one that uses a database instead of an in-memory map:

// In the core module
package com.mycompany.core;

import com.mycompany.api.User;
import com.mycompany.api.UserService;
import java.sql.*;
import java.util.UUID;

public class DatabaseUserServiceImpl implements UserService {
    private static final String DB_URL = "jdbc:sqlite:users.db";

    @Override
    public User getUser(String id) {
        String sql = "SELECT * FROM users WHERE id = ?";
        // ...
    }

    @Override
    public void updateUser(User user) {
        String sql = "INSERT OR REPLACE INTO users(id, username, email, active) VALUES(?,?,?,?)";
        // ...
    }
}

Now, to use this new implementation, we only need to change the provides clause in our module-info.java file:

module com.mycompany.core {
    requires com.mycompany.api;
    requires java.sql;  // We need this for JDBC
    provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl;
}

That’s it! We’ve now swapped out our in-memory implementation for a database-backed one. The beauty of this approach is that we didn’t have to change any code in the UserController or any other consuming classes. They’re still working with the UserService interface, unaware that the underlying implementation has changed.

To add another implementation of the UserService without replacing the existing one, we’ll first modify the core module to include both implementations:

// In the core module
module com.mycompany.core {
    requires com.mycompany.api;
    requires java.sql;  // We need this for JDBC

    provides com.mycompany.api.UserService with 
        com.mycompany.core.UserServiceImpl,
        com.mycompany.core.DatabaseUserServiceImpl;
}

Now, the module system knows that there are two providers for UserService.

Next, we can update the UserServiceLocator to return all available implementations:

// In the app module
module com.mycompany.app {
    requires com.mycompany.api;
    uses com.mycompany.api.UserService;
}

package com.mycompany.app;

import com.mycompany.api.UserService;
import java.util.ServiceLoader;
import java.util.List;
import java.util.stream.Collectors;

public class UserServiceLocator {
    private static final ServiceLoader<UserService> loader = ServiceLoader.load(UserService.class);

    public static List<UserService> getUserServices() {
        return loader.stream()
                     .map(ServiceLoader.Provider::get)
                     .collect(Collectors.toList());
    }
}

This method returns a list of all available UserService implementations.

Now, we can update the UserController to use, for example, all available services:

package com.mycompany.app;

import com.mycompany.api.User;
import com.mycompany.api.UserService;

import java.util.List;

public class UserController {
    private final List<UserService> userServices;

    public UserController() {
        this.userServices = UserServiceLocator.getUserServices();
    }

    public void handleUserUpdate(String id, UserUpdateRequest request) {
        for (UserService userService : userServices) {
            User user = userService.getUser(id);
            // Update user based on request
            userService.updateUser(user);
        }
    }
}

In this setup, UserController will iterate through all available UserService implementations and call the getUser and updateUser methods on each.

This service-oriented approach allows us to build more modular, flexible applications. A well-designed application using the Java Module System’s service feature can easily extend and modify its functionality over time.

Getting Module Details

As your modular application grows, you might need to inspect your modules to understand their structure, dependencies, and how they’re being resolved. Java provides several command-line tools to help with this.

Describing a Module

Let’s start with describing a module. The java command with the --describe-module (or -d) option allows us to see details about a specific module:

java --describe-module java.sql

This might output something like:

java.sql@18.0.3
exports java.sql
exports javax.sql
requires java.logging transitive
requires java.transaction.xa transitive
requires java.base mandated
requires java.xml transitive
uses java.sql.Driver

This tells us what packages the module exports, what other modules it requires, and what services it uses. It’s a quick way to get an overview of a module’s structure and dependencies.

You can also describe modules that aren’t part of the Java runtime. For example, if you have a com.mycompany.core module in a JAR file:

java --module-path mods --describe-module com.mycompany.core

This might output:

com.mycompany.core@1.0
requires java.base mandated
requires com.mycompany.api
provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl

Listing Available Modules

Sometimes, you might want to see all the modules available to your application. You can do this with the --list-modules option:

java --list-modules

This will list all modules in the Java runtime. If you want to include your own modules, you can use:

java --module-path mods --list-modules

This will list both the Java runtime modules and any modules in the mods directory (by convention, the directory where you store the compiled modules).

Module Resolution

When you’re dealing with complex module dependencies, it can be helpful to see how the module system resolves these dependencies. You can do this with the --show-module-resolution option:

java --show-module-resolution --module-path mods -m com.mycompany.app/com.mycompany.app.Main

This will show detailed information about how each module is resolved as the application starts up. It’s particularly useful for debugging issues with module dependencies.

Using the jar Command

While the java command is great for describing modules at runtime, sometimes you’ll want to inspect a module without running it. The jar command can help with this:

jar --describe-module --file mods/com.mycompany.core.jar

This might output something like:

com.mycompany.core jar:file:///.../mods/com.mycompany.core.jar/!module-info.class
requires java.base mandated
requires com.mycompany.api
provides com.mycompany.api.UserService with com.mycompany.core.DatabaseUserServiceImpl

This provides similar information to the java --describe-module command, but it works directly on the JAR file without needing to set up the module path.

Here’s a more complex example. Let’s say we have a multi-module application and want to understand how all the pieces fit together:

# List all modules
java --module-path mods --list-modules

# Describe each of our modules
java --module-path mods --describe-module com.mycompany.api
java --module-path mods --describe-module com.mycompany.core
java --module-path mods --describe-module com.mycompany.app

# Show module resolution for our main application
java --show-module-resolution --module-path mods -m com.mycompany.app/com.mycompany.app.Main

# Describe our core module JAR file
jar --describe-module --file mods/com.mycompany.core.jar

By running these commands, you can get a comprehensive view of your application’s module structure, from the high-level list of all modules, through the details of each module, to the step-by-step resolution process when you run your application.

Analyzing Dependencies With jdeps

jdeps is a tool that provides powerful capabilities for analyzing and visualizing dependencies at both the module and class level. It allows you to examine the relationships between modules, packages, and classes, enabling you to make informed decisions about the structure and organization of your codebase.

To get started with jdeps, let’s explore its basic syntax and common options. The general format for running jdeps is as follows:

jdeps [options] path

Here, path represents the location of the Java class files, JAR files, or directories you want to analyze. The options allow you to customize the behavior of jdeps according to your specific needs.

These are some of the most important general options:

These are the module dependence analysis options:

These are the options to filter dependences:

And these are the options to filter classes to be analyzed:

One of the primary use cases of jdeps is analyzing module dependencies. By running jdeps on a module, you can obtain a detailed report of the modules it depends on and the packages it uses from each module:

jdeps --module-path mods --add-modules com.example.myapp mymodule.jar

In this example, we specify the module path using the --module-path option, which points to the directory containing the module definitions. The --add-modules option is used to specify the main module of our application. Finally, we provide the path to the JAR file representing our module.

jdeps will analyze the dependencies and generate a report that looks something like this:

mymodule.jar -> java.base
   com.example.myapp                         -> java.io
   com.example.myapp                         -> java.lang
   com.example.myapp                         -> java.util
mymodule.jar -> java.desktop
   com.example.myapp                         -> java.awt
   com.example.myapp                         -> javax.swing

This report shows the dependencies of mymodule.jar on other modules, such as java.base and java.desktop. It also lists the specific packages within mymodule.jar that depend on packages from those modules.

In addition to module-level analysis, jdeps allows you to examine dependencies at the class level. By running jdeps on individual class files or directories containing class files, you can gain insights into the relationships between classes and packages.

Consider this example:

jdeps --verbose --class-path lib/* com/example/MyClass.class

Here, we use the --class-path option to specify the classpath containing the required dependencies. The --verbose flag provides more detailed output, showing the specific classes and members being used.

The class-level dependency report generated by jdeps will include information like this:

com.example.MyClass -> java.lang.Object
com.example.MyClass -> java.lang.String
com.example.MyClass -> java.util.ArrayList
com.example.MyClass -> java.util.List
com.example.MyClass -> com.example.HelperClass

This report indicates that MyClass depends on classes from the java.lang and java.util packages, as well as another class named HelperClass from the same package.

jdeps also provides the ability to generate comprehensive dependency reports in various formats. By using the --dot-output option, you can generate a DOT file that visualizes the dependencies as a graph. This graphical representation can be extremely helpful in understanding complex dependency structures and identifying potential issues.

jdeps --dot-output docs --module-path mods --add-modules com.example.myapp mymodule.jar

In this example, jdeps will generate a DOT file named after the module in the docs directory. You can then use tools like Graphviz to render the DOT file into a visual graph.

Another useful feature of jdeps is its ability to identify usage of internal APIs. The --jdk-internals flag helps you detect and analyze the use of internal JDK APIs within your code. This is important because relying on internal APIs can lead to compatibility issues and unexpected behavior when upgrading to newer Java versions.

jdeps --jdk-internals --class-path lib/* com/example/MyClass.class

If MyClass uses any internal JDK APIs, jdeps will report them in the output, allowing you to take necessary actions to refactor or remove the dependencies on internal APIs.

If you provide the path to a JAR file or directory, jdeps will recursively analyze all the classes within it and generate a comprehensive dependency report.

Here’s an example:

jdeps --recursive lib/myapp.jar

The --recursive option ensures that jdeps traverses all the classes and nested directories within the specified JAR file or directory, providing a complete picture of the dependencies.

jdeps also offers a feature called recursive dependency analysis, which allows you to understand the transitive dependencies of your code. By analyzing not only the direct dependencies but also the dependencies of those dependencies, jdeps helps you identify potential issues and conflicts.

Consider this example:

jdeps --recursive --module-path mods --add-modules com.example.myapp mymodule.jar

With the --recursive flag, jdeps will traverse the entire dependency graph, starting from the specified module or JAR file. It will generate a report that includes all the transitive dependencies, giving you a comprehensive view of your project’s dependency structure.

Using Module Files with jmod

jmod is a command-line tool that operates on a file format called JMOD. JMOD files are similar to JAR files in the way that they package Java classes, resources, and metadata. However, JMOD files are specifically designed to work with the module system and offer additional capabilities compared to traditional JAR files.

The JMOD File Format

The JMOD file format is optimized for the JPMS and serves as a container for modular content. It encapsulates not only the compiled Java classes and resources but also includes module descriptors, native libraries, and other module-specific information. JMOD files have the .jmod file extension and follow a specific directory structure to organize their contents.

JMOD files do not replace JAR files.

JAR (Java Archive) files are the most common and widely used format for packaging Java classes and resources. They are essentially zip files that contain compiled Java classes, metadata, and resources. Additionally, JAR files can be placed on the classpath for easy access by Java programs.

There are some key differences between JAR and JMOD files:

So, when should you use JMOD files instead of JAR files? Here are some guidelines:

However, if you are developing a non-modular Java application or a library that needs to be compatible with older versions of Java, JAR files are still the preferred choice.

One of the key advantages of JMOD files is their ability to include native libraries and executables. This is particularly useful for modules that have platform-specific dependencies or require native code integration. By packaging native libraries within the JMOD file, the module can be easily distributed and deployed across different platforms.

JMOD files also support versioning, allowing modules to specify their version information. This is important for managing dependencies and ensuring compatibility between various versions of modules. The module descriptor in the module-info.class file can include version-related annotations to provide version metadata.

Operation Modes

This is the basic syntax of the jmod command:

jmod (create|extract|list|describe|hash) [options] jmod-file

The main operation modes are:

These are the most important options:

Here are some examples that demonstrate the basic usage of each operation mode:

  1. Create mode:
     jmod create \
         --class-path classes \
         --main-class com.example.Main \
         --module-version 1.0 \
         --module-path lib \
         mymodule.jmod
    

    This command creates a new JMOD archive file named mymodule.jmod. It includes classes from the classes directory, sets the main class to com.example.Main, specifies the module version as 1.0, and uses the lib directory as the module path.

  2. Extract mode:
     jmod extract --dir extracted_files mymodule.jmod
    

    This command extracts all files from mymodule.jmod into a directory named extracted_files.

  3. List mode:
     jmod list mymodule.jmod
    

    This command prints the names of all entries in mymodule.jmod.

  4. Describe mode:
     jmod describe mymodule.jmod
    

    This command prints the module details of mymodule.jmod.

  5. Hash mode:
     jmod hash --module-path lib \
         --hash-modules java.base \
         mymodule.jmod
    

    This command determines leaf modules and records the hashes of dependencies that directly and indirectly require them. It uses the lib directory as the module path and considers modules matching the pattern java.base.

Best Practices and Limitations

When working with JMOD files, there are some best practices to keep in mind:

While JMOD files offer many benefits for modular Java development, there are some limitations and considerations to keep in mind:

Traditionally, Java applications have relied on the Java Runtime Environment (JRE) to execute. The JRE includes a wide range of modules and libraries, many of which may not be necessary for a particular application. This can lead to larger distribution sizes and potentially unnecessary dependencies.

With jlink, we can create custom runtime images that include only the modules required by our application. These custom runtime images are self-contained and can be distributed as standalone executables. They provide several benefits, such as reducing distribution size, improving startup time, and enhancing security by minimizing the attack surface.

To create a custom runtime image using jlink, we use the following basic syntax:

jlink [options] --module-path <modulepath> --add-modules <modules>

Let’s break down the key components of the jlink command:

One of the primary goals of creating custom runtime images is to minimize the size and include only the necessary modules. jlink provides options to create a minimal runtime that includes only the essential modules required for our application to run.

Here are some of the most important options:

To create a minimal runtime, we can use the following command:

jlink --module-path <modulepath> \
      --add-modules <modules> \
      --compress 2 \
      --strip-debug \
      --no-header-files \
      --no-man-pages \
      --output <path>

In this command, we use several options to optimize the runtime image:

By specifying only the necessary modules with --add-modules and using these optimization options, we can create a minimal runtime image that is tailored to our application’s specific requirements.

jlink also allows us to explicitly include or exclude modules from the runtime image. This gives us fine-grained control over the modules that are packaged into the image.

To include specific modules, we can use the --add-modules option followed by a comma-separated list of module names. For example:

jlink --module-path <modulepath> \
      --add-modules module1,module2,module3 \
      --output <path>

This command will create a runtime image that includes only module1, module2, and module3, along with their transitive dependencies.

On the other hand, if we want to exclude certain modules from the runtime image, we can use the --exclude-modules option followed by a comma-separated list of module names. For example:

jlink --module-path <modulepath> \
      --add-modules ALL-MODULE-PATH \
      --exclude-modules module4,module5 \
      --output <path>

In this case, jlink will include all modules found on the module path except for module4 and module5.

Plugins

Plugins are additional components that extend the functionality of the jlink tool. They allow developers to customize the creation of runtime images in various ways, such as optimizing the generated image, adding or removing resources, and configuring how the image is laid out.

If you execute:

jlink --list-plugins

You’ll get the list of all available plugins. For example:

Here are some examples of how you might use these plugins with the jlink command:

# Create a runtime image with maximum compression,
# exclude specific files, and strip debug information
jlink --module-path $JAVA_HOME/jmods \
      --add-modules java.base \
      --compress zip-9 \
      --exclude-files "**.java,glob:/java.base/lib/client/**" \
      --strip-debug \
      --output custom-runtime-image

# Create a runtime image that includes only the
# specified locales and uses the server VM
jlink --module-path $JAVA_HOME/jmods \
      --add-modules java.base \
      --include-locales en,ja \
      --vm server \
      --output custom-runtime-image

Optimizing Runtime Images

In addition to creating a minimal runtime image, jlink provides options to further optimize the generated runtime. These optimizations can help reduce the size of the runtime image and improve its performance.

One important optimization is compression. By default, jlink does not compress the generated runtime image. However, we can enable compression using the --compress option followed by a compression level. The compression level can be set to 0 (no compression), 1 (constant string sharing), or 2 (ZIP compression). For example:

jlink --module-path <modulepath> 
      --add-modules <modules> \
      --compress 2 \
      --output <path>

Using --compress 2 applies ZIP compression to the generated runtime image, significantly reducing its size.

Another optimization is to strip debug information from the runtime image. Debug information is useful during development but is not necessary for production deployments. We can remove debug information using the --strip-debug option:

jlink --module-path <modulepath> 
      --add-modules <modules> \
      --strip-debug \
      --output <path>

This way, we can further reduce the size of the runtime image.

Migrating an Application

Migrating an existing application to use modules can be a challenging task. It’s doable, but requires careful planning and execution.

Before embarking on the migration process, it’s important to understand how the packages and libraries in the existing application are structured. This involves analyzing the codebase and identifying the dependencies between different parts of the application.

One approach to gain insights into the application structure is to use jdeps. By running jdeps on the application’s JAR files or class files, we can generate a dependency report that provides valuable information about the relationships between packages and classes.

Here’s an example of running jdeps on an application JAR file:

jdeps -s -recursive application.jar

The -s option generates a summary output, and the -recursive option analyzes all dependent JAR files as well.

The output of jdeps will give us an overview of the packages and their dependencies. It will highlight any dependencies on JDK internal APIs, which is important to note as these APIs may not be accessible in future Java versions.

If you want more detail, you can use the -verbose option:

jdeps -verbose application.jar

The output will show the dependencies between packages and classes, as well as the dependencies on external libraries.

It’s important to identify and resolve any circular dependencies or unnecessary dependencies at this stage. Circular dependencies can cause issues when modularizing the application, as modules cannot have cyclic dependencies. Unnecessary dependencies can bloat the application and make it harder to modularize effectively.

Now that we have a map of the application dependencies, it’s time to start planning our migration. One common strategy is to split our big project into smaller, more manageable modules. This process involves identifying logical boundaries within the application and separating the code into distinct modules based on functionality and dependencies.

JPMS gives us a few tools to ease this transition: unnamed modules and automatic modules.

Let’s say we have a big monolithic application called BigApp. We might start by putting it on the module path without defining a module-info.java file:

java --module-path BigApp.jar 
     --add-modules ALL-UNNAMED

This puts BigApp into an unnamed module. It’s not a proper JPMS module yet, but it’s a start. We can now start breaking BigApp into smaller, more manageable pieces.

For third-party libraries that aren’t yet modularized, we can use automatic modules. Let’s say we’re using a library named CoolLib. We can put it on the module path:

java --module-path BigApp.jar:CoolLib.jar 
     --add-modules ALL-UNNAMED

Now CoolLib becomes an automatic module. The module system derives its name from the JAR filename, and it exports all its packages. But remember, these are temporary solutions. Our end goal is to have proper, explicit modules for everything.

When splitting the project into modules, it’s important to consider the dependencies between the modules. Aim to minimize the coupling between modules and promote loose coupling with well-defined interfaces and APIs.

Here are some strategies for properly splitting a big project into modules:

  1. Package-based splitting: One approach is to create modules based on the existing package structure. Each package or a group of related packages can be converted into a separate module. This helps in maintaining a clear separation of concerns and encapsulation.

  2. Layered architecture: If the application follows a layered architecture (presentation layer, business logic layer, data access layer), each layer can be split into its own module. This allows for better modularity and easier maintenance of each layer independently.

  3. Feature-based splitting: Another approach is to split the application based on its features or functional areas. Each major feature or functionality can be encapsulated within its own module, promoting reusability and maintainability.

  4. Dependency-based splitting: Analyzing the dependencies between different parts of the application can help identify natural module boundaries. Strongly coupled components can be grouped together into a module, while loosely coupled components can be split into separate modules.

But you might be wondering what strategies can we take for the migration in general. Here are a few approaches:

  1. Incremental migration: In this approach, the migration is done gradually, one module at a time. Start by identifying a suitable module to migrate first, typically one with minimal dependencies on other parts of the application. Once the module is successfully migrated, move on to the next module, and so on.

  2. Bottom-up migration: This strategy involves starting the migration from the lowest-level modules and gradually moving up the dependency hierarchy. Begin by modularizing the modules that have no dependencies on other modules, and then proceed to modules that depend on already modularized modules.

  3. Top-down migration: In contrast to the bottom-up approach, the top-down migration starts with the high-level modules and works its way down the dependency chain. This strategy is useful when the high-level modules have a clear separation of concerns and can be easily modularized.

  4. Parallel development: If time and resources permit, parallel development can be employed. In this approach, a separate branch or codebase is created for the modularized version of the application, while the existing non-modularized version continues to be maintained. Development can proceed simultaneously on both versions, gradually migrating modules to the modularized branch.

For example, we might start modularizing a part of our application using a bottom-up approach:

module com.myapp.core {
    requires java.base;
    requires com.coollib;  // This is our automatic module
    exports com.myapp.core.api;
}

This module-info.java file defines a new module com.myapp.core. We’re starting with a core module that likely has fewer dependencies, which is characteristic of the bottom-up approach. It requires the java.base module (which is implicit but we’re being explicit here) and CoolLib, which is an automatic module. It also exports a package com.myapp.core.api for other modules to use.

As we create more modules, we’ll need to think carefully about our module boundaries.

One important thing to keep in mind is that while we’re in this phase, we might need to open up more than we’d like. For example:

open module com.myapp.core {
    requires java.base;
    requires com.coollib;
    exports com.myapp.core.api;
}

By making this an open module, we allow deep reflection into all its packages. It’s not ideal for security, but it might be necessary during the migration to keep things working. As we progress in our migration, we’ll want to tighten these permissions, exporting and opening only what’s necessary.

Remember, migration is a process. It’s okay to use unnamed and automatic modules as stepping stones. The key is to have a clear migration plan and to move steadily towards a fully modularized system. Breaking down the migration process into smaller, manageable tasks helps in tracking progress and identifying any challenges or roadblocks along the way.

Key Points

Practice Questions

1. Which of the following are types of modules in the Java Platform Module System (JPMS)? (Choose all that apply.)

A) Automatic module
B) Default module
C) Unnamed module
D) Core module
E) Primary module

2. Which of the following is the correct way to declare a module named com.example in the Java Platform Module System (JPMS)?

A) module com.example { exports com.example.api; }
B) declare module com.example { }
C) create module com.example { requires java.base; }
D) module com.example { }
E) module com.example requires java.base;

3. Which of the following access control statements correctly restricts access to the com.example.internal package so that it is only accessible to the com.example.client module?

A) module com.example { exports com.example.internal to com.example.client; }
B) module com.example { opens com.example.internal to com.example.client; }
C) module com.example { requires com.example.internal; }
D) module com.example { provides com.example.internal to com.example.client; }
E) module com.example { uses com.example.internal; }

4. Given the following module declarations, which statement is correct regarding the accessibility of the com.example.api package for deep reflection by the com.example.client module?

module com.example {
    exports com.example.api;
    opens com.example.internal to com.example.client;
}

module com.example.client {
    requires com.example;
}

A) The com.example.client module can access the com.example.api package for deep reflection.
B) The com.example.client module cannot access the com.example.api package for deep reflection.
C) The com.example.api package is opened to all modules for deep reflection.
D) The com.example.internal package is exported to the com.example.client module.
E) The com.example.api package is exported to the com.example.client module for deep reflection.

5. Which of the following statements is correct regarding core Java modules and their functionalities?

A) The java.base module provides the Swing and AWT libraries for building graphical user interfaces.
B) The java.logging module is responsible for handling collections, including lists, sets, and maps.
C) The java.desktop module provides the classes for implementing standard input and output streams.
D) The java.xml module includes the classes for processing XML documents.
E) The java.naming module provides APIs for accessing and processing annotations.

6. Which of the following command-line statements correctly compiles the module located in the src/com.example directory and outputs the compiled module to the out directory?

A) javac -d out src/com.example/module-info.java src/com.example/com/example/*.java
B) javac -sourcepath src -d out com.example/module-info.java com.example/com/example/*.java
C) javac -d out --module-source-path src -m com.example
D) javac -modulepath out -d src src/com.example/module-info.java src/com.example/com/example/*.java
E) javac --module-path src --module com.example -d out

7. Given the following multi-module application structure, which command compiles both modules correctly?

src/
├── com.foo/
│   ├── module-info.java
│   └── com/foo/Foo.java
└── com.bar/
    ├── module-info.java
    └── com/bar/Bar.java

A) javac --module-source-path src -d out $(find src -name "*.java")
B) javac -d out --module com.foo,com.bar --module-source-path src
C) javac -sourcepath src -d out src/com.foo/module-info.java src/com.foo/com/foo/*.java src/com.bar/module-info.java src/com.bar/com/bar/*.java
D) javac -modulepath src -d out src/com.foo/*.java src/com.bar/*.java
E) javac --module-source-path src/com.foo,src/com.bar -d out

8. Which of the following statements correctly specifies a service provider implementation for the service com.example.Service in the module-info.java of the com.provider module?

A) requires com.example.Service with com.provider.ServiceImpl;
B) exports com.example.Service with com.provider.ServiceImpl;
C) provides com.example.Service with com.provider.ServiceImpl;
D) uses com.example.Service with com.provider.ServiceImpl;

9. Which of the following command-line statements correctly describes the com.example module using the --describe-module option?

A) java --describe-module com.example/module-info.java
B) javac --describe-module com.example
C) jar --describe-module com.example
D) java --describe-module com.example

10. Which of the following command-line statements correctly uses jdeps to analyze the dependencies of a JAR file named example.jar? (Choose all that apply)

A) jdeps --list-deps example.jar
B) jdeps -verbose example.jar
C) jdeps -s example.jar
D) jdeps --check example.jar

11. Which of the following command-line statements correctly creates a JMOD file from the contents of the mods/com.example directory?

A) jmod create --class-path mods/com.example --output com.example.jmod
B) jmod --create --class-path mods/com.example --output com.example.jmod
C) jmod --create --dir mods/com.example --output com.example.jmod
D) jmod create --dir mods/com.example --output com.example.jmod

12. Which of the following command-line statements correctly creates a custom runtime image using the jlink tool with the modules java.base and com.example and outputs it to the myimage directory?

A) jlink --module-path java.base:com.example --output myimage
B) jlink --module-path mods --add-modules java.base,com.example --output myimage
C) jlink --add-modules java.base,com.example --image myimage
D) jlink --modules java.base,com.example --dir myimage

13. Which of the following statements is correct regarding the migration of a legacy application to the Java Platform Module System using unnamed and automatic modules?

A) An unnamed module can depend on named modules and other unnamed modules.
B) Automatic modules must have a module-info.java file to be placed on the module path.
C) Unnamed modules can export their packages to named modules using module-info.java.
D) An automatic module is created when a JAR file without a module-info.java is placed on the module path, and it can read all other modules.

Do you like what you read? Would you consider?


Do you have a problem or something to say?

Report an issue with the book

Contact me