Skip to content

Packaging and Deploying Java Code

Step 13 of 20

Packaging and Deploying Java Code

Introduction

This lesson covers the seventh objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Packaging and Deploying Java Code. In practice this means the Java Platform Module System (JPMS, introduced in Java 9), the command-line tools that build and inspect modules (javac, jar, java, jdeps, jlink) and the service mechanism (ServiceLoader). Oracle does not publish a weighting per objective, but questions in this area follow a pattern: a short module-info.java or a shell command is shown, and you must decide whether it compiles, runs or fails, and why.

The topic feels abstract until you have seen one working example, so the lesson builds a small three-module application (com.acme.api, com.acme.impl, com.acme.app) and uses it throughout. All commands in this lesson were run on a Java 21 JDK.

Learning objectives

After this lesson you will be able to:

  • Write a module-info.java with requires, requires transitive, requires static, exports, exports ... to, opens, uses and provides ... with.
  • Distinguish named (explicit), automatic and unnamed modules, and the class path from the module path.
  • Compile, package and run modular code with javac, jar and java using --module-path, --module and --module-source-path.
  • Inspect and migrate code with java --list-modules, --describe-module, jar --describe-module and jdeps.
  • Build a custom runtime image with jlink.
  • Implement and consume a service with ServiceLoader.
  • Explain what strong encapsulation allows for reflection.

1. Why modules

Before Java 9, every public class in every JAR on the class path was visible to every other class. Modules add strong encapsulation (a package is hidden unless exported) and reliable configuration (the JVM checks at startup that every required module is present, with no cycles). The JDK itself is modular: java.base (always read by every module, including java.lang, java.util, java.io), java.sql, java.logging, java.desktop and so on. java --list-modules prints the modules in your runtime.

A module is a set of packages plus a descriptor, module-info.java, placed at the root of the source tree. It compiles to module-info.class at the root of the JAR.

2. The module descriptor

Here are the three descriptors of our example application:

// com.acme.api/module-info.java
module com.acme.api {
    exports com.acme.api;
}

// com.acme.impl/module-info.java
module com.acme.impl {
    requires com.acme.api;
    provides com.acme.api.Greeter with com.acme.impl.Hello;
}

// com.acme.app/module-info.java
module com.acme.app {
    requires com.acme.api;
    uses com.acme.api.Greeter;
}

The directives you must know:

Directive Meaning
requires m; This module reads m. Needed at compile time and run time.
requires transitive m; Same, and every module that requires this one also reads m ("implied readability").
requires static m; m is required at compile time only; optional at run time.
exports p; The public types of package p are accessible to all modules.
exports p to m1, m2; Qualified export: only the named modules can access p.
opens p; Deep reflection (including private members) on package p at run time.
opens p to m; Qualified opens.
uses S; This module consumes service interface S through ServiceLoader.
provides S with C; This module supplies implementation class C for service S.
open module m { } Opens every package for reflection at run time.

Rules that are tested again and again:

  • java.base is always required implicitly. You never write requires java.base; (it is allowed, but redundant).
  • exports applies to one package only, never its subpackages: exporting com.acme does not export com.acme.api.
  • exports makes public types accessible at compile time and run time. opens grants run-time reflective access (including setAccessible(true) on private members) but not compile-time access. A package can be both exported and opened.
  • An open module cannot also contain opens directives (it is redundant, so the compiler rejects it).
  • Module dependencies cannot be cyclic. If A requires B, B cannot require A, directly or indirectly.
  • Two modules in the module graph cannot contain the same package (split package error).
  • A module name usually follows the reverse-domain convention; the module source folder must match the module name when you use --module-source-path.
  • A readable module is not enough: if A requires B and B requires C (without transitive), code in A cannot use the types of C. Add requires C to A, or declare requires transitive C in B.

3. Compiling, packaging and running

With one folder per module (the folder names equal the module names), the whole example compiles in one command:

javac -d out --module-source-path . $(find . -name '*.java')
# out/com.acme.api, out/com.acme.impl, out/com.acme.app now hold the classes

mkdir mods
jar --create --file mods/api.jar  -C out/com.acme.api  .
jar --create --file mods/impl.jar -C out/com.acme.impl .
jar --create --file mods/app.jar  -C out/com.acme.app  .

Run it by naming the module path and the main class as module/class:

