๐ Chapter 1: Introduction
1. Overview ๐
๐ก Before diving into syntax and declarations โ pause: Why did Java need modules at all? What problem does JPMS solve that packages couldnโt?
Java 9 introduced a groundbreaking architectural shift: the Java Platform Module System (JPMS) โ more commonly called Modules. ๐ฆโก๏ธ๐งฑ
This isnโt just โpackages, but bigger.โ Itโs a new level of encapsulation and dependency management, sitting above packages and below JARs โ bringing true modularity to the Java platform for the first time.
๐ Core Ideas
| Concept | Before JPMS | With JPMS |
|---|---|---|
| Encapsulation |
public = visible to all
|
public + module export = visible only to allowed modules
|
| Dependencies | Implicit (classpath chaos ๐ช๏ธ) | Explicit (requires declarations โ
) |
| JRE Size | Monolithic rt.jar (~70MB+) |
Slim, custom runtimes via jlink ๐ ๏ธ |
๐ค Reflect: Have you ever faced โJar Hellโ? Classpath conflicts? Accidental API exposure? JPMS was born from these pains.
๐ฏ What Weโll Do
In this tutorial, weโll:
- โ
Understand module declarations (
module-info.java) - โ
Explore
requires,exports,opens,uses,provides - โ Build a small, modular project step-by-step โ learning by doing
- โ
See how
jdeps,jlink, andjmodempower modular deployment
๐ Our goal: Not just know modules โ but think in modules.
Letโs begin our journey โ from the monolith ๐ฐ to the modular world ๐งฉ.
๐ฆ Chapter 2: What Is a Module?
๐ Before asking โHow do I use modules?โ โ letโs ask: What problem does a โmoduleโ *actually solve? What makes it more than just a folder of packages?*
A module is not just a bundle โ itโs a contract.
It groups related packages + resources, andโcruciallyโdeclares what it offers, what it needs, and what it hides.
Think of it as:
๐ฆ A package of packages โ plus intentionality.
Letโs unpack its anatomy ๐งฌ:
2.1 Packages โ The Familiar Foundation ๐งฑ
โ
Still the same com.example.util you know and love.
โ
Still organize code, avoid naming collisions.
โก๏ธ But now:
Even if a class is public, itโs not accessible outside the module unless its package is explicitly exported.
๐ค Pause: Why might hiding public APIs be a good thing? (Hint: stability, security, maintainability.)
2.2 Resources โ No More โWhereโs That Config?โ ๐ผ๏ธโ๏ธ
Before JPMS: resources scattered in src/main/resources, global classpath โ hard to trace ownership.
With modules:
- Resources live alongside the code that uses them.
- Each module ships its own assets (images, configs, i18n files).
- No more accidental overwrites or โwho owns
logback.xml?โ debates.
โก๏ธ Result: Self-contained, relocatable units. ๐
2.3 The Heart: module-info.java โค๏ธ๐
This tiny file is where intention becomes reality. It declares:
| Directive | Purpose | Default? |
|---|---|---|
module my.app { |
Name โ e.g., com.baeldung.core (Reverse-DNS โ
) or my.app (project-style โ
) |
โ |
requires java.sql; |
Dependencies โ explicit, compile-time enforced | โ (NoClassDefFoundError if missing) |
exports com.myapp.api; |
Public API โ only exported packages are visible externally | โ (all packages private by default!) |
opens com.myapp.internal; |
Reflection access โ allows frameworks (e.g., Spring, Hibernate) to access non-public members | โ (reflection blocked by default!) |
provides MyService with MyServiceImpl; |
Service provider โ contributes an implementation | โ |
uses MyService; |
Service consumer โ declares intent to use a service | โ |
๐ Naming tip: Use
lower.case.with.dotsโ no dashes, no uppercase.
๐ Critical insight: Encapsulation is now strict by default. Freedom requires explicit permission.
2.4 Module Types โ Whoโs on the Path? ๐งญ
Not all modules are created equal. The JVM sees four kinds:
| Type | How Itโs Loaded | Access Privileges | Example |
|---|---|---|---|
| System Modules ๐ฅ๏ธ | Built into JDK (java.base, java.sql, etc.) |
Highly restricted; java.base is the root |
java.base, jdk.jshell
|
| Application Modules ๐งฉ |
module-info.java โ module-info.class in JAR |
Full JPMS rules apply | Our own code โ |
| Automatic Modules ๐ค | Plain JAR on --module-path (no module-info) |
Reads all modules โ ๏ธ (loose coupling) | Legacy commons-lang3-3.12.jar
|
| Unnamed Module ๐ณ๏ธ | On --class-path (legacy mode) |
Reads all modules, but exports nothing | Old-school classpath apps |
๐ก Reflect: Why might automatic modules be a โbridge,โ not a destination? What risks do they introduce?
2.5 Distribution โ One Module, One JAR ๐ฆ๐ฆ
๐ง Rule: One module = one JAR.
You can distribute as:
- โ
A standard modular JAR (
META-INF/versions/9/module-info.classinside) - โ An โexplodedโ directory (e.g., during dev/testing)
๐ฆ For multi-module projects (e.g., app + core + utils):
โ Each module builds to its own JAR โ assembled together at runtime.
โ ๏ธ Gotcha: Trying to cram two modules into one JAR? The JVM will reject it. ๐ซ
๐งฉ Summary: A Module Isโฆ
A named, self-describing, encapsulated unit of code + resources
โ with explicit dependencies, APIs, and boundaries.
It turns implicit assumptions into explicit contracts.
Ready to see it in action? ๐ ๏ธ
โก๏ธ In the next chapter, weโll build our first module โ from module-info.java to runtime.
๐งฑ Chapter 3: Default Modules โ The JDKโs Modular Heart
๐ก Before writing your first
module-info.java, pause: What does the JDK itself look like now? If *it is modular, what can we learn from its design?*
With Java 9+, the JDK itself was refactored into modules โ no more monolithic rt.jar! ๐ชโก๏ธ๐งฉ
This wasnโt just internal cleanup: it enables custom runtimes, stronger security, and faster startup.
Letโs explore this new landscape.
๐ Discovering System Modules
Run this in your terminal:
java --list-modules
Youโll see dozens of entries like:
java.base@17
java.sql@17
java.xml@17
jdk.jconsole@17
javafx.controls@17 # if installed
Each is a named, self-contained system module โ compiled, versioned, and interdependent.
๐งญ The Four Module Families
The JDKโs modules fall into four logical groups โ each with a purpose and visibility boundary:
| Prefix | Purpose | Examples | Key Insight |
|---|---|---|---|
java.* ๐ |
Java SE Platform API โ what youโre allowed to depend on in portable apps |
java.base, java.sql, java.xml, java.desktop
|
java.base is the root โ every module implicitly requires it ๐ฑ |
javafx.* ๐ผ๏ธ |
JavaFX UI Toolkit (modularized separately since Java 11) |
javafx.controls, javafx.fxml
|
Not part of SE โ must be added explicitly (e.g., via SDK or Maven) |
jdk.* โ๏ธ |
JDK Tools & Implementation Details โ internal to the JDK |
jdk.jshell, jdk.compiler, jdk.jdi
|
โ Avoid depending on these โ no stability guarantees! |
oracle.* ๐ก๏ธ |
Oracle-Specific Extensions (e.g., commercial features) |
oracle.jdbc, oracle.security
|
Vendor-specific โ not portable across JVMs (OpenJDK wonโt have these) |
๐ค Reflect: Why separate `java.
(public spec) fromjdk.` (implementation)? How does this help long-term evolution and security?
๐ The Module Graph โ Dependencies in Action
Every module declares its dependencies. For example:
-
java.sqlโrequires java.logging,requires java.xml,requires transitive java.base -
java.desktopโrequires java.prefs,requires transitive java.datatransfer
You can visualize dependencies with:
java --list-modules --verbose # or
jdeps --list-deps $(java --list-modules | grep java.base | cut -d@ -f1)
๐ This is exactly the same model youโll use for your own modules โ just scaled up.
The JDK is the ultimate case study in modular design. ๐
โ Key Takeaways
- โ The JDK is now a collection of modules, not a single JAR.
- โ
java.baseis the universal foundation โ minimal, essential, and stable. - โ
Separation of concerns is enforced: public API (
java.*) vs. internal tools (jdk.*). - โ Your appโs modules will sit alongside these โ depending only on what they truly need.
๐ Next: Letโs create our own module โ and see how it integrates with
java.base,java.sql, or others.
๐ Chapter 4: Module Declarations โ The Contract of Intent
๐ค Before writing
module-info.java: What makes a good contract? Clarity? Minimalism? Explicit boundaries? Modules force us to negotiate these intentionally.
Every module starts with one file:
๐ module-info.java โ at the root of your source tree (side-by-side with your top-level package).
This is your moduleโs manifesto โ declaring what it is, what it needs, and what it offers.
module my.app {
// directives go here โ all optional, but rarely *all* omitted!
}
Letโs explore each directive โ not just what it does, but when (and why) to use it.
๐ 4.1 requires โ The Baseline Dependency
requires java.sql;
โ
What: Declares a mandatory compile-and-runtime dependency.
โ
Effect: Public types from java.sql (e.g., Connection, Statement) are now usable in your module.
โ ๏ธ Note: If java.sql isnโt on the module path? โ compile error.
๐ก Ask: Is this dependency truly required for my module to function? If yes โ
requires.
โ๏ธ 4.2 requires static โ Optional at Runtime
requires static org.slf4j;
โ
Compile-time only.
โ
Your code can reference SLF4J types โ but if SLF4J isnโt present at runtime? โ no error (assuming you guard usage with Class.forName() or DI).
๐ฏ Use case: Optional integrations (logging, metrics, debug tooling).
๐ค Reflect: How does this help library authors avoid โdependency bloatโ for consumers?
๐ 4.3 requires transitive โ โBring My Friendsโ
requires transitive com.fasterxml.jackson.databind;
โ
If Module A requires your module โ A automatically reads jackson.databind, too.
โ
Critical for API libraries where your public types return or accept types from a dependency.
๐จ Anti-pattern: Overusing
transitiveโ unnecessary coupling.
โ Best practice: Only for dependencies whose types leak into your public API.
๐ช 4.4 exports โ Opening the Gate (Selectively)
exports com.myapp.api;
โ
Makes public types in com.myapp.api accessible to all modules that require yours.
๐ Default: All packages are module-private โ even public classes are hidden.
๐ก Rule of thumb: Export only your *stable, intended public API โ not internals.*
๐ฏ 4.5 exports โฆ to โ Invite-Only Access
exports com.myapp.internal to com.myapp.test, com.myapp.debug;
โ
Grants access only to specified modules.
๐ก๏ธ Use for:
- Test-only APIs
- Friend modules (e.g.,
coreโcliandgui, but not public consumers)
๐คซ Security win: Your โinternalโ stays internal โ except for trusted allies.
๐ 4.6 uses โ โI Consume This Serviceโ
uses javax.persistence.PersistenceProvider;
โ
Declares: โI will look up implementations of this service interface at runtime (via ServiceLoader)โ.
โ
Does not imply requires โ the provider module supplies the interface + impl.
๐งฉ Key insight: Decouples consumers from providers. Your module only needs the interface, not the impl.
๐ 4.7 provides โฆ with โ โI Am a Service Providerโ
provides javax.persistence.spi.PersistenceProvider
with com.myapp.MyPersistenceProvider;
โ
Registers your class as an implementation of a service.
โ
At runtime, ServiceLoader.load(PersistenceProvider.class) will find it โ if your module is on the module path.
๐ Pattern: Clean separation of API (in one module) and implementations (in others).
๐ช 4.8 open module โ Full Reflection (Use Sparingly!)
open module my.app {}
โ
Grants all modules full reflective access to all packages (including private members).
โ ๏ธ Only use for:
- Legacy frameworks that require deep reflection (e.g., older Hibernate, Spring versions)
- Quick prototyping โ not production!
๐ซ Avoid if possible โ breaks encapsulation.
๐ 4.9 opens โ Reflect on This Package
opens com.myapp.config;
โ
Grants all modules reflective access to one package.
โ
Safer than open module โ but still broad.
๐ก Use when a specific package needs injection/mapping (e.g., config beans).
๐ฏ 4.10 opens โฆ to โ Reflection, by Invitation Only
opens com.myapp.domain to spring.core, hibernate.core;
โ
Grants reflective access only to listed modules.
โ
Best practice for modern apps: explicit, minimal, secure.
๐ Gold standard for production modules needing framework integration.
๐งฉ Putting It All Together โ A Realistic Example
module com.baeldung.app {
requires java.sql;
requires static org.slf4j;
requires transitive com.fasterxml.jackson.core;
exports com.baeldung.api;
exports com.baeldung.spi to com.baeldung.impl;
opens com.baeldung.domain to spring.core;
uses com.baeldung.spi.Plugin;
provides com.baeldung.spi.Plugin with com.baeldung.plugins.DefaultPlugin;
}
โ Minimal dependencies
โ Clear API boundaries
โ Secure reflection
โ Service-based extensibility
๐ Pro Tips
- ๐ Keep
module-info.javaclean: Group related directives (e.g., allrequires, thenexports, etc.). - ๐งช Test early: Use
jdepsto analyze dependencies;java --describe-moduleto inspect at runtime. - ๐ Avoid:
exports/openstoALL-UNNAMEDโ it weakens modularity.
โ๏ธ Chapter 5: Command-Line Mastery โ Beyond javac & java
๐ค If modules are declared in
module-info.java, why do we need CLI flags? When does runtime flexibility outweigh compile-time rigidity?
While Maven/Gradle handle most build plumbing, CLI options give you surgical control โ for:
- ๐ Debugging module resolution
- ๐งช Patching or overriding in development
- ๐ ๏ธ Running legacy code in modular JVMs
- ๐ Understanding how the module system really works
Letโs demystify the key flags โ with why, when, and how.
๐งญ Essential Module Path Flags
| Flag | Purpose | Example | When to Use |
|---|---|---|---|
--module-path (or -p) |
๐ Where to find modules (replaces CLASSPATH for modular code) |
java -p mods:lib -m my.app/com.myapp.Main |
โ Always โ for any modular app |
--class-path (or -cp) |
๐ณ๏ธ For non-modular (unnamed module) code only | java -cp legacy.jar com.LegacyApp |
โ ๏ธ Avoid mixing with -p unless bridging old/new |
๐ก Pro tip:
-p mods=mods/contains JARs (or exploded dirs) withmodule-info.class.
๐ ๏ธ Runtime Overrides โ โDynamic Directivesโ
These let you patch module behavior without recompiling โ powerful, but use with care.
| Flag | Replaces | Example | Why? |
|---|---|---|---|
--add-reads <module>=<other> |
requires (but runtime-only) |
--add-reads my.app=java.sql |
๐ง Fix missing requires in 3rd-party JARs (e.g., automatic modules) |
--add-exports <module>/<pkg>=<target> |
exports โฆ to |
--add-exports java.base/sun.nio.ch=my.app |
๐จ Access internal JDK APIs (e.g., for performance hacks โ not recommended for prod!) |
--add-opens <module>/<pkg>=<target> |
opens โฆ to |
--add-opens java.base/java.lang=my.app |
๐งช Allow reflection into JDK internals (e.g., for testing, mocking, or legacy frameworks) |
--patch-module <module>=<path> |
Replace/extend a module | --patch-module java.base=patches/ |
๐ ๏ธ Hotfix JDK bugs during dev; inject test doubles |
โ ๏ธ Warning: Overuse breaks encapsulation โ these are escape hatches, not design features.
๐ค Reflect: How might--add-openshelp migrate a Spring 4 app to Java 17? What trade-offs does it introduce?
๐ Inspection & Control
| Flag | Purpose | Example | Insight |
|---|---|---|---|
--list-modules |
๐ Show all resolved modules (name + version) | `java --list-modules \ | grep java.` |
--describe-module <name> |
๐ Deep-dive into a moduleโs structure | java --describe-module java.sql |
View exports, requires, services โ like module-info.java at runtime! |
--add-modules <mod1>,<mod2> |
โ Explicitly resolve extra modules |
--add-modules java.xml.bind (in Java 9โ10) |
Needed for modules not required by your app but used indirectly (e.g., via reflection) |
๐ก๏ธ Strong Encapsulation โ The --illegal-access Lever
Java 9+ blocks illegal reflective access by default โ but offers a grace period:
| Mode | Effect | CLI | Reality Check |
|---|---|---|---|
permit (default โค Java 16) |
๐ก Warn once at startup | --illegal-access=permit |
โWorks, but noisyโ โ deprecated in Java 17+ |
warn |
๐ Warn every time illegal access occurs | --illegal-access=warn |
Find hidden reflection issues |
deny (default โฅ Java 17) |
๐ด Fail fast on illegal access | --illegal-access=deny |
โ Production best practice |
๐ก In Java 17+,
--illegal-accessis ignored โ illegal access is always denied.
๐ ๏ธ Fix properly with--add-opensor refactor.
๐งช Real-World Example: Running a โBrokenโ Modular App
Imagine my-app.jar forgets to requires java.sql โ but uses JDBC.
โ Fails with:
java.lang.module.ResolutionException: Module my.app does not read module java.sql
โ
Fix temporarily via CLI:
java \
--module-path mods \
--add-reads my.app=java.sql \
-m my.app/com.myapp.Main
โ Works! But now you know: go fix module-info.java ๐ ๏ธ.
โ Key Principles
- Compile-time declarations > runtime overrides โ use CLI for debugging, not design.
-
Least privilege: Prefer
--add-opens โฆ to my.moduleover global opens. -
Know your defaults:
--illegal-access=denyis the new normal in modern Java.
๐ Chapter 6: Visibility & Reflection โ The New Rules of Access
๐ค Before Java 9: โIf itโs loaded, I can reflect on it.โ
After Java 9: โIf itโs not explicitly opened โ no reflection, not even withsetAccessible(true).โ
Why did this change? What does โsecure by defaultโ really mean?
Strong encapsulation isnโt just about hiding code โ itโs about predictability, security, and evolvability.
But yes โ it does break reflection-heavy frameworks. ๐
Letโs navigate this wisely.
๐งฑ The New Visibility Hierarchy
In Java 9+, accessibility is a two-layer gate:
| Layer | Gatekeeper | What It Controls |
|---|---|---|
| 1. Module Readability |
requires / --add-reads
|
Can Module A see Module B at all? |
| 2. Package Accessibility |
exports / opens / CLI flags |
Can Module A access types or members in Module Bโs packages? |
โก๏ธ Both must be satisfied โ even for reflection.
๐ Whatโs Really Accessible? (By Default)
| Member Type | Normal Access (new, method call) |
Reflection (getDeclaredField() + setAccessible(true)) |
|---|---|---|
public in exported package |
โ Yes | โ Yes |
public in non-exported package |
โ No | โ No |
private/protected/package-private in exported package |
โ No | โ No โ InaccessibleObjectException! |
| Any member in opened package | โ (compile) / โ (runtime via reflection) | โ Yes โ if module opened it to you |
๐ฅ Critical:
setAccessible(true)does not bypass module encapsulation.
It only bypasses Java language access checks โ not module system checks.
๐ ๏ธ How to Grant Reflection Access (The Right Way)
โ
Preferred: Declare It in module-info.java
| Directive | Scope | When to Use |
|---|---|---|
open module my.module { } |
Entire module | โ Quick dev/test; framework-heavy apps (e.g., older Spring) |
opens com.my.pkg; |
One package โ all modules | โ ๏ธ Rare โ too permissive |
opens com.my.pkg to spring.core, junit; |
One package โ specific modules | ๐ Production best practice |
// module-info.java โ clean, intentional, auditable
module com.baeldung.app {
opens com.baeldung.domain to spring.core, hibernate.core;
opens com.baeldung.config to spring.core;
}
๐ ๏ธ Escape Hatch: CLI Overrides (When You Canโt Change the Module)
If youโre using a 3rd-party library thatโs not modular (or poorly modularized):
java \
--module-path mods \
--add-opens java.base/java.lang=com.example.app \
--add-opens java.desktop/sun.awt=com.example.app \
-m com.example.app/com.example.Main
๐ฏ Use cases:
- Running legacy frameworks on Java 17+
- Patching missing
opensin automatic modules- CI/CD environments where you control JVM args
โ ๏ธ Limitations:
- Requires control over launch command (โ not possible in some cloud/serverless envs)
- Doesnโt help if the framework itself doesnโt use
setAccessible(true)properly
๐งช Real-World Examples
๐ง Spring Boot (Pre-3.0)
Many beans use reflection on private fields.
โ
Fix:
opens com.myapp.domain to spring.core, spring.beans;
Or (temporarily):
--add-opens com.myapp/com.myapp.domain=spring.core
๐งช JUnit 5
Uses reflection to instantiate/test private methods.
โ
Fix:
opens com.myapp to org.junit.platform.commons;
(Or use @ExtendWith and public test methods โ even better! ๐)
๐ซ Anti-Patterns to Avoid
| What | Why Itโs Bad |
|---|---|
--add-opens ALL-UNNAMED=ALL-UNNAMED |
โ Defeats modularity; insecure |
| Exporting internal packages just for reflection | โ Confuses API contract (exports โ opens!) |
Ignoring InaccessibleObjectException
|
โ Hides design debt โ will break in future JDKs |
๐ก Pro Tips for Library Authors
-
Separate API from implementation:
-
exportsyour public interfaces -
opensonly internal packages to your own test module
-
- Prefer constructor/setter injection over field injection โ reduces reflection needs.
-
Document reflection requirements in your module README:
> โน๏ธ This module requires
--add-opens com.lib/internal=your.appif used with Framework X.
๐ The Bigger Picture
This shift isnโt about making life harder โ itโs about:
- ๐ก๏ธ Preventing accidental coupling to internals (e.g.,
sun.misc.Unsafe) - ๐ Enabling JVM optimizations (e.g., ahead-of-time compilation, smaller images)
- ๐ฑ Allowing JDK teams to evolve internal APIs safely
As Brian Goetz said:
โModules donโt take away reflection โ they take away *surprise reflection.โ*
๐งฉ Chapter 7: Putting It All Together โ A Modular Hello World
๐ค Now that we know the rules โ can we *feel modularity? Letโs build, break, and fix โ with nothing but
javac,java, and intention.*
Weโll create a two-module app โ then extend it with services โ all from the command line.
No Maven. No Gradle. Just pure JPMS. ๐ฅ๏ธโจ
๐ 7.1 Project Structure โ Modular by Design
Letโs build a clean, scalable layout:
mkdir -p module-project/simple-modules
cd module-project
๐ Final structure:
module-project/
โโโ compile-simple-modules.sh # โ build script
โโโ run-simple-module-app.sh # โ run script
โโโ simple-modules/
โโโ hello.modules/ # โ Library module
โ โโโ module-info.java
โ โโโ com/baeldung/modules/hello/
โ โโโ HelloModules.java
โ โโโ HelloInterface.java # โ added later
โ
โโโ main.app/ # โ Application module
โโโ module-info.java
โโโ com/baeldung/modules/main/
โโโ MainApp.java
๐ก Why this layout?
simple-modules/isolates all modules โ easy to add more (util,config, etc.)- Flat sibling structure โ clean
--module-source-path
๐ฆ 7.2 Module 1: hello.modules โ The API Provider
โ Step 1: Create the class
simple-modules/hello.modules/com/baeldung/modules/hello/HelloModules.java
package com.baeldung.modules.hello;
public class HelloModules {
public static void doSomething() {
System.out.println("Hello, Modules!");
}
}
โ Step 2: Declare the module
simple-modules/hello.modules/module-info.java
module hello.modules {
exports com.baeldung.modules.hello;
}
๐ค Reflect: What happens if we omit
exports? Try it โ see the compile error!
๐ Encapsulation in action: Withoutexports,HelloModulesis invisible โ even though itโspublic.
๐ 7.3 Module 2: main.app โ The Consumer
โ Step 1: Declare dependency
simple-modules/main.app/module-info.java
module main.app {
requires hello.modules; // โ explicit, compile-time enforced
}
โ Step 2: Use the API
simple-modules/main.app/com/baeldung/modules/main/MainApp.java
package com.baeldung.modules.main;
import com.baeldung.modules.hello.HelloModules;
public class MainApp {
public static void main(String[] args) {
HelloModules.doSomething(); // โ works because package is exported!
}
}
๐ก Note: No
import staticneeded โdoSomething()is static, not a service (yet!).
๐จ 7.4 Build Script โ One Command to Rule Them All
compile-simple-modules.sh
#!/usr/bin/env bash
set -e # exit on error
echo "๐ Compiling all modules..."
javac \
-d outDir \
--module-source-path simple-modules \
$(find simple-modules -name "*.java")
echo "โ
Modules built to: outDir/"
ls -R outDir
๐ Key flags:
-
-d outDirโ output directory -
--module-source-path simple-modulesโ tellsjavac: โThis is a multi-module projectโ -
$(find ...)โ auto-includes all.javafiles (no manual lists!)
๐ ๏ธ Run it:
chmod +x compile-simple-modules.sh ./compile-simple-modules.sh
โ๏ธ Expect:
outDir/
โโโ hello.modules/
โ โโโ com/baeldung/modules/hello/HelloModules.class
โโโ main.app/
โโโ com/baeldung/modules/main/MainApp.class
โถ๏ธ 7.5 Run It โ The Moment of Truth!
run-simple-module-app.sh
#!/usr/bin/env bash
java \
--module-path outDir \
-m main.app/com.baeldung.modules.main.MainApp
๐
--module-path outDir= where to find compiled modules
๐-m main.app/...= runMainApp.main()in modulemain.app
๐ Run it:
chmod +x run-simple-module-app.sh
./run-simple-module-app.sh
๐ฏ Expected output:
Hello, Modules!
๐ Success! Youโve built your first modular app.
๐ค What if you swap
requires hello.modulesโrequires static hello.modulesand remove the call? Does it still compile? Run?
๐ 7.6 Level Up: Services with provides โฆ with & uses
Letโs replace static calls with pluggable services โ the real power of JPMS.
โ Step 1: Define the service interface
simple-modules/hello.modules/com/baeldung/modules/hello/HelloInterface.java
package com.baeldung.modules.hello;
public interface HelloInterface {
void sayHello();
}
โ Step 2: Implement it
Update HelloModules.java:
public class HelloModules implements HelloInterface { // โ now an impl
public static void doSomething() {
System.out.println("Hello, Modules!");
}
@Override
public void sayHello() {
System.out.println("Hello from Service!");
}
}
โ Step 3: Declare the service provider
Update hello.modules/module-info.java:
module hello.modules {
exports com.baeldung.modules.hello;
provides com.baeldung.modules.hello.HelloInterface // โ service contract
with com.baeldung.modules.hello.HelloModules; // โ implementation
}
โ Step 4: Declare the consumer
Update main.app/module-info.java:
module main.app {
requires hello.modules;
uses com.baeldung.modules.hello.HelloInterface; // โ "I will load this service"
}
โ Step 5: Load the service
Update MainApp.java:
package com.baeldung.modules.main;
import com.baeldung.modules.hello.HelloInterface;
import java.util.ServiceLoader;
public class MainApp {
public static void main(String[] args) {
// Static call (still works)
com.baeldung.modules.hello.HelloModules.doSomething();
// Service-based call (new!)
ServiceLoader<HelloInterface> loader = ServiceLoader.load(HelloInterface.class);
HelloInterface service = loader.findFirst()
.orElseThrow(() -> new RuntimeException("No HelloInterface found!"));
service.sayHello();
}
}
โ Step 6: Recompile & Run
./compile-simple-modules.sh
./run-simple-module-app.sh
๐ฏ New output:
Hello, Modules!
Hello from Service!
๐ฏ Why this matters:
- Your app no longer depends on
HelloModulesโ only onHelloInterface.- Swap implementations without recompiling
main.appโ just drop in a new module!- Hide implementations in non-exported packages (e.g.,
com.baeldung.internal) โ only the interface is public.
๐งช Try It Yourself! (Mini Challenges)
Now that youโve got the foundation โ experiment!
- ๐ช Move
HelloModulestocom.baeldung.internalโ can you still use it via service? (Hint: noexportsneeded!) - ๐งฉ Add a second implementation (
HelloImpl2) โ what doesServiceLoaderreturn? - ๐ Add
opens com.baeldung.modules.helloโ can you reflect onprivatefields now? - ๐ซ Remove
usesโ does it still compile? Run? (Spoiler: compile โ , runtime โ if no impl found)
๐ Key Takeaways
- โ Modules = explicit contracts, not implicit assumptions.
- โ
exportsโopensโ API vs. reflection access. - โ
Services (
provides/uses) enable loose coupling and runtime discovery. - โ
CLI tools (
javac,java) are your best friends for learning.
๐ณ๏ธ Chapter 8: The Unnamed Module โ Javaโs Backward-Compatibility Lifeline
๐ค If modules are so powerful โ why does Java still allow code *outside the module system? What trade-offs did the designers make to avoid breaking the world?*
The unnamed module is not a โmoduleโ in the formal sense โ itโs a compatibility construct:
๐ฆ All code on the
--class-path(not--module-path) lives here โ as one big, flat, โlegacyโ module.
It has special privileges โ and limitations โ designed to keep pre-Java 9 code running while encouraging migration.
๐งฉ What Is the Unnamed Module?
| Property | Unnamed Module | Named Module |
|---|---|---|
| How created | Put JAR/class on --class-path
|
Put JAR/dir with module-info.class on --module-path
|
| Name |
null (no name) |
e.g., com.baeldung.app
|
| Reads | โ All other modules (system + named + automatic) | โ Only modules it requires
|
| Exports | โ Exports nothing โ all packages are module-private | โ
Only packages explicitly exports
|
| Opens | โ Opens nothing for reflection (unless CLI overrides used) | โ
Controlled via opens/open module
|
๐ก Key insight:
The unnamed module is omnivorous (it can use anything) but mute (it offers nothing to others).
โ Great for running old apps โ poor for building modular ones.
๐ Why Add Modules Explicitly? (--add-modules)
Even though the unnamed module reads all modules, some modules are *not resolved by default* โ especially if theyโre not required by anything in the root set.
๐ฏ Common Scenarios
| Problem | Cause | Fix |
|---|---|---|
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException |
java.xml.bind was removed from default root set in Java 9+ (later deleted in Java 11) |
--add-modules java.xml.bind (Java 9โ10 only)
|
ServiceConfigurationError: No implementation for javax.persistence.spi.PersistenceProvider |
JPA impl (e.g., Hibernate) needs java.sql, but unnamed module app doesnโt pull it in |
--add-modules java.sql |
ClassNotFoundException: com.sun.xml.internal.ws.spi.ProviderImpl |
Internal JDK service provider not resolved |
--add-modules jdk.xml.ws (if available)
|
โ ๏ธ Note: In Java 11+,
java.xml.bind,java.activation, etc., are gone โ you must add them as dependencies (e.g.,jakarta.xml.bind-api).
โ๏ธ How --add-modules Works
java --add-modules java.sql,java.xml -cp legacy-app.jar com.LegacyMain
- ๐ Tells the JVM: โEven if no module explicitly
requiresthese, include them in the module graph.โ - โ Resolves the module + its transitive dependencies
- โ Makes their exported packages available to the unnamed module (via its โread allโ privilege)
๐ค Reflect: Why not just auto-resolve *all system modules?*
๐ฏ Answer: To keep minimal runtimes lean โ unused modules arenโt loaded.
๐งช Real-World Example: Running a Java 8 Spring App on Java 17
Your old spring-boot-1.5 app uses JAXB for REST โ fails on Java 17 with:
java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlRootElement
โ
Solution 1 (Temporary): Add Jakarta EE API + CLI flag
<!-- pom.xml -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.0</version>
</dependency>
java \
--add-modules ALL-SYSTEM \ # resolves *all* system modules
--add-opens java.base/java.lang=ALL-UNNAMED \
-jar legacy-app.jar
โ
Solution 2 (Better): Migrate to Jakarta XML Binding + Spring Boot 3
โ No CLI hacks needed โ fully modular-friendly. ๐
๐ ๏ธ In Build Tools
Maven (maven-compiler-plugin)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
<compilerArgs>
<arg>--add-modules</arg>
<arg>java.sql,java.xml</arg>
</compilerArgs>
</configuration>
</plugin>
Gradle
tasks.withType(JavaCompile) {
options.compilerArgs += ['--add-modules', 'java.sql,java.xml']
}
๐ก Pro tip: Prefer adding only what you need โ
ALL-SYSTEMbloats the classpath and hides real dependencies.
๐ซ Anti-Patterns to Avoid
| What | Why Itโs Bad |
|---|---|
--add-modules ALL-UNNAMED |
โ Invalid โ ALL-UNNAMED isnโt a module name |
Relying on --add-modules forever |
โ Masks design debt โ migrate to named modules! |
Using removed modules (e.g., java.xml.bind in Java 11+) |
โ Wonโt work โ replace with Jakarta EE APIs |
๐ The Bridge Forward
The unnamed module is a temporary harbor, not a destination.
Use --add-modules to:
- ๐ข Migrate incrementally (run old app โ modularize one module at a time)
- ๐งช Diagnose missing dependencies (
jdeps --print-module-deps your-app.jar) - ๐ Audit your legacy code before full modularization
As the JDK evolves, fewer modules will be โmissing by defaultโ โ because fewer apps will need them.
Youโre not just fixing a runtime error โ youโre future-proofing your code. ๐ก๏ธ
๐ Chapter 9: Conclusion โ Youโve Crossed the Modular Threshold
๐ค Look back: How has your understanding of โencapsulationโ changed since Chapter 1? Was it just about
privatefields โ or something deeper?
Youโve done it.
Youโve moved from implicit assumptions to explicit contracts.
From classpath chaos ๐ช๏ธ to intentional architecture ๐งฉ.
From โit works (for now)โ to โitโs designed to evolve.โ
Letโs recap the journey:
| ๐ Chapter | ๐ฏ Core Insight |
|---|---|
| 1. Overview | Modules are not packages 2.0 โ theyโre a new layer of design intention. |
| 2. Whatโs a Module? | A module = packages + resources + module-info.java โ a self-describing unit. |
| 3. Default Modules | Even the JDK practices what it preaches โ modularity starts at the top. ๐ฅ๏ธ |
| 4. Module Declarations |
requires, exports, opens, providesโฆ each directive is a promise you make. |
| 5. Command Line | The JVM speaks modular โ learn its language to debug, optimize, and understand. |
| 6. Visibility | Strong encapsulation isnโt restrictive โ itโs liberating (once you adapt). ๐ |
| 7. Hands-On | Theory becomes real when you type javac --module-source-path and see it work. ๐ ๏ธ |
| 8. Unnamed Module | Backward compatibility is a bridge โ not a destination. Walk across with care. ๐ |
๐ฑ Where to Go From Here
You now hold the keys to:
โ
Build modular libraries โ with clean APIs, secure internals, and service extensibility.
โ
Diagnose migration issues โ using jdeps, --describe-module, and --add-modules.
โ
Prepare for the future โ where custom runtimes (jlink) and native images (GraalVM) are the norm.
๐ Next Steps (If Youโre Curiousโฆ)
| Path | What Youโll Explore |
|---|---|
| ๐ฆ Multi-Module Builds | Maven/Gradle modular projects โ moditect, module path vs. classpath |
๐ jlink: Custom Runtimes
|
Strip the JDK down to only what your app needs โ 50MB โ 20MB! |
| โ๏ธ Modular Microservices | How modules fit (or donโt) in containerized, cloud-native worlds |
| โก GraalVM Native Image | Can modular apps be compiled to native? (Spoiler: Yes โ with care!) |
๐ Final Thought
โModules donโt make Java harder โ they make *bad design harder to ignore.โ*
The module system rewards clarity, foresight, and respect for boundaries โ between your code, your dependencies, and the platform itself.
You didnโt just learn syntax.
Youโve begun thinking like a modular architect. ๐๏ธ
Top comments (0)