Thursday, September 17, 2026

Java JPMS: A Developer's Guide

Java Introduction and Setup - A Deep Dive into the Java Module System (JPMS)

Java has long been one of the most popular programming languages in the world, powering everything from enterprise applications to mobile devices. With the introduction of the Java Platform Module System (JPMS) in Java 9, the language underwent a significant architectural transformation that fundamentally changed how Java applications are structured, deployed, and maintained.

Java Introduction and Setup - A Deep Dive into the Java Module System (JPMS)


Understanding Java and Its Evolution

Java was first released by Sun Microsystems in 1995 and quickly gained popularity for its "write once, run anywhere" philosophy. The language's robust ecosystem, object-oriented design, and automatic memory management made it a favorite among developers for building scalable applications. Before Java 9, applications were typically deployed as collections of JAR files on a flat classpath, which led to several challenges including the infamous "JAR hell" where dependencies conflicted, making application maintenance increasingly complex.

The evolution of Java has been marked by regular updates, with new features and improvements being added with each release. Major milestones included Java 5 with generics, Java 8 with lambdas and streams, and Java 9 with the introduction of the Java Platform Module System. This modular approach represents a fundamental shift in how Java applications are organized, addressing long-standing issues with classpath-based applications while introducing new capabilities for developers.

JPMS, also known as Project Jigsaw, was a multi-year effort to modularize the Java platform itself. This major change was driven by the need to address several long-standing issues in Java development, including the "JAR hell" problem, security vulnerabilities, and the inability to scale Java to smaller devices.

What is JPMS? The Java Platform Module System Explained

The Java Platform Module System (JPMS), introduced in Java 9, is a modular programming feature that helps developers organize applications into independent and reusable modules. A module is essentially a named, self-describing group of packages, classes, resources, and configuration files that work together as a single unit. The module system adds a layer above packages, providing explicit boundaries between different parts of an application.

JPMS addresses several key challenges that existed with the traditional classpath approach:

  • Strong encapsulation: Modules can explicitly declare which packages they make public to other modules
  • Reliable configuration: The module system ensures that all dependencies are declared and available at startup
  • Scalable Java platforms: The Java platform itself is now modular, allowing applications to include only the necessary modules
  • Improved security: By restricting access to internal packages, JPMS enhances the security of Java applications
  • Better performance: With smaller footprints and more efficient class loading

At the heart of JPMS is the module-info.java file, which lives at the root of each module and declares two critical things: what the module requires from other modules (dependencies) and what it offers to other modules (exports). This explicit declaration of dependencies and exports eliminates many of the ambiguities and conflicts that plagued classpath-based applications.

Setting Up Your Java Environment for JPMS

To work with JPMS, you'll need Java 9 or later installed on your system. Setting up your development environment for JPMS involves installing the correct Java version, choosing an appropriate IDE, and understanding the new project structure that JPMS introduces.

First, ensure you have Java 9 or later installed. You can download it from the official Oracle website or use OpenJDK. To verify your installation, open a terminal or command prompt and run:

java -version

This should display version 9 or higher. If you're using a build tool like Maven or Gradle, make sure it supports Java 9+ projects.

For IDE support, modern Java IDEs like IntelliJ IDEA, Eclipse, and NetBeans have added robust JPMS support. When creating a new project, look for "Java Module" or similar options to set up a modular project structure.

Creating your first modular project involves a few key steps:

1. Create a project structure with modules

2. Add a module-info.java file to each module

3. Declare dependencies between modules

4. Configure your build system to handle modules

Here's an example of a basic modular project structure:

my-modular-app/
├── module1/
│   ├── src/
│   │   └── com/
│   │       └── example/
│   │           └── module1/
│   │               ├── Main.java
│   │               └── module-info.java
│   └── build.gradle
├── module2/
│   ├── src/
│   │   └── com/
│   │       └── example/
│   │           └── module2/
│   │               ├── Service.java
│   │               └── module-info.java
│   └── build.gradle
└── settings.gradle