java --module-path mods --module com.acme.app/com.acme.app.Main
# Hello Ana
# com.acme.app true

Short forms exist: -p for --module-path, -m for --module. If the JAR declares a main class (jar --create --file app.jar --main-class com.acme.app.Main -C ...), java -p mods -m com.acme.app is enough. Do not confuse this with java -jar app.jar, which starts the JAR on the class path (unnamed module).

When you compile one module whose dependencies are already packaged, use javac -d out --module-path mods $(find src -name '*.java'). The compiler needs module-info.java among the files. Note the difference between the two flags: --module-source-path points to sources of several modules, --module-path points to compiled modules (JARs or exploded directories).

Useful switches for inspection:

java -p mods --list-modules                       # modules available (includes the JDK ones)
java -p mods --describe-module com.acme.impl      # requires, provides, contains
jar --describe-module --file mods/app.jar         # same, for a JAR

The output for the implementation module is:

com.acme.impl file:///.../mods/impl.jar
requires com.acme.api
requires java.base mandated
provides com.acme.api.Greeter with com.acme.impl.Hello
contains com.acme.impl

The word mandated marks the implicit dependency on java.base.

4. Named, automatic and unnamed modules

Kind Where it comes from Reads Exports
Named (explicit) module A JAR with module-info.class on the module path Only what it requires (plus java.base) Only what it exports
Automatic module A plain JAR (no descriptor) on the module path Every other module, including the unnamed module All its packages
Unnamed module Everything on the class path Every module All its packages

Key consequences:

  • A named module cannot read the unnamed module. Code on the class path is invisible to requires, because the unnamed module has no name to require.
  • The unnamed module can read all named modules that are resolved (the root modules and their dependencies). Old applications therefore still run on the class path.
  • An automatic module gets its name from the manifest attribute Automatic-Module-Name if present. Otherwise the name derives from the file name: the .jar extension and the version suffix are removed, and every non-alphanumeric character becomes a dot. For example commons-text-1.10.0.jar becomes commons.text. Because file names are fragile, library authors should set Automatic-Module-Name.
  • Running the example on the class path shows the difference:
java -cp mods/api.jar:mods/impl.jar:mods/app.jar com.acme.app.Main
# null false

Main.class.getModule().getName() is null and isNamed() is false: everything is in the unnamed module. The line Hello Ana is also missing, because on the class path ServiceLoader finds providers only through META-INF/services files, not through provides directives. That is a typical "works on the module path, silently does nothing on the class path" question.

Migration strategies. Bottom-up: modularize the libraries with no dependencies first, then those that depend on them; unmodularized dependencies are placed on the module path as automatic modules. Top-down: start from the application, place everything else on the class path or as automatic modules, and modularize the remaining pieces gradually. Both approaches use jdeps to discover dependencies.

5. Services with ServiceLoader

A service is an interface (or abstract class) with provider implementations discovered at run time. This decouples the consumer from the implementations: the consumer module never requires the provider module.

// consumer, module com.acme.app
ServiceLoader<Greeter> loader = ServiceLoader.load(Greeter.class);
for (Greeter g : loader) {
    System.out.println(g.greet("Ana"));          // Hello Ana
}
loader.findFirst().ifPresent(g -> ...);           // first provider, as Optional
loader.stream().map(ServiceLoader.Provider::get); // lazy: types can be inspected before instantiation

Rules to remember:

  • The consumer module must declare uses Greeter;. Without it, ServiceLoader.load(Greeter.class) throws ServiceConfigurationError at run time.
  • The provider module declares provides Greeter with Hello;. The implementation class needs a public no-argument constructor, or a public static provider() method returning the instance.
  • The provider package does not need to be exported: the service is reachable through the interface.
  • The provider module must be resolved (present in the module graph). Putting it on the module path is not enough unless something causes it to be resolved: when the consumer has uses, the module system adds providers found on the module path to the graph (service binding). For jlink, however, you must include the provider explicitly or use --bind-services (see below).
  • ServiceLoader finds zero providers without error if none are present; the iterator is simply empty.
  • On the class path, providers are declared in META-INF/services/<fully qualified service name> files.

6. Reflection and strong encapsulation

