Java Module 17 – Modules, packaging and distribution
The Java Platform Module System (JPMS)
Write module-info.java with requires, exports and opens, compile and run modules on the module path, and read the errors strong encapsulation gives.
What you will learn
- Write module-info.java with requires, exports and opens
- Compile and run a modular application on the module path
- Explain strong encapsulation, opens and --add-opens
Before you start
What you’ll learn · Before you start
What you will learn
- Write module-info.java with requires, exports and opens
- Compile and run a modular application on the module path
- Explain strong encapsulation, opens and --add-opens
Before you start
On this page
A module is a named group of packages with a declaration, module-info.java, that says which other modules it
needs (requires) and which of its packages other code may use (exports). The Java Platform Module System (JPMS),
part of Java since Java 9, checks those declarations when code compiles and again when the JVM starts. A package that
a module does not export is invisible to every other module, even its public classes: this is strong
encapsulation, and reflection into such a package needs an opens directive. You compile modules with
javac --module-source-path and run them from the module path with
java --module-path mods --module app/com.example.Main. Use modules for libraries whose internals must stay hidden,
and to build small runtime images with jlink; plain JARs on the class path keep working beside them.
This lesson splits the shop’s billing program from the JAR lesson into two modules, runs them on Eclipse Temurin 25.0.4.1, and shows the two compile errors that the module system adds.
A module declaration
The program becomes two modules, each in its own folder with its declaration at the top of its sources. kirana.bill,
in bill/src, holds the bill and a formatter in an internal package; kirana.app, in app/src, holds the program
and declares that it needs kirana.bill:
bill/src/module-info.java · bill/src/module-info.java
/** The bill model. Its API is the package com.example.kirana.bill; the internal package stays inside the module. */
module kirana.bill {
exports com.example.kirana.bill;
} app/src/module-info.java · app/src/module-info.java
/** The shop's program. It reads kirana.bill, and so can use the packages kirana.bill exports. */
module kirana.app {
requires kirana.bill;
} bill/src/com/example/kirana/bill/Bill.java
package com.example.kirana.bill;
import com.example.kirana.bill.internal.Paise;
import java.util.ArrayList;
import java.util.List;
/** A shop's bill: lines of goods, each with a quantity and a price in paise. */
public final class Bill {
public record Line(String item, int quantity, long paise) {}
private final String shop;
private final List<Line> lines = new ArrayList<>();
public Bill(String shop) {
this.shop = shop;
}
public Bill add(String item, int quantity, long paise) {
lines.add(new Line(item, quantity, paise));
return this;
}
public List<Line> lines() {
return List.copyOf(lines);
}
public long totalPaise() {
return lines.stream().mapToLong(l -> l.quantity() * l.paise()).sum();
}
/** The total in rupees with Indian digit grouping, such as ₹1,01,340.00. */
public String total() {
return shop + ": " + Paise.asRupees(totalPaise());
}
} app/src/com/example/kirana/app/Main.java
package com.example.kirana.app;
import com.example.kirana.bill.Bill;
public class Main {
public static void main(String[] args) {
Bill bill = new Bill("Sharma Kirana Store")
.add("Basmati rice, 25 kg", 12, 2_450_00)
.add("Toor dal, 30 kg", 4, 4_875_00)
.add("Sugar, 50 kg", 10, 2_250_00);
System.out.println(bill.lines().size() + " lines, " + bill.total());
System.out.println("Main is in module " + Main.class.getModule().getName()
+ ", Bill in module " + Bill.class.getModule().getName());
}
} A declaration holds directives, and these are all of them:
| Directive | What it does |
|---|---|
requires M; |
Reads module M, so this module’s code may use the packages M exports |
requires transitive M; |
Also makes every module that reads this one read M |
requires static M; |
Needs M to compile; at run time M may be absent |
exports P; |
Lets every module that reads this one use the public types of package P |
exports P to M; |
The same, for the named friend modules only |
opens P; |
Allows reflection on everything in P at run time, private members included |
opens P to M; |
The same, for the named modules only |
open module |
Opens every package of the module for reflection |
uses S; and provides S with C; |
Look up implementations of a service, and offer one (the next lesson) |
Compile and run two modules
Readability and exports between the lesson's two modules
Text description of the diagram
The diagram shows the two modules of the lesson and the platform module they both read.
- kirana.app, at the top, holds the package com.example.kirana.app. Its declaration says requires kirana.bill, so it reads that module.
- kirana.bill holds two packages. com.example.kirana.bill, with the class Bill, is exported. com.example.kirana.bill.internal, with the formatter Paise, is not exported; Bill uses it from inside the module.
- A solid arrow from kirana.app to the exported package is labelled "import Bill: allowed". A dashed arrow from kirana.app to the internal package is labelled "import Paise: compile error", because a package that is not exported is not visible outside its module, even though Paise is a public class.
- kirana.bill, like every module, reads java.base, which every module depends on without saying so.
from session import run
# How many modules the JDK itself has; then two modules compiled with one javac command, each from its own folder,
# run from the output folder, packed as modular JARs, described, and run again from the JARs.
run(
"""
java --list-modules | wc -l
javac -d out --module-source-path kirana.bill=bill/src --module-source-path kirana.app=app/src --module kirana.app
find out -name "*.class" | sort
java --module-path out --module kirana.app/com.example.kirana.app.Main
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
jar --describe-module --file mods/kirana.bill.jar
jar --describe-module --file mods/kirana.app.jar
java --module-path mods --module kirana.app
"""
) Output
# openjdk version "25.0.4.1" LTS
$ java --list-modules | wc -l
69
$ javac -d out --module-source-path kirana.bill=bill/src --module-source-path kirana.app=app/src --module kirana.app
$ find out -name "*.class" | sort
out/kirana.app/com/example/kirana/app/Main.class
out/kirana.app/module-info.class
out/kirana.bill/com/example/kirana/bill/Bill.class
out/kirana.bill/com/example/kirana/bill/Bill$Line.class
out/kirana.bill/com/example/kirana/bill/internal/Paise.class
out/kirana.bill/module-info.class
$ java --module-path out --module kirana.app/com.example.kirana.app.Main
3 lines, Sharma Kirana Store: ₹71,400.00
Main is in module kirana.app, Bill in module kirana.bill
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ jar --create --file mods/kirana.app.jar --main-class com.example.kirana.app.Main -C out/kirana.app .
$ jar --describe-module --file mods/kirana.bill.jar
kirana.bill jar:mods/kirana.bill.jar!/module-info.class
exports com.example.kirana.bill
requires java.base mandated
contains com.example.kirana.bill.internal
$ jar --describe-module --file mods/kirana.app.jar
kirana.app jar:mods/kirana.app.jar!/module-info.class
requires java.base mandated
requires kirana.bill
contains com.example.kirana.app
main-class com.example.kirana.app.Main
$ java --module-path mods --module kirana.app
3 lines, Sharma Kirana Store: ₹71,400.00
Main is in module kirana.app, Bill in module kirana.bill
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 modules_session.py
- The JDK is modules too. This JDK 25 lists 69 modules, from
java.baseto thejdk.*tools. --module-source-path kirana.bill=bill/srcsays where a module’s sources are. javac’s documentation calls this module-specific form the simpler one when only a few modules are involved.--module kirana.appcompiled that module and the module it requires, each into its own folder belowout, named after the module.--module kirana.app/com.example.kirana.app.Mainnames a module and its main class. At run time each class reports its own module, soMainandBillreally ran as two modules.- A modular JAR is a JAR with
module-info.classat its root.jar --describe-modulereads it back:exportslists the API,containslists the packages kept inside, andrequires java.base mandatedis the dependence every module has without writing it. With--main-classrecorded,--module kirana.appwas enough to start the program.
What strong encapsulation stops
Two copies of the program are compiled against the packed kirana.bill: one imports the formatter from the internal
package, and one has a declaration without requires:
from session import run
# kirana.bill is compiled on its own and packed as a modular JAR; then two versions of the program are compiled
# against it: one that uses the internal package, and one whose declaration leaves out the requires directive.
run(
"""
javac -d out/kirana.bill $(find bill/src -name "*.java")
jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")
javac -d noread-out --module-path mods $(find noread/src -name "*.java")
""",
fails=(
'javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")',
'javac -d noread-out --module-path mods $(find noread/src -name "*.java")',
),
) Output
# openjdk version "25.0.4.1" LTS
$ javac -d out/kirana.bill $(find bill/src -name "*.java")
$ jar --create --file mods/kirana.bill.jar -C out/kirana.bill .
$ javac -d leaky-out --module-path mods $(find leaky/src -name "*.java")
leaky/src/com/example/kirana/app/Main.java:4: error: package com.example.kirana.bill.internal is not visible
import com.example.kirana.bill.internal.Paise;
^
(package com.example.kirana.bill.internal is declared in module kirana.bill, which does not export it)
1 error
[exit status 1]
$ javac -d noread-out --module-path mods $(find noread/src -name "*.java")
noread/src/com/example/kirana/app/Main.java:3: error: package com.example.kirana.bill is not visible
import com.example.kirana.bill.Bill;
^
(package com.example.kirana.bill is declared in module kirana.bill, but module kirana.app does not read it)
1 error
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 encapsulation_session.py
The two errors name two different rules. Exports: Paise is a public class, but its package is “declared in
module kirana.bill, which does not export it”, so outside that module the class might as well not exist. Across
modules, public means public to the modules that may see the package. Readability: Bill is in an exported
package, but “module kirana.app does not read it”, because nothing requires kirana.bill. The JVM checks exports
again at run time, for code that javac never saw.
Reflection follows its own rule. exports allows reflection on public members only, and code that reads private
fields, as many mapping and serialisation libraries do, needs the package opened to it with opens. The JDK’s own
internal packages have been closed to such reflection since Java 17 (JEP 403). The --add-opens option opens one
package for one run, and --add-exports does the same for compile-time and run-time access to public types.
Code on the class path lives in the unnamed module, which reads every module. A plain JAR on the module path
becomes an automatic module, which exports and opens all its packages.
Check yourself
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Reflection across modules: opens, open modules and --add-opens, failures reproduced Pro: opens with a Pro, Premium or Ultimate pass
Libraries that turn objects into JSON, table rows or forms read private fields by reflection, and an exported package does not let them. This part reproduces that refusal, compares the four ways to allow it, and does the same for the JDK’s own internals.
In this part:
- A reflective library refused, and two ways to let it in
- Exports, opens and open modules compared
- JDK internals since Java 17
4 runnable examples (Inspector.java, Java, Python, Report.java, report/src/module-info.java) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Migrating class-path code: automatic modules, the unnamed module and split packages Pro: opens with a Pro, Premium or Ultimate pass
Most programs move to modules one JAR at a time, while some libraries still have no module declaration. This part reproduces the problems that a migration meets and the rule behind each one.
In this part:
- A plain JAR on the module path: names and warnings, reproduced
- The class path is invisible to named modules, and a package lives in one module
- Three kinds of modules compared
3 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Exercise: split a three-package app into two modules with minimal exports Pro: opens with a Pro, Premium or Ultimate pass
A minimal module exports the packages its users need and nothing more. This exercise asks for exactly that, with checks you can run yourself, and a rubric for the parts a command cannot check.
In this part:
- The khata app and the task
- Checks you can run
- Rubric: what a minimal, correct split shows
- When another module really needs the internal package
2 runnable examples (Khata.java, Main.java, MemoryStore.java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
-
Fresher to mid · Warm-up
What is the difference between exports and opens in a module declaration?
exports P makes the public types of package P, with their public and protected members, usable by other modules at compile time and at run time, reflection on those members included. opens P gives no compile-time access at all; at run time it allows reflection on every type and member of P, private ones included, which is what setAccessible(true) needs. A package can be both exported and opened. Both have qualified forms, exports P to M and opens P to M, that limit access to the named modules, and an open module opens every package at once. In practice you export your API and open only the packages a framework reads by reflection, ideally to that framework's module alone.
-
Mid to senior
After your application moves to the module path, a JSON library fails with InaccessibleObjectException while it serialises your Order class. Explain what changed, and give the fix you would ship and the fix you would avoid.
-
Mid to senior
You move an application to the module path and it no longer starts: two library JARs contain the same package. How do you find out which JARs, and how do you fix it without changing either library's source?
-
Senior to staff
Plan the move of a server application with 40 JAR files from the class path to modules. Where do you start, what do you measure first, and how does the application stay shippable after every step?
Key takeaways
module-info.javanames a module, the modules itrequiresand the packages itexports.- Compile modules with
javac --module-source-pathand run them withjava --module-path … --module m/main.Class. - A public class in a package that is not exported cannot be used from another module.
- Code must read a module, through
requires, before it can use that module’s exported packages. - Reflection on private members needs
opens;--add-opensopens a package for one run.
References
- JEP 261: Module System (OpenJDK)
- JEP 403: Strongly Encapsulate JDK Internals (OpenJDK)
- The Java Language Specification, Java SE 25: 7.7 Module Declarations (Oracle)
- Java SE 25 API: package java.lang.module (resolution) (Oracle)
- Java SE 25 API: ModuleDescriptor (automatic modules) (Oracle)
- Java SE 25 API: ModuleFinder.of (automatic module names) (Oracle)
- The javac Command (Java SE 25): --module-source-path and -Xlint (Oracle)
- The java Command (Java SE 25): --module-path, --add-opens and --add-exports (Oracle)
- The jdeps Command (Java SE 25): --generate-module-info (Oracle)
- JAR File Specification (Java SE 25): modular JAR files and Add-Opens (Oracle)
- JEP 254: Compact Strings (OpenJDK)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress