Java module-info.java Declaration
Declare a Java module with module-info.java — requires, exports, opens, uses, provides.
Java module-info.java Declaration
A module is declared in one special source file, module-info.java, placed at the root of the module's source tree (next to the top package, not inside it). It compiles to module-info.class. The file contains no ordinary code — only a module block listing directives that describe the module's boundary.
The shape of the file
module com.acme.orders {
requires com.acme.common; // I depend on this module
requires transitive java.sql; // ...and so does anyone who requires me
exports com.acme.orders.api; // public to everyone
exports com.acme.orders.spi to com.acme.web; // public to one module only
opens com.acme.orders.model; // deep reflective access (e.g. for Jackson)
uses com.acme.orders.PricingRule; // I consume this service
provides com.acme.orders.PricingRule // I supply an implementation
with com.acme.orders.StandardPricing;
}The module name (com.acme.orders) is a dotted identifier, conventionally the reverse-DNS prefix of the packages it contains. It is not a package — it is its own namespace, and two modules may not share a package.
requires — declaring dependencies
requires <module> says "I need that module's exported packages to compile and run." The resolver fails at startup if a required module is missing. Two important modifiers:
requires transitive— re-exports the dependency. Any module that requires you automatically reads it too. Use it when a required module's types appear in your own public signatures (a method returning ajava.sql.Connectionforces callers to seejava.sql).requires static— a compile-time-only dependency, optional at runtime (for annotation processors, optional integrations).
java.base is required implicitly; you never write it.
exports — declaring your public API
exports <package> makes that package's public types visible to other modules. Everything not exported is strongly encapsulated — invisible even though public. The qualified form, exports <package> to <module>, <module>, narrows visibility to a named allow-list, useful for SPI packages shared only between your own modules.
Note exports is per-package, never recursive: exporting com.acme.api does not export com.acme.api.internal.
opens — allowing deep reflection
exports grants compile-time access to public members. It does not grant reflective access to non-public members. Frameworks like Jackson, Hibernate, and Spring use setAccessible(true) to reach private fields — that needs opens:
opens <package>— grants runtime reflective access (including toprivatemembers) to all modules.opens <package> to <module>— qualified, to named modules only.open module com.acme.orders { … }— opens every package (a blunt migration aid).
The split matters: you exports an API package, but you opens a package of data classes you want a serializer to reflect over without making it part of your compile-time API.
uses / provides — services
These wire up the ServiceLoader pattern: uses <Service> declares you consume a service interface, and provides <Service> with <Impl> declares an implementation. They get their own chapter; here just note they live in the same descriptor.
A worked example: building a descriptor with the API
You normally write module-info.java and let javac produce the descriptor. But the same structure is available programmatically through ModuleDescriptor.newModule(...), which is a faithful mirror of the directive syntax — building one is the clearest way to see what each directive becomes.
What to take from the run:
- The builder method names line up one-to-one with the directives:
.requires(...),.exports(...),.opens(...),.uses(...),.provides(...). Reading the output back, the descriptor is exactly the information in amodule-info.java— proof that the file is pure metadata, not executable code. - The
java.sqlrequire printed with a[TRANSITIVE]modifier whilecom.acme.commonprinted with none. That modifier is what re-exportsjava.sqlto downstream modules; the plain require keeps the dependency private to this module. - The two exports printed differently:
com.acme.orders.apias "(to all)" andcom.acme.orders.spias "to [com.acme.web]". A qualified export carries its target allow-list inside the descriptor — the resolver enforces it, so no other module can read the SPI package. opensappeared in its own section, separate fromexports. The descriptor keeps compile-time exposure and runtime reflective access as distinct facts, which is why a serializer needsopenseven when the package is already exported.usesandprovidesare recorded right alongside the rest — the service declarations are part of the module boundary, not a separate configuration file as they were on the classpath (META-INF/services). The next-but-one chapter turns these directives into a workingServiceLoader.
Common mistakes
- Putting
module-info.javainside a package directory — it must sit at the source root. - Confusing
exports(compile-time, public members) withopens(runtime, all members). AJsonMappingExceptionabout an inaccessible field almost always means a missingopens. - Forgetting
requires transitivewhen your public methods expose another module's types, forcing every caller to add the require manually.
The next chapter steps back to the three kinds of module — named, automatic, and unnamed — and how a half-modularized application keeps working while you migrate.
Practice
A module 'com.acme.orders' has a public method that returns a 'java.sql.Connection'. Callers in other modules keep failing to compile because they cannot see the 'java.sql.Connection' type, even though they already 'requires com.acme.orders'. What directive in 'com.acme.orders' fixes this without forcing every caller to add 'requires java.sql' themselves?