Since Java 17 (JEP 403) the JDK strongly encapsulates its internals, and the same rule applies to your own modules. For a reflective call such as field.setAccessible(true) on a private field of class p.C from another module, the package p must be opened to the caller (or the module must be open). Exporting is not enough:

module com.shop.model {
    exports com.shop.model;                      // other modules can compile against public types
    opens com.shop.model to com.fasterxml.jackson.databind;   // only the JSON library can reflect deeply
}

If the package is exported but not opened, field.get(obj) on a private field throws InaccessibleObjectException (a RuntimeException) at run time, while public members are still reachable by reflection. The command-line escape hatches --add-exports, --add-opens and --add-reads exist for legacy code; they do not replace proper descriptors.

7. jdeps and jlink

jdeps analyzes dependencies of compiled code:

jdeps -s --module-path mods mods/app.jar          # summary
# com.acme.app -> com.acme.api
# com.acme.app -> java.base

jdeps --print-module-deps app.jar                  # comma-separated JDK modules needed, input for jlink
jdeps --generate-module-info out/ app.jar          # draft module-info.java for a non-modular JAR

jlink assembles a custom runtime image containing only the modules you need, which shrinks size and attack surface:

jlink --module-path mods:$JAVA_HOME/jmods \
      --add-modules com.acme.app,com.acme.impl \
      --output rt
rt/bin/java --list-modules
# com.acme.api
# com.acme.app
# com.acme.impl
# java.base@21.0.12.1

Important points:

  • The module path for jlink must contain the JDK jmods directory plus your modules.
  • jlink accepts only named modules; an automatic module makes it fail. This is why libraries must be modularized (or wrapped) before creating an image.
  • jlink resolves requires but does not add service providers by default. In the example, com.acme.impl appears in the image only because it was listed in --add-modules; use --bind-services to include providers automatically.
  • Useful options: --launcher name=module/class creates a launcher script, --compress reduces the size, --strip-debug removes debug information.
  • jmod creates .jmod files (a packaging format for jlink input and native code); it cannot be used on the regular module path at run time.

8. Non-modular packaging reminders

Not all questions concern modules. Remember the basics of JAR packaging:

  • jar --create --file app.jar --main-class com.acme.Main -C classes . writes the Main-Class manifest attribute; java -jar app.jar then uses it. With -jar, the -cp option is ignored; dependencies must be listed in the manifest Class-Path attribute.
  • Package names map to directories: class com.acme.Main must be in com/acme/Main.class. javac -d out creates these directories.
  • A multi-release JAR holds version-specific classes under META-INF/versions/N/, selected by the running Java version.
  • An import is a compile-time convenience only; it has no run-time effect and never loads a class.

Worked example: implied readability

Statement. Three modules are defined as follows. Does module reports compile?

module orders  { exports com.orders; }                       // contains public class Order
module billing { requires orders; exports com.billing; }     // com.billing.Invoice has a public method Order order()
module reports { requires billing; }                         // Report.java calls invoice.order().getId()

Step 1. reports reads billing and may use com.billing.Invoice.

Step 2. The call invoice.order() returns an Order, a type from orders. To use Order, even implicitly through a method chain, reports must read orders.

Step 3. reports does not require orders, and billing uses a plain requires orders, so readability is not implied.

Solution. The code does not compile: Order is not accessible because module reports does not read module orders. The best fix is requires transitive orders; in billing, because billing exposes Order in its public API.

Why the other choices are worse. Changing the exports of orders does not help: com.orders is already exported to everyone, so accessibility is fine and readability is the missing piece. Adding requires orders; in reports works but must be repeated in every consumer of billing. Using opens is irrelevant: it concerns reflection at run time.

Second worked example: reflection and opens

Statement. A mapper library in module lib.mapper runs this code on an object of class com.shop.model.Product, which belongs to module com.shop.model:

Field f = Product.class.getDeclaredField("price");   // private double price
f.setAccessible(true);
double price = f.getDouble(product);

The descriptor of com.shop.model is module com.shop.model { exports com.shop.model; }. What happens, and what is the minimum fix?

Step 1. getDeclaredField succeeds: retrieving the field object does not check access.

Step 2. setAccessible(true) on a private member needs deep reflection, which requires the package to be opened to the caller module (or to all modules).

Step 3. exports does not provide that permission.

