Skip to content
academia.sh

Lesson 05 / 19

Automatic Configuration

If no component is declared for an interface, the IoC container puts its own default implementation in place; when the user declares their own component, the container silently steps back and never installs its own default at all. What is automatic is conditional, and that condition appears nowhere in the source with any word at all.

Published Updated

In this lesson

  • Measures, by comparing two setups, that the container puts its own default in place when no component is declared for an interface, and steps back when one is declared
  • Runs the same container in two different setups and counts how many behaviors are born with no annotation at all
  • Proves by showing that a component is never constructed that adding one component can silently disable another
Contents

The previous lesson measured that a setting falls to the embedded default when it is found in no source at all — but what fell was a value, the content of a field that already existed. This lesson carries the same idea one level up: sometimes what falls is not a value, it is the component itself. When no one at all declares an implementation for an interface, the container fills that interface on its own — by bringing in a class of its own choosing. This behavior’s name is automatic configuration, and this lesson’s question is: how conditional is the thing that is actually “automatic”?

The fourth lesson’s precedence chain queried three sources in order — external source, file, embedded default — and for every setting, only one of the three won. This lesson’s question is shaped differently: there is no ordered chain here, only a binary decision — did the user declare something or not. This binary is what separates automatic configuration from the previous lesson: what determines the winner is not a precedence order, it is an existence test. If a class satisfying the requested interface exists in the set the user declared, the container never steps in at all; if not, it brings in the only candidate it has.

An Undeclared Interface, the Container’s Own Decision

In the measurement below there are three interfaces — Notifier, Cache, Metric — and for each one the container has an automatic default class ready on its own: AutoNotifier, AutoCache, AutoMetric. This measurement does not use the previous lessons’ @Container. Component marker — here, “declaring” only means a class being added to a small list given by the user, but its role is the same: this list carries the candidates that will take the place of the managed components the container would otherwise construct on its own. Before making a decision for an interface, the container looks at whether the user has declared a component implementing that interface. Did they declare one? Then the container steps back — it never installs its own automatic default at all. Did they not? The container puts in its own default, and the program never explicitly asked for this decision anywhere.

This “looking” happens in the resolve method: the method walks through the list of components the user declared, and if there is a class in the list implementing the requested interface, it returns it; if not, it increments its own counter and constructs the automatic default. In this measurement, “the list of components the user declared” is not a real class path scan, it is a small list given by hand — but the mechanism itself is the same: a real application framework asks the same question too, just over a far larger set of candidates.

The check is done with iface.isAssignableFrom(candidate), not instanceof — because there is no object in hand yet, only a Class<?> reference. isAssignableFrom asks whether one type can be assigned to another, without constructing any instance at all, only by comparing two Class objects. This distinction matters: to decide which class implements which interface, the container does not have to construct that class first — it answers the question entirely at the type level, and only constructs the actual object with getDeclaredConstructor().newInstance() once the winning candidate is known. The automatic default candidates are constructed the same way; the only difference between them is that the automatic default is called only when no candidate is found at all.

  • CC22. “The components the user declared” here is a small list given by hand; there is no real class path scan or package discovery in this measurement. All three interfaces have exactly one automatic default; choosing among more than one automatic candidate is not within this measurement’s scope.
  • CC23. The container runs two setups one after another within the same run; each setup carries its own component list, there is no shared state between them.
  • CC24. The “behavior born with no annotation” counter only counts the cases where the automatic default was constructed; a component the user declared themselves never enters this counter at all.
  • CC25. How many times a class was constructed is tracked with a counter added to the class’s own constructor; no real timestamp or memory measurement is used.

This measurement is deliberately narrower than the previous three lessons’ Container: the resolve method calls getDeclaredConstructor().newInstance(), looking for a parameterless constructor — unlike the second lesson’s get method, it does not recursively resolve constructor parameters. None of the four classes in this measurement (AutoNotifier, AutoCache, AutoMetric, CustomNotifier) has a dependency, so this simplification does not change the result; but in a real application framework, an automatically configured component can also have its own dependencies, and then the construction process would need a combination of this lesson’s resolve and the second lesson’s get.

// AutomaticConfiguration.java — if a component is never declared the container puts its own default in place, if declared it steps back
import java.util.*;

public class AutomaticConfiguration {

    static int noAnnotationCount = 0;

