12.2 Module Directives: `requires`, `requires transitive`, `exports`, and Qualified Exports
Key Takeaways
Every module implicitly depends on java.base; declaring 'requires java.base;' is legal but redundant, while all other dependencies must be explicitly declared with 'requires'.
The 'requires transitive' directive implements implied readability, propagating module readability to downstream consumers and preventing compilation failures when public APIs expose third-party types.
The
exports <package>;directive takes a package name (never a module name) and grants compile-time and runtime accessibility to public and protected members of public types in that package.Qualified exports (
exports <package> to <module>;) restrict package accessibility to designated friend modules, encapsulating sensitive APIs against unauthorized consumers.Split packages—where two or more modules contain or export types in the identical package name—are strictly prohibited by the JPMS and trigger compile-time or runtime startup failures.
Module Directives: requires, requires transitive, exports, and Qualified Exports
Module declarations configure the access control boundaries of modular applications through explicit directives. In JPMS, accessibility across module boundaries requires two independent conditions to be satisfied: readability (the consuming module must declare that it reads the target module) and accessibility (the target module must export the specific package containing the referenced type). Understanding the exact syntax, propagation semantics, and restrictions of these directives is essential for modular design.
The requires Directive and java.base
The requires directive specifies that the declaring module depends on another named module at compile time and runtime:
module com.example.orders {
requires com.example.inventory;
requires java.sql;
}
By declaring requires com.example.inventory;, the com.example.orders module gains readability to com.example.inventory. Readability means the types in the exported packages of com.example.inventory become visible to code within com.example.orders.
The Universal java.base Dependency
The java.base module is the foundational root of the Java module graph. It exports core packages such as java.lang, java.util, java.io, java.nio, and java.net. Every module in the Java ecosystem implicitly requires java.base without needing an explicit declaration.
module com.example.demo {
// Implicitly present: requires java.base;
}
Note
Explicitly declaring requires java.base; in your module-info.java is legal but redundant. Adding a modifier to it, as in requires static java.base; or requires transitive java.base;, is a compile-time error (javac: modifier static not allowed here).
Optional Dependencies with requires static
The static modifier on a requires directive establishes a compile-time-only dependency (analogous to "provided" dependency scope in build automation tools):
module com.example.analytics {
requires static com.optional.profiler;
}
With requires static, the module compiles successfully against com.optional.profiler. At runtime, the presence of com.optional.profiler on the module-path is optional. If present, the module reads it; if absent, the JVM launches without error. However, if the application attempts to load a class from com.optional.profiler when it is not present on the module-path at runtime, a NoClassDefFoundError is thrown.
Implied Readability with requires transitive
By default, readability in JPMS is strict and non-transitive. If Module A requires Module B, and Module B requires Module C, Module A does not automatically read Module C.
[Module A] ---> requires ---> [Module B] ---> requires ---> [Module C]
(Module A cannot access exported types from Module C!)
This isolation presents a problem when a library exposes types from an underlying dependency in its own public API. Consider the following scenario:
// Module B: com.data.repository
package com.data.repository;
import java.sql.Connection; // from java.sql
public class DatabaseManager {
public Connection getConnection() { ... } // Returns java.sql.Connection
}
If Module A requires com.data.repository and invokes DatabaseManager.getConnection(), Module A must inspect and assign the returned Connection instance. Because Connection belongs to java.sql, Module A must read java.sql. Without implied readability, every developer consuming com.data.repository would be forced to manually add requires java.sql; to their own module descriptors.
The "Leaked Type" Rule
To solve this, JPMS provides requires transitive. When a module declares requires transitive, any module that requires the declaring module implicitly reads the transitive module as well:
module com.data.repository {
requires transitive java.sql; // Implied readability granted downstream
exports com.data.repository;
}
Now, any consumer module can use types from java.sql without an explicit requires java.sql; directive:
module com.app.client {
requires com.data.repository; // Automatically reads java.sql via implied readability!
}
Warning
The Leaked Type Exam Rule
Whenever a module exposes a type from a dependent module in any of its public API signatures—such as a method return type, method parameter, public field type, or public superclass/interface—it must declare that dependency with requires transitive. Failing to do so causes compilation errors in consumer modules attempting to call those methods or reference those types.
The exports Directive
While requires establishes readability between modules, the exports directive establishes accessibility to specific packages within a module.
module com.example.crypto {
exports com.example.crypto.api;
exports com.example.crypto.ciphers;
}
Rules and Nuances of exports
- Package Target Syntax: The
exportsdirective names a package of the current module, never a module or a class. Writingexports com.example.crypto.MyCipher;(a class name) fails because no such package exists in the module (javac:package is empty or does not exist); the same error appears if you export a package that contains no classes. - Public and Protected Elements Only: Exporting a package exposes only
publictypes, andprotectedmembers to subclasses. Package-private and private members remain inaccessible outside the module. - Subpackages Are Not Exported Automatically: Exporting a parent package does not export its child packages. For example,
exports com.example.crypto;does NOT exportcom.example.crypto.ciphersorcom.example.crypto.internal. Every subpackage containing public APIs must be individually exported. - Unexported Packages Remain Strongly Encapsulated: Any package not declared with an
exportsdirective is internal to the module. Even if a class within an unexported package is markedpublic, no code outside that module can compile against or access it.
Qualified Exports (exports ... to)
In enterprise systems and large frameworks, certain internal utility packages need to be shared across a trusted set of "friend" modules without being exposed to general consumers. JPMS accommodates this pattern through qualified exports:
module com.core.framework {
// Standard public API exported to all consumers:
exports com.core.framework.api;
// Internal SPI exported ONLY to designated partner modules:
exports com.core.framework.internal to com.core.security, com.core.testsuite;
}
Semantics of Qualified Exports
- Only the modules explicitly listed in the
toclause (separated by commas) gain access to the public types incom.core.framework.internal. - Any other module that declares
requires com.core.framework;cannot import or access types fromcom.core.framework.internal. - If a listed target module does not exist at compile time, the compiler issues a warning (not an error), ensuring decoupling during partial builds.
The Split Packages Prohibition
A split package occurs when the same package name is defined across two or more distinct modules (for example, if both module-alpha and module-beta contain classes residing in package com.enterprise.common).
module com.alpha {
package com.enterprise.common; // Class CommonA
}
module com.beta {
package com.enterprise.common; // Class CommonB
}
In the legacy classpath world, classloaders merged classes from duplicate packages across multiple JARs into a single namespace. Under JPMS, split packages are strictly illegal on the module-path.
If two modules on the module-path contain or export the same package namespace:
- At compile time: a module that reads both modules fails with an error such as
module com.client reads package com.enterprise.common from both com.alpha and com.beta. - At JVM startup: Module layer resolution detects the conflict and throws
java.lang.module.ResolutionExceptionorjava.lang.LayerInstantiationException.
Warning
No Split Packages Allowed Split packages are prohibited even if the two modules define completely different, non-conflicting class names within that package, and even if neither module exports the package! Every package in a modular runtime must belong uniquely and unambiguously to exactly one module.
Module store.inventory contains the following declaration:
module store.inventory {
requires catalog.api;
exports com.store.inventory;
}
Inside com.store.inventory, the public class Warehouse contains the public method:
public com.catalog.model.Product getProduct(int id) { ... }
A consumer module, store.client, declares requires store.inventory; and invokes warehouse.getProduct(101). What occurs when attempting to compile store.client?
Compilation fails because store.inventory must declare a qualified export of com.store.inventory to store.client.
Compilation succeeds because public classes in exported packages automatically grant access to all referenced types.
Compilation succeeds, but invoking getProduct(101) throws a NoClassDefFoundError at runtime.
Compilation fails in store.client unless store.inventory declares requires transitive catalog.api; or store.client explicitly declares requires catalog.api;.
Which of the following is a valid directive inside a module-info.java declaration?
requires package com.corp.orders to com.corp.billing, com.corp.shipping;
exports com.corp.orders., com.corp.orders.api.;
exports com.corp.orders to com.corp.billing, com.corp.shipping;
exports module com.corp.orders to com.corp.billing;
What happens if two distinct modular JARs placed on the module-path contain classes in the exact same package name com.enterprise.common?
The JVM merges the contents of both packages into a single unified runtime namespace.
The application executes normally unless classes in com.enterprise.common attempt to access package-private members across modules.
The JVM rejects the configuration at compile time or application startup due to a split package violation.
The modular JAR appearing first on the module-path takes precedence, while the second is silently shadowed.
Sections you finish are checked off in the contents.