The Classpath and JAR Files

Understand how the JVM finds classes and resources, what a JAR and its manifest hold, and how to debug the errors a wrong class path causes.

Syntaxjava --class-path 'out:lib/*' com.example.shop.App # ';' instead of ':' on Windows jar --create --file app.jar --main-class com.example.shop.App -C out . java -jar app.jar # ignores -cp and CLASSPATH

The class path is the list of places the JVM searches for application classes and resources. Each entry is a directory of compiled classes or a JAR file, and order matters: for each class name, the application class loader walks the entries from left to right and takes the first match.

Setting it

Pass it with --class-path (or -cp). Entries are separated with : on Linux and macOS and ; on Windows. A directory entry is a root: com.example.shop.App is looked up as com/example/shop/App.class below it, so point at out, not at out/com/example/shop. An entry ending in /* means every JAR in that directory, not sub-directories and not loose class files. Quote it so the shell does not expand it first. With no option and no CLASSPATH variable, the class path is the current directory.

JAR files

A JAR is a zip file of class files and resources, with an optional manifest at META-INF/MANIFEST.MF. The manifest's Main-Class attribute is what makes java -jar app.jar work, and its Class-Path attribute can list further JARs by relative path. With -jar, the JAR is the only source of application classes: -cp and CLASSPATH are ignored. That surprises people who try to add a JDBC driver on the command line.

Services usually ship in one of two shapes. A "fat" or "uber" JAR unpacks every dependency into one archive (the Maven Shade plugin does this). A Spring Boot JAR keeps the dependency JARs intact under BOOT-INF/lib and uses its own launcher to load them. Both start with java -jar.

When it goes wrong

  • ClassNotFoundException: code asked for a class by name (Class.forName, a driver or plug-in named in configuration) and no entry had it.
  • NoClassDefFoundError: a class that was there at compile time is missing at run time, or its static initialiser failed earlier. A dependency with provided scope that the runtime does not actually supply is a common cause.
  • NoSuchMethodError: code compiled against one version of a library runs against another. Two versions on the class path, or a build that resolved a different version than you expected. mvn dependency:tree or ./gradlew dependencies shows what was picked.

Resources

Files under src/main/resources are copied to the root of the class path, so application.properties or db/schema.sql are read with getResourceAsStream, never with a file path: inside a JAR they are not files. ClassLoader.getResource("db/schema.sql") takes a path from the root with no leading slash. Class.getResource treats a name without a leading slash as relative to that class's package and one with a slash as starting from the root.

Example

Example · bash
# Compile against the JARs in lib/, writing classes into out/ by package
javac -d out --class-path 'lib/*' $(find src/main/java -name '*.java')

# Copy resources to the class path root, as Maven and Gradle do
cp -R src/main/resources/. out/

# Run: compiled classes first, then every JAR in lib/
java --class-path 'out:lib/*' com.example.shop.App

# Package the classes and resources; --main-class writes Main-Class into the manifest
jar --create --file shop.jar --main-class com.example.shop.App -C out .
jar --list --file shop.jar | head -5
unzip -p shop.jar META-INF/MANIFEST.MF

# Runs, but only if shop.jar holds every class the app needs
java -jar shop.jar

When to use it

  • Diagnose a NoSuchMethodError caused by two Jackson versions on a service's class path.
  • Load a SQL schema file from src/main/resources so it works both in the IDE and inside the packaged JAR.
  • Work out why adding a JDBC driver with -cp has no effect when the service starts with java -jar.

More examples

Compile, package and load a class from a JAR

The class sits at com/acme/text/Slug.class inside the JAR because that path is its package name. The same name lookup succeeds through the loader that has the JAR and fails with ClassNotFoundException through the one that does not.

Example · java
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.jar.Attributes;

void main() throws Exception {
    Path dir = Files.createTempDirectory("cp");
    Path src = dir.resolve("src/com/acme/text/Slug.java");
    Files.createDirectories(src.getParent());
    Files.writeString(src, """
        package com.acme.text;

        public class Slug {
            public static String of(String s) {
                return s.strip().toLowerCase().replace(' ', '-');
            }
        }
        """);

    // javac -d classes src/com/acme/text/Slug.java
    Path classes = dir.resolve("classes");
    int rc = javax.tools.ToolProvider.getSystemJavaCompiler()
            .run(null, null, null, "-d", classes.toString(), src.toString());
    IO.println("javac exit: " + rc);

    // jar --create --file text.jar -C classes .
    Path jar = dir.resolve("text.jar");
    Manifest mf = new Manifest();
    mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
    try (var out = new JarOutputStream(Files.newOutputStream(jar), mf)) {
        out.putNextEntry(new JarEntry("com/acme/text/Slug.class"));
        out.write(Files.readAllBytes(classes.resolve("com/acme/text/Slug.class")));
        out.closeEntry();
    }
    try (var jf = new JarFile(jar.toFile())) {
        jf.stream().map(JarEntry::getName).forEach(n -> IO.println("in jar: " + n));
    }

    // A class loader whose class path is just this one JAR
    try (var loader = new URLClassLoader(new URL[] { jar.toUri().toURL() }, null)) {
        Class<?> slug = Class.forName("com.acme.text.Slug", true, loader);
        IO.println(slug.getMethod("of", String.class).invoke(null, " Spring Sale "));
    }
    // Our own class path does not include the JAR
    try {
        Class.forName("com.acme.text.Slug");
    } catch (ClassNotFoundException e) {
        IO.println("ClassNotFoundException: " + e.getMessage());
    }

    try (var walk = Files.walk(dir)) {
        walk.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete());
    }
}

// Output:
// javac exit: 0
// in jar: META-INF/MANIFEST.MF
// in jar: com/acme/text/Slug.class
// spring-sale
// ClassNotFoundException: com.acme.text.Slug

Loading a resource from a class path root

A leading slash makes ClassLoader.getResource return null, which then surfaces as a NullPointerException far from the cause. The same code works unchanged whether the root is a directory or a JAR, which is why resources should never be read as files.

Example · java
void main() throws Exception {
    // Stand-in for src/main/resources after the build has copied it
    Path root = Files.createTempDirectory("res");
    Files.createDirectories(root.resolve("config"));
    Files.writeString(root.resolve("config/app.properties"), "greeting=hello\nport=8080\n");

    try (var loader = new URLClassLoader(new URL[] { root.toUri().toURL() }, null)) {
        IO.println(loader.getResource("config/app.properties") != null);
        // ClassLoader paths never start with a slash
        IO.println(loader.getResource("/config/app.properties"));

        Properties props = new Properties();
        try (InputStream in = loader.getResourceAsStream("config/app.properties")) {
            props.load(in);
        }
        IO.println(props.getProperty("greeting") + " on " + props.getProperty("port"));
    }

    Files.delete(root.resolve("config/app.properties"));
    Files.delete(root.resolve("config"));
    Files.delete(root);
}

// Output:
// true
// null
// hello on 8080

Finding which JAR a class came from

When a NoSuchMethodError appears after a dependency bump, -verbose:class shows the JAR the class was actually loaded from, and the dependency tree shows why that version was chosen. That usually answers the question in minutes.

Example · bash
# Log every class as it loads, with its source, then filter
java -verbose:class -jar shop.jar 2>&1 | grep 'com.fasterxml.jackson.databind.ObjectMapper '

# Which version did the build resolve, and who pulled it in?
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core
./gradlew dependencies --configuration runtimeClasspath

# What is inside a JAR, and what does its manifest say?
jar --list --file target/shop.jar | grep ObjectMapper
unzip -p target/shop.jar META-INF/MANIFEST.MF

Discussion

  • Be the first to comment on this lesson.