Introduction: Building with Blocks – Understanding Java Modules
Welcome back, future Java architect! Up until now, we’ve mostly worked with individual .java files, then grouped them into packages, and finally bundled them into JARs. This approach works wonderfully for smaller projects, but as applications grow, they can become behemoths of tangled dependencies, making them hard to manage, understand, and secure.
Enter Java Modules, also known as Project Jigsaw, a revolutionary feature introduced in Java 9 and refined in subsequent versions, including our current focus, Java 25. Modules provide a powerful new way to structure your applications, bringing strong encapsulation, reliable configuration, and improved maintainability. Think of it like building with LEGOs: instead of a pile of bricks, you have well-defined, interconnected blocks, each with a clear purpose and explicit connections to other blocks.
In this chapter, you’ll learn what Java Modules are, why they’re essential for modern, large-scale Java applications, and how to build your own modular projects step-by-step using Java Development Kit (JDK) 25. We’ll cover the core concepts of the Java Module System (JMS), including module declarations, exports, and requirements. By the end, you’ll be able to confidently structure your Java projects in a way that promotes clarity, security, and scalability. Ready to modularize? Let’s dive in!
Before we start, make sure you have a working JDK 25 installation. You should also be comfortable with basic Java syntax, packages, and compiling/running Java code from the command line, as covered in earlier chapters.
Core Concepts: The Building Blocks of Modularity
Before we write any code, let’s understand the fundamental ideas behind Java Modules. Don’t worry, we’ll break it down into digestible pieces!
The Problem: Before Modules (The “Classpath Hell”)
Imagine you’re building a massive Java application. You have hundreds of classes, organized into many packages, and you rely on dozens of third-party libraries (JAR files). This setup often leads to several headaches:
- Weak Encapsulation: By default, if a class is public within a JAR, any other code on the classpath can access it. This means you might accidentally depend on internal implementation details of a library that weren’t meant for public use. When the library updates, your code breaks.
- Classpath Hell: Managing a long list of JARs on the classpath can be tricky. You might have version conflicts (different libraries requiring different versions of the same dependency), or accidentally include duplicate JARs, leading to unpredictable behavior.
- Unreliable Configuration: It’s hard to know exactly what classes your application truly needs. The Java Virtual Machine (JVM) loads everything it finds on the classpath, even if it’s never used. This can lead to larger application sizes and slower startup times.
- Security Concerns: With everything accessible, it’s harder to restrict what parts of your application can access sensitive code or data.
These problems are what the Java Module System (JMS) was designed to solve!
What is a Module?
At its heart, a module is a named, self-describing collection of code and data. Think of it as a super-powered JAR file. Instead of just containing classes and resources, a module explicitly declares:
- Its Name: A unique identifier (e.g.,
com.example.myapp). - What it Exports: Which of its packages are accessible to other modules. Everything else is strongly encapsulated and hidden by default.
- What it Requires: Which other modules it needs to function.
- What Services it Uses or Provides: (We’ll touch on this briefly later, but it’s for more advanced scenarios).
The magic happens in a special file called module-info.java.
The module-info.java File: Your Module’s Blueprint
Every module has a module-info.java file at its root. This file is called the module descriptor. It’s where you declare all the crucial information about your module.
Let’s look at a simple example:
// This is NOT actual code yet, just an illustration!
module com.example.myfeature {
exports com.example.myfeature.api; // This package is public
requires com.example.utility; // This module needs 'utility' module
}
This snippet tells us:
- This module is named
com.example.myfeature. - It exposes only the
com.example.myfeature.apipackage to other modules. All other packages withincom.example.myfeatureare hidden. This is strong encapsulation in action! - It depends on another module called
com.example.utility. Ifcom.example.utilityisn’t present, this module won’t even compile or run. This is reliable configuration.
Key Keywords in module-info.java
Let’s break down the most important keywords you’ll use:
module <module_name> { ... }: Declares a new module with a unique name. By convention, module names follow a reverse-domain naming pattern, similar to packages (e.g.,com.mycompany.mymodule).exports <package_name>;: This is how a module explicitly states which of its packages are available for other modules to use. If a package isn’t exported, it’s considered an internal implementation detail and is inaccessible from outside the module.requires <other_module_name>;: This declares a dependency. Your module needs the specified<other_module_name>to compile and run. The Java Module System ensures that all required modules are present and correctly resolved at compile-time and run-time.requires transitive <other_module_name>;: This is a special type ofrequires. If Module Arequires transitiveModule B, and Module CrequiresModule A, then Module C automatically gains access to Module B’s exported packages without explicitly requiring Module B itself. This is useful for library modules that expose functionality from their dependencies as part of their own API.opens <package_name>;: Whileexportsallows compilation and direct access to public types,opensallows runtime reflection access to all types (public and non-public) within the specified package. This is crucial for frameworks (like Spring or Hibernate) that often use reflection to inspect and manipulate objects. If you don’topena package, reflection will fail for non-exported types.uses <service_interface_name>;: Declares that this module uses a service defined by<service_interface_name>. This is part of the Java Service Provider Interface (SPI) mechanism.provides <service_interface_name> with <service_implementation_class>;: Declares that this module provides an implementation of a service defined by<service_interface_name>.
For our initial steps, module, exports, and requires will be our primary focus.
Modular JARs vs. Traditional JARs
When you compile a modular project, the output is still a JAR file. However, this JAR now contains a module-info.class file (the compiled version of module-info.java) at its root. This makes it a modular JAR.
- Modular JARs: Placed on the module path (
--module-pathor-poption forjavacandjava). The JMS reads theirmodule-info.classdescriptors to enforce strong encapsulation and reliable configuration. - Traditional JARs: Placed on the classpath (
-classpathor-cpoption). They don’t havemodule-info.class. When a traditional JAR is placed on the module path, it becomes an automatic module.
Automatic Modules and the Unnamed Module
Automatic Modules: If you place a traditional JAR (one without a
module-info.java) on the module path, the Java Module System treats it as an “automatic module.” Its module name is derived from its JAR file name (e.g.,my-library-1.0.jarbecomesmy.library). An automatic module exports all its packages andrequiresall other modules on the module path. This provides a backward-compatibility bridge, allowing you to gradually modularize your application while still using non-modular libraries.Unnamed Module: Any code that is compiled and run without being explicitly part of a named module (e.g.,
.javafiles directly on the classpath, or traditional JARs on the classpath) belongs to the unnamed module. The unnamed module can “see” all other modules (named or automatic), but no named module can require the unnamed module. This is another compatibility mechanism.
Phew! That was a lot of theory, but now you have the foundational knowledge. Let’s get our hands dirty and build a modular application!
Step-by-Step Implementation: Building Our First Modular Application
We’re going to create a very simple multi-module application. We’ll have two modules:
com.example.greeting: This module will provide a simpleGreeterclass to generate a greeting message.com.example.app: This module will be our main application, which uses thecom.example.greetingmodule.
This structure will clearly demonstrate how exports and requires work.
Project Setup: Creating the Directory Structure
First, let’s set up our project directory. Open your terminal or command prompt.
mkdir java-modules-demo
cd java-modules-demo
mkdir -p src/com.example.greeting src/com.example.app
You should now have a structure like this:
java-modules-demo/
├── src/
│ ├── com.example.app/
│ └── com.example.greeting/
Module 1: com.example.greeting
This module will contain our Greeter class and its module-info.java file.
Step 1: Create the Greeter Class
Inside src/com.example.greeting, create the package directory com/example/greeting and then the Greeter.java file.
mkdir -p src/com.example.greeting/com/example/greeting
# For Linux/macOS
touch src/com.example.greeting/com/example/greeting/Greeter.java
# For Windows
# type nul > src\com.example.greeting\com\example\greeting\Greeter.java
Now, open src/com.example.greeting/com/example/greeting/Greeter.java in your code editor and add the following code:
// src/com.example.greeting/com/example/greeting/Greeter.java
package com.example.greeting;
/**
* A simple class that provides greeting messages.
*/
public class Greeter {
/**
* Generates a personalized greeting message.
* @param name The name of the person to greet.
* @return A greeting string.
*/
public String getGreeting(String name) {
return "Hello, " + name + " from the Java Module System!";
}
// This method is internal and won't be exported
private String getInternalMessage() {
return "This is an internal message.";
}
}
Explanation:
- We define a
package com.example.greeting;which is standard Java. - The
Greeterclass has a public methodgetGreeting(String name)that returns a string. - Notice the
getInternalMessage()method isprivate. This is already encapsulated at the class level. Modules take encapsulation a step further at the package level.
Step 2: Create the module-info.java for com.example.greeting
Now, let’s define our module descriptor. Create the module-info.java file directly inside src/com.example.greeting.
# For Linux/macOS
touch src/com.example.greeting/module-info.java
# For Windows
# type nul > src\com.example.greeting\module-info.java
Open src/com.example.greeting/module-info.java and add:
// src/com.example.greeting/module-info.java
module com.example.greeting {
exports com.example.greeting; // Export the package containing Greeter
}
Explanation:
module com.example.greeting { ... }: This declares our module and gives it the namecom.example.greeting.exports com.example.greeting;: This is crucial! It tells the Java Module System that thecom.example.greetingpackage (which contains ourGreeterclass) is accessible to other modules that requirecom.example.greeting. Any other packages we might add to this module (e.g.,com.example.greeting.internal) would remain hidden unless explicitly exported.
Module 2: com.example.app
This module will be our main application, which requires com.example.greeting and uses its Greeter class.
Step 1: Create the Main Application Class
Inside src/com.example.app, create the package directory com/example/app and then the Main.java file.
mkdir -p src/com.example.app/com/example/app
# For Linux/macOS
touch src/com.example.app/com/example/app/Main.java
# For Windows
# type nul > src\com.example.app\com\example\app\Main.java
Open src/com.example.app/com/example/app/Main.java and add:
// src/com.example.app/com/example/app/Main.java
package com.example.app;
import com.example.greeting.Greeter; // We need to import Greeter
public class Main {
public static void main(String[] args) {
Greeter greeter = new Greeter();
String message = greeter.getGreeting("Modular World");
System.out.println(message);
}
}
Explanation:
- We attempt to
import com.example.greeting.Greeter;. This import will only work ifcom.example.greetingmodule exports thecom.example.greetingpackage AND ourcom.example.appmodule explicitly requirescom.example.greeting. - We then create an instance of
Greeterand use itsgetGreetingmethod.
Step 2: Create the module-info.java for com.example.app
Now, create the module-info.java file directly inside src/com.example.app.
# For Linux/macOS
touch src/com.example.app/module-info.java
# For Windows
# type nul > src\com.example.app\module-info.java
Open src/com.example.app/module-info.java and add:
// src/com.example.app/module-info.java
module com.example.app {
requires com.example.greeting; // This module needs com.example.greeting
}
Explanation:
module com.example.app { ... }: Declares our main application module.requires com.example.greeting;: This is the explicit dependency declaration. It tells the Java Module System thatcom.example.appneedscom.example.greetingto compile and run. Without this, the compiler would complain that it cannot find theGreeterclass, even ifcom.example.greetingexported it!
Compiling and Running Our Modular Application
Now for the exciting part: compiling and running this multi-module project! We’ll use the javac and java commands with their module-specific options.
Step 1: Compile the Modules
We need to compile both modules. The javac command needs to know where to find the source code for all modules and where to place the compiled .class files.
Navigate to the java-modules-demo directory in your terminal.
# Create an 'out' directory for our compiled modules
mkdir out
# Compile both modules
# --module-source-path src: Tells javac where to find module source code (in the 'src' directory)
# -d out: Tells javac to put compiled output into the 'out' directory
# --module com.example.app,com.example.greeting: Specifies which modules to compile
javac --module-source-path src -d out --module com.example.app,com.example.greeting
If everything is correct, you should see no output (which is good!) and a new out directory created with the compiled modules:
java-modules-demo/
├── out/
│ ├── com.example.app/
│ │ ├── com/example/app/Main.class
│ │ └── module-info.class
│ └── com.example.greeting/
│ ├── com/example/greeting/Greeter.class
│ └── module-info.class
└── src/
├── com.example.app/
│ ├── com/example/app/Main.java
│ └── module-info.java
└── com.example.greeting/
├── com/example/greeting/Greeter.java
└── module-info.java
Explanation of the javac command:
--module-source-path src: This tells the compiler that thesrcdirectory contains the root of our module source code. It will look formodule-info.javafiles directly undersrc/<module_name>.-d out: This specifies the output directory for the compiled classes. Each module will get its own subdirectory withinout.--module com.example.app,com.example.greeting: This explicitly lists the modules we want to compile. For a small project like this, it’s fine. For larger projects, you might just specify the root module (com.example.appin this case), and the compiler will find its dependencies automatically.
Step 2: Run the Modular Application
Now, let’s run our com.example.app module.
# Run the application
# --module-path out: Tells the JVM where to find compiled modules (in the 'out' directory)
# --module com.example.app/com.example.app.Main: Specifies the main module and its main class
java --module-path out --module com.example.app/com.example.app.Main
You should see the following output:
Hello, Modular World from the Java Module System!
Explanation of the java command:
--module-path out: This is similar to the classpath, but for modules. It tells the Java Virtual Machine (JVM) where to find the compiled modules (.classfiles andmodule-info.class). The JVM will then use the module descriptors to resolve dependencies and enforce encapsulation.--module com.example.app/com.example.app.Main: This specifies two things:com.example.app: The name of the module that contains our main application entry point.com.example.app.Main: The fully qualified name of the class within that module that contains themainmethod.
Congratulations! You’ve successfully compiled and run your first multi-module Java 25 application. You’ve now experienced the benefits of strong encapsulation and reliable configuration firsthand.
Mini-Challenge: Adding a Farewell Module
Let’s put your new knowledge to the test!
Challenge:
Add a new module called com.example.farewell.
- This module should contain a
FarewellGreeterclass with a public methodgetFarewell(String name)that returns a farewell message (e.g., “Goodbye, [name]! See you later.”). - The
com.example.farewellmodule must export its packagecom.example.farewell. - Modify the
com.example.appmodule to also requirecom.example.farewelland use theFarewellGreeterclass to print a farewell message after the greeting.
Hint:
- Follow the same steps as you did for
com.example.greeting. - Remember to update the
module-info.javaforcom.example.appto reflect the newrequiresdependency. - You’ll need to recompile all relevant modules after making changes.
What to observe/learn:
- How adding a new module impacts existing modules.
- Reinforce your understanding of
exportsandrequires. - The modular system will catch any missing
requiresstatements during compilation.
Take your time, try to solve it independently, and remember, trial and error is part of the learning process!
Click for Solution (if you get stuck!)
Solution Steps:
Create
com.example.farewellmodule directory:mkdir -p src/com.example.farewell/com/example/farewellCreate
FarewellGreeter.java:src/com.example.farewell/com/example/farewell/FarewellGreeter.javapackage com.example.farewell; public class FarewellGreeter { public String getFarewell(String name) { return "Goodbye, " + name + "! See you later."; } }Create
module-info.javaforcom.example.farewell:src/com.example.farewell/module-info.javamodule com.example.farewell { exports com.example.farewell; }Update
Main.javaincom.example.app:src/com.example.app/com/example/app/Main.javapackage com.example.app; import com.example.greeting.Greeter; import com.example.farewell.FarewellGreeter; // New import public class Main { public static void main(String[] args) { Greeter greeter = new Greeter(); String greetingMessage = greeter.getGreeting("Modular World"); System.out.println(greetingMessage); FarewellGreeter farewellGreeter = new FarewellGreeter(); // New usage String farewellMessage = farewellGreeter.getFarewell("Modular World"); System.out.println(farewellMessage); } }Update
module-info.javaforcom.example.app:src/com.example.app/module-info.javamodule com.example.app { requires com.example.greeting; requires com.example.farewell; // New requirement }Recompile all modules:
# Make sure you are in the 'java-modules-demo' directory javac --module-source-path src -d out --module com.example.app,com.example.greeting,com.example.farewellRun the application:
java --module-path out --module com.example.app/com.example.app.Main
Expected Output:
Hello, Modular World from the Java Module System!
Goodbye, Modular World! See you later.
Common Pitfalls & Troubleshooting
Working with modules introduces a few new types of errors. Here are some common ones and how to resolve them:
“Package is not visible” or “Package not found” errors:
- Symptom: Your code tries to
importa class from another module, but the compiler complains it can’t find the package. - Cause:
- The module providing the package hasn’t explicitly
exportedthat package in itsmodule-info.java. - The module requiring the package hasn’t explicitly
requiredthe providing module in itsmodule-info.java. - You’re trying to access an internal package that was never meant to be public.
- The module providing the package hasn’t explicitly
- Solution: Double-check both
module-info.javafiles. Ensure the providing moduleexportsthe package and the consuming modulerequiresthe providing module.
- Symptom: Your code tries to
“Module not found” errors during compilation or runtime:
- Symptom:
javacorjavareports that it cannot find a module, even though you know it exists. - Cause:
- The module isn’t on the module path.
- A typo in the module name in your
requiresstatement or the command line. - The module’s directory structure or
module-info.javais incorrect (e.g.,module-info.javais not at the root of the module source).
- Solution: Verify your
--module-source-path(for compilation) and--module-path(for runtime) settings. Ensure the module name inmodule-info.javaexactly matches the name used inrequiresstatements and command-line arguments. Check your directory structure.
- Symptom:
Reflection Issues (
InaccessibleObjectException):- Symptom: Your application uses reflection (e.g.,
Class.forName().getDeclaredMethod()) to access fields or methods of a class in another module, and you get anInaccessibleObjectExceptionor similar error, even if the package is exported. - Cause: While
exportsallows direct access to public types, it does not automatically grant reflective access to non-public members, or even public members if the reflective access is considered “deep.” For frameworks that heavily rely on reflection to modify private fields or call non-public methods,exportsisn’t enough. - Solution: The module whose internals are being reflectively accessed needs to explicitly
opens <package_name>;in itsmodule-info.java. If you only want to open it to specific modules, you can useopens <package_name> to <target_module_name>;.
- Symptom: Your application uses reflection (e.g.,
Circular Dependencies:
- Symptom: The compiler reports a circular dependency between modules (e.g., Module A requires Module B, and Module B requires Module A).
- Cause: This indicates a design flaw where two modules are too tightly coupled. The module system prevents this to ensure a clear, directed acyclic graph of dependencies.
- Solution: Refactor your code. Extract the common functionality or the parts causing the circular dependency into a third, independent module that both original modules can then require. This often leads to better, more cohesive module design.
Remember to leverage the detailed error messages provided by the Java compiler and runtime. They are usually quite descriptive about what went wrong in the module system. The official Oracle JDK 25 documentation on the module system is an excellent resource for deeper dives and specific error codes: JDK 25 Documentation - Project Jigsaw (Navigate to “Guides” -> “Module System”).
Summary: Your Modular Journey Begins
Phew, that was a comprehensive dive into Java Modules! You’ve taken a significant step towards understanding how to build robust, scalable, and maintainable Java applications.
Here are the key takeaways from this chapter:
- Java Modules (Project Jigsaw) address challenges like “classpath hell” and weak encapsulation in large Java applications.
- A module is a named, self-describing collection of code and data, defined by its
module-info.javadescriptor. - The
module-info.javafile uses keywords likemodule,exports,requires, andopensto declare a module’s identity, public API, dependencies, and reflective access permissions. exportsmakes packages visible to other modules, enforcing strong encapsulation by default.requiresdeclares a module’s explicit dependencies, ensuring reliable configuration.requires transitiveallows a module’s dependencies to be implicitly required by modules that depend on it.opensis used to grant runtime reflective access to packages, essential for many frameworks.- You learned how to compile and run multi-module applications using
javac --module-source-path -d out --module ...andjava --module-path out --module .... - You tackled a mini-challenge, reinforcing your understanding of module declarations and dependencies.
- You’re now aware of common pitfalls like “package not visible” errors and reflection issues, and how to troubleshoot them.
By adopting the Java Module System, you’re embracing modern Java best practices for structuring complex applications. This not only improves maintainability and reliability but also opens doors for more advanced deployment scenarios like custom runtime images.
What’s next? In the upcoming chapters, we’ll continue building on this foundation. We might explore more advanced module features like services, or we’ll transition to how popular frameworks like Spring Boot leverage modularity (and sometimes automatic modules) to manage dependencies effectively. Keep up the great work!