Search Tutorials


1Z0-830 Java SE 21 - Modules and Packaging | JavaInUse

1Z0-830 Modules and Packaging - Java SE 21 Certification Prep

The Java Platform Module System (JPMS), introduced in Java 9, adds a new layer of encapsulation and dependency management above packages. A module is a named, self-describing group of packages. The 1Z0-830 exam tests your ability to write and read 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)

The module-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 a module-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 with uses; 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. The jar 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