skip to content
everything.tooling.classpathToModulepath

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).

code in a =
named modulemodule path
a jar with module-info.class
gives others: exactly the packages it exports. opens only what it opens.
→ readsonly the ones it requires
automatic modulemodule path
a plain jar, no module-info
gives others: every package, exported and open. it has no idea what's internal.
→ readsif it requires it, by its derived name
unnamed moduleclass path
every jar on -cp, mixed into one
gives others: every package, but no named module can say requires on it (it has no name).
✗ blindnever. named modules can't see the class path
pick where your code lives. a named module is the only kind that's blind to something: it can't see the class path at all. the other two read everything.

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:

predict the output
java
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));
}
output
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.

jar =
or type =
manifest =
  1. 1. drop .jarcommons-lang3-3.17.0
  2. 2. cut the versioncommons-lang3the first "-" followed by a digit starts the version: 3.17.0
  3. 3. non-alphanumerics → .commons.lang3
  4. 4. squash dotscommons.lang3
$ jar --describe-module --file commons-lang3-3.17.0.jar
commons.lang3@3.17.0 automatic
pick a jar or type one. the manifest entry beats the file name every time. every preset here gave exactly this answer from 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:

META-INF/MANIFEST.MF
Automatic-Module-Name: com.acme.json

it costs nothing, it doesn't make the jar a module, and it reserves the name forever.

quick check
what module name does my-utils_v2-1.4.jar get on the module path?

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.

plan =
app.jar
class path
billing.jar
class path
util.jar
class path
acme-json-2.1.jar
class path · 3rd party
logkit-3.0.jar
class path · 3rd party
class path (unnamed) automatic named depends on

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.Main
step 1/4
switch plans, then step. the amber jars are the ones that changed. bottom-up works when you own the whole tree. top-down is what you do when you don't own your dependencies.

either way, you need a starting module-info. jdeps writes one for you from the bytecode:

terminal
$ 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.

flagwhat it does
--add-modules mresolve m even though nothing requires it (needed when main is on the class path)
--add-reads a=bmake module a read module b (b can be ALL-UNNAMED)
--add-exports m/p=aexport package p of m to a, at compile time and/or runtime
--add-opens m/p=aopen p of m for deep reflection by a
--patch-module m=diradd or replace classes inside module m (tests, hotfixes)
one level deeper: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.

keylines
  • 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-Name today, it's one line.
  • a package can live in only one module. split packages have to be fixed, not flagged around.