Migrating to Java Modules
Strategies for migrating classpath-based Java applications to the Java Platform Module System.
Migrating to Java Modules
You do not have to modularize to move to Java 9+. A classpath application runs unchanged — everything becomes one big unnamed module. Migration is an optional step you take when you want strong encapsulation, a jlink custom runtime, or a cleaner dependency graph. The art is doing it incrementally, because the three module kinds from the earlier chapter let modular and non-modular code coexist.
First: just run on Java 9+ (no modules)
Before adding any module-info.java, recompile and run your existing app on the new JDK with everything on the classpath. The likely breakages have nothing to do with your modules and everything to do with the now-modular JDK:
- Removed Java EE modules —
java.xml.bind(JAXB),java.activation, CORBA, etc. were removed from the JDK. Add them back as ordinary dependencies. - Encapsulated internals —
sun.misc.Unsafeand friends are no longer accessible.--add-exports/--add-opensflags are an escape hatch while you remove the usage. - Split packages — two JARs contributing the same package now conflict on the module path (but not the classpath).
Get the app green on the classpath first. Only then start modularizing.
Bottom-up migration
The preferred strategy when you control the whole codebase:
- Start with the leaf libraries — modules that depend on nothing of yours.
- Give each a
module-info.javaand move it to the module path. - Their dependents can now
requiresthem. Work up the dependency tree toward the application's entry point.
This works because a named module may require other named modules and automatic modules — so as long as a leaf's own dependencies are at least automatic, you can modularize it. Each step keeps the build green.
Top-down migration
When you don't control the libraries (third-party JARs without descriptors), go the other way:
- Modularize your own top-level code first.
- Place the still-non-modular third-party JARs on the module path, where they become automatic modules.
requiresthem by their automatic name (from the JAR filename orAutomatic-Module-Name).- As each library ships a real
module-info, swap the automatic dependency for the named one — no change to yourrequires.
Automatic modules are the scaffolding that makes top-down possible; they let your named code depend on JARs that are not modular yet.
Tools and gotchas
jdepsanalyses a JAR's actual dependencies and can even generate a first-draftmodule-info.java(jdeps --generate-module-info). Start there rather than writing directives by hand.Automatic-Module-Name— if you publish a library, add this manifest entry before writing a full descriptor. It pins a stable module name so downstream automatic-module users are not broken when you later rename the JAR.- Split packages must be merged. The module path forbids two modules owning the same package; classpath tolerated it. This is the most common migration blocker.
opensfor reflection. Frameworks reflecting over your classes (Jackson, JPA, Spring) needopens, or you will getInaccessibleObjectExceptionat runtime even though compilation passed.
A worked example: inspecting a dependency graph like jdeps
Migration planning starts with "what depends on what." This program reads the boot layer's module descriptors at runtime — the same requires information jdeps reports — and also detects whether it is running as a named module or on the classpath, the very check that tells you how far a migration has progressed.
What to take from the run:
- The program reported it was running as an UNNAMED module — exactly what you expect from classpath code, and the runtime signal that this codebase has not been modularized yet. Re-running after adding a
module-infoand moving to the module path would flip this to NAMED, giving you a concrete "are we there yet" check during migration. inspect("java.sql")printed its realrequireslist, including ajava.transaction.xaorjava.loggingstyle transitive dependency. This is the same informationjdepssurfaces — knowing a module's true dependencies is step one of writing itsmodule-info.java, and the descriptor already holds it.- Some requires printed
(transitive). Those are the dependencies a module re-exports; when yourequires java.sql, you automatically read its transitive dependencies too. Spotting them tells you whichrequires transitivelines to copy when you modularize code that wraps such a module. findModulereturned anOptional, reinforcing that a module may simply not be present in a given runtime — ajlink-trimmed image might omitjava.desktopentirely. Migration plans must account for which modules actually ship in the target runtime.- Every module ultimately depends on
java.base, and it never appears in arequireslist because it is implicit. The total boot-module count shows the JDK is a graph you can query programmatically — the foundation that tools likejdepsandjlinkbuild on to analyse and shrink an application.
A sensible migration order, summarized
- Run on the classpath on the new JDK; fix removed-module and internal-API breakages.
- Use
jdepsto map dependencies and find split packages. - Add
Automatic-Module-Nameto any libraries you publish. - Modularize bottom-up if you own the tree, top-down (leaning on automatic modules) if you don't.
- Add
openswhere frameworks reflect; verify at runtime, not just at compile time.
That completes the Java Platform Module System part: you now know what modules are, how to declare one, the three kinds and how they coexist, how services decouple modules, and how to migrate an existing application onto the module path without a rewrite.
Practice
Your team owns the full source tree of an application and wants strong encapsulation throughout. Library A depends on nothing of yours; library B depends on A; the executable depends on B. Which migration order keeps every intermediate build green with the fewest automatic-module workarounds?