    static Object resolve(Class<?> iface, List<Class<?>> userComponents, Class<?> autoDefault) throws Exception {
        for (Class<?> candidate : userComponents) {
            if (iface.isAssignableFrom(candidate)) {
                System.out.printf("%-12s user declared %-18s container stepped back%n", iface.getSimpleName(), candidate.getSimpleName());
                return candidate.getDeclaredConstructor().newInstance();
            }
        }
        noAnnotationCount++;
        System.out.printf("%-12s no declaration at all, container put its default: %s%n", iface.getSimpleName(), autoDefault.getSimpleName());
        return autoDefault.getDeclaredConstructor().newInstance();
    }

    static void setup(String name, List<Class<?>> userComponents) throws Exception {
        System.out.println("-- setup: " + name + " --");
        noAnnotationCount = 0;
        Notifier n = (Notifier) resolve(Notifier.class, userComponents, AutoNotifier.class);
        Cache c = (Cache) resolve(Cache.class, userComponents, AutoCache.class);
        Metric m = (Metric) resolve(Metric.class, userComponents, AutoMetric.class);
        System.out.println("notifier result   : " + n.send());
        System.out.println("cache result      : " + c.get("x"));
        System.out.println("metric result     : " + m.count());
        System.out.println("behavior count born with no annotation at all: " + noAnnotationCount);
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        setup("no user component at all", List.of());
        setup("user wrote their own notifier", List.of(CustomNotifier.class));

        System.out.println("how many times was AutoNotifier constructed across two setups: " + AutoNotifier.constructionCount);
    }
}

interface Notifier { String send(); }
interface Cache { String get(String key); }
interface Metric { int count(); }

class AutoNotifier implements Notifier {
    static int constructionCount = 0;
    AutoNotifier() { constructionCount++; }
    public String send() { return "auto-notification"; }
}
class AutoCache implements Cache {
    public String get(String key) { return "auto-cache:" + key; }
}
class AutoMetric implements Metric {
    public int count() { return 0; }
}

class CustomNotifier implements Notifier {
    public String send() { return "custom-notification"; }
}
-- setup: no user component at all --
Notifier     no declaration at all, container put its default: AutoNotifier
Cache        no declaration at all, container put its default: AutoCache
Metric       no declaration at all, container put its default: AutoMetric
notifier result   : auto-notification
cache result      : auto-cache:x
metric result     : 0
behavior count born with no annotation at all: 3

-- setup: user wrote their own notifier --
Notifier     user declared CustomNotifier     container stepped back
Cache        no declaration at all, container put its default: AutoCache
Metric       no declaration at all, container put its default: AutoMetric
notifier result   : custom-notification
cache result      : auto-cache:x
metric result     : 0
behavior count born with no annotation at all: 2

how many times was AutoNotifier constructed across two setups: 1

The Same Container, Two Setups, Two Separate Numbers

In the first setup, the user declares no component at all — setup("no user component at all", List.of()) gives an empty list. All three interfaces receive the container’s automatic defaults: AutoNotifier, AutoCache, AutoMetric. The counter is 3 — all three behaviors were born with no annotation at all. There is not a single line in the source naming AutoNotifier; the container made this decision entirely on its own, because the question it asked (“did the user declare something”) got the answer no.

In the second setup, the user only declares their own component for Notifier — CustomNotifier. Now one of the three results changes: the Notifier request is now met with CustomNotifier, the output becomes "custom-notification", and the container explicitly says that it stepped back (“container stepped back”). But Cache and Metric still received no declaration at all, so they still get their automatic defaults. The counter this time is 2 — two of three behaviors are still born with no annotation at all, one now comes from a declaration that really stands in the source.

These two numbers — 3 and 2 — are two separate results the same resolve method produces, with the same three interfaces, with only one input difference. The container itself never changed; the only thing that changed is the list given by the user. The word “automatic” does not mean “always the same” here — it means conditional: if the condition is not met (no one declared anything), the automatic default kicks in; if the condition is met (someone declared their own component), the automatic default is never installed at all.

The setup itself also carries an observation: noAnnotationCount is reset to zero before the setup method is called, because the counter is static — the two setups share the same static field, and without the reset, the second setup’s count would pile up on top of the first’s. This is the measurement’s own internal detail, but it carries a familiar warning: shared state can mix up two scenarios run one after another; here this mixing is deliberately prevented with the line noAnnotationCount = 0;, but the same precaution is not taken for AutoNotifier.constructionCount — that counter deliberately accumulates across the two setups, because what is meant to be measured is exactly this: how many times it was constructed in total.