Solution. setAccessible throws InaccessibleObjectException. The minimum fix is opens com.shop.model to lib.mapper; in com.shop.model (opening to a named module is preferable to opening to everyone).

Why the other choices fail. Changing the field to public breaks encapsulation. requires lib.mapper in the model module reverses the dependency and does not grant reflection access. --add-opens on the command line works but hides the requirement outside the code and is meant as a temporary workaround.

Common pitfalls and exam tips

  • Exam tip: for each question, first check the readability (who requires whom), then the accessibility (what is exported), and only then the reflection (what is opened).
  • exports is per package; exports com.acme does not cover com.acme.util.
  • requires is not transitive by default; use requires transitive when your API exposes a type from the dependency.
  • Cyclic dependencies and split packages are errors, even between a named module and a JAR on the module path.
  • A named module cannot depend on code from the class path. Put such JARs on the module path as automatic modules.
  • module-info.java is not a regular class: no package declaration, it must be at the root, and import statements are allowed only to simplify names in directives.
  • uses is required for ServiceLoader.load; provides ... with alone does not allow the provider module to consume the service.
  • jlink rejects automatic modules and does not bind services unless asked.
  • The short flags are -p and -m for java; javac uses -p too, but --module-source-path has no short form.

Mini case study: modularizing the shipping application

Scenario. A shipping application is split into ship.core (domain types), ship.fedex and ship.ups (carrier plugins) and ship.cli (the executable). The team wants plugins to be swappable without recompiling the CLI. They also use a library geo-utils-2.3.jar that has no descriptor, and a Jackson-style JSON library that reflects on ship.core entities.

Questions. (1) How should the descriptors of ship.core, ship.fedex and ship.cli look? (2) How is geo-utils-2.3.jar handled? (3) What must be done to create a jlink image?

Solution.

  1. ship.core defines the interface Carrier in the exported package ship.core.spi and the entities in ship.core.model. The descriptor is module ship.core { exports ship.core.spi; exports ship.core.model; opens ship.core.model to com.fasterxml.jackson.databind; }. Each plugin is module ship.fedex { requires ship.core; provides ship.core.spi.Carrier with ship.fedex.FedexCarrier; } and the CLI is module ship.cli { requires ship.core; uses ship.core.spi.Carrier; }. The CLI never requires a plugin; it loads them with ServiceLoader.load(Carrier.class). Adding a carrier means adding a JAR to the module path.
  2. geo-utils-2.3.jar goes on the module path as an automatic module named geo.utils (file name minus the version). Modules that need it declare requires geo.utils;. The better long-term fix is a library release that sets Automatic-Module-Name or ships a real descriptor.
  3. jlink refuses automatic modules, so either geo.utils must be modularized (for example with jdeps --generate-module-info and a repackaged JAR), or the team ships the application as a regular module-path deployment on a full JDK. For the image they would call jlink --module-path mods:$JAVA_HOME/jmods --add-modules ship.cli,ship.fedex,ship.ups --output image and, if they want the providers selected automatically, add --bind-services.

Key terms

  • Module: a named set of packages with a descriptor declaring its dependencies and exports.
  • Module path: the location searched for modules, as opposed to the class path.
  • Readability: the relation "module A can use module B", created by requires (or implied by requires transitive).
  • Strong encapsulation: only exported packages are accessible; only opened packages allow deep reflection.
  • Automatic module: a plain JAR on the module path, with a derived name and open access to everything.
  • Unnamed module: the module holding all class-path code.
  • Service / provider: an interface and an implementation discovered with ServiceLoader.
  • jlink: the tool that builds a custom runtime image from modules.
  • jdeps: the tool that lists the dependencies of classes and JARs.

Summary checklist

  • I can write requires, requires transitive, requires static, exports, exports to, opens, uses and provides with correctly.
  • I know java.base is implicit and that exports are per package.
  • I can tell the difference between readability, accessibility and reflection access.
  • I can use javac --module-source-path, jar, java -p ... -m module/class.
  • I know the three kinds of modules and how an automatic module name is derived.
  • I know a named module cannot read the class path.
  • I can implement a service with uses, provides with and ServiceLoader.
  • I know when InaccessibleObjectException occurs and how opens fixes it.
  • I can use jdeps -s, --print-module-deps and jlink --add-modules --output.
  • I know jlink rejects automatic modules and needs --bind-services for providers.

Official sources