Java · lesson 38 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Java Module System (JPMS)
Build Java modules step by step: module-info.java, requires and exports, what strong encapsulation blocks, opens for reflection, services and jlink.
By SkillByExample · Updated
A module is a named set of packages with a descriptor, module-info.java, saying which other
modules it requires and which of its packages it exports. Everything not exported is hidden,
even public classes. The examples build one small project in a working directory: each code block
starts with a // file: comment giving its path, and each $ line is run from the project root
with JDK 21’s javac, java, jar and jlink.
A first module
A module lives in a directory named after it, with module-info.java at its root.
// file: src/com.greet/module-info.java
module com.greet {
}
// file: src/com.greet/com/greet/Main.java
package com.greet;
public class Main {
public static void main(String[] args) {
Module m = Main.class.getModule();
System.out.println("Hello from module " + m.getName());
System.out.println("reads java.base? " + m.canRead(Object.class.getModule()));
}
}
$ javac -d out --module-source-path src -m com.greet
$ java --module-path out -m com.greet/com.greet.Main
Hello from module com.greet
reads java.base? true
--module-source-path src tells javac that each subdirectory of src is a module; -m com.greet
compiles that module and anything it needs. At runtime, --module-path replaces the classpath and
-m module/class names the entry point. Every module reads java.base implicitly.
requires and exports
Here an API module exports one package and keeps another internal; an app module requires it.
// file: src/com.shop.api/module-info.java
module com.shop.api {
exports com.shop.api; // com.shop.api.internal is NOT exported
}
// file: src/com.shop.api/com/shop/api/Prices.java
package com.shop.api;
import com.shop.api.internal.Tax;
public class Prices {
public static String withTax(int cents) {
return String.format("%.2f", Tax.apply(cents) / 100.0);
}
}
// file: src/com.shop.api/com/shop/api/internal/Tax.java
package com.shop.api.internal;
public class Tax {
public static int apply(int cents) { return cents * 120 / 100; }
}
// file: src/com.shop.app/module-info.java
module com.shop.app {
requires com.shop.api;
}
// file: src/com.shop.app/com/shop/app/App.java
package com.shop.app;
import com.shop.api.Prices;
public class App {
public static void main(String[] args) {
System.out.println("price: " + Prices.withTax(1000));
}
}
$ javac -d out --module-source-path src -m com.shop.app
$ java --module-path out -m com.shop.app/com.shop.app.App
price: 12.00
Compiling com.shop.app pulled in com.shop.api automatically, because the requires clause names
it. Inside its own module, Prices uses the internal Tax class freely.
Strong encapsulation: public is not enough
Another module tries to use the internal package directly.
// file: src/com.sneaky/module-info.java
module com.sneaky {
requires com.shop.api;
}
// file: src/com.sneaky/com/sneaky/Sneaky.java
package com.sneaky;
import com.shop.api.internal.Tax;
public class Sneaky {
public static void main(String[] args) {
System.out.println(Tax.apply(100));
}
}
$ javac -d out --module-source-path src -m com.sneaky
src/com.sneaky/com/sneaky/Sneaky.java:4: error: package com.shop.api.internal does not exist
import com.shop.api.internal.Tax;
^
src/com.sneaky/com/sneaky/Sneaky.java:8: error: cannot find symbol
System.out.println(Tax.apply(100));
^
symbol: variable Tax
location: class Sneaky
2 errors
Tax is public, yet from inside com.sneaky its package does not exist at all: it was never
exported, so it is not part of what com.sneaky can read. On the classpath
this import would have compiled, which is how so much code came to depend on JDK internals such as
sun.misc. The API author can now change com.shop.api.internal without breaking anyone.
opens: allowing reflection
Exporting gives compile-time access to public members. Reflection into private members needs
opens, which this module does not have yet.
// file: src/com.data/module-info.java
module com.data {
exports com.data;
}
// file: src/com.data/com/data/User.java
package com.data;
public class User {
private String name = "ada";
}
// file: src/com.reflect/module-info.java
module com.reflect {
requires com.data;
}
// file: src/com.reflect/com/reflect/Peek.java
package com.reflect;
import com.data.User;
import java.lang.reflect.Field;
public class Peek {
public static void main(String[] args) throws Exception {
Field f = User.class.getDeclaredField("name");
try {
f.setAccessible(true);
System.out.println("name = " + f.get(new User()));
} catch (RuntimeException e) {
System.out.println(e.getClass().getSimpleName());
}
}
}
$ javac -d out --module-source-path src -m com.reflect
$ java --module-path out -m com.reflect/com.reflect.Peek
InaccessibleObjectException
setAccessible(true) throws InaccessibleObjectException because com.data is exported but not
opened. Change the descriptor to open the package — the way you would for Jackson or Hibernate:
// file: src/com.data/module-info.java
module com.data {
exports com.data;
opens com.data; // or: opens com.data to some.framework;
}
$ javac -d out --module-source-path src -m com.data,com.reflect
$ java --module-path out -m com.reflect/com.reflect.Peek
name = ada
javac -m recompiles only sources newer than their class files in out, so com.data is named
explicitly here; compiling just com.reflect would have kept the old descriptor.
opens com.data to some.framework restricts deep reflection to one named module, which is safer
than opening to everyone. A whole module can also be declared open module.
Services: provides and uses
ServiceLoader lets a module find implementations of an interface without knowing who provides
them. Here the API module declares the interface and the app uses it.
// file: src/com.fmt.api/module-info.java
module com.fmt.api {
exports com.fmt.api;
uses com.fmt.api.Formatter;
}
// file: src/com.fmt.api/com/fmt/api/Formatter.java
package com.fmt.api;
import java.util.ServiceLoader;
public interface Formatter {
String format(String s);
static void main(String[] args) {
ServiceLoader.load(Formatter.class).stream()
.map(ServiceLoader.Provider::get)
.forEach(f -> System.out.println(f.getClass().getSimpleName() + ": " + f.format("Modules")));
}
}
// file: src/com.fmt.upper/module-info.java
module com.fmt.upper {
requires com.fmt.api;
provides com.fmt.api.Formatter with com.fmt.upper.Upper;
}
// file: src/com.fmt.upper/com/fmt/upper/Upper.java
package com.fmt.upper;
import com.fmt.api.Formatter;
public class Upper implements Formatter {
public String format(String s) { return s.toUpperCase(); }
}
$ javac -d out --module-source-path src -m com.fmt.api,com.fmt.upper
$ java --module-path out -m com.fmt.api/com.fmt.api.Formatter
Upper: MODULES
com.fmt.api never mentions Upper, and Upper’s package is not even exported. The provides
clause registers it, and the module system wires it in because com.fmt.upper was on the module
path. Drop a different provider module on the path and the output changes with no recompilation.
Inspecting modules
The JDK is itself modular, and the same tools describe your modules.
$ java --describe-module java.sql
java.sql@21.0.12.1
exports java.sql
exports javax.sql
requires java.xml transitive
requires java.base mandated
requires java.transaction.xa transitive
requires java.logging transitive
uses java.sql.Driver
$ java --list-modules | wc -l
69
java.sql uses requires transitive, so any module that requires java.sql can also read
java.logging, java.transaction.xa and java.xml without naming them. The full JDK is made of
dozens of such modules; jar --describe-module --file x.jar prints the same summary for a modular
JAR.
jlink: a runtime with only what you need
jlink assembles a Java runtime containing just the modules an application requires.
$ jlink --module-path out --add-modules com.greet --output greet-runtime
$ greet-runtime/bin/java --list-modules
com.greet
java.base@21.0.12.1
$ greet-runtime/bin/java -m com.greet/com.greet.Main
Hello from module com.greet
reads java.base? true
The image holds only com.greet and java.base, the one module it needs. The full JDK counted above has
many more; images like this are a common way to shrink container images.
Practice
- Make
com.shop.appprint the result ofTax.applyby changing onlycom.shop.api’s descriptor, using a qualified export so no other module gains access.
Solution
// file: src/com.shop.api/module-info.java
module com.shop.api {
exports com.shop.api;
exports com.shop.api.internal to com.shop.app;
}
// file: src/com.shop.app/com/shop/app/App.java
package com.shop.app;
import com.shop.api.Prices;
import com.shop.api.internal.Tax;
public class App {
public static void main(String[] args) {
System.out.println("price: " + Prices.withTax(1000));
System.out.println("tax applied: " + Tax.apply(1000));
}
}
$ javac -d out --module-source-path src -m com.shop.app
$ java --module-path out -m com.shop.app/com.shop.app.App
price: 12.00
tax applied: 1200
$ javac -d out --module-source-path src -m com.sneaky
src/com.sneaky/com/sneaky/Sneaky.java:4: error: package com.shop.api.internal does not exist
import com.shop.api.internal.Tax;
^
src/com.sneaky/com/sneaky/Sneaky.java:8: error: cannot find symbol
System.out.println(Tax.apply(100));
^
symbol: variable Tax
location: class Sneaky
2 errors
exports ... to com.shop.app is visible to that one module only; com.sneaky still fails to
compile.
- Add a second provider module,
com.fmt.reverse, whoseReverseformatter reverses the string. Run theFormatterentry point again without changingcom.fmt.api.
Solution
// file: src/com.fmt.reverse/module-info.java
module com.fmt.reverse {
requires com.fmt.api;
provides com.fmt.api.Formatter with com.fmt.reverse.Reverse;
}
// file: src/com.fmt.reverse/com/fmt/reverse/Reverse.java
package com.fmt.reverse;
import com.fmt.api.Formatter;
public class Reverse implements Formatter {
public String format(String s) { return new StringBuilder(s).reverse().toString(); }
}
$ javac -d out --module-source-path src -m com.fmt.reverse
$ java --module-path out -m com.fmt.api/com.fmt.api.Formatter
Reverse: seludoM
Upper: MODULES
Only the new module was compiled; ServiceLoader discovers both providers at runtime. Providers
are found in module order, so do not rely on a particular order between them.
- Remove the
requires com.shop.apiline fromcom.shop.app(leaving an emptymodule com.shop.app {}) and compile. What error do you get?
Solution
// file: src/com.shop.app/module-info.java
module com.shop.app {
}
$ javac -d clean-out --module-source-path src -m com.shop.app
src/com.shop.app/com/shop/app/App.java:4: error: package com.shop.api does not exist
import com.shop.api.Prices;
^
src/com.shop.app/com/shop/app/App.java:5: error: package com.shop.api.internal does not exist
import com.shop.api.internal.Tax;
^
src/com.shop.app/com/shop/app/App.java:9: error: cannot find symbol
System.out.println("price: " + Prices.withTax(1000));
^
symbol: variable Prices
location: class App
src/com.shop.app/com/shop/app/App.java:10: error: cannot find symbol
System.out.println("tax applied: " + Tax.apply(1000));
^
symbol: variable Tax
location: class App
4 errors
Compiling into a fresh clean-out directory forces every source to be compiled (-m skips
up-to-date classes). Without requires, the module cannot read com.shop.api, so even its exported package is not
visible. Dependencies must be declared — the “reliable configuration” half of JPMS.