This conditionality carries automatic configuration’s real promise and its real risk at the same time. The promise: getting a reasonable implementation for a commonly used interface without writing anything at all — three working components come out of a three-line setup, and no one has to know the name AutoNotifier. The risk comes from the same place: the container decides on its own under which condition it steps back, and this condition (whether the isAssignableFrom check returns true or false) appears explicitly in no line of the code at all. Adding a component is not just “installing something new,” it can also mean “taking the previous one’s place,” and this second effect does not show up in the source.

Limiting Measurement: Silent Deactivation

The last line shows how complete this stepping-back is: the AutoNotifier class’s constructor keeps its own construction count, and across two setups that number is only 1 — AutoNotifier was constructed only in the first setup, in the second setup it was never constructed at all. The container did not even attempt to build it; the resolve method left the loop the moment it found CustomNotifier, and never touched the automatic default at all.

This is the lesson’s limiting claim: adding a component can silently disable another component that is never mentioned in the source at all. The person adding CustomNotifier probably only wanted their own notification logic; they do not need to know that AutoNotifier exists, or that it is no longer constructed, because both stand behind the same Notifier interface and the calling side can call send() without knowing which one is running. Deactivation produces no warning, no log line, no exception at all — only a class that is never constructed. Cache and Metric are still automatic in this second setup too, but this too is written nowhere in the source explicitly; it is only the natural result of the silence of “no one declared anything.”

The difference between this defect and the fourth lesson’s spelling difference is that nothing here needs to be misspelled. CustomNotifier was written correctly, implemented the correct interface, worked as expected — the defect is not in its own code, it is in its existence causing another class to never be constructed at all. The defects seen in the second and third lessons (an unbound interface, an unrecognized scope name) were both a shortage; this “defect” is the exact opposite — something being added. None of the three sources measured throughout the course (call, annotation, default) is “good” or “bad” on its own; this lesson shows that the comfort of an automatic default carries an invisibility cost at the same time.

Three Independent Decisions, One Method

The resolve method is called separately for each interface, and each call makes its own decision entirely on its own — the decision made for Notifier has no effect at all on the decision made for Cache or Metric. This independence is deliberate: in a real application framework too, dozens of automatic configuration candidates are evaluated at the same time, and each one answers its own “did the user declare it” question independently for its own interface. This measurement’s three interfaces were chosen to make this independence visible: only Notifier changed in the second setup, the others stayed the same — if these three were tied to each other, changing Notifier could have affected whether Cache stayed automatic too. In this measurement it does not, because the resolve calls never see each other at all.

This independence also draws a limit: the resolve method never asks what to do if it finds more than one candidate in the user’s declared list — the loop returns the moment it finds the first matching candidate, it never looks at the rest of the list at all. In this measurement there is at most one user candidate per interface, so this situation never comes up; but in a real system, two separate components can implement the same interface and both can be declared, and then the winner would no longer be determined by “did the user declare it” but by the list’s order — a defect very similar to the fourth lesson’s ordering dependency in the chain, inside a different mechanism.

Summary

  • When no component is declared for an interface, the container installs its own automatic default; this decision appears in no line of the source at all.
  • When the user declares their own component, the container steps back and never installs the automatic default at all — two behaviors come from the same resolve method, only with an input difference.
  • When the same container is run in two setups, the count of behavior born with no annotation drops from 3 to 2; this difference is explained only by the one component the user declared.
  • What is “automatic” is actually conditional: the condition (no one having declared anything) appears with no word in the source at all.
  • Adding a component can silently disable another component never mentioned in the source at all — the disabled class is never constructed, and it produces no warning at all.

This topic’s five lessons brought up the same three sources over and over: a behavior was born either from a call standing in the source, or from an annotation in the source, or from the container’s own rule — a default. This lesson showed that this third source has its own grade within itself: sometimes the default is a fixed value (the fourth lesson), sometimes a conditional component choice (this lesson). Both share the same point: someone reading the source from top to bottom, without knowing the container’s rule in advance, cannot predict which result will be reached.

Next Step

In both setups, Cache and Metric worked through the same interface, without the call site ever being touched — the calling side never had to know which implementation stood behind it at all. This topic’s last lesson carries this same observation one step further: is it possible to add something from outside to a component’s behavior, without ever touching the call site at all? The next lesson measures this — how a cross-cutting concern (a behavior belonging to no single class, like logging, counting, or auditing) is added without changing the call site.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close