Skip to Content
Course content

181: Understanding the Java Platform Module System (JPMS)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably spent years just throwing JARs onto a classpath and letting Java figure it out at runtime. But since Java 9, we have the Java Platform Module System (JPMS). For a lot of developers, JPMS feels like a wall they keep running into. You’ll be writing a perfectly valid class, you'll import it, and then the compiler will scream at you—not because the class doesn't exist, but because it's "not visible."

The Mystery of the Invisible Package

Imagine you're building a small system with two parts: a payment-engine that handles the logic and a payment-app that runs the main loop. You've organized your code into modules. In your payment-engine module, you have a utility class that calculates taxes:

// File: src/payment.engine/com/payment/utils/TaxCalculator.java
package com.payment.utils;

public class TaxCalculator {
    public static double calculate(double amount) {
        return amount * 0.15;
    }
}

Now, you try to use this in your payment-app module:

// File: src/payment.app/com/payment/main/Main.java
package com.payment.main;
import com.payment.utils.TaxCalculator;

public class Main {
    public static void main(String[] args) {
        System.out.println("Tax: " + TaxCalculator.calculate(100));
    }
}

On the surface, this is standard Java. But when you try to compile this as a module, you get a punch in the face from the compiler: package com.payment.utils is not visible. You check your imports, you check your folder structure, and everything looks correct. The class is public, the method is public, yet Java is acting like the class doesn't even exist.

Opening the Gates with module-info.java

Here is the thing about JPMS: public no longer means "available to everyone." In the old classpath world, if a class was public, any other class on the classpath could touch it. In the module world, a public class is only available if the package containing it is explicitly exported by its module, and the consuming module explicitly requires that module.

To fix this, we need to define the boundaries. First, we tell the payment-engine that it's okay for other people to use the utils package. We do this by creating a module-info.java file in the root of the module's source folder:

// File: src/payment.engine/module-info.java
module payment.engine {
    exports com.payment.utils;
}

That exports keyword is the key. Without it, the com.payment.utils package is encapsulated—hidden from the rest of the world, even if the classes inside it are marked public. I actually find this incredibly useful for hiding "internal" helper classes that you don't want other developers relying on.

But we're only halfway there. The payment-app still doesn't know it's allowed to look at the payment-engine. We need to declare the dependency in the app's own module descriptor:

// File: src/payment.app/module-info.java
module payment.app {
    requires payment.engine;
}

Now, when you compile, the magic happens. The compiler sees that payment.app requires payment.engine, and payment.engine explicitly allows access to com.payment.utils. The bridge is built, and your code finally runs.

Handling Internal Logic with Encapsulation

You might be wondering, "Why go through all this trouble?" I've seen too many legacy projects where a developer used a "public" class that was actually meant to be internal, and then six months later, they couldn't change that class because fifty other classes were depending on it. JPMS solves this.

If you had a package called com.payment.engine.internal containing complex math that should never be touched by the Main class, you simply don't export it. Even if every class in that package is public, the JPMS will block any attempt to import them from outside the module. It's a hard wall that prevents architectural drift.




📋 Practical Task

Exercise: Modularizing a Legacy Utility Library

You have been handed a small project that was written using a flat classpath, and your lead engineer wants it converted to the Java Platform Module System to improve encapsulation.

The Setup: Create two separate module directories: com.service.core and com.service.client.

  1. In com.service.core:
    • Create a package com.service.core.api containing a public class ServiceManager with a public method String getStatus().
    • Create a package com.service.core.internal containing a public class InternalConfig with a public method String getConfigValue().
    • Create a module-info.java file that exports the api package but does not export the internal package.
  2. In com.service.client:
    • Create a module-info.java file that requires com.service.core.
    • Create a Main class that attempts to call ServiceManager.getStatus() (this should work).
    • Create a second attempt in the Main class to call InternalConfig.getConfigValue().

Goal: Successfully compile the project such that the api call works, but the internal call triggers a compilation error. Once you see the error, comment out the illegal call to allow the project to build. This confirms your encapsulation is working as intended.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.