When creating a new project with JPMS, your project structure will look different from traditional Java projects. Each module will have its own directory containing the module-info.java file at the root, along with the packages and classes that belong to that module. This organized structure makes it easier to understand the relationships between different parts of your application and to manage dependencies more effectively.

Creating Your First Module with module-info.java

Creating a module with JPMS is straightforward once you understand the basic concepts. Let's create a simple module called com.example.greeting that contains a utility class for generating greetings. First, create the directory structure for your module:

src/com.example.greeting/
    module-info.java
    com/example/greeting/
        GreetingUtils.java

The module-info.java file is where you define your module. Here's what a basic module declaration looks like:

module com.example.greeting {
    exports com.example.greeting;
}

This module declaration does two things:

1. It declares a module named com.example.greeting

2. It exports the com.example.greeting package, making its public classes visible to other modules that depend on it

Now, let's create the GreetingUtils.java class:

package com.example.greeting;

public class GreetingUtils {
    public static String generateGreeting(String name) {
        return "Hello, " + name + "!";
    }
}

This simple utility class contains a static method that generates a greeting message. Because the package is exported in module-info.java, other modules can access this class and its public methods.

To compile this module, navigate to the root directory of your project (where the src directory is located) and run:

javac --module-source-path src -d out src/com.example.greeting/module-info.java src/com.example.greeting/com/example/greeting/*.java

This command tells the Java compiler to:

1. Look for module sources in the src directory (--module-source-path src)

2. Place compiled classes in the out directory (-d out)

3. Compile the module-info.java file and all Java files in the module

After compilation, you'll have compiled classes in the out/com.example.greeting directory that you can use in other modules or run as a standalone application.

Module Declarations and Dependencies

At the heart of JPMS is the module-info.java file, which serves as the declaration file for each module. This file defines the module's name, its dependencies on other modules, and which packages it exposes to other modules.

A basic module declaration looks like this:

module com.example.module1 {
    requires com.example.module2;
}

This declaration states that the com.example.module1 module requires the com.example.module2 module. With this requirement in place, we can use classes from the com.example.module2 module in our code.

Let's extend our example by creating a second module called com.example.app that uses our com.example.greeting module. First, create the directory structure:

src/com.example.app/
    module-info.java
    com/example/app/
        Main.java

Now, create the module-info.java file for the app module:

module com.example.app {
    requires com.example.greeting;
}

This declaration states that the com.example.app module requires the com.example.greeting module. With this requirement in place, we can use classes from the com.example.greeting module in our code.

Here's the Main.java class:

package com.example.app;

import com.example.greeting.GreetingUtils;

public class Main {
    public static void main(String[] args) {
        String greeting = GreetingUtils.generateGreeting("World");
        System.out.println(greeting);
    }
}

This class imports and uses the GreetingUtils class from our greeting module. To compile both modules together, run:

javac --module-source-path src -d out src/com.example.greeting/module-info.java src/com.example.greeting/com/example/greeting/*.java src/com.example.app/module-info.java src/com.example.app/com/example/app/*.java

To run the application, use the java command with the --module option:

java --module-path out -m com.example.app/com.example.app.Main

This command tells the Java runtime to:

1. Look for modules in the out directory (--module-path out)

2. Run the com.example.app/com.example.app.Main class (-m com.example.app/com.example.app.Main)

Managing Module Dependencies and Services

One of the most powerful features of JPMS is its ability to explicitly declare and manage module dependencies. When working with multiple modules, you need to specify which modules your module depends on and which packages from those dependencies you actually use.

JPMS also supports services, which allow modules to provide implementations of interfaces and other modules to consume those implementations. This pattern is particularly useful for creating extensible applications where different implementations can be plugged in at runtime.

Let's create a service example. First, define a service interface in one module:

package com.example.greeting.api;

public interface GreetingService {
    String getGreeting(String name);
}

Then, implement this service in another module:

package com.example.greeting.impl;

import com.example.greeting.api.GreetingService;

public class EnglishGreetingService implements GreetingService {
    @Override
    public String getGreeting(String name) {
        return "Hello, " + name + "!";
    }
}

In your module-info.java files, you would declare the service and its provider:

// In the module with the service interface
module com.example.greeting.api {
    exports com.example.greeting.api;
}

// In the module with the service implementation
module com.example.greeting.impl {
    requires com.example.greeting.api;
    provides com.example.greeting.api.GreetingService 
        with com.example.greeting.impl.EnglishGreetingService;
}

And in the module that uses the service:

module com.example.app {
    requires com.example.greeting.api;
    
    uses com.example.greeting.api.GreetingService;
}

This allows the application to use the service provider interface pattern to create pluggable components.

Advanced JPMS Concepts and Best Practices

As you become more comfortable with JPMS, you'll encounter several advanced concepts that can help you build more sophisticated modular applications. One such concept is the use of "requires transitive" dependencies, which propagate dependencies through the module graph. When you declare requires transitive module.name, any module that depends on your module will automatically depend on module.name as well.

Another important concept is "opens" and "opens transitive" declarations, which allow for deeper reflection access to internal packages. While exports makes public classes of a package accessible, opens makes all types in a package accessible for deep reflection, even to modules that don't explicitly depend on your module.

Here's an example of using opens:

module com.example.app {
    requires transitive com.example.greeting;
    opens com.example.app.internal;
}

This declaration:

1. Requires the com.example.greeting module transitively

2. Opens the com.example.app.internal package for reflection to any module that needs it

Here are some best practices to follow when working with JPMS:

  • Keep modules cohesive: Each module should have a single, well-defined responsibility
  • Minimize module dependencies: The fewer modules your module depends on, the simpler and more maintainable it will be
  • Use versioned modules where appropriate: JPMS supports module versions, which can be helpful for managing different versions of the same module
  • Leverage services for extensibility: Use the service provider interface pattern to create pluggable components
  • Test module boundaries thoroughly: Ensure that modules are properly encapsulated and dependencies are correctly declared
  • Use requires transitive sparingly: While useful, overuse can lead to dependency bloat
  • Consider splitting large modules: If a module becomes too large, consider splitting it into smaller, more focused modules

JPMS also provides several command-line tools for working with modules, including jdeps for analyzing dependencies, jlink for creating custom runtime images, and jmod for creating module JMOD files. These tools can help you optimize your modular applications for deployment.

For example, you can use jdeps to analyze module dependencies:

jdeps --module-path out --list-deps com.example.app

And you can use jlink to create a custom runtime image:

jlink --module-path out --add-modules com.example.app --output custom-runtime

Conclusion

The Java Platform Module System represents a significant evolution in how Java applications are structured and deployed. By introducing strong encapsulation, explicit dependencies, and improved scalability, JPMS addresses many of the challenges that existed with the traditional classpath approach. While adoption has been gradual, with many enterprise applications still running on the classpath, JPMS offers compelling benefits for developers building modern applications.

As you explore the Java module system, you'll discover that it not only solves long-standing problems but also opens up new possibilities for application design and deployment. Whether you're building small utilities or large enterprise systems, JPMS provides the tools you need to create more maintainable, secure, and scalable Java applications. By embracing modular development with JPMS, you can take your Java skills to the next level and build applications that are better structured, easier to maintain, and more resilient to change.

Frequently Asked Questions

  • What is JPMS in Java?
    JPMS stands for Java Platform Module System, introduced in Java 9. It's a modular programming feature that helps developers organize applications into independent and reusable modules with strong encapsulation and explicit dependencies.
  • How does JPMS solve the JAR hell problem?
    JPMS eliminates JAR hell by requiring explicit declarations of module dependencies in module-info.java files. This ensures that all required modules are available at startup and prevents conflicts between different versions of the same library.
  • What is the purpose of module-info.java?
    The module-info.java file declares a module's name, its dependencies on other modules, and which packages it exposes to other modules. This file serves as the entry point for each module and defines its boundaries and relationships.
  • How do I create a basic JPMS module?
    To create a JPMS module, create a directory structure with a module-info.java file at the root. In this file, declare your module name with the 'module' keyword and specify which packages to export. Then compile using javac with the --module-source-path option.

No comments:

Post a Comment