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.javawithrequires,requires transitive,requires static,exports,exports ... to,opens,usesandprovides ... with. - Distinguish named (explicit), automatic and unnamed modules, and the class path from the module path.
- Compile, package and run modular code with
javac,jarandjavausing--module-path,--moduleand--module-source-path. - Inspect and migrate code with
java --list-modules,--describe-module,jar --describe-moduleandjdeps. - 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.baseis always required implicitly. You never writerequires java.base;(it is allowed, but redundant).exportsapplies to one package only, never its subpackages: exportingcom.acmedoes not exportcom.acme.api.exportsmakes public types accessible at compile time and run time.opensgrants run-time reflective access (includingsetAccessible(true)on private members) but not compile-time access. A package can be both exported and opened.- An
open modulecannot also containopensdirectives (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. Addrequires Cto A, or declarerequires transitive Cin 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-Nameif present. Otherwise the name derives from the file name: the.jarextension and the version suffix are removed, and every non-alphanumeric character becomes a dot. For examplecommons-text-1.10.0.jarbecomescommons.text. Because file names are fragile, library authors should setAutomatic-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)throwsServiceConfigurationErrorat run time. - The provider module declares
provides Greeter with Hello;. The implementation class needs a public no-argument constructor, or apublic 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). Forjlink, however, you must include the provider explicitly or use--bind-services(see below). ServiceLoaderfinds 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
jlinkmust contain the JDKjmodsdirectory plus your modules. jlinkaccepts only named modules; an automatic module makes it fail. This is why libraries must be modularized (or wrapped) before creating an image.jlinkresolvesrequiresbut does not add service providers by default. In the example,com.acme.implappears in the image only because it was listed in--add-modules; use--bind-servicesto include providers automatically.- Useful options:
--launcher name=module/classcreates a launcher script,--compressreduces the size,--strip-debugremoves debug information. jmodcreates.jmodfiles (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 theMain-Classmanifest attribute;java -jar app.jarthen uses it. With-jar, the-cpoption is ignored; dependencies must be listed in the manifestClass-Pathattribute.- Package names map to directories: class
com.acme.Mainmust be incom/acme/Main.class.javac -d outcreates these directories. - A multi-release JAR holds version-specific classes under
META-INF/versions/N/, selected by the running Java version. - An
importis 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).
exportsis per package;exports com.acmedoes not covercom.acme.util.requiresis not transitive by default; userequires transitivewhen 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.javais not a regular class: nopackagedeclaration, it must be at the root, andimportstatements are allowed only to simplify names in directives.usesis required forServiceLoader.load;provides ... withalone does not allow the provider module to consume the service.jlinkrejects automatic modules and does not bind services unless asked.- The short flags are
-pand-mforjava;javacuses-ptoo, but--module-source-pathhas 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.
ship.coredefines the interfaceCarrierin the exported packageship.core.spiand the entities inship.core.model. The descriptor ismodule ship.core { exports ship.core.spi; exports ship.core.model; opens ship.core.model to com.fasterxml.jackson.databind; }. Each plugin ismodule ship.fedex { requires ship.core; provides ship.core.spi.Carrier with ship.fedex.FedexCarrier; }and the CLI ismodule ship.cli { requires ship.core; uses ship.core.spi.Carrier; }. The CLI never requires a plugin; it loads them withServiceLoader.load(Carrier.class). Adding a carrier means adding a JAR to the module path.geo-utils-2.3.jargoes on the module path as an automatic module namedgeo.utils(file name minus the version). Modules that need it declarerequires geo.utils;. The better long-term fix is a library release that setsAutomatic-Module-Nameor ships a real descriptor.jlinkrefuses automatic modules, so eithergeo.utilsmust be modularized (for example withjdeps --generate-module-infoand a repackaged JAR), or the team ships the application as a regular module-path deployment on a full JDK. For the image they would calljlink --module-path mods:$JAVA_HOME/jmods --add-modules ship.cli,ship.fedex,ship.ups --output imageand, 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 byrequires 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,usesandprovides withcorrectly. - I know
java.baseis 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 withandServiceLoader. - I know when
InaccessibleObjectExceptionoccurs and howopensfixes it. - I can use
jdeps -s,--print-module-depsandjlink --add-modules --output. - I know
jlinkrejects automatic modules and needs--bind-servicesfor providers.
Official sources
- Oracle exam page 1Z0-830 (objectives): https://education.oracle.com/java-se-21-developer-professional/pexam_1Z0-830 (opens in a new tab)
- Java SE 21 documentation, module system: https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html (opens in a new tab)
- Java Language Specification SE 21, modules (chapter 7.7): https://docs.oracle.com/javase/specs/jls/se21/html/jls-7.html#jls-7.7 (opens in a new tab)
jlinktool reference: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jlink.html (opens in a new tab)jdepstool reference: https://docs.oracle.com/en/java/javase/21/docs/specs/man/jdeps.html (opens in a new tab)ServiceLoaderAPI: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ServiceLoader.html (opens in a new tab)