π 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)