classpath to module path
unnamed modules, automatic modules, and migrating for real.
- chapter
- chapter 12.03
- reading time
- 8 min read
the class path and the module path live side by side. everything on the class path becomes one big unnamed module that reads everything. a plain jar on the module path becomes an automatic module, named after its file. jars with a module-info are named modules.
you can migrate one jar at a time, which is exactly why java 9 didn't break the world.
the picture
think of moving house. the class path is the old flat: everything in one room, every box open, you can grab anything from anywhere. the module path is the new house with labelled rooms and doors. you don't have to move every box on day one. some boxes go into proper rooms (named modules), some get dumped in the hallway with a sticky note on them (automatic modules), and the old flat stays rented for whatever's left (the unnamed module).
that one red box explains most migration pain. the moment you write a module-info.java, every dependency you use has to be on the module path too, named or automatic, because named modules don't read the unnamed module. ever.
your own code can tell you which module it's in:
void main() {
Module mine = getClass().getModule();
Module base = String.class.getModule();
IO.println(base);
IO.println(mine.isNamed());
IO.println(mine.canRead(base));
IO.println(base.canRead(mine));
}module java.base false true false
automatic modules and their names
an automatic module is a bridge. it's a plain jar that gets treated as a module: it exports and opens every package (it has no idea which ones are internal), it reads every other module, and it gets a name so named modules can requires it. that name comes from the manifest, or failing that, from the jar's file name.
- 1. drop .jarcommons-lang3-3.17.0
- 2. cut the versioncommons-lang3the first "-" followed by a digit starts the version: 3.17.0
- 3. non-alphanumerics → .commons.lang3
- 4. squash dotscommons.lang3
jar --describe-module on jdk 25, including the two failures.see the problem? the name of a module your code requires is now an accident of how someone named a file. the library ships 1.0 as acme-json-1.0.jar (name acme.json), then publishes a real module-info named com.acme.json, and your requires acme.json; stops compiling. that's why library authors add one line to their manifest long before they modularize:
Automatic-Module-Name: com.acme.jsonit costs nothing, it doesn't make the jar a module, and it reserves the name forever.
two ways to migrate
there are two classic orders. bottom-up starts at the jars that depend on nothing and works upwards. top-down throws everything onto the module path as automatic modules first, then converts from the top. step through both.
the starting point. five jars on the class path, all one big unnamed module. it works, it has always worked.
$ java -cp app.jar:billing.jar:util.jar:acme-json-2.1.jar:logkit-3.0.jar com.app.Maineither way, you need a starting module-info. jdeps writes one for you from the bytecode:
$ jdeps --generate-module-info gen-out money-1.0.jar
writing to gen-out/money/module-info.java
$ cat gen-out/money/module-info.java
module money {
exports shop.money;
}it exports everything it found and guesses requires from what the classes touch. treat it as a draft: delete the exports for packages that are really internal, and give the module a proper reverse-DNS name.
the escape hatches
when a dependency isn't ready, you can patch the module graph from the command line. every one of these is a sign you're working around something.
| flag | what it does |
|---|---|
--add-modules m | resolve m even though nothing requires it (needed when main is on the class path) |
--add-reads a=b | make module a read module b (b can be ALL-UNNAMED) |
--add-exports m/p=a | export package p of m to a, at compile time and/or runtime |
--add-opens m/p=a | open p of m for deep reflection by a |
--patch-module m=dir | add or replace classes inside module m (tests, hotfixes) |
do i actually need to modularize my app?+
honestly, often no. plenty of production java runs entirely on the class path, including most spring boot fat jars, and the JDK's own strong encapsulation applies to you either way. modules pay off when you're a library that wants a real API boundary, when you want jlink to build a small runtime that includes your code, or when a big codebase needs walls the compiler enforces. if you publish a library, the minimum is still worth it: add Automatic-Module-Name so people who do modularize get a stable name.
- class path → one unnamed module that reads everything. module path → named or automatic modules.
- named modules can't see the class path. that's the rule behind every migration headache.
- automatic modules export everything and get their name from the manifest or the file name.
- libraries: add
Automatic-Module-Nametoday, it's one line. - a package can live in only one module. split packages have to be fixed, not flagged around.