Skip to content
academia.sh

Lesson 10 / 12

Logging Facade and Implementation

The measurement reads the caller's class file's constant pool to show that it references the facade but never the implementation; the implementation enters the runtime class path through a service declaration, and when two implementations are present, the class path order decides which one is selected. The limiting measurement shows that when no implementation is found at all, the caller still compiles and runs, but produces no records.

Published

In this lesson

  • Shows, by reading the constant pool, that the caller's class file references the facade but not the implementation classes
  • Measures that the implementation enters the runtime class path through a service declaration, and that when two implementations are present, the class path order decides the selection
  • Shows that when no implementation is found at all, the caller compiles and runs but produces no records, and that this silence is not a defect but the mechanism's own contract
Contents

We built the class path the test saw and knew exactly what to put on it: which fake type stood in for which real type, which setup added which dependency to the test class path, all written step by step by us. In this lesson, the writing side changes. The caller compiles against an interface and never writes the name of the class that will stand behind it — it does not import it, does not instantiate it, does not even mention it as a string. Yet when the program runs, that nameless class is found and called. The question is: how does a name that was never written in the source enter the runtime class path, and when more than one candidate is present there, what decides which one wins?

This topic covers logging across three lessons, but this first lesson is not concerned with keeping a log itself — it is concerned with the dependency direction that a logging facade sets up. A facade — here, a single-method interface — becomes the only type the caller speaks to; the implementation behind it never becomes visible to the caller. The measurement below builds this in four steps: first, that compilation passes without ever seeing the implementation and that the class file does not reference it; then, by what kind of declaration the implementation enters the runtime class path; then, when more than one implementation is present, what decides the winner; and finally, what happens when no implementation is present at all.

Only the Facade Is on the Compile Class Path

LO1. The caller’s source text writes only the name of the facade interface; the names of the implementation classes never appear in the source, so the compiler has no need to see them. LO2. The class file’s constant pool carries only the types whose names appear in the source; a name never written in the source is absent from the constant pool as well.

The rest of the measurement repeats four tasks: writing a source file to disk, compiling it against a given class path, running a compiled class with a given class path, and reading which types are named in a class file’s constant pool. These four are gathered in the helper class below; the topic’s remaining two lessons use the same four.

