IoC and DI Explained: What Exactly Gets Inverted

bee2026-10-0838 min read0 views
Starting from order code that explodes on every change: the three readings of IoC, three injection styles, DIP and the Hollywood Principle — the cornerstone for everything else in the kernel.
1 / 126
Section
0. The 30-second version
2 / 126

This article is the conceptual turning point of the whole Spring kernel. The five articles before it taught you how to write code; this one changes who makes the decisions.

3 / 126

In plain words: every class you write needs other classes to work with it (an order service needs a stock repository, a payment client, a mail sender). The question was never "how do they collaborate" but "who supplies those collaborators". The classic answer is to new them yourself inside your code. The Spring answer is that you write only "I need a stock repository", and a dedicated object-butler — the container, i.e. the ApplicationContext object that builds, wires and shelves every bean at startup, waiting for you to pick one up — does the rest.

4 / 126
类比|Analogy

ordering takeout. You do not need to cook, you do not need to know whose wok is whose or when to light the fire — you tap "one tomato-and-egg dish" in the app and it arrives at your door. News-ing your own dependencies is cooking from buying the groceries every single meal; handing it to the container leaves you with exactly one job: stating what you want. What gets inverted is never the line count but who takes the initiative: previously your code went out to hunt for dependencies; now the container delivers them to you, and your constructor parameters are simply the delivery address.

5 / 126
Diagram
Figure · Chapter map: the four things this article is about
Figure · Chapter map: the four things this article is about
6 / 126

After this article you should be able to answer three questions:

7 / 126
  • Which three concrete duties does IoC invert, and where do DI and DIP each sit?
  • At which step of the assembly sequence do constructor, setter and field injection differ, and why is the constructor the official default?
  • When A and B need each other, why can field injection survive while constructor injection only produces an error?
8 / 126
Section
1. The scene: an OrderService that explodes on every change
9 / 126

Look at this familiar piece of code — it works, until a requirement changes:

10 / 126
java
package com.example.order;import com.example.pay.AlipayClient;import com.example.stock.MysqlStockRepository;import com.example.mail.SmtpMailSender;public class OrderService {    // Dependencies welded in with three "new" calls    private final MysqlStockRepository stockRepo = new MysqlStockRepository("jdbc:mysql://...");    private final AlipayClient alipayClient = new AlipayClient("APP_ID", "PRIVATE_KEY");    private final SmtpMailSender mailSender = new SmtpMailSender("smtp.example.com", 465);    public void placeOrder(Long userId, Long productId, int count) {        stockRepo.deduct(productId, count);        alipayClient.pay(userId, productId, count);        mailSender.send(userId, "Order paid");    }}
11 / 126

It "runs", yet it welds three concrete implementations into the business logic. Change one thing and three break:

12 / 126
  • Want to switch to WeChat Pay? You must edit OrderService's fields and every related call site
  • Want a fake stock repository in a unit test? Impossible — new MysqlStockRepository(...) runs the moment the object is constructed, so any test hits a real database
  • Want the stock service to shard? Back to the business class you go
13 / 126

The root cause is not "ugly code"; it is that the decision of "which implementation to use" has been inverted. OrderService both "processes orders" and "decides which stock, which payment, which mail client" — two responsibilities crammed into one class, so a change on either side ripples into the other.

14 / 126
Section
2. Three readings of inversion of control
15 / 126
Diagram
Figure 1 · Before vs after inversion
Figure 1 · Before vs after inversion
16 / 126

"Inversion of Control" sounds mystical, but it breaks down into three concrete things:

17 / 126
Table
What is invertedBeforeAfter
Who creates objectsThe business class calls newThe container creates them
Who wires dependenciesThe business class picks impls and assigns themThe container injects by declaration
Who owns the lifecycleCreate on demand, let GC collectContainer handles create / init / destroy / singleton cache
18 / 126

They share one idea: the initiative for creation and wiring moves from the user to an external container. The business class drops from "director + actor" to "actor only" — it no longer cares where its co-star comes from; it just declares "I need a stock repository" when needed.

19 / 126
Key point

IoC is not a specific technology but a class of design idea. Spring calls itself a "container" precisely because it took over all three jobs: hand it an interface and the concrete implementation is chosen at runtime.

20 / 126
Section
3. DI is how IoC is implemented: three injection styles
21 / 126

IoC is the goal; dependency injection (DI) is the primary means. The same dependency can be injected three ways:

22 / 126
java
@Servicepublic class OrderService {    // (1) Constructor injection (recommended)    private final StockRepository stockRepo;    private final PayClient payClient;    public OrderService(StockRepository stockRepo, PayClient payClient) {        this.stockRepo = stockRepo;        this.payClient = payClient;    }}
23 / 126
java
@Servicepublic class OrderService {    // (2) Setter injection    private StockRepository stockRepo;    @Autowired    public void setStockRepo(StockRepository stockRepo) {        this.stockRepo = stockRepo;    }}
24 / 126
java
@Servicepublic class OrderService {    // (3) Field injection (least effort, least recommended)    @Autowired    private StockRepository stockRepo;}
25 / 126
Table
AspectConstructorSetterField
TestabilityPass a mock via new, no containerConstruct then setRequires reflection or a container
Immutabilityfinal field, fixed after constructionMutableMutable
Circular dependencyExposed immediately, startup failsCan break the cycle via early exposureRelies on container leniency
Official guidanceRecommendedFor optional dependenciesDiscouraged
Missing dependencyFails at compile timeFound at runtimeFound at runtime
26 / 126
Tip

Spring's own documentation recommends — constructor injection for required dependencies, setter injection for optional ones, and field injection only for the simplest cases, if at all. The reason is concrete: constructor injection makes it impossible to create an object with incomplete dependencies.

27 / 126
Section
4. The Dependency Inversion Principle: means versus principle
28 / 126

Interviews love to ask: "How do IoC and DIP relate?" The answer is clean: DIP is the design principle; IoC is one means of realizing it.

29 / 126
java
// X Violates DIP: high-level business depends on a low-level detailpublic class OrderService {    private final AlipayClient alipayClient = new AlipayClient();  // depends on a concrete type}// OK Satisfies DIP: both sides depend on an abstractionpublic class OrderService {    private final PayClient payClient;   // interface only    public OrderService(PayClient payClient) {        this.payClient = payClient;    }}
30 / 126

DIP has two inversions:

31 / 126
  • High-level modules should not depend on low-level modules; both should depend on abstractions. OrderService (high level) should not stare at AlipayClient (low level); it should target the PayClient interface.
  • Abstractions should not depend on details; details should depend on abstractions. The PayClient interface should not know Alipay's signing details — rather, AlipayClient implements the interface.
32 / 126

So the chain is: DIP says "depend on abstractions" → DI provides the mechanism to "inject a concrete implementation of that abstraction" → the IoC container decides "which implementation to inject at runtime". One line: the principle sets the direction, the container does the dirty work.

33 / 126
Note

new itself is not wrong. The test for violating DIP is not "did you call new" but "does the high-level module depend on a concrete implementation type". Calling new inside a factory method or config class is exactly the right way to flip the dependency direction back.

34 / 126
Section
5. The Hollywood Principle: don't call the container, it will call you
35 / 126
Animation
Animation · Don't call us, we'll call you
Animation · Don't call us, we'll call you
36 / 126

"Don't call us, we'll call you" is what a Hollywood casting director tells actors: stop phoning to ask if there is a role, show your ability, and we will reach out. The IoC container is that director. The animation above shows the whole sweep; the six cells below can be clicked one by one — cell ③ in particular explains something most people never think about: the creation order is not something you wrote.

37 / 126
Diagram
FlowThe six Hollywood steps: how the container finds you1 / 6
Click from ① to ⑥; pay attention to ③ — the order is computed, not written by you
→
→
→
→
→
① You declare the dependency
The constructor parameter is the line you wrote into the contract: I need a StockRepository. Note that it names an interface only — no implementation class, no getBean call anywhere.
All clearYou wrote Step ① and the container performs the other five — that is the whole of inversion.
38 / 126

Contrast this with the "service locator" pattern, where business code actively calls ctx.getBean(...) — that is "I phone the director", with lookup logic coupled into the class. DI is "the director calls me", and the class is unaware the container exists. Active lookup versus passive reception — that is inversion of control in its most direct form.

39 / 126
Section
6. What the container gives you: how much code it saves
40 / 126

Many think DI is just "a few fewer new calls", but the container does far more:

41 / 126
Table
CapabilityCode you would write yourselfWhat the container provides
Singleton managementDouble-checked locking + static fieldscope="singleton" out of the box
Dependency resolutionManual topological sort for creation orderAutomatic graph resolution and ordering
Lifecycle callbacksAgree on init/destroy methods and call them on time@PostConstruct / @PreDestroy
Conditional wiringHand-written if/else on environmentThe @Conditional family
AOP integrationHand-written JDK / CGLIB proxy wrappingDeclare @Aspect and it applies
Config injectionRead and parse config files by hand@Value / @ConfigurationProperties
42 / 126
Note

the point of this table is not "how great the container is" but to give you evidence when judging "should I adopt a container". When your needs fall in this table, the container saves you hundreds of lines of glue code.

43 / 126
Section
7. When a container is not worth it
44 / 126

A container is no panacea; all its power rests on the premise that objects need to be managed centrally. For the following, forcing one in is a burden:

45 / 126
Code
Codejava
// A 30-line data-cleaning script: setting up an ApplicationContext is pure overheadpublic class CsvOneOff {    public static void main(String[] args) throws Exception {        try (var reader = Files.newBufferedReader(Path.of(args[0]))) {            reader.lines()                  .filter(line -> !line.startsWith("#"))                  .forEach(System.out::println);        }    }}
Notes
  • One-off tasks, small CLI tools, a single main that exits — container startup cost and mental overhead are not worth it
  • Pure algorithms / pure function libraries: nothing to wire, nothing for the container to touch
  • Minimal function compute that needs extreme cold-start speed: reflection and scanning slow startup down

In one sentence: a container solves "how objects are assembled". When your code has no collaborating objects at all, there is no IoC to invert.

46 / 126
Section
8. Try it: after the container takes over
47 / 126

The demo below shows dependency injection happening for real inside a container. Toggle the conditions and watch the wiring chain react (this one is a kernel lab; the parameter-driven sandbox that shows results instantly lives in Section 11):

48 / 126
Kernel lab
49 / 126
Section
9. Four injection styles on one chart: pick your corner first
50 / 126

Put the four styles on one coordinate system — horizontal: how testable; vertical: whether the dependency can still change after it is set. The choice becomes obvious at a glance. A unit test here means a small test you can run with plain new, without booting the application; a Mock is a stand-in dependency you fabricate to replace the real database or payment gateway.

51 / 126
Diagram
Figure · Four injection styles: immutable vs mutable x testable vs hard to test
Figure · Four injection styles: immutable vs mutable x testable vs hard to test
52 / 126

The chart already shows which quadrant each style sits in. The harder half to remember is the other column: what each style does to you. Tables are easy to skim and hard to recall, so play one round instead — click a style on the left, then click what you think it costs you.

53 / 126
Match
MatchFour styles, four consequencesMatched 0/6 · Missed 0
Left: code you can actually write. Right: the price or the payoff you get for it
Pick a card on the left first
54 / 126

The three styles differ in when the wiring happens, which is the whole answer to "why can a field-injected object be born with nulls". The first experiment shows the cleanest case, constructor injection: the dependency is a parameter, so the object is complete the instant it exists.

55 / 126
Kernel lab
TeaVMConstructor injection: complete at birthidle
Watch the log order: lower beans are built first, then your constructor runs
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
56 / 126

The second switches to field injection — same beans, same chain, but assembly moves to after instantiation. Once you see the gap between "instance created" and "dependencies injected", you understand the source of every trap in Section 12.

57 / 126
Kernel lab
TeaVMField injection: object first, dependencies lateridle
Compare with the previous lab: when the constructor finishes, the field is still null
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
58 / 126

The third is the most valuable cell: A needs B, B needs A, and both use constructor injection. You will watch the container give up at step four — because at that moment not even a half-built A exists, so there is no reference to lend anybody.

59 / 126
Kernel lab
TeaVMConstructor injection meets an A/B cycle: nothing to lendidle
Note when the error fires: it fails before population even starts
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
60 / 126
类比|Analogy

DI is assembling a PC. The motherboard manual only says "a graphics card goes here, PCIe 5.0" — it does not care which brand you bought; any card with the right interface boots the machine. Your class is that motherboard: constructor parameters are the slot specification, and the container is the person who plugs the card in. The tighter you define the slot (final + constructor), the earlier a bad build is caught — nobody trusts a machine whose loose cable only smokes after it has booted.

61 / 126
Section
10. Circular dependencies: what the three-level cache can and cannot rescue
62 / 126

A circular dependency means two or more objects need each other: A holds B and B holds A. The three-level cache is the trio of Maps the container uses to break such cycles, and the crucial one is level three, singletonFactories — it does not store the object itself but a "pickup slip": a factory able to hand out an early, half-finished reference when someone urgently needs one.

63 / 126
Animation
Animation · The A/B cycle: what the three-level cache can and cannot rescue
Animation · The A/B cycle: what the three-level cache can and cannot rescue
64 / 126

Steps 2 and 3 of the animation are the entire story: instantiation (calling a constructor reflectively to get an empty shell) happens before population (filling the shell with dependencies). Field and setter injection both follow "shell first, contents later", so the shell can be lent out at step 3; constructor injection requires the contents to be arguments, so no shell ever exists and there is nothing to borrow.

65 / 126

Cache on versus off differs in exactly these two log sequences:

66 / 126
Kernel lab
TeaVMThree-level cache on and off: one cycle, two fatesidle
Press caches ON first, then caches OFF, and compare which step fails
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
67 / 126
Table
CombinationOutcomeWhy
Singleton + field/setter injectionStarts fineA half-built object exists right after instantiation, so the slip can be redeemed
Singleton + constructor injectionFails at startupThere is not a single reference to hand over yet
Prototype + any injectionAlways failsPrototypes never enter the singleton pool, so there is no warehouse to lend from
68 / 126
Trap

since Spring Boot 2.6, spring.main.allow-circular-references defaults to false, meaning even cycles the three-level cache could have solved are now rejected up front at startup. Do not rush to flip it back to true — that merely permits the bad structure to keep existing; it is not a new solution. The real fixes are extracting the shared part into a third bean, or adding @Lazy at the injection point (see article 10).

69 / 126
Section
11. The container gives you an object — but how many? Scope and lifecycle
70 / 126

Everything so far is about how dependencies arrive. One more distinction matters: does the container hand out one instance of a type or many? That switch is scope — singleton (the default: one instance per container) or prototype (a fresh one per request).

71 / 126

First, see the stations a bean passes from birth to destruction, and where @PostConstruct (initialization callback) and @PreDestroy (destroy callback) land:

72 / 126
Kernel lab
TeaVMThe life of a bean: the full singleton routeidle
Memorize the station order — the error table in Section 12 leans on it
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
73 / 126

Switch the scope to prototype and the last station disappears: once the container hands a prototype over, it lets go completely.

74 / 126
Kernel lab
TeaVMNow as prototype: the container stops at deliveryidle
Check whether the destroy callback line still appears
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
75 / 126

Those two labs only cover singleton and prototype. Real applications add request and session on top, together with two traps of their own: a prototype's destroy callback never runs, and a request-scoped bean injected into a singleton is dead on arrival. The next lab turns "how many live instances exist right now" into a counter you can watch:

76 / 126
Kernel lab
TeaVMHow many instances does each scope keep? Count themidle
Press the lookup step repeatedly and watch the counter: singleton stays at 1, prototype climbs by one every press
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
77 / 126
Kernel lab
TeaVMRequest scope and the scoped-proxy trapidle
First see what happens when a request-scoped bean is injected into a singleton, then switch proxyMode=targetMode and see why that fixes it
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
78 / 126

Now the question beginners get wrong most often: if I call getBean twice for the same UserService, do I get the same object? Change scope and @Lazy on the left; the right panel immediately shows the hashCode shape and the verdict.

79 / 126
Sandbox
SandboxTwo getBean calls on UserService: same object or not
Result
getBean #1 -> UserService@7a3b19
getBean #2 -> UserService@7a3b19
== comparison: true
Singleton pool singletonObjects: {userService=UserService@7a3b19}
# built during startup, so the first lookup costs nothing
The default answer: the same object. One shared instance for the whole application, so state in it is global state.
80 / 126
Note

UserService@7a3b19 in the sandbox is the hexadecimal hashCode shape printed by toString() — same hash, same object. From now on, the fastest way to settle "did I get the same instance?" is to print one hashCode line in two places instead of guessing.

81 / 126

The sandbox answers "what does each combination produce". The console below answers something different: what this container actually holds right now. Every response is computed by the real Java kernel running in your browser, so type boot first, then beans, and walk the claims of this article one command at a time:

82 / 126
Console
83 / 126
Tip

it is worth remembering how beans and di differ — beans reports who is in the container (name, scope, laziness), di reports who survives on whom. Nine out of ten errors in the next section can be pinned down by running one command of each.

84 / 126
Section
12. Common errors, searchable by exact wording
85 / 126

Every entry in the first column can be pasted straight into a search box — do not paraphrase or shorten it:

86 / 126
Table
Error text (excerpt)Real cause30-second fixDig deeper in
java.lang.NullPointerException: Cannot invoke "com.example.StockRepository.deduct(java.lang.Long, int)" because "this.stockRepo" is nullThe object in your hand was new-ed by you, so it never passed through the container and its @Autowired fields were never filledTake it from the container instead: ctx.getBean(OrderService.class); to prove it, print stockRepo right after constructionSections 1 & 5 · Article 5
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderService' availableNo bean of that type exists: the class lacks @Service/@Component, or its package sits outside the scan pathCheck the component annotation, then whether the main class is in a parent package; if both look right, log getBeanNamesForType(OrderService.class)Articles 5 and 7
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.PayClient' available: expected single matching bean but found 2: alipayClient,wechatPayClientTwo implementations of one interface were both scanned and the container cannot pickName it at the injection point with @Qualifier("alipayClient"), or mark the default implementation with @PrimaryThe quadrant chart in Section 9 · Article 7
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?A and B wait on each other with constructor injection (or prototype scope), so the three-level cache has no half-built reference to lendRead the ASCII cycle diagram under The dependencies of some of the beans in the application context form a cycle, then switch to field/setter injection or extract the shared part into bean CSection 10 · Article 10
@Autowired on a static field or static method does nothing, and calling it throws NullPointerExceptionInjection writes values onto an instance member; static belongs to the class, not to any instance, so the container's population step has nowhere to landDrop static and use an instance member; if you truly need a static utility, pass the dependency in as a parameterSection 3
You added @Qualifier and the container still cannot choose, or two injection points want different implementations@Primary answers "who is the default" (global fallback); @Qualifier answers "who do I want right here" (local precision) — when both are present, the qualifier winsKeep exactly one @Primary per interface, write @Qualifier per injection point, or receive everything with Map<String, PayClient> / List<PayClient>Articles 7 and 8
87 / 126
Tip

when searching these errors, query only the first English fragment after the colon (for example expected single matching bean but found 2) — hit rates beat the full sentence, since the trailing wording varies across Spring versions.

88 / 126

Row three is the crash this article sees most often. Do not read the answer first — in the real stack below, click the frame you think is guilty; wrong picks come with an explanation too:

89 / 126
Triage
Error triageNoUniqueBeanDefinitionException

You swap the payment implementation from Alipay to WeChat. Your machine runs fine; the colleague who pulls your branch fails half a second after startup.

APPLICATION FAILED TO START
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.pay.PayClient' available: expected single matching bean but found 2: alipayClient,wechatPayClient
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1289)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:412)
at com.example.order.OrderService.<init>(OrderService.java:22)
... 41 more
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
90 / 126
Section
13. Check yourself
91 / 126
Quiz
Check yourselfAfter switching to constructor injection, does `private final StockRepository stockRepo;` still need an extra annotation to be injected?
Pick one — you get feedback right away
92 / 126
Quiz
Check yourselfWith field injection, why is the dependency still null at the moment your constructor finishes?
Pick one — you get feedback right away
93 / 126
Section
14. News it yourself or hand it to the container: a one-line trade-off
94 / 126

Compress the whole article into one contrast. You have been walking the left-hand road all along, just without seeing where its bill arrives.

95 / 126
Animation
Animation · The handover: who now owns the four jobs
Animation · The handover: who now owns the four jobs
96 / 126
Table
Situationnew yourselfHand it to the container
Swap one implementationAll 20 call sites must changeOne config line or one annotation
Write a unit testDrag up a real database and mail serverPass a mock into the constructor
Add a caching layer temporarilyWrap it by hand at every call siteMark the caching implementation @Primary
Wire the dependency wrongThe compiler will not catch it; humans grepStartup fails and names the missing party
97 / 126
类比|Analogy

IoC is ordering takeout again. You need no cooking skill, no wok, no farm — you place the order and the dish arrives. News-ing dependencies yourself means starting from grocery shopping every single meal. What is inverted is not the amount of code but who acts: your code used to go out hunting for dependencies; now the container brings them to your door while you only keep the delivery address in your constructor signature.

98 / 126

This home-page-sized container demo deserves a few minutes. Flip the condition switches and notice that when a bean's condition fails, the error surfaces at the first downstream bean that needs it, not on the offender itself — which is exactly how the NoSuchBeanDefinitionException row in Section 12 comes to life.

99 / 126
Kernel lab
100 / 126
Section
15. Practice in three levels
101 / 126
Section
Level 1 · Follow along
102 / 126

Goal: produce and read the two errors from this article with your own hands. Note that we deliberately keep the container out of it:

103 / 126
java
package com.example.order;import com.example.stock.StockRepository;import org.springframework.beans.factory.annotation.Autowired;public class OrderService {    @Autowired    private StockRepository stockRepo;   // field injection    public void placeOrder(Long productId, int count) {        stockRepo.deduct(productId, count);   // ← blows up here    }    public StockRepository getStockRepo() {        return stockRepo;                     // demo only: peek at whether it is null    }}
104 / 126
java
package com.example.demo;import com.example.order.OrderService;public class ManualNewDemo {    public static void main(String[] args) {        OrderService service = new OrderService();  // ← root cause: this object is not in the container        System.out.println("stockRepo = " + service.getStockRepo());   // null        service.placeOrder(1L, 2);                  // ← crashes on the next line    }}
105 / 126

Expected output (on JDK 17+ the NPE message names the missing variable for you):

106 / 126
text
Exception in thread "main" java.lang.NullPointerException:    Cannot invoke "com.example.StockRepository.deduct(java.lang.Long, int)" because "this.stockRepo" is null        at com.example.order.OrderService.placeOrder(OrderService.java:12)        at com.example.demo.ManualNewDemo.main(ManualNewDemo.java:9)
107 / 126

Now let the container instantiate it — add a StockRepository bean and register the service — and the same business line works:

108 / 126
java
package com.example.demo;import com.example.order.OrderService;import com.example.stock.FakeStockRepository;import com.example.stock.StockRepository;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationclass AppConfig {    @Bean    StockRepository stockRepository() {        return new FakeStockRepository();   // a test double, no database involved    }    @Bean    OrderService orderService() {        return new OrderService();          // the container instantiates it, so injection happens    }}public class ContainerDemo {    public static void main(String[] args) {        try (AnnotationConfigApplicationContext ctx =                     new AnnotationConfigApplicationContext(AppConfig.class)) {            ctx.getBean(OrderService.class).placeOrder(1L, 2);  // wiring done by the container        }    }}
109 / 126

Done when: you can say out loud why @Autowired did nothing in the first version — because that object never went through the container's population step.

110 / 126
Section
Level 2 · Variants
111 / 126

Three tiny edits, one variable at a time; record what you observe:

112 / 126
  1. Convert the field injection above to constructor injection and still call new OrderService(). You will observe the compiler refusing outright with constructor OrderService in class OrderService cannot be applied to given types — the missing dependency is caught while you type, which is the dividend of the top-left quadrant.
  2. Let AServiceImpl and BServiceImpl require each other through constructors. You will observe startup failing with The dependencies of some of the beans in the application context form a cycle plus an ASCII cycle diagram; switch one side to field injection and the three-level cache breaks the cycle immediately. Then replay the circular lab from Section 10 with caches OFF and watch even field injection collapse.
  3. Annotate UserService with @Scope("prototype") and print the hashCode of getBean(UserService.class) from two places. You will observe two different hashes; remove the annotation and they match again — the Section 11 sandbox is the static version of exactly that pair.
113 / 126
Section
Level 3 · Build something
114 / 126

Build a small three-injection-styles comparison project that turns this article's claims into runnable evidence.

115 / 126
  • One interface PayClient with two implementations AlipayClient and WechatPayClient (each prints a line in its constructor so creation order is visible)
  • Three business classes obtaining PayClient via constructor injection, setter injection and field injection respectively
  • One main: boot an AnnotationConfigApplicationContext, print each class's hashCode and whether its dependency is null, then create the field-injected class with new to reproduce the NPE
  • Finish by letting both implementations coexist, triggering NoUniqueBeanDefinitionException, and fixing it once with @Primary and once with @Qualifier to compare their reach
116 / 126

Acceptance checklist: ① the constructor-injected class is testable with plain JUnit + new, no Spring started; ② the field-injected class fails under the same test and you can name the step responsible; ③ you can explain "who creates the object" in three sentences a colleague understands.

117 / 126
Section
16. Self-check
118 / 126
Self-check

from memory, name the three duties IoC inverts (who creates, who wires, who owns the lifecycle) and give one before/after example for each.

119 / 126
Self-check

what do IoC, DI and DIP each govern? Chain them into one sentence of cause and effect.

120 / 126
Self-check

why can constructor injection keep fields final while field injection cannot? Which step ordering explains it?

121 / 126
Self-check

for an A↔B cycle, field injection survives and constructor injection does not — does the dividing line fall on instantiation or on population? Why do prototypes fail either way?

122 / 126
Self-check

with two implementations of one type, what problem does @Primary solve and what does @Qualifier solve? When should you collect everything with List<T> instead?

123 / 126
Mnemonic

news it yourself means you act; the container supplying you is the inversion. Constructors decide for life; field injection is a restock delivered late.

124 / 126
Section
17. Can you actually use field injection?
125 / 126
Decision
Decisionin a three-person team maintaining a Spring Boot service, a colleague uses `@Autowired` field injection everywhere, arguing "it is shorter and clearer". Should you spend time converting it to constructor injection?
126 / 126
Summary

IoC inverts "who owns creation and wiring"; DI is the means to realize IoC, with constructor injection as the first choice; DIP is the design principle behind it and the container its executor; the Hollywood Principle captures it all in one line — don't call the container, it will call you. To decide whether you need a container, ask just one thing: does your code contain multiple objects that collaborate?