How do mixed classpath + module path setups work during a migration to JPMS, and what should you watch out for?
answer
- Run -cp and -p together; migrate leaf-first (bottom-up)
- Module path: module-info = named, plain JAR = automatic module
- Unnamed reads all; named can't read unnamed → the constraint
- Hazards: split packages (hard error), unstable auto names, reflection needs opens
- Tooling: jdeps, --patch-module/--add-opens/--add-reads escape hatches
basics
~20 sYou can run with both -cp and -p at once. JARs you move onto the module path become modules (named or automatic); the rest stay classpath code in the unnamed module. You usually migrate from the leaf libraries inward because named modules can't see classpath code.
solid answer
~50 sJPMS is designed for incremental adoption, so you run the JVM with both a classpath and a module path simultaneously. JARs on the module path become modules: those with a module-info are *named* modules; plain JARs there become *automatic* modules (named from Automatic-Module-Name or the filename, requiring all modules and exporting everything). Everything still on the classpath is the unnamed module — permissive, read-all. The key asymmetry: the unnamed module can read every module, but named/automatic modules cannot read the unnamed module (no name to require). So you migrate **bottom-up**: leaf libraries first. Watch out for split packages (the same package in two modules is a hard error — common with old JARs that scatter a package across artifacts), unstable automatic-module names derived from filenames (pin Automatic-Module-Name in the manifest), and reflection breaking because non-exported packages need `opens`. Use `jdeps` to map dependencies before writing each module-info.
code
java · 13 lines// A leaf library, migrated first. It must declare reflection access
// and re-export API types so consumers compile.
module com.example.core {
requires transitive com.example.model; // API exposes model types
exports com.example.core.api;
opens com.example.core.entity; // let an ORM reflect in
}
// Mixed launch during migration:
// java -p mods \
// -cp not-yet-migrated.jar \
// --add-opens com.example.core/com.example.core.internal=spring.core \
// -m com.example.app/com.example.app.Maingo deeper
Knows you can run with both -cp and -p and that migration is gradual, not all-at-once.
Explains named vs automatic modules, the unnamed-module read direction, and that migration goes leaf-first.
Plans a real migration: uses jdeps, handles split packages, opens for reflection, requires transitive, and pins automatic-module names.
Owns a multi-team modularization roadmap, sets manifest/naming conventions for published libraries, weighs payoff vs effort, and decides where escape-hatch flags are acceptable vs technical debt.
## The goal: migrate without a big-bang rewrite JPMS (Java 9+) is explicitly built so you can adopt it one JAR at a time. The mechanism is running with **both** a classpath and a module path in the same launch: ``` java -p mods -cp legacy1.jar:legacy2.jar -m com.example.app/...Main ``` ## What each region is - **Module path (`-p`)** holds modules. A JAR there with a compiled `module-info` is a **named module** (declares `requires`/`exports`). A *plain* JAR there becomes an **automatic module**: it gets a name (from the `Automatic-Module-Name` manifest header, or derived from the filename), implicitly `requires` every other module (`requires transitive` to its readers), and `exports` all its packages. Automatic modules are the bridge between unmodularized JARs and real modules. - **Classpath (`-cp`)** holds everything else, lumped into the permissive **unnamed module** (read-all, export-all). ## The directional rule that shapes everything Readability flows one way across the boundary: - unnamed module → named/automatic modules: **yes** (unnamed reads all). - named/automatic module → unnamed module: **no** (you cannot `requires` something with no name). So a named module is sealed off from anything left on the classpath. This forces **bottom-up migration**: convert the leaves (libraries with no remaining classpath-only dependencies) first; their consumers can be converted next, and so on toward the application. ## Concrete hazards 1. **Split packages.** If the same package (say `javax.annotation`) is exported by two modules, resolution **fails hard** — unlike the classpath, which silently picked one. Legacy JARs that spread one package across multiple artifacts trigger this. Fixes: merge the artifacts, patch a module (`--patch-module`), or keep the offenders on the classpath for now. 2. **Unstable automatic-module names.** If a JAR has no `Automatic-Module-Name`, its module name is derived from the filename. A version bump or rename changes the name and breaks every `requires` that referenced it. Library authors should add a stable `Automatic-Module-Name` to the manifest *before* anyone modularizes against them. 3. **Reflection / deep access.** Frameworks (DI, ORM, serialization) reflect into your classes. In a named module, non-exported packages are closed to reflection unless you add `opens <pkg>` (or `opens <pkg> to <framework.module>`), or run with `--add-opens`. Code that 'just worked' on the classpath may throw `InaccessibleObjectException` after modularization. 4. **`requires` vs `requires transitive`.** If your module's *public API* exposes types from another module, use `requires transitive` so consumers implicitly read it; otherwise they get compile errors using your API. 5. **Internal JDK APIs.** `sun.*` / non-exported `jdk.*` usage that was tolerated on the classpath (with warnings) becomes blocked under strong encapsulation; `jdeps --jdkinternals` finds these. ## Tooling - **`jdeps`** analyses a JAR's dependencies, suggests a `module-info`, and flags JDK-internal and split-package issues. Run it before authoring each module declaration. - **`--patch-module`, `--add-modules`, `--add-reads`, `--add-opens`, `--add-exports`** are escape hatches to keep things running mid-migration without editing source. ## Practical recipe 1. Get the app running on Java 9+ entirely on the classpath (everything in the unnamed module) — proves no encapsulation regressions. 2. Run `jdeps` to map the graph and pick leaf libraries. 3. Move leaves to the module path (as automatic modules first if needed), pin their `Automatic-Module-Name`. 4. Author `module-info` leaf-first, adding `exports`/`opens`/`requires transitive` as `jdeps` and test failures reveal. 5. Repeat inward until the application module is named. The whole point of the dual-path design is that at every step the app keeps running, with the module path holding what you've migrated and the classpath holding what you haven't.
- What causes a split-package error and how do you resolve it during migration?Two modules exporting the same package. Resolve by merging the artifacts, using --patch-module to fold one into the other, or leaving the conflicting JARs on the classpath (one unnamed module tolerates the duplicate).
- Why should library authors set Automatic-Module-Name before fully modularizing?Without it, the automatic module name is derived from the filename and changes when the file is renamed/versioned, breaking every downstream requires. A pinned name is a stable contract while the real module-info is still pending.
- Why might reflection-based frameworks break after you modularize a JAR?Named modules close non-exported packages to deep reflection; the framework gets InaccessibleObjectException unless you add opens (or --add-opens) for the reflected packages.
Renovating a house room by room while living in it: the module path is the finished, up-to-code rooms; the classpath is the rest still under the old wiring. You finish the back rooms first because the new rooms' inspected circuits can't legally tie into the old, unlabeled wiring.
saying these in an interview costs you the question
- Suggesting top-down migration (app first) — named modules can't see classpath leaves, so it must be bottom-up
- Treating split packages as a warning — on the module path they abort startup
- Assuming reflection keeps working without opens after modularization
- Relying on filename-derived automatic module names as if they were stable