// Tool.java — shared routines for writing sources, compiling, running and reading class files (helper class, no main)
import java.io.*;
import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;
import java.net.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Tool {
    static Path write(Path dir, String name, String source) throws IOException {
        Files.createDirectories(dir);
        Path p = dir.resolve(name + ".java");
        Files.writeString(p, source);
        return p;
    }

    static boolean compile(Path output, List<Path> classPath, List<Path> sources) throws IOException {
        Files.createDirectories(output);
        List<String> args = new ArrayList<>(List.of("-d", output.toString()));
        if (!classPath.isEmpty()) {
            StringJoiner sj = new StringJoiner(File.pathSeparator);
            for (Path p : classPath) sj.add(p.toString());
            args.add("-classpath");
            args.add(sj.toString());
        }
        for (Path s : sources) args.add(s.toString());
        ToolProvider compiler = ToolProvider.findFirst("javac").orElseThrow();
        StringWriter silentOutput = new StringWriter();
        int result = compiler.run(new PrintWriter(silentOutput), new PrintWriter(silentOutput),
                args.toArray(new String[0]));
        return result == 0;
    }

    static void run(List<Path> classPath, String mainClass) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
        }
    }

    static Object field(List<Path> classPath, String mainClass, String fieldName) throws Exception {
        URL[] url = new URL[classPath.size()];
        for (int i = 0; i < url.length; i++) url[i] = classPath.get(i).toUri().toURL();
        try (URLClassLoader loader = new URLClassLoader(url, Tool.class.getClassLoader().getParent())) {
            Class<?> cls = Class.forName(mainClass, true, loader);
            cls.getMethod("main", String[].class).invoke(null, (Object) new String[0]);
            return cls.getField(fieldName).get(null);
        }
    }

    static Set<String> references(Path classFile) throws IOException {
        Set<String> names = new TreeSet<>();
        ClassModel model = ClassFile.of().parse(classFile);
        for (PoolEntry e : model.constantPool()) {
            if (e instanceof ClassEntry ce) names.add(ce.asInternalName().replace('/', '.'));
        }
        return names;
    }

    static void show(String label, Object value) {
        System.out.printf("%-62s: %s%n", label, value);
    }

    static void declare(Path dir, String service, String provider) throws IOException {
        Path d = dir.resolve("META-INF").resolve("services");
        Files.createDirectories(d);
        Files.writeString(d.resolve(service), provider + "\n");
    }

    static String facadeSource() {
        return "public interface LoggingFacade { void write(String message); int recordCount(); }";
    }

    static String callerSource() {
        return "import java.util.ServiceLoader;\n"
             + "public class Caller {\n"
             + "  static final class Silent implements LoggingFacade {\n"
             + "    public void write(String message) { }\n"
             + "    public int recordCount() { return 0; }\n"
             + "  }\n"
             + "  public static String selectedClass = \"none\";\n"
             + "  public static int recordsProduced = -1;\n"
             + "  static LoggingFacade find() {\n"
             + "    for (LoggingFacade g : ServiceLoader.load(LoggingFacade.class, Caller.class.getClassLoader())) return g;\n"
             + "    return new Silent();\n"
             + "  }\n"
             + "  public static void main (String[] a) {\n"
             + "    LoggingFacade g = find();\n"
             + "    selectedClass = g.getClass().getSimpleName();\n"
             + "    g.write(\"start\"); g.write(\"progress\"); g.write(\"end\");\n"
             + "    recordsProduced = g.recordCount();\n"
             + "  }\n"
             + "}";
    }

    static String implementationSource(String name) {
        return "public class " + name + " implements LoggingFacade {\n"
             + "  private int counter = 0;\n"
             + "  public void write(String message) { counter++; }\n"
             + "  public int recordCount() { return counter; }\n"
             + "}";
    }
}

callerSource() produces the caller’s source: Caller imports only the LoggingFacade interface and ServiceLoader, and writes the name of no implementation class. If it finds none, it falls back to the Silent inner class it defines itself — this, too, is an implementation, but it lives in the caller’s own source, not brought in from outside.

// OnlyFacade.java — does the caller compile when only the facade is on the compile class path, and which types does the class file reference
import java.nio.file.*;
import java.util.*;

public class OnlyFacade {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl1");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        boolean callerCompiled = Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        Set<String> refs = Tool.references(callerDir.resolve("Caller.class"));

        Tool.show("did the caller compile with no implementation on the compile class path", callerCompiled);
        Tool.show("does the caller's class file reference LoggingFacade", refs.contains("LoggingFacade"));
        Tool.show("does the caller's class file reference ImplementationA", refs.contains("ImplementationA"));
        Tool.show("does the caller's class file reference ImplementationB", refs.contains("ImplementationB"));
    }
}
did the caller compile with no implementation on the compile class path: true
does the caller's class file reference LoggingFacade          : true
does the caller's class file reference ImplementationA        : false
does the caller's class file reference ImplementationB        : false

ImplementationA and ImplementationB really exist in this measurement: both are compiled, and both class files sit on disk. Even callerDir‘s compile class path does not include them — only facadeDir is there — and Caller still compiles (LO1). When the constant pool is read, LoggingFacade is there, but ImplementationA and ImplementationB are not (LO2). This is not a guess: a name never written in the source appears in none of the instructions the compiler could produce, because a class file is a translation of the source and invents nothing the source did not know. This is exactly where a facade’s whole power lies: the caller does not even need to know, at compile time, whether the class that will stand behind it exists at all.

The Implementation Enters the Path via a Declaration

