1Z0-830 Modules and Packaging - Java SE 21 Certification Prep
module-info.java descriptors, understand all module directives, work with the three module types (named, unnamed, and automatic), use ServiceLoader for decoupled service discovery, and apply the correct migration strategy when moving existing code to the module system.
Java Platform Module System (JPMS)
Before JPMS, all public classes were accessible to all other code on the classpath, and missing dependencies were only discovered at runtime when a class was first loaded. JPMS solves both problems: packages are inaccessible by default unless explicitly exported, and the module system verifies that all required modules are present at startup before running any application code.
Key Benefits of JPMS:
- Strong encapsulation - Packages within a module are private to that module by default. Other modules can only access packages that are explicitly exported. This means internal implementation classes can never be accessed from outside the module, even if they are public.
- Explicit dependencies - Each module declares exactly which other modules it depends on. This makes dependencies visible and machine-verifiable, rather than implicit and discovered by trial and error.
- Reliable configuration - The module system checks that all required modules are present and that there are no circular dependencies or split packages at startup, before any application code runs.
- Smaller runtime images - Tools like jlink can create a custom JRE containing only the modules your application actually uses, reducing deployment size significantly.
// Typical module directory structure
// module-info.java must be at the ROOT of the module's source tree
mymodule/
module-info.java // Module descriptor - defines the module's identity and directives
com/
example/
api/
MyService.java // Exported package - accessible by other modules
internal/
Helper.java // Non-exported package - hidden from all other modules
// Even though Helper.java is public, it is inaccessible outside this module
// because the com.example.internal package is not exported.
// This is the key difference from the pre-module classpath model.
Module Descriptor (module-info.java)
Themodule-info.java file is the module descriptor. It declares the module's name and contains directives that specify dependencies, exported packages, and service relationships. It is compiled like any other Java source file but lives at the root of the module source tree, not inside any package.
// Minimal module-info.java
module com.example.myapp {
// Declare which modules this module depends on
requires java.sql;
requires java.logging;
// Declare which packages other modules may use
exports com.example.api;
// com.example.internal is NOT exported - it is invisible to all other modules
// even though its classes may be declared public
}
// The three module types - understanding these is critical for the exam
// 1. Named module
// - Has a module-info.java
// - On the module path (--module-path)
// - Strong encapsulation enforced
// - Must explicitly export packages and declare dependencies
// 2. Unnamed module
// - Code on the CLASSPATH with no module-info.java
// - Automatically reads all other modules
// - Exports all its packages to other unnamed modules
// - Named modules CANNOT declare a dependency on the unnamed module
// - This is the backward-compatibility layer for pre-module code
// 3. Automatic module
// - A plain JAR placed on the MODULE PATH (not the classpath)
// - Has no module-info.java but gets a module name automatically
// - Module name comes from the Automatic-Module-Name MANIFEST entry,
// or is derived from the JAR file name (dots replace hyphens, version stripped)
// - Exports ALL its packages to all modules
// - Reads ALL other modules including the unnamed module
// - Named modules CAN depend on automatic modules
Module Directives
Module directives are the statements inside amodule-info.java block. There are six directives: requires, requires transitive, requires static, exports, exports to, opens, opens to, uses, and provides...with. Knowing precisely what each one does and when to use it is one of the most heavily tested areas of this topic.
module com.example.myapp {
// requires - declares a compile-time and runtime dependency
// This module cannot start if java.sql is not present on the module path
requires java.sql;
// requires transitive - implied readability
// Any module that requires com.example.myapp also implicitly reads java.logging
// Use this when your exported API returns or accepts types from another module,
// so callers do not need to add a separate requires for that module
requires transitive java.logging;
// requires static - compile-time only dependency
// The module is needed at compile time but is optional at runtime
// Useful for annotation processors or optional features
requires static java.compiler;
// exports - makes a package readable and accessible to all other modules
// Both compile-time and runtime access are granted
exports com.example.api;
// exports to - restricts package access to named modules only
// com.example.internal is invisible to all modules except com.example.testing
// Useful for tightly coupled module pairs such as an app and its test module
exports com.example.internal to com.example.testing;
// opens - grants deep reflective access at runtime to all modules
// The package itself is NOT accessible at compile time (unlike exports)
// Required for frameworks that use reflection to access private fields and methods,
// such as Jackson (JSON), Hibernate (JPA), and Spring
opens com.example.model;
// opens to - grants reflective access at runtime to named modules only
// Hibernate can reflect on com.example.entity; no other module can
opens com.example.entity to org.hibernate.core;
// uses - declares that this module is a consumer of a service
// The module may load implementations of MyService via ServiceLoader
uses com.example.spi.MyService;
// provides...with - declares that this module provides an implementation of a service
// MyServiceImpl is the concrete class; MyService is the service interface
provides com.example.spi.MyService
with com.example.impl.MyServiceImpl;
}
// open module - a shorthand that opens ALL packages for deep reflection at runtime
// Equivalent to adding an opens directive for every package
// Useful during migration when you need maximum reflective access temporarily
open module com.example.openapp {
requires java.sql;
exports com.example.api;
// All packages are implicitly open even without individual opens directives
}
Exam Tip:
exports grants compile-time AND runtime access to public types in a package. opens grants runtime reflection access only - the package cannot be used at compile time by other modules. The distinction matters for frameworks: Spring and Hibernate need opens because they access private fields and methods via reflection at runtime, not via normal compilation. An open module opens everything; you cannot combine open module with individual opens directives inside it.
Transitive Dependencies
requires transitive creates implied readability: when module C requires module B, and module B has requires transitive A, then C can read module A without explicitly declaring it. This is necessary when a module's exported API exposes types from another module. Without requires transitive, every consumer would have to add a separate requires for that dependency, which is inconvenient and error-prone.
// Module A - a core library
module com.example.core {
exports com.example.core;
}
// Module B - uses core internally AND exposes core types in its own public API
// Because B's exported API returns types from com.example.core,
// callers of B need to be able to read com.example.core as well.
// requires transitive makes that happen automatically.
module com.example.service {
requires transitive com.example.core; // Implies readability to consumers of service
exports com.example.service;
}
// Module C - uses the service API, which returns com.example.core types
module com.example.app {
requires com.example.service;
// com.example.core is readable here automatically via the transitive chain
// No separate "requires com.example.core" is needed
}
// If B had used plain "requires" instead of "requires transitive":
// Module C would have to declare both dependencies explicitly:
module com.example.app {
requires com.example.service;
requires com.example.core; // Would be required without the transitive
}
// A real-world example: java.desktop has "requires transitive java.xml"
// Any module that requires java.desktop can use java.xml types
// without adding a separate requires java.xml directive.
Command Line Tools
The exam tests your ability to read and construct the command line options for compiling, running, and inspecting modular applications. The key flags are--module-source-path, --module-path, and -m (short for --module). Note that the module path flag replaces -classpath for modular code.
// Compile a module
// -d specifies the output directory
// --module-source-path points to the root of module source directories
// -m (or --module) specifies which module to compile
javac -d out --module-source-path src -m com.example.myapp
// Run the compiled module
// --module-path (or -p) is the module equivalent of -classpath
// -m specifies module/mainclass in the format modulename/fully.qualified.MainClass
java --module-path out -m com.example.myapp/com.example.Main
// Create a modular JAR with a main class
jar --create --file myapp.jar \
--main-class com.example.Main \
-C out/com.example.myapp .
// Run a modular JAR
java --module-path myapp.jar -m com.example.myapp
// Inspect a JAR's module descriptor
jar --describe-module --file myapp.jar
java --module-path myapp.jar --describe-module com.example.myapp
// List all modules available in the current JDK installation
java --list-modules
// Print module resolution details - useful for diagnosing startup failures
java --show-module-resolution --module-path out -m com.example.myapp
// Create a custom runtime image using jlink
// --module-path includes the JDK modules and your application JAR
// --add-modules specifies the root module(s) to include
// --output specifies the directory for the generated runtime image
jlink --module-path $JAVA_HOME/jmods:myapp.jar \
--add-modules com.example.myapp \
--output myruntime
// Run the application using the custom image (no JDK installation needed)
myruntime/bin/java -m com.example.myapp
// Analyze module dependencies with jdeps
jdeps --module-path libs -m com.example.myapp // Analyze a named module
jdeps -summary myapp.jar // Quick dependency summary for a JAR
jdeps --generate-module-info out myapp.jar // Generate a module-info.java candidate
Services (ServiceLoader)
The service provider pattern in JPMS allows a module to define a service interface and discover implementations at runtime without any compile-time dependency on the implementing modules. The consuming module declares what service it needs withuses; each providing module declares what it supplies with provides...with. ServiceLoader connects them at runtime by scanning the module path.
// --- API module: defines the service interface ---
// com.example.api module
package com.example.api;
public interface PaymentProcessor {
void process(double amount);
String getName();
}
// API module-info.java
module com.example.api {
exports com.example.api; // Export the interface so others can use and implement it
}
// --- Provider module: supplies one implementation ---
// com.example.stripe module
package com.example.stripe;
public class StripeProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing " + amount + " via Stripe");
}
@Override
public String getName() { return "Stripe"; }
}
// Provider module-info.java
module com.example.stripe {
requires com.example.api; // Needs the interface to implement it
// Declares that this module provides an implementation of PaymentProcessor
// The class after "with" must implement the service interface
// and must have a public no-arg constructor
provides com.example.api.PaymentProcessor
with com.example.stripe.StripeProcessor;
}
// --- Consumer module: discovers implementations at runtime ---
// Consumer module-info.java
module com.example.app {
requires com.example.api; // Needs the interface to use it
// Declares intent to load implementations via ServiceLoader
// Without this directive, ServiceLoader.load() returns no results
uses com.example.api.PaymentProcessor;
}
// Loading and using services in application code
ServiceLoader<PaymentProcessor> loader =
ServiceLoader.load(PaymentProcessor.class);
// Iterate over all available implementations
for (PaymentProcessor processor : loader) {
processor.process(100.0);
}
// Stream API for more control - Provider wrapper gives lazy access
loader.stream()
.filter(p -> p.type().getSimpleName().contains("Stripe"))
.map(ServiceLoader.Provider::get) // Instantiate only the ones you want
.forEach(p -> p.process(100.0));
// Find the first available implementation
Optional<PaymentProcessor> first = loader.findFirst();
first.ifPresent(p -> p.process(50.0));
// Reload implementations discovered after the ServiceLoader was created
loader.reload();
Exam Tip: The
uses directive in the consumer module is required - without it, ServiceLoader.load() will not find any implementations even if provider modules are present on the module path. The provider class listed after with must have a public no-argument constructor (or a public static provider() factory method). The consumer module does NOT need to require the provider module - that coupling is intentionally avoided.
Migration Strategies
Most existing Java applications were written before JPMS and run as unnamed modules on the classpath. Migrating to the module system is optional but the exam tests your understanding of the two main strategies: bottom-up (convert leaves first) and top-down (convert the application first). Understanding how named, unnamed, and automatic modules interact is essential for planning a migration.
// Unnamed Module
// - Code placed on the CLASSPATH with no module-info.java
// - Automatically reads all named modules (can use their exported packages)
// - All its own packages are accessible to other unnamed module code
// - Named modules CANNOT declare "requires" on the unnamed module
// - This is the backward-compatibility mechanism; pre-module code just works
// Running pre-module code unchanged:
java --class-path myapp.jar com.example.Main
// Automatic Module
// - A plain JAR placed on the MODULE PATH (not the classpath)
// - Has no module-info.java but is treated as a named module
// - Module name comes from MANIFEST.MF Automatic-Module-Name header (preferred)
// or is derived from the JAR file name: hyphens become dots, version suffix stripped
// Example: jackson-databind-2.15.0.jar becomes jackson.databind
// - Exports ALL its packages to all named modules
// - Reads ALL other modules, including the unnamed module
// - Named modules CAN declare "requires automaticmodulename"
// Setting Automatic-Module-Name in MANIFEST.MF (add to JAR before migration):
// Automatic-Module-Name: com.example.library
// Bottom-up migration strategy:
// Start with library modules that have no dependencies on non-modular code.
// 1. Add module-info.java to leaf libraries (those with no dependencies first)
// 2. Work toward the middle of the dependency graph
// 3. Convert the application module last
// Advantage: each step is clean; no automatic modules in the final result
// Disadvantage: requires converting third-party libraries you may not control
// Top-down migration strategy:
// Start with your own application module; treat third-party JARs as automatic modules.
// 1. Add module-info.java to your application module
// 2. Put third-party JARs on the module path as automatic modules
// 3. Use "requires automaticmodulename" for each third-party dependency
// 4. Migrate third-party libraries over time as they release modular versions
// Advantage: your own code benefits from JPMS immediately
// Disadvantage: relies on automatic modules, which are less stable than named modules
// Common migration problems and solutions:
// Problem 1: Split packages - the same package name appears in two different JARs
// JPMS does not allow split packages between named modules
// Solution: consolidate the package into one module, or rename one package
// Problem 2: Framework reflection fails at runtime
// Jackson, Hibernate, Spring use reflection to access private fields
// Solution: add opens directives in module-info.java, or use open module
// Example in module-info.java:
// opens com.example.model to com.fasterxml.jackson.databind;
// Problem 3: A named module needs access to JDK internal APIs
// Solution: use --add-exports or --add-opens on the command line temporarily
// These are escape hatches for migration; remove them as you fix the root cause
// Command line escape hatches for hard migration cases
java --add-exports java.base/sun.nio.ch=com.example.myapp \
--add-opens java.base/java.lang=com.example.myapp \
--add-reads com.example.myapp=java.sql \
-m com.example.myapp/com.example.Main
// --add-exports module/package=targetmodule - exports package to target at runtime
// --add-opens module/package=targetmodule - opens package for reflection to target
// --add-reads sourcemodule=targetmodule - makes source read target without requires
// Use ALL (not a module name) as the target to apply to all modules
JAR Files
JAR (Java Archive) files package compiled classes and resources into a single file for distribution. Thejar tool uses GNU-style long options in modern Java. Understanding the common options and how multi-release JARs work is tested on the exam.
// Creating a JAR from a directory of compiled classes
jar --create --file app.jar -C classes .
// Creating a JAR with an explicit manifest file
jar --create --file app.jar --manifest MANIFEST.MF -C classes .
// Creating an executable JAR with a main class
// The main class is written to the MANIFEST.MF Main-Class attribute automatically
jar --create --file app.jar --main-class com.example.Main -C classes .
// Running an executable JAR
java -jar app.jar
// Listing the contents of a JAR without extracting
jar --list --file app.jar
jar -tf app.jar // Short form: t = list, f = file
// Extracting a JAR
jar --extract --file app.jar
jar --extract --file app.jar com/example/Main.class // Extract a specific file
// Updating an existing JAR (adds or replaces files)
jar --update --file app.jar -C newclasses .
// Multi-release JAR (Java 9+)
// A single JAR can contain different class implementations for different Java versions.
// The JVM automatically uses the version-specific class if the running JVM qualifies.
// Older JVMs ignore the META-INF/versions directory and use the base classes.
jar --create --file app.jar \
-C classes . \ // Base classes (compatible with Java 8 and above)
--release 11 -C classes11 . // Java 11-specific versions of the same classes
// Internal structure of a multi-release JAR:
// app.jar
// META-INF/
// MANIFEST.MF // Must contain: Multi-Release: true
// versions/
// 11/
// com/example/Helper.class // Used when running on Java 11 or later
// com/example/
// Main.class // Used by all Java versions
// Helper.class // Default version - used on Java 8 and 9-10
// Signing a JAR (for security verification)
// jarsigner -keystore keystore.jks -signedjar signed.jar app.jar myalias
Exam Tip: For a multi-release JAR to work, the MANIFEST.MF must contain the entry
Multi-Release: true. Without it, the JVM ignores the META-INF/versions/ directory entirely and always uses the base classes. Also note that the class in the versioned directory must have the same fully qualified name as the base class it replaces - you cannot add new public types in a versioned directory.
1Z0-830 Java SE 21 Certification - Table of Contents
Master all exam topics with comprehensive study guides and practice examples.
Popular Posts
1Z0-830 Java SE 21 Developer Certification
Azure AI Foundry Hello World
Azure AI Agent Hello World
Foundry vs Hub Projects
Build Agents with SDK
Bing Web Search Agent
Function Calling Agent
Spring Boot + Azure Key Vault Hello World Example
Spring Boot + Elasticsearch + Azure Key Vault Example
Spring Boot Azure AD (Entra ID) OAuth 2.0 Authentication
Deploy Spring Boot App to Azure App Service
Secure Azure App Service using Azure API Management
Deploy Spring Boot JAR to Azure App Service
Deploy Spring Boot + MySQL to Azure App Service
Spring Boot + Azure Managed Identity Example
Secure Spring Boot Azure Web App with Managed Identity + App Registration
Elasticsearch 8 Security - Integrate Azure AD OIDC