Lesson 02 / 19
Dependency Injection
When an IoC container supplies a dependency through the constructor, it is born from an annotation; the same dependency given by hand is born from a call standing in the source — and the same class works with both. When no implementation is bound to an interface, the container throws an exception immediately, but not while the container is being built — at the first request.
Published Updated
In this lesson
- Shows with reflection that constructor-based injection supplies a dependency without any assignment at all in the source
- Proves by running it that both the container-built and the manually built form of the same class work
- Measures when and how an unbound interface produces an exception in the container
Contents
The previous lesson closed by saying that the new Service(manual) line was hand-imitating a
mechanism inside the object container: giving a dependency to a constructor. The Service
class’s single constructor takes a Store parameter, and the container resolves this parameter
on its own while constructing Service — without ever writing new InMemoryStore() in the
source. This lesson’s question is: is this resolution really an injection, or is it just
another name for construction? And more importantly: does the same Service class also work
when its dependency is given by hand?
Dependency injection itself is not a new idea in this course — the Design Patterns course established it as a pattern that provides the separation between principle and detail, the cornerstone of testable design. This lesson does not re-grind that pattern; it measures which mechanism the container applies it with and what it leaves in the source. The question asked is the same: did this behavior come from a call in the source, an annotation, or neither?
The Container Fills the Constructor’s Inside
The container’s get method, when a type is requested, first finds that type’s constructor,
then walks through the constructor’s parameter types one by one and, for each one, calls
itself again: arg[i] = get(constructor.getParameterTypes()[i]);. Since Service’s only
parameter is Store, this loop runs once and calls get(Store.class); since Store is an
interface, the container looks it up in the bindings map, constructs the InMemoryStore it
finds, and returns it. In the end, when constructor.newInstance(arg) is called, arg[0] is
already a ready InMemoryStore instance — there is no line in the source that fills this value
at all, filling it happens entirely inside this recursive loop.
This is the definition of constructor-based injection: a dependency is resolved before the
object is constructed and passed to the constructor as an argument. Injection through a field
(the object first being constructed and then a value being written to its field through
reflection) is not built at all in this topic — the container only fills constructor parameters,
because Service‘s store field is final and is assigned in the constructor. This constraint
is deliberate: a final field cannot be changed after it is constructed, so it is impossible for
Service to exist without its dependency — the compiler itself enforces this, not the
container’s courtesy.
The constructor.setAccessible(true) line is worth a separate observation. Service‘s
constructor carries no access modifier at all, meaning it has package visibility; an ordinary
new Service(store) call would work with this visibility anyway, since it would come from the
same package too. But the container itself constructs Service through a general-purpose
Class<?> object, without knowing in advance which package it came from; reflection’s normal
access check steps in in this case, and without the setAccessible(true) call it would throw an
IllegalAccessException. This line is the source of the container’s guarantee that it can
“construct every managed component regardless of its visibility” — and this guarantee, too, is a
decision that never shows up in the source, it only stands in the container’s own code.
The container’s constructor selection also carries an assumption of its own: the get method
makes no selection at all, it says directly type.getDeclaredConstructors()[0] — it assumes from
the start that the class has exactly one constructor, and that even if there is more than
one, the first is correct. Since both Service and InMemoryStore have a single constructor,
this measurement never stresses this assumption, but the container itself never checks it
anywhere. If a second constructor had been added — an overload like
Service(Store store, String label) — the order of the array returned by
getDeclaredConstructors() is not guaranteed by the Java language specification; the
container could not know in advance which constructor it would choose, and if the chosen
constructor’s parameter carried a type that could not be resolved as a managed component (like
String), the recursive get call would throw an exception. This is a fragility this lesson
does not measure but the model carries: constructor-based injection rests on the assumption that
a class has a single way to be constructed, and this assumption is not written explicitly in
the source at all — it can only be understood by looking at how many constructors the class
declares.
The injection measured by this topic is also, at the same time, a narrow injection: the
constructor can only request another managed component or an interface, not a bare value like
String or int. Filling a value-typed dependency requires a separate mechanism — the Value
annotation — and this measurement does not build that at all; this is exactly why InMemoryStore
returns its own data as a fixed value.
- CC6. Constructor parameter resolution is depth-first: if a parameter has its own
dependency, that too is resolved first, through the same
getcall. - CC7.
Service’sstorefield isfinal; this guarantees at compile time that the dependency cannot be assigned through any path in the source other than the constructor. - CC8. The same
InMemoryStoreinstance is constructed both through the container and by hand, and called through the same interface; the measured difference is the construction style, not the call contract. - CC9. The
bindingsmap holds only interface–implementation pairs; the decision of which implementation is selected sits not in the source, but in thebindcall itself. - CC10. When an unbound interface is requested, the container throws an exception naming which type is missing; this exception is not caught, it surfaces to the party making the first request.
// Container.java — did a call in the source produce this dependency, or an annotation
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
public class Container {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
@interface Component {}
static int call = 0, annotation = 0;
static void observe(String behavior, String source) {
if (source.equals("call")) call++; else annotation++;
System.out.printf("%-38s %s%n", behavior, source);
}
static void summary() {
System.out.printf("behavior: %d call, %d annotation%n", call, annotation);
}
final Map<Class<?>, Class<?>> bindings = new HashMap<>();
Container bind(Class<?> iface, Class<?> implementation) { bindings.put(iface, implementation); return this; }
Object get(Class<?> type) throws Exception {
if (type.isInterface()) {
Class<?> implementation = bindings.get(type);
if (implementation == null)
throw new IllegalStateException("no implementation bound to interface: " + type.getSimpleName());
return get(implementation);
}
Component c = type.getAnnotation(Component.class);
if (c == null) throw new IllegalStateException("no component annotation: " + type.getSimpleName());
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object[] arg = new Object[constructor.getParameterCount()];
for (int i = 0; i < arg.length; i++) arg[i] = get(constructor.getParameterTypes()[i]);
return constructor.newInstance(arg);
}
String tryGet(Class<?> type) {
try { get(type); return "ok"; }
catch (Exception e) { return e.getClass().getSimpleName(); }
}
public static void main(String[] args) throws Exception {
Container container = new Container().bind(Store.class, InMemoryStore.class);
Service service = (Service) container.get(Service.class);
Container.observe("dependency supplied", service.store != null ? "annotation" : "call");
InMemoryStore manual = new InMemoryStore();
Container.observe("direct method call", "call");
Container.observe("dependency given by hand", "call");
Service manualService = new Service(manual);
System.out.println("value returned through the container : " + service.report());
System.out.println("value returned through manual wiring : " + manualService.report());
Container.summary();
}
}
interface Store { String read(); }
@Container.Component
class InMemoryStore implements Store {
public String read() { return "data"; }
}
@Container.Component
class Service {
final Store store;
Service(Store store) { this.store = store; }
String report() { return store.read(); }
}
dependency supplied annotation direct method call call dependency given by hand call value returned through the container : data value returned through manual wiring : data behavior: 2 call, 1 annotation
This topic’s first lesson measured this same question across twelve behaviors and four defects,
and arrived at a total of 3 call, 6 annotation, 3 default and 1 error, 2 silent, 1 ineffective; this lesson stays only in that core’s dependency-supplying slice.
Two Paths, the Same Contract
The dependency supplied line comes out annotation because the service.store field fills
with no source line at all — the container gave it to the constructor. The dependency given by hand line comes out call because the same field fills through an argument that really stands
in the source, in the new Service(manual) call. The only difference is where it came from;
Service itself does not know which path constructed it, and it does not need to — the store
field looks at the same Store interface in both cases.
This connects directly to testability: service.report() and manualService.report() both
work, both return the same "data" string, and the direct method call line proves this
separately by calling manual.read() directly, with the container never stepping in at all —
Service can be built by hand and tested. The principle the Design Patterns course established
becomes concrete right here: Service depends on an interface, not on a specific
implementation, which is why passing a fake Store implementation in place of the real
InMemoryStore during a test is possible without changing a single line of Service. The
container does not apply this principle — Service itself was already written according to
this principle; the container only records which implementation corresponds to which interface,
through the bind call, and hands it back at the right moment.
The bindings map itself is also a measurement point: the method
Container bind(Class<?> iface, Class<?> implementation) is explicitly called in the source —
new Container().bind(Store.class, InMemoryStore.class) is a call, not an annotation. The
decision of which implementation is chosen sits not on the classes, but inside the code that
builds the container. This is the lesson’s second observation: even though injection itself is
born from an annotation, the decision of which implementation gets injected comes from a
line that really stands in the source, one that writes it.
The importance of this distinction becomes visible once a second Store implementation is put
in InMemoryStore’s place. The Service class only mentions the Store interface — in the
constructor signature, in the report() body, InMemoryStore’s name appears nowhere at all. If
the line bind(Store.class, AnotherStore.class) replaced bind(Store.class, InMemoryStore.class), not a single character in Service’s source file would change; the only
thing that changes is the line that builds the container. The principle and the detail
the Design Patterns course separated stand exactly at this boundary: the principle is the Store
interface, the detail is the InMemoryStore class, and the decision that connects them is
written not inside Service but outside the container. What this topic measures is that this
decision, too, leaves a trace in the source — just not inside Service.
A small side note: since bindings is a map, calling bind twice for the same interface
silently overwrites the first with the second — no exception, no warning. This lesson does not
measure this separately, but it is an observation from the same family as the third lesson’s
scope measurement: writing to a map carries a different rule than adding to a list.
Limiting Measurement: An Unbound Interface
In the setup above, Store was always bound to InMemoryStore. What happens if this binding is
skipped?
// InterfaceError.java — the container throws an error if no implementation is bound to the interface, and exactly at the first request
public class InterfaceError {
public static void main(String[] args) throws Exception {
Container unbound = new Container();
System.out.println("container built, no request made yet");
System.out.println("number of bound implementations : " + unbound.bindings.size());
String result = unbound.tryGet(Service.class);
System.out.println("exception thrown when Service requested : " + result);
}
}
container built, no request made yet number of bound implementations : 0 exception thrown when Service requested : IllegalStateException
The new Container() line performs no check at all — even if bind was never called, the
container is constructed without a problem, the bindings map stays empty, and no warning
comes out. The defect falls when Service is really requested: get(Service.class), while
trying to resolve its constructor parameter, enters get(Store.class), sees that Store is an
interface, bindings.get(Store.class) returns null, and the container throws an
IllegalStateException explicitly naming the type. This is the only noisy one of this
course’s four defects — the remaining three (a misspelled setting name, an unrecognized scope
name, a wrap marker placed on an interfaceless class) stay silent or ineffective, but an unbound
interface does not leave the container silent.
Timing matters here: the error falls not while the container is built, but at the first
request. A program can execute new Container().bind(...) lines and move on without getting
any error, then crash minutes later the first time Service is requested. The moment the
container is built and the moment the dependency graph is actually validated are not the same
moment — this distinction determines when you will find out if there is a missing link between
managed components.
The Container in this measurement was deliberately left in its simplest form: the get
method notices a missing binding only when that binding is actually requested, it does not scan
and validate the entire dependency graph in advance the moment the container is built. As a
result, a missing binding can go unnoticed for as long as the program never runs that path —
unlike a type mismatch the compiler could catch, this defect only surfaces once the relevant
code path is actually run.
This choice carries a trade-off: early validation (scanning the whole graph while the container
is built and finding missing bindings in advance) risks crashing right at the program’s start
but catches the error before it reaches production; late validation (the path this measurement
chose) keeps the container’s construction fast and simple, but a missing link can go unnoticed
for as long as that code path is never run. This lesson models the second path, because the
container itself is nothing more than a decision that does not show up in the source either —
when the get method validates is, just like where a dependency comes from, a choice that can
only be understood by reading the container’s own code.
Summary
- Constructor-based injection is the mechanism that resolves a dependency before the object is constructed and gives it to the constructor as an argument; there is no assignment anywhere in the source that fills this value.
- The same class can be constructed both through the container and by hand, and both work — as long as the dependency is bound to an interface, the container steps in without breaking testability.
- The decision of which implementation is injected into which interface comes from a
bindcall that really stands in the source; this is a behavior born from a call, not an annotation. - When an unbound interface is requested, the container throws an exception immediately and by name — this is the only noisy one of the four defects.
- This error falls not on the line where the container is built, but at the first moment the dependency is really requested; construction and validation do not happen at the same time.
Next Step
This lesson measured where Service‘s dependency came from, but it never asked when Service
itself is reconstructed by the container — does container.get(Service.class) return the same
instance every time it is called, or does it construct a new one each time? The Component
annotation has a scope field, and this lesson never used it at all; the first lesson’s Service
staying a singleton, and Request‘s reconstruction on every call, both came from exactly this
field’s value. The next lesson opens this field: when does a managed component stay the same,
when is it reconstructed, and at exactly what moment is the init method called?
To keep your progress and take notes, Log in
My notes
Log in to take notes.