38. Compiling, Packaging, and Running Modules
Table of Contents
- 38.1 The Module Path vs the Classpath
- 38.2 Module-Related Command-Line Options
- 38.3 Compiling a Single Module
- 38.4 Compiling Multiple Interdependent Modules
- 38.5 Packaging a Module into a Modular JAR
- 38.6 Running a Modular Application
- 38.7 Module Directives Explained
- 38.8 Named, Automatic, and Unnamed Modules
- 38.9 Top-Down and Bottom-Up Approaches to Modularizing an Application
- 38.10 Inspecting Modules and Dependencies
- 38.11 Creating Custom Runtime Images with jlink
- 38.12 Creating Self-Contained Applications with jpackage
- 38.13 Final Summary JPMS in Practice
Once a module is defined with a module-info.java file, it must be compiled, packaged, and executed using module-aware tools.
This section explains how the Java toolchain changes when modules are involved.
38.1 The Module Path vs the Classpath
JPMS introduces a new concept: the module path.
It exists alongside the traditional classpath, but the two behave very differently.
| Aspect | Classpath | Module path |
|---|---|---|
| Structure | Flat list of JARs | Modules with identities |
| Encapsulation | None | Strong |
| Dependency checking | None | Strict |
| Split packages | Allowed | Forbidden (named modules) |
| Resolution order | Order-dependent | Deterministic |
Note
- A JAR placed on the
module pathis treated as amodule:- If it contains a
module-info.class, it becomes anamed module. - If it does not contain a module descriptor, it becomes an
automatic module.
- If it contains a
- A JAR placed on the classpath is not treated as a module.
- Instead, it becomes part of the unnamed module, together with all other classpath entries.
- A modular JAR (i.e., a JAR containing module-info.class) can still be used as a regular JAR.
- If it is placed on the classpath instead of the module path, it is treated as part of the unnamed module, allowing non-modular applications to use it without adopting the module system.
- Split packages:
- Are allowed on the classpath (multiple JARs may contain classes in the same package).
- Are forbidden for named or automatic modules on the module path.
38.2 Module-Related Command-Line Options
When working with the Java Module System, both java and javac provide specific options for compiling and running modular applications.
Some options are shared, while others are specific to one tool.
38.2.1 Options Available in Both java and javac
These options can be used during compilation as well as execution:
-
--moduleor-m
Used to compile or run only the specified module. -
--module-pathor-p
Specifies the paths wherejavaorjavacwill look for module definitions.
The Java Module System provides three special command-line options, usable with both javac and java, that allow you to override module access rules at runtime or compilation time without modifying the module-info.java files.
These options affect only the current command execution and do not permanently change the module descriptors.
The three options are:
--add-reads--add-exports--add-opens
They are typically used for testing, backward compatibility, migration scenarios, or when working with third-party modules that cannot be modified.
For example, suppose moduleA needs to access public types from moduleB, but:
moduleAdoes not declarerequires moduleB;moduleBdoes not export the required package tomoduleA
Instead of editing the module-info.java files, you can temporarily grant the necessary access using:
javac --add-reads moduleA=moduleB \
--add-exports moduleB/com.modB.package1=moduleA \
...
java --add-reads moduleA=moduleB \
--add-exports moduleB/com.modB.package1=moduleA \
...
Here is what each option means:
-
--add-reads moduleA=moduleB
Temporarily declares thatmoduleAreadsmoduleB.
This is equivalent to addingrequires moduleB;insidemoduleA’s descriptor.
It allowsmoduleAto access the exported packages ofmoduleB. -
--add-exports moduleB/com.modB.package1=moduleA
Temporarily exports the packagecom.modB.package1frommoduleBtomoduleA.
This is equivalent to adding:
exports com.modB.package1 to moduleA;
insidemoduleB’s descriptor.
Important distinction:
--add-readsestablishes module-level readability.--add-exportsgrants access to specific packages.--add-opens(not shown above) is similar to--add-exportsbut additionally allows deep reflection access, typically required by frameworks.
These options do not modify the compiled module metadata; they only adjust the module graph for that particular invocation of javac or java.
38.2.2 Options Applicable Only to javac
These options apply only at compile time:
-
--module-source-path
(no shortcut)
Used byjavacto locate source module definitions. -
-d
Specifies the output directory where the.classfiles will be generated after compilation.
38.2.3 Options Applicable Only to java
These options apply only at runtime:
-
--list-modules
(no shortcut)
Lists all observable modules and then exits. -
--show-module-resolution
(no shortcut)
Displays module resolution details during application startup. -
--describe-moduleor-d
Describes a specified module and then exits.
38.2.4 Important Distinctions
The option -d has different meanings depending on the tool:
- In
javac,-ddefines the output directory for compiled class files. - In
java,-dis a shortcut for--describe-module.
Additionally, -d must not be confused with -D (uppercase D).
-Dis used when running a Java program to define system properties as name-value pairs on the command line.
java -Dconfig.file=app.properties com.example.Main
In this example, -Dconfig.file=app.properties sets a system property that can be accessed at runtime using System.getProperty("config.file").
38.3 Compiling a Single Module
To compile a module, you must specify the module source path and the destination directory.
javac -d out \
src/com.example.hello/module-info.java \
src/com.example.hello/com/example/hello/Main.java
A more scalable approach uses --module-source-path.
javac --module-source-path src \
-d out \
$(find src -name "*.java")
Note
--module-source-path tells javac where to find multiple modules at once.
38.4 Compiling Multiple Interdependent Modules
When modules depend on each other, their dependencies must be resolvable at compile time.
--module-path mods (sample dir containing interdependent moules) should contain already-compiled modular JARs or compiled module directories (each with its own module-info.class).
javac -d out \
--module-source-path src \
--module-path mods \
$(find src -name "*.java")
Here:
--module-source-pathlocates module source trees--module-pathprovides already-compiled modules
38.5 Packaging a Module into a Modular JAR
After compilation, modules are typically packaged as JAR files.
A modular JAR contains a module-info.class at its root.
If module-info.class is present, the JAR becomes a named module automatically and its name is taken from the descriptor (not the filename).
jar --create \
--file mods/com.example.hello.jar \
--main-class com.example.hello.Main \
-C out/com.example.hello .
Note
A JAR with module-info.class is a named module, not an automatic module.
When a JAR contains a module-info.class, its module name is taken from that file and not inferred from the filename.
38.6 Running a Modular Application
To run a modular application, you use the module path and specify the module name.
java --module-path mods \
--module com.example.hello/com.example.hello.Main
You can shorten this using the -p and -m options.
java -p mods -m com.example.hello/com.example.hello.Main
Note
When using named modules, the classpath is ignored for resolution of module dependencies.
38.7 Module Directives Explained
The module-info.java file contains directives that describe dependencies, visibility, and services.
Each directive has a precise meaning.
38.7.1 requires
The requires directive declares a dependency on another module.
Without it, types from the dependency module cannot be used.
module com.example.app {
requires com.example.lib;
}
Effects of requires:
- Dependency must be present at compile and runtime
- Exported packages of the required module become accessible
38.7.2 requires transitive
requires transitive exposes a dependency to downstream modules.
It propagates readability.
module com.example.lib {
requires transitive com.example.util;
exports com.example.lib.api;
}
Meaning:
- Any module requiring com.example.lib automatically reads com.example.util
- Callers do not need to declare requires com.example.util explicitly
Note
This is similar to “public dependencies” in other module systems.
Readable ≠ exported: a transitive requirement does not export your packages automatically.
38.7.3 exports
exports makes a package accessible to other modules.
Only exported packages are visible outside the module.
module com.example.lib {
exports com.example.lib.api;
}
Non-exported packages remain strongly encapsulated.
38.7.4 exports ... to (Qualified Exports)
A qualified export restricts access to specific modules.
module com.example.lib {
exports com.example.internal to com.example.friend;
}
Only the listed modules can access the exported package.
38.7.5 opens
opens allows deep reflective access to a package.
This is primarily for frameworks using reflection.
module com.example.app {
opens com.example.app.model;
}
Note
opens does NOT make a package accessible at compile time. It only affects runtime reflection.
38.7.6 opens ... to (Qualified Opens)
You can restrict reflective access to specific modules.
module com.example.app {
opens com.example.app.model to com.fasterxml.jackson.databind;
}
Note
opens affects reflection; exports affects compilation and type visibility.
38.7.7 Table of Core Directives
| Directive | Purpose |
|---|---|
requires |
Declare a dependency |
requires transitive |
Propagate dependency |
exports |
Expose a package |
exports ... to |
Expose to specific modules |
opens |
Allow runtime reflection |
opens ... to |
Restrict reflective access |
38.7.8 Exports vs Opens — Compile-Time vs Runtime Access
| Visibility | Compile-time? | Runtime reflection? |
|---|---|---|
exports |
Yes | No |
opens |
No | Yes |
exports ... to |
Yes (limited modules) | No |
opens ... to |
No | Yes (limited modules) |
Important
JPMS adds a module path, but the classpath still exists. They can coexist, but named modules take precedence.
38.8 Named, Automatic, and Unnamed Modules
JPMS supports different kinds of modules to allow gradual migration from the classpath.
JPMS must interoperate with legacy code.
To support gradual adoption, the JVM recognizes three different module categories.
38.8.1 Named Modules
A named module has a module-info.class and a stable identity.
- Strong encapsulation
- Explicit dependencies
- Full JPMS support
38.8.2 Automatic Modules
A JAR without module-info placed on the module path becomes an automatic module.
Its name is derived from the JAR file name.
- Reads all other modules
- Exports all packages
- No strong encapsulation
Note
Automatic modules exist to ease migration. They are not suitable as a long-term design.
38.8.3 Unnamed Module
Code on the classpath belongs to the unnamed module.
- Reads all named modules
- All packages are open
- Cannot be required by named modules
Note
The unnamed module preserves legacy classpath behavior.
38.8.4 Comparison Summary
| Module type | module-info present? | Encapsulation | Reads |
|---|---|---|---|
Named |
Yes | Strong | Declared only |
Automatic |
No | Weak | All modules |
Unnamed |
No | None | All modules |
38.9 Top-Down and Bottom-Up Approaches to Modularizing an Application
When migrating an existing (non-modular) application to the Java Platform Module System (JPMS), two principal strategies can be adopted: top-down and bottom-up. Both approaches require a clear understanding of how named modules, automatic modules, and the unnamed module interact.
38.9.1 Top-Down Approach
In a top-down approach, you begin by modularizing the main application module and then progressively migrate its dependencies.
38.9.1.1 Fundamental Rules
- A JAR placed on the module path becomes an automatic module.
- Its name is determined either:
- From the
Automatic-Module-Nameentry in its manifest, or - Derived from the JAR filename (hyphens are replaced with dots and version numbers are ignored).
Example:
mysql-connector-java-8.0.11.jar→mysql.connector.java
- From the
-
An
automatic module:- Exports all its packages.
- Reads all other modules.
-
A JAR placed on the classpath belongs to the unnamed module.
- The
unnamed module:- Exports all its packages.
- Can read all other modules.
-
However, it has no name, so no other module can declare
requireson it. -
Explicitly named modules (those with a
module-info.java) - Can declare dependencies using:
requires some.module; - Can depend on:
- Other named modules
- Automatic modules
- Cannot depend on the unnamed module (since it has no name).
Important consequence:
A
named modulecan read anautomatic module, but it cannot read theunnamed module.
38.9.1.2 Practical Implications
Suppose:
- Application JAR =
A Adirectly depends onBBdepends onC
If you modularize A first:
Amust declarerequires B;- Therefore,
Bmust be on the module path (named or automatic) - If
Bbecomes a named module, then: Cmust also move to the module path (as named or automatic)
Thus, in a top-down migration:
- You start from the application layer.
- You progressively modularize dependencies outward.
- Automatic modules are often used temporarily during transition.
38.9.1.3 Access Rules Summary
| Module Type | Exports | Can Read |
|---|---|---|
Named module |
Only declared exports | Only required modules |
Automatic module |
All packages | All modules |
Unnamed module |
All packages | All modules |
Important
Automatic and unnamed modulesare permissive.Named modulesenforce explicit dependency and export rules.
38.9.2 Bottom-Up Approach
In a bottom-up approach, you begin by modularizing the lowest-level libraries first, and then progressively move upward toward higher-level modules and finally the main application.
38.9.2.1 Core Strategy
You first convert foundational libraries into proper named modules with explicit module-info.java descriptors.
Then:
- Modules that depend on them are modularized.
- Finally, the main application becomes a named module.
This approach emphasizes:
- Explicit
requiresrelationships - Controlled
exports - Strong encapsulation from the beginning
38.9.2.2 Architectural Advantages
Compared to automatic modules:
- Named modules export only what is explicitly declared.
- They do not implicitly read every other module.
- Encapsulation boundaries are clearly defined.
Bottom-up modularization generally results in:
- A cleaner dependency graph
- Better maintainability
- Stronger module boundaries
38.9.3 Conceptual Comparison
Top-Down
- Start from the main application.
- Modularize dependencies as required.
- Often rely temporarily on automatic modules.
- Faster initial migration.
Bottom-Up
- Start from core libraries.
- Define strict module descriptors early.
- Progressively move upward.
- Produces a more disciplined and robust modular architecture.
38.9.4 Migration Insight
In practice, real-world projects often combine both strategies:
- A top-down migration is used to enable modular execution quickly.
- A bottom-up refinement phase replaces automatic modules with properly defined named modules.
This hybrid approach allows incremental adoption of JPMS while gradually strengthening encapsulation and architectural clarity.
38.10 Inspecting Modules and Dependencies
38.10.1 Describing Modules with java
java --describe-module java.sql
This shows exports, requires, and services of a module.
38.10.2 Describing Modular JARs
jar --describe-module --file mylib.jar
38.10.3 Analyzing Dependencies with jdeps
jdeps analyzes class and module dependencies statically.
jdeps myapp.jar
jdeps --module-path mods --check my.module
To detect use of JDK internal APIs:
jdeps --jdk-internals myapp.jar
38.11 Creating Custom Runtime Images with jlink
jlink builds a minimal Java runtime containing only the modules required by an application.
jlink
--module-path $JAVA_HOME/jmods:mods
--add-modules com.example.app
--output runtime-image
Benefits:
- smaller runtime
- faster startup
- no unused JDK modules
38.12 Creating Self-Contained Applications with jpackage
jpackage builds platform-specific installers or application images.
jpackage
--name MyApp
--input mods
--main-module com.example.app/com.example.Main
jpackage can produce:
- .exe / .msi (Windows)
- .pkg / .dmg (macOS)
- .deb / .rpm (Linux)
38.13 Final Summary JPMS in Practice
JPMSintroducesstrong encapsulationand reliable dependenciesModulesreplace fragile classpath conventionsServicesenable decoupled architecturesAutomaticandunnamed modulessupport migrationjlinkandjpackageenable modern deployment models