Creating & Using Modules
Creating & Using Modules
The previous lesson introduced the why behind the Java Platform Module System (JPMS). This lesson is hands-on: you will create modular JARs, write and read module-info.java declarations, and understand the critical difference between the module path and the legacy classpath. These mechanics are what you actually deal with daily in a JPMS-based project.
Anatomy of module-info.java
Every module is defined by a single file at the root of the source tree: module-info.java. It declares the module's name, what it exposes, and what it depends on. Here is a realistic example for a library module:
Key directives and their meanings:
exports— makes a package'spublictypes accessible to code in other modules. Packages that are not exported are completely hidden at compile time and runtime, even if their types arepublic.requires— declares a compile-time and runtime dependency on another module.requires static— optional at runtime; used for annotation processors or APIs that are only needed during compilation.requires transitive— implies the dependency for any module that depends on yours. If your API returns types fromcom.example.currency, callers need those types too, so you re-export them.opens … to— grants qualified deep reflection access (bypassing encapsulation) to a specific module. Use this for frameworks that rely on reflection at runtime.
Project Layout for a Multi-Module Build
A typical multi-module project groups each module in its own directory, each containing its module-info.java:
Each module compiles to its own modular JAR. The module name (in module-info.java) and the JAR filename are independent — but by convention they match, and Maven/Gradle automatically produce the correct artifacts.
Compiling and Packaging a Modular JAR by Hand
Understanding the manual process helps you debug build-tool problems. Assume you are compiling com.example.logging first (no dependencies), then com.example.payments which depends on it:
module-info.java together with the module's source files in a single javac invocation. Compiling it separately or in the wrong order causes confusing "module not found" errors at compile time.
The Module Path vs. the Classpath — the Critical Distinction
This is the most important concept to nail. Both the module path and the classpath are ways to tell the JVM where to find code, but they behave fundamentally differently:
- Classpath (
-classpath/-cp): a flat list of JARs and directories. All code on the classpath belongs to the unnamed module. There is no encapsulation: every public type in every JAR is accessible from everywhere else. This is how Java has worked since 1995. - Module path (
--module-path/-p): a list of directories or JARs where the JVM looks for named modules. Each JAR that contains amodule-info.classis treated as a named module with its own encapsulation rules. JARs withoutmodule-info.classbecome automatic modules (see below).
The --module flag specifies the root module and its main class as moduleName/fullyQualifiedClassName. The JVM resolves the module graph from there, loading only what is required.
Automatic Modules — Bridging Old JARs
Legacy JARs (no module-info.class) placed on the module path become automatic modules. The JVM derives their module name from the JAR filename (hyphens become dots, version suffixes are stripped — e.g. jackson-databind-2.17.0.jar → jackson.databind). Automatic modules:
- Export all their packages to all other modules.
- Can read the unnamed module (i.e., code still on the classpath).
- Can be declared in
requiresclauses of named modules.
Many libraries publish an explicit Automatic-Module-Name entry in their MANIFEST.MF to guarantee a stable module name before they add full module support. Always prefer that name over the derived one when it exists.
Running with Maven (Practical)
When using Maven, the compiler plugin automatically detects module-info.java and switches to module-aware compilation. You only need to ensure the Java release is 9 or higher:
For multi-module Maven projects, each module is a separate Maven sub-module. Maven passes the compiled siblings on the --module-path automatically during compilation. At runtime (e.g., via exec-maven-plugin), pass the main module explicitly:
Qualified Exports and Targeted Opens
Sometimes you want a package visible only to specific trusted modules — for example, a testing helper that should only be accessible from your test module:
This is qualified export / qualified open. It gives you fine-grained control: internal helpers remain invisible to third-party code while still being accessible where legitimately needed.
Service Loader Integration
JPMS has first-class support for the ServiceLoader pattern, cleanly replacing manual classpath scanning:
At runtime, ServiceLoader.load(PaymentGateway.class) discovers all providers in the module graph without the consumer knowing the implementation class name. This is how the JDK itself wires its own extensible services (e.g., java.sql.Driver).
Summary
A module is defined by module-info.java, which declares exports, requires, opens, and provides/uses directives. The module path enforces encapsulation; the classpath does not — understanding which mechanism is active for each JAR is the single most important debugging skill in a JPMS project. Legacy JARs become automatic modules when placed on the module path. Maven handles module-aware compilation transparently once a module-info.java is present. Qualified exports and opens let you expose implementation details only to trusted consumers.