12.5 Compiling and Packaging Code: Non-Modular JARs, Modular JARs, and Migration Strategies

Key Takeaways

  • jar cfe app.jar com.x.Main -C classes . creates a JAR whose manifest Main-Class lets java -jar app.jar run it.

  • A modular JAR placed on the classpath runs in the unnamed module because its module-info.class is ignored.

  • A non-modular JAR on the module path becomes an automatic module, so legacy-tools-2.1.jar is named legacy.tools.

  • Bottom-up migration converts dependency-free libraries into named modules first while unmigrated code stays on the classpath.

  • Top-down migration places every JAR on the module path as an automatic module and then modularizes the application first.

Last updated: October 2026

12.5 Compiling and Packaging Code: Non-Modular JARs, Modular JARs, and Migration Strategies

The second module objective is practical: "Compile Java code, produce modular and non-modular jars, runtime images, and implement migration using unnamed and automatic modules." Sections 12.1 and 12.4 covered modular compilation, jlink, and the module types. This section adds the classpath workflow, shows how a JAR's location decides its module type, and walks through the two official migration strategies.


Compiling and Running Non-Modular Code

Classic classpath code has no module-info.java:

# Compile: -d sets the output root; package folders are created under it
javac -d classes src/com/x/Main.java

# Run from the class directory (fully qualified class name, no .class suffix)
java -cp classes com.x.Main

# Java 11+ single-file source launcher: compiles in memory and runs
java Hello.java arg1 arg2

The classpath can be given as -cp, -classpath, or --class-path, and it lists directories or JAR files separated by : (Linux and macOS) or ; (Windows).

Creating a Non-Modular JAR

# Long options
jar --create --file app.jar --main-class com.x.Main -C classes .
# Equivalent short options: c = create, f = file, e = entry point (Main-Class)
jar cfe app.jar com.x.Main -C classes .

java -jar app.jar            # uses the Main-Class attribute in META-INF/MANIFEST.MF
java -cp app.jar com.x.Main  # names the main class explicitly

-C classes . tells jar to change into classes and add everything there, so the JAR entries start at com/x/Main.class. When you run java -jar, the JVM ignores any -cp option and uses only the JAR plus its manifest Class-Path entries.

A modular JAR is built with the same commands; the only difference is that module-info.class sits at the root of the JAR (see 12.1 for jar --create --file ... --main-class ... -C out/module .).


Where a JAR Goes Decides Its Module Type

JAR contentsPlaced on the module path (-p)Placed on the classpath (-cp)
Has module-info.class (modular)Named module with its declared nameUnnamed module (the descriptor is ignored)
No module-info.class (non-modular)Automatic module (name from Automatic-Module-Name or the file name)Unnamed module

You can confirm this at runtime: a class from a modular JAR run with java -cp mapp.jar com.app.App reports getModule().getName() as null, while java -p mods -m com.app reports com.app. For a non-modular file named legacy-tools-2.1.jar on the module path, jar --describe-module --file legacy-tools-2.1.jar prints legacy.tools@2.1 automatic because the version is stripped and the hyphen becomes a dot.

Useful inspection commands:

CommandPurpose
java --list-modulesLists the modules in the current runtime (for example java.base@17...)
java --describe-module java.sql (-d)Shows a module's exports and requires
jar --describe-module --file x.jar (-d)Shows the descriptor of a JAR, or the derived automatic module
java --show-module-resolution -p mods -m appPrints how the module graph was resolved at startup
jdeps -s app.jar / jdeps --jdk-internals app.jarSummarizes dependencies and finds internal JDK API use (see 12.4)

Migration Rules You Must Combine

Three facts drive every migration question:

  1. A named module cannot read the unnamed module. You cannot write requires for classpath code, so anything a named module depends on must be on the module path.
  2. The unnamed module reads every module. Classpath code can still use named and automatic modules.
  3. Automatic modules read every module, including the unnamed module, and export and open all of their packages.

Bottom-up migration

Start with the lowest-level JARs, the ones that depend on nothing else in the application:

  1. Add a module-info.java to a leaf library, exporting the packages others use, and move it to the module path as a named module.
  2. Leave everything that has not been migrated on the classpath. That code is in the unnamed module, which can read the new named module.
  3. Repeat, moving up the dependency graph one layer at a time, until the application module itself is named.

Bottom-up works best when you control the lower-level libraries.

Top-down migration

Start with the application and treat everything else as automatic modules:

  1. Move all JARs to the module path. Every non-modular JAR becomes an automatic module.
  2. Add module-info.java to the top-level application, using requires with the automatic module names (check them with jar --describe-module).
  3. Convert the remaining JARs to named modules later, or leave third-party libraries as automatic modules if you do not own them.

Top-down works when the application depends on third-party JARs that you cannot change. If two modules being migrated depend on each other, JPMS forbids the cycle; move the shared code into a new module that both can require.


Runtime Images for Modular and Non-Modular Applications

jlink (see 12.4) links named modules only; an application that still depends on automatic modules cannot be linked as a whole. A classpath application can still benefit: jlink --add-modules java.base,java.sql --output rt builds a smaller runtime containing only the JDK modules it needs, and rt/bin/java -cp app.jar com.x.Main runs the classpath code on that runtime.

Test Your Knowledge

A team migrates an application whose lowest-level library, util.jar, depends on nothing else. They add module-info.java to util.jar, place it on the module path, and leave every other JAR on the classpath. Which migration strategy is this, and why can the classpath code still use util?

A

Bottom-up migration, because the unnamed module reads every module, including the new named module

B

Top-down migration, because the unnamed module is converted into an automatic module

C

Bottom-up migration, because the named util module automatically exports to the classpath

D

Top-down migration, because named modules can declare requires on classpath code

Test Your Knowledge

The file reports.jar contains module-info.class declaring module com.reports. A developer runs java -cp reports.jar com.reports.Main. Inside Main, what does Main.class.getModule().isNamed() return?

A

true, because the JAR becomes an automatic module named reports

B

false, because a modular JAR on the classpath belongs to the unnamed module

C

true, because module-info.class is always honored

D

The JVM refuses to start because modular JARs cannot be placed on the classpath

Test Your Knowledge

Which command creates app.jar from the compiled classes in the classes directory so that java -jar app.jar starts com.x.Main?

A

javac -d app.jar --main-class com.x.Main classes

B

jar cf app.jar com/x/Main.java

C

jar cfe app.jar com.x.Main -C classes .

D

jar --create --file app.jar --module com.x.Main classes

Sections you finish are checked off in the contents.