Warm-up

Produce four linkage errors

10 minjunior015 yrs

One concept, guided. Near-impossible to fail.

What this teaches

  • A linkage error is thrown by the JVM, not by any library
  • The message names a full descriptor, which identifies the version you compiled against
  • Linking is lazy and per call site, which is why tests pass
  • A compile-time constant is inlined, so the same defect can fail silently

Starter

Starter.java
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

/**
 * Warm-up: break a classpath on purpose, four different ways.
 *
 * Everything here is the JDK. javax.tools gives you the compiler at runtime,
 * and a URLClassLoader with a null parent gives you an isolated classpath —
 * which together are enough to reproduce a dependency conflict without
 * downloading a single jar.
 */
public class Starter {

    static final JavaCompiler JAVAC = ToolProvider.getSystemJavaCompiler();

    /** Compiles one source file into `out`, optionally against `classpath`. */
    static void compile(Path root, String fileName, String source, Path out, Path classpath)
            throws Exception {
        Files.createDirectories(out);
        Path file = root.resolve(fileName);
        Files.writeString(file, source);
        List<String> options = new ArrayList<>(List.of("-d", out.toString(), "-nowarn"));
        if (classpath != null) { options.add("-cp"); options.add(classpath.toString()); }
        var fm = JAVAC.getStandardFileManager(null, null, null);
        if (!JAVAC.getTask(null, fm, null, options, null,
                fm.getJavaFileObjects(file.toFile())).call())
            throw new IllegalStateException("could not compile " + fileName);
    }

    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("warmup");

        // TODO 1: compile two versions of a class called Lib into two separate
        // output directories. Version one has `go(String)`. Version two has
        // `go(String, int)` and nothing else.

        // TODO 2: compile an App against version one, calling Lib.go("x").
        // Confirm it compiles cleanly — that is the point.

        // TODO 3: load App with a URLClassLoader whose URLs are [app, v2] and
        // whose PARENT IS NULL, then invoke run(). Print the error.
        //
        //   new URLClassLoader(new URL[]{ appDir.toUri().toURL(), v2.toUri().toURL() }, null)
        //
        // The null parent matters: without it, the loader delegates upward and
        // may find classes from your own classpath instead.

        // TODO 4: read the error message carefully. It names a full signature.
        // Say what that tells you that a bare method name would not.

        // TODO 5: repeat for a removed non-constant field (public static int
        // MAX = 100). Which error do you get?

        // TODO 6: repeat for an interface that gained a method your class does
        // not implement. You will need three types: an interface, a caller
        // that invokes the new method, and your App implementing the old
        // interface. A .java file may hold several top-level types as long as
        // only one is public.

        // TODO 7: repeat for a class that was removed entirely.

        // TODO 8: now the important one. Give App two static methods — one
        // calling a method that exists in both versions, one calling the
        // method that vanished. Load the class WITHOUT calling anything.
        // Then call the safe method. Then call the broken one. Explain, in one
        // sentence, why a passing test suite is not evidence of a clean
        // classpath.

        // TODO 9: make Lib expose `public static final int TIMEOUT = 30` in
        // version one and 60 in version two. Compile App against version one,
        // run it against version two, and print the value. Explain the result
        // and say why it is more dangerous than a NoSuchFieldError.
    }
}

Run it locally:

cd exercises/java/dependencies/transitive-conflicts/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Done when

  • You produced NoSuchMethodError, NoSuchFieldError, AbstractMethodError and NoClassDefFoundError for real
  • You showed a class loading cleanly while one of its call sites is unlinkable
  • You showed a static final constant keeping its old value after an upgrade
  • You can say what the descriptor in the error message tells you

← Back to Two libraries need different versions of the same dependency. What happens?