LO3. The implementation enters the runtime class path through a service declaration file: a file named META-INF/services/<facade-interface's-full-name> carries the provider class’s name as a line of text. LO4. ServiceLoader.load(...) reads this file to find and instantiate the provider; the caller never calls the provider’s constructor anywhere in its own code.

// ServiceDeclaration.java — the implementation enters the path through a service declaration, the caller finds it with ServiceLoader
import java.nio.file.*;
import java.util.*;

public class ServiceDeclaration {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl2");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");

        Object selectedBefore = Tool.field(List.of(callerDir, facadeDir), "Caller", "selectedClass");
        Tool.show("class the caller falls back to when no implementation is on the runtime path", selectedBefore);

        List<Path> runtimePathAfter = List.of(callerDir, facadeDir, dirA);
        Object selectedAfter = Tool.field(runtimePathAfter, "Caller", "selectedClass");
        Object recordsAfter = Tool.field(runtimePathAfter, "Caller", "recordsProduced");
        Tool.show("class selected once the implementation enters the path through a declaration", selectedAfter);
        Tool.show("number of records produced", recordsAfter);
    }
}
class the caller falls back to when no implementation is on the runtime path: Silent
class selected once the implementation enters the path through a declaration: ImplementationA
number of records produced                                    : 3

The dirA directory carries two things: ImplementationA.class and a declaration file named META-INF/services/LoggingFacade that holds the single line ImplementationA. Once dirA is added to the runtime class path, the ServiceLoader.load call inside Caller reads this file, loads ImplementationA, and instantiates it — without a single line of code ever writing new ImplementationA() (LO3, LO4). All three write calls reach this instance, and the counter climbs to three. Once dirA is removed from the runtime class path, the same Caller.class, without any recompilation, falls back to its own Silent again. The only thing that changes is the content of the runtime class path; the Caller.class file is the exact same byte sequence in both cases.

With Two Implementations, the Path Order Decides

LO5. When more than one provider is declared for the same service on the runtime class path, the first provider ServiceLoader returns depends on the order of the class path entries. LO6. When the order changes, the selected implementation changes, but neither compilation nor execution fails — both end with an “OK” result.

// OrderWinner.java — with two implementations on the runtime class path, the path order decides the winner
import java.nio.file.*;
import java.util.*;

public class OrderWinner {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl3");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());
        Path sImplA = Tool.write(source, "ImplementationA", Tool.implementationSource("ImplementationA"));
        Path sImplB = Tool.write(source, "ImplementationB", Tool.implementationSource("ImplementationB"));

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Path dirA = root.resolve("recordA");
        Path dirB = root.resolve("recordB");

        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));
        Tool.compile(dirA, List.of(facadeDir), List.of(sImplA));
        Tool.compile(dirB, List.of(facadeDir), List.of(sImplB));
        Tool.declare(dirA, "LoggingFacade", "ImplementationA");
        Tool.declare(dirB, "LoggingFacade", "ImplementationB");

        Object aFirst = Tool.field(List.of(callerDir, facadeDir, dirA, dirB), "Caller", "selectedClass");
        Object bFirst = Tool.field(List.of(callerDir, facadeDir, dirB, dirA), "Caller", "selectedClass");

        Tool.show("selected class when A is given first on the runtime class path", aFirst);
        Tool.show("selected class when B is given first on the runtime class path", bFirst);
        Tool.show("did both run without error and differ only by order", !aFirst.equals(bFirst));
    }
}
selected class when A is given first on the runtime class path: ImplementationA
selected class when B is given first on the runtime class path: ImplementationB
did both run without error and differ only by order           : true

dirA and dirB are two directories unaware of each other; each carries its own class and its own service declaration. When the runtime class path is set up in the order [callerDir, facadeDir, dirA, dirB], ServiceLoader reaches dirA’s declaration first and returns ImplementationA; when the same two directories are given in the order dirB, dirA, the winner becomes ImplementationB (LO5). Caller‘s source, the LoggingFacade interface, and the two implementations themselves — none of these changed between the two runs; only the order of the list changed. Nowhere in the code is there a conflict warning or an error (LO6): finding two providers falls into the same pattern as K05’s shared definition — “when two implementations are found on the runtime class path, the selection changes with the path order, with no error” — seen once more here at the interface level.

When No Implementation Is Found

LO7. When no provider is found on the runtime class path, ServiceLoader.load(...) returns an empty iterator; the caller meets this not with an exception, but with a default it defines itself. LO8. In this case the program runs without error, but because no implementation increments recordCount(), the number of records produced is zero.

// NoImplementation.java — with no implementation on the runtime path at all, the caller runs without error but produces no records
import java.nio.file.*;
import java.util.*;

public class NoImplementation {
    public static void main(String[] args) throws Exception {
        Path root = Files.createTempDirectory("gl4");
        Path source = root.resolve("source");

        Path sFacade = Tool.write(source, "LoggingFacade", Tool.facadeSource());
        Path sCaller = Tool.write(source, "Caller", Tool.callerSource());

        Path facadeDir = root.resolve("facade");
        Path callerDir = root.resolve("caller");
        Tool.compile(facadeDir, List.of(), List.of(sFacade));
        Tool.compile(callerDir, List.of(facadeDir), List.of(sCaller));

        List<Path> runtimePath = List.of(callerDir, facadeDir);
        boolean ranWithoutError;
        try {
            Tool.run(runtimePath, "Caller");
            ranWithoutError = true;
        } catch (Exception e) {
            ranWithoutError = false;
        }
        Object selected = Tool.field(runtimePath, "Caller", "selectedClass");
        Object records = Tool.field(runtimePath, "Caller", "recordsProduced");

        Tool.show("does it run without error when no implementation is on the runtime path", ranWithoutError);
        Tool.show("class fallen back to", selected);
        Tool.show("number of records produced", records);
    }
}
does it run without error when no implementation is on the runtime path: true
class fallen back to                                          : Silent
number of records produced                                    : 0

This measurement’s runtime class path has neither ImplementationA nor ImplementationB — only the facade and the caller are there. The program still completes, throwing no exception (LO7), and recordsProduced stays at zero (LO8). This is the lesson’s limiting measurement: the facade does not remove the dependency between caller and implementation, it reverses its direction. When no implementation exists at all, the caller sees this not as an error but as a gap, and fills it with its own Silent fallback. The number of records produced being zero is not the sign of a defect here; it is the outcome of a rule the caller wrote in its own source. A logging facade’s real-world implementations carry the same promise: when no implementation is wired in, the program does not crash, it just stays silent. This is the real guarantee that the facade mechanism gives the caller — the caller itself can be written without knowing which implementation will enter the runtime class path, because the worst case is “no records at all,” not “the program does not run.” This guarantee carries a cost as well: when the runtime class path is set up incorrectly, there is no error message anywhere, so noticing the records that are missing is far harder than noticing a line the compiler rejected.

Summary

  • The caller’s source text writes only the name of the facade interface; the caller compiles even when no implementation at all is present on the compile class path.
  • The class file’s constant pool carries the types written in the source: LoggingFacade is there, but ImplementationA and ImplementationB, which never appear in the source, are not.
  • The implementation enters the runtime class path through a service declaration file; ServiceLoader reads this declaration to find and instantiate the provider without its name ever appearing in a single line of the caller.
  • When more than one provider is declared on the runtime class path, the order of the class path entries decides the winner; the selection changes when the order changes, but no error occurs.
  • Limiting measurement: when no implementation is found at all, the caller still compiles and runs without error, but produces no records — the facade does not remove the dependency, it reverses its direction.

Next Step

What this lesson measured was how a name enters the path: through a declaration, through order, or not at all. The next lesson holds the same set of calls fixed and turns the one thing that changes into a threshold — when the same three calls are run against two different thresholds, how many of them actually turn into a record, and does a call that falls below the threshold still carry a cost?

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close