BeanDefinition in Detail: The Registry Behind Every Bean

bee2026-10-0840 min read0 views
A container manages definitions, not objects. Walk every core field of BeanDefinition, its three registration sources, parent merging and the exact moment post-processors rewrite it.
1 / 123
Section
0. The 30-second version
2 / 123

Beginners tend to picture Spring as "a factory that news objects for me in bulk". The truth is: during startup the container does not build objects at all — it first collects a book of instructions on how to build them, recording class names, scopes and init-method names. That instruction sheet is the BeanDefinition; the real object (the bean) is only produced from it later inside refresh(). Everything in this article hangs on one sentence: the container manages definitions, not objects.

3 / 123

Five words, one line each (they recur throughout):

4 / 123
  • Container: the "housekeeper" object (ApplicationContext / BeanFactory) that creates and manages your objects; you ask it for things with getBean and it looks them up by name
  • Bean: your plain Java object that has been handed to the container to create and manage
  • BeanDefinition: a piece of data (not code) describing how to build one bean — which class, singleton or prototype, lazy or not, which init/destroy methods
  • Registry: the Map holding every definition, beanName → BeanDefinition; any lookup starts there
  • Reflection: calling a constructor or method by name at runtime. The container never binds your class at compile time — it builds the object reflectively from the definition's beanClass
5 / 123
类比|Analogy

picture the container as a restaurant kitchen. A BeanDefinition is the recipe card pinned to the wall: dish name (beanName), ingredients (class), portion size and heat (scope, lazyInit, initMethodName). Only when a customer orders (getBean) does the kitchen cook the dish (the bean). Cards can be pinned in advance, copied and amended from a parent (inheritance), or rewritten wholesale by the head chef (BeanFactoryPostProcessor) — and all of that happens before a single dish is cooked. That is the whole meaning of "the definition precedes the object".

6 / 123
Diagram
Figure · Chapter map: what a BeanDefinition actually stores
Figure · Chapter map: what a BeanDefinition actually stores
7 / 123

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

8 / 123
  • Where exactly does the @Scope("prototype") I wrote on a class land — which table, which field?
  • Are "XML configuration" and "annotation configuration" two mechanisms, or two entrances to the same one?
  • Why must changes to which beans exist happen before step 5 of refresh(), while changes to what an object looks like need a different interface?
9 / 123
Section
1. Why BeanDefinition exists: the object is the result, the definition is the blueprint
10 / 123

Ask "what does the container hold?" and the intuitive answer is "objects". That is precisely the common misconception — what really gets registered, looked up and rewritten during startup is the definition of an object, not the object itself.

11 / 123

Think of a household registry: a Bean is the actual person, and a BeanDefinition is that person's file — name (beanName), address (class), family members (dependsOn), notes (scope, lazy init, init method...). To look someone up, you first read the file; what gets copied, merged and edited is also the file. The object is "produced" from the file only at the very last moment.

12 / 123
Diagram
Figure 1 · The registry entry behind each bean
Figure 1 · The registry entry behind each bean
13 / 123

This buys two crucial capabilities:

14 / 123
  • Deferral and conditions: with definitions in hand, the container can "see the whole picture before deciding anything" — merging parent definitions, evaluating conditional wiring, and pre-building AOP proxies all happen before any object exists
  • Programmability: drop a definition into the registry and the container treats it like any other bean. That is exactly how auto-configuration, @Bean and @Import conjure beans out of thin air
15 / 123
Section
2. The core fields, one by one
16 / 123

BeanDefinition is an interface; the real carrier is AbstractBeanDefinition and its subclasses. The core fields:

17 / 123
Table
FieldIn plain words
beanClassThe class used to instantiate the bean (or the class holding the factory method)
scopesingleton / prototype — one instance or many
lazyInitWhen true, skipped during startup pre-instantiation; created on first use
primaryPreferred among multiple candidates of the same type
dependsOnNames that must be created before this bean (ordering only, not injection)
initMethodNameInit callback method name (the XML init-method)
destroyMethodNameDestroy callback method name (the XML destroy-method)
autowireModeAutowire mode (byName / byType / constructor / no)
abstractAn abstract definition used only as a parent template, never instantiated
parentNameThe parent definition's beanName, used for merging
18 / 123

Here is what it looks like in code:

19 / 123
Code
Codejava
// Build a definition with the builder style (recommended since Spring 5.0)AbstractBeanDefinition bd = BeanDefinitionBuilder        .genericBeanDefinition(HelloServiceImpl.class)        .setScope(BeanDefinition.SCOPE_SINGLETON)        .setLazyInit(true)        .setDependsOn("dataSource")        .setInitMethodName("warmUp")        .setDestroyMethodName("clear")        .addConstructorArgValue("Hello")        .getBeanDefinition();System.out.println(bd.getBeanClassName());   // com.example.HelloServiceImplSystem.out.println(bd.getScope());           // "" 未显式设置时为空串,isSingleton() 才是 true
Notes
  • BeanDefinitionBuilder is the official builder, sparing you from instantiating concrete subclasses directly
  • addConstructorArgValue("Hello") mirrors <constructor-arg value="Hello"/> in XML
  • Note that setDependsOn only guarantees creation order; no object is injected — an easy confusion
  • One more real trap: the // singleton above reflects the case where you set it explicitly. If you never did, getScope() returns an empty string, not "singleton" (the default singleton is decided by isSingleton()), so read definitions with isSingleton() / isPrototype() instead of comparing strings — the Level-1 exercise in Section 14 shows you that empty cell

Tip: dependsOn and "dependency injection" are two different things. The first means "build that one before you use me"; the second means "pass it into my constructor / setter". The first controls order, the second transfers values.

20 / 123

Those ten fields are only information on paper. Six steps still separate them from the object in your hand — scan, build the definition, register, get rewritten, instantiate, inject and initialize. Learn that route in ten seconds and every later section will occupy a spot on it:

21 / 123
Animation
Animation · The six steps from fields to an object (quick view)
Animation · The six steps from fields to an object (quick view)
22 / 123
Section
3. The three registration sources
23 / 123
Animation
Animation · From definition to instance
Animation · From definition to instance
24 / 123

Where do definitions come from? Three common paths. First, XML:

25 / 123
java
// XmlBeanDefinitionReader: turns <bean> tags into definitionsDefaultListableBeanFactory factory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);reader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));
26 / 123

Second, annotation scanning:

27 / 123
java
// ClassPathBeanDefinitionScanner: scans packages, reads annotations -> ScannedGenericBeanDefinitionAnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();ctx.scan("com.example");   // internally a ClassPathBeanDefinitionScannerctx.refresh();
28 / 123

Third, @Bean methods:

29 / 123
Code
Codejava
@Configurationpublic class AppConfig {    @Bean                       // parsed by ConfigurationClassBeanDefinitionReader    public HelloService helloService() {        return new HelloServiceImpl("Hello");    }}
Notes
  • All three produce the same BeanDefinition; only the subclass differs, and all land in the same BeanDefinitionRegistry
  • XML goes through XmlBeanDefinitionReader, scanning through ClassPathBeanDefinitionScanner, and @Bean methods through ConfigurationClassBeanDefinitionReader
  • So "XML or annotations" differs only at the reading stage; registration, merging and instantiation are completely unified afterwards
30 / 123

Place the same configuration written both ways side by side and the fields line up one for one — that is the visual proof of the sentence above:

31 / 123
Diagram
Figure · One config: annotations versus XML
Figure · One config: annotations versus XML
32 / 123
类比|Analogy

a restaurant that takes orders both through its app and at the front desk. The two channels look nothing alike, yet both print the very same ticket into the kitchen's system (the BeanDefinition) — the kitchen neither knows nor cares where the order came from. So the "XML people vs annotation people" debate argues about the front of house; the kitchen runs exactly one pipeline.

33 / 123
Section
4. Registering a BeanDefinition by hand
34 / 123

Since definitions are the core, we can register one manually at runtime — "I can be a container too":

35 / 123
java
package com.example;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.context.support.GenericApplicationContext;public class ManualRegistryDemo {    public static void main(String[] args) {        // 1. An empty container: no scanning, no XML loading        GenericApplicationContext ctx = new GenericApplicationContext();        // 2. Register a definition manually: beanName = "dynamicHelloService"        RootBeanDefinition bd = new RootBeanDefinition(HelloServiceImpl.class);        bd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hi");        ctx.registerBeanDefinition("dynamicHelloService", bd);        // 3. refresh() is required before the container instantiates registered definitions        ctx.refresh();        // 4. Use it like any other bean        HelloService hello = ctx.getBean("dynamicHelloService", HelloService.class);        System.out.println(hello.sayHello("Manual"));        ctx.close();    }}
36 / 123

The console prints:

37 / 123
Code
Codetext
Hi, Manual
Notes
  • GenericApplicationContext is a "clean" container: no auto-scanning, no auto-loading XML; you register everything explicitly
  • registerBeanDefinition(name, bd) still needs a refresh() — registering is only "taking out a file"; refresh is "starting production"
  • The Hi, Manual output proves the constructor argument really was injected — without a single line of XML or annotations

Trap: calling registerBeanDefinition after refresh() throws IllegalStateException (the container is already active). To add beans dynamically at runtime, use a BeanDefinitionRegistryPostProcessor or register before refresh(); if you must add later, operate on DefaultListableBeanFactory directly and handle the already-instantiated singleton cache yourself.

38 / 123
Section
5. The BeanDefinition family
39 / 123

Six class names that look like a memorization drill — but only one question matters about each: which door did this definition come in through. Play a round: click the class, then click its origin.

40 / 123
Match
MatchSix definition subclasses, matched to their originMatched 0/6 · Missed 0
Left: the class name you meet in framework source. Right: the door it entered through
Pick a card on the left first
41 / 123
Key point

these subclasses differ only in "where the metadata comes from and how the parent is set"; all of it is normalized onto AbstractBeanDefinition's fields. You need not memorize the names day to day, but when reading framework source, seeing ScannedGenericBeanDefinition tells you "this came from scanning".

42 / 123
Section
6. Merging and post-processors: when a definition gets rewritten
43 / 123

Parent and child definitions are merged into one complete RootBeanDefinition before instantiation — the MergedBeanDefinition:

44 / 123
Code
Codexml
<bean id="baseService" abstract="true">    <property name="timeout" value="3000"/></bean><!-- child inherits baseService: gets timeout=3000 and may override it --><bean id="userService" class="com.example.UserService" parent="baseService">    <property name="timeout" value="5000"/></bean>
Notes
  • The child writes only its differences; undeclared fields come from the parent, declared fields override it
  • A parent with abstract="true" is never instantiated; it is only a template
  • Merging detail: class cannot be overridden (that throws), and property lists override by name rather than merge
45 / 123

Even more powerful: a definition can be rewritten in bulk by a post-processor before instantiation. BeanFactoryPostProcessor runs at step 5 of refresh(), when all definitions are registered but no singleton has been created yet — the last window to change definitions:

46 / 123
Code
Codejava
@Componentpublic class LazyUpgrader implements BeanFactoryPostProcessor {    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {        BeanDefinition bd = beanFactory.getBeanDefinition("userService");        bd.setLazyInit(true);          // turn a bean into a lazy one        System.out.println("rewrote definition: " + bd.getBeanClassName());    }}
Notes
  • ConfigurationClassPostProcessor is the heavyweight one: it scans @Configuration, processes @Bean and @Import, and "translates" the annotation world into definitions
  • Because it runs before pre-instantiation, it decides "which beans exist and what each one looks like"

Note: BeanFactoryPostProcessor rewrites definitions; BeanPostProcessor rewrites instances. The former runs at refresh step 5, before objects exist; the latter runs around initialization. One word apart in the name, worlds apart in timing — a favorite interview distinction.

47 / 123

Those two names that differ by one word are actually half a lifetime apart on the timeline. Cells ①→④ in the animation are the window where the paperwork can still be edited; once ④ passes, nobody reads definitions again, and everything in ⑤⑥ happens to objects:

48 / 123
Animation
Animation · The last window for editing a definition
Animation · The last window for editing a definition
49 / 123
Section
7. Traps at the definition level
50 / 123
Trap

@Bean and @Component produce non-equivalent definitions. A @Bean method defaults to full mode (the @Configuration class is CGLIB-enhanced, so cross-method calls return the same singleton), while a @Bean inside an ordinary class is lite mode (no enhancement; the method is called as a plain method and may create multiple instances). Move a @Bean from a @Configuration class to a @Component class and the instance count may go from 1 to 2 — same definition shape, different semantics.

51 / 123

This one deserves an animation, because "identical definition, one extra object" is the kind of difference beginners refuse to believe. Watch cell ②: in full mode a call to your own @Bean method is intercepted and answered from the singleton pool. Lite mode has no interception — the method is an ordinary method, so every call really does construct something:

52 / 123
Animation
Animation · The same @Bean method: full versus lite
Animation · The same @Bean method: full versus lite
53 / 123
Trap

@Lazy only flips one flag. It moves no code; it just sets lazyInit to true on the definition. So a lazy bean that is genuinely depended upon at startup is still created — @Lazy does not mean "never created", it means "not created while nobody needs it".

54 / 123
Warning

the most misunderstood parent/child rule. If the parent declares a class and the child declares a different one, startup fails with Cannot override bean class. Remember: what is inherited is config such as properties, constructor args and scope; what cannot be overridden is class itself.

55 / 123
Section
8. Try it: watch a definition become a bean
56 / 123

The demo below visualizes "how each definition changes state in the Beans list" — from registration, through being rewritten by a post-processor, to final instantiation:

57 / 123
Kernel lab
58 / 123

That one shows the result of wiring. To follow this article's actual thread — how an annotation becomes a definition — go back to the scanning scene. The lab below is that chain itself: pick "Scan & parse" to see how @Component is translated into an AnnotatedGenericBeanDefinition, then "scope / lazy attrs" to see which field your annotation value actually lands in:

59 / 123
Kernel lab
TeaVMThe birth of a BeanDefinition: annotation to fileidle
Walk scan → attrs → registry in order; finish with 'several candidates' to see two beans of one type coexisting in the registry
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
60 / 123

The XML path works the same way, only its entrance is a file reader. This lab unfolds the whole XML container startup, and its last argument — "a typo in class" — maps directly onto the first row of the error table in Section 13:

61 / 123
Kernel lab
TeaVMInside the XML container start-upidle
Go parse → register → getbean, then 'a typo in class' to see that a bad definition is caught at registration time
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
62 / 123

A definition is not fixed once written: step 5 of refresh() runs processors whose entire job is adding and editing definitions. That is exactly how auto-configuration conjures beans. First locate that step in the twelve-step lab:

63 / 123
Kernel lab
TeaVMWhen definitions are completed and when they freezeidle
Pick 'all twelve' and watch invokeBeanFactoryPostProcessors, then 'the four that matter' to see when beanDefinitionMap becomes final
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
64 / 123

Conditional assembly (@Conditional) decides whether a definition enters the registry at all. Thousands of Boot auto-configured beans are discarded precisely this way:

65 / 123
Kernel lab
TeaVMIf the condition fails, the definition never existsidle
Switch @ConditionalOnClass / OnBean / OnProperty and watch candidates get struck off one by one; the final report is what --debug prints
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
66 / 123

One last definition-level special case belongs here: some definitions name a class that is not the type you receive. FactoryBean is precisely that — a definition that says "factory" while the container hands out "product". That is what Section 2 meant by "beanClass may point at the class holding the factory method". Start with the headline behaviour:

67 / 123
Kernel lab
TeaVMgetBean returns the product, not the factoryidle
Read the type in the log: the registry holds the factory class, the variable holds the product class
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
68 / 123

And the type question — the container answers "what type is this bean?" by calling getObjectType(), which is what type-based injection and @ConditionalOnBean key off. Get that wrong and you get the strangest message in this article: "the definition is there, yet no bean of this type exists":

69 / 123
Kernel lab
TeaVMThe type-check trap: getObjectType decidesidle
Compare with the & prefix: fetching the factory by name and the product by type take different branches
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
70 / 123
Section
8.1 Seven steps of one lookup, executed line by line
71 / 123

Everything above answers where definitions come from. This answers how one is spent. The debugger below shows the real skeleton of getBean; the right-hand panel refreshes the variables and the call stack as you step — press next seven times and watch step ④, the moment the paperwork gets merged and then finally consumed:

72 / 123
Stepper
StepperOne getBean: from opening the file to handing over the object1 / 7
Step seven times; in step 4 the definition is merged into a RootBeanDefinition
Code under debug
1ctx.getBean("userService"); // the only line you write
2String beanName = transformedName(name); // aliases and the & prefix are flattened here
3Object cached = getSingleton(beanName); // ask the singleton pool: already built?
4RootBeanDefinition mbd = mergedDefinition(name); // the file gets merged at this moment
5Object raw = createBeanInstance(mbd, args); // reflection calls the constructor from the file
6populateBean(raw, mbd); // wiring: fill fields and setters
7initializeBean(raw, mbd); // callbacks, then publish it to the pool
Variables now
what you wrote: one lookup call
container staterefresh() already finished
Call stack
1ApplicationContext.getBean
1Beginners read this line as 'the container news an object'. The accurate version is: the container uses beanName to look up a file in the registry. The name is the only key — which is why this whole article is about definitions, not objects.
73 / 123
Section
8.2 Same content, now from the command line: inspect the file, not the dish
74 / 123

Time to type the commands yourself. This console talks to the real container running in your browser; beans prints what the registry holds right now (name, scope, laziness) while di prints the wiring graph. Walk the claims of this article one line at a time:

75 / 123
Console
76 / 123
Tip

lab xmlbean typo followed immediately by beans is the pair worth rehearsing — the first puts a broken definition into the registry, the second shows you that "registered" and "usable" are two different moments. The sigletion cell in the sandbox below is the string version of exactly that gap.

77 / 123
Section
10. Sandbox: what a wrong scope or lazy flag does
78 / 123

What makes BeanDefinition feel abstract for beginners is this: the annotation you write on a class merely fills one cell of one table. Fill it right and you save memory; fill it wrong and every request gets a brand-new object — with no exception anywhere. The sandbox below folds "scope + lazy" into one switch; each position shows the consequence immediately:

79 / 123
Sandbox
SandboxDefinition fields sandbox: scope × lazyInit
Result
beanDefinition.scope = "singleton", lazyInit = false
At startup: preInstantiateSingletons creates 1 instance
Three consecutive getBean(userService) -> == comparison: true true true
Live instances in memory: 1
singletonObjects pool: {userService=UserService@1a2b}
The default: one instance shared process-wide, so its fields must be thread-safe.
80 / 123
Tip

run the last position on purpose once. A typo like @Scope("singletion") is not caught by the IDE and not reported at startup; its only symptom is "my bean never seems to be the same object" — and that sentence is itself the best diagnostic clue.

81 / 123
Section
11. Check yourself
82 / 123

A warm-up, straight from the field table in Section 2 and the sandbox above:

83 / 123
Quiz
Check yourselfYou wrote `@Scope("singletion")` on UserService (one letter short). The app boots normally and the endpoints keep returning data. What is the most likely symptom?
Pick one — you get feedback right away
84 / 123

The heavier one, tying Section 6 and the decision card together:

85 / 123
Quiz
Check yourselfDuring startup you want every bean whose name ends in `Report` to become lazy, and you want one bean's definition switched to a different implementation class. What is the right tool?
Pick one — you get feedback right away
86 / 123
Section
9. Registering beans dynamically: which approach?
87 / 123
Decision
Decisionyou are building a multi-tenant backend where each tenant's data source must be registered into the container at runtime from config, and once registered it must be injectable into other beans. Which route?
88 / 123
Summary

a container never manages objects; it manages the definitions of objects. Keep three things — BeanDefinition is the blueprint, described by fields like beanClass / scope / lazyInit / dependsOn; definitions come from XML, annotation scanning and @Bean methods, all flowing into one registry; and before instantiation a definition can be merged from a parent and rewritten by a BeanFactoryPostProcessor — the very foothold of auto-configuration and dynamic registration.

89 / 123
Section
12. Common errors, searchable by exact wording
90 / 123

Every excerpt below can be pasted into a search box unchanged. They share one family trait: each is a complaint about the paperwork — a missing card, two cards on the same drawer, or a card with the wrong cell filled in.

91 / 123
Table
Error text (excerpt)Real cause30-second fixDig deeper in
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.UserService' availableThe registry holds no such definition: the class sits outside the scan path, forgot @Service/@Component, or it comes from an @Bean method whose configuration class never got loadedAsk "does the definition exist", not "can the object be built": print ctx.getBeanDefinitionNames() and search it; if the type lookup fails, try getBean("userService") by nameSection 3 · #5 your first Spring project
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.Notifier' available: expected single matching bean but found 2: smsNotifier,mailNotifier (actually thrown as its subclass NoUniqueBeanDefinitionException)Several candidates share the type and the injection point carries neither @Qualifier nor @Primary to disambiguateAdd @Qualifier("smsNotifier") at the injection point, or @Primary on the preferred implementation; the multi argument of the bd lab shows two definitions living side by sideThe primary row of the Section 2 field table · #6 IoC and DI
org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'userService' defined in URL [file:beans.xml]: There is already [Generic bean: class [com.example.UserService]] bound.The same beanName was registered twice with incompatible definitions. Since Boot 2.1, spring.main.allow-bean-definition-overriding defaults to false, so startup stops right thereNine times out of ten it is XML mixed with annotations, or a duplicated @ComponentScan. Run with -Ddebug=true to see who registered twice; only open the override switch when you truly need it (not long-term)Section 4 · #19 conditional beans
java.lang.IllegalStateException: No Scope registered for scope name 'sigletion'The scope string in the definition is misspelled. Writing it was perfectly legal (nobody validates annotation values), and it only surfaces the first time the bean is looked upThe error site is far from the cause, so skip the business code: ctx.getBeanDefinition("userService").getScope() prints the raw value and you will see the missing letterSection 10 sandbox · #9 bean lifecycle
org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userController' for bean class [com.bee.web.UserController] conflicts with existing, non-compatible bean definition of same name and class [com.bee.api.UserController]Two same-named classes in two packages were both scanned, and the default bean name is the simple class name with a lowercase first letter, so they collideName one explicitly, @RestController("apiUserController"), or narrow the @ComponentScan base packagesSection 7 · #17 SpringApplication
java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContextYou looked a bean up before refresh(), or after close(); typically a hand-built container where definitions were registered but refresh() was never calledThe order is fixed: register → refresh() → look up. This is the one line beginners omit when writing a GenericApplicationContext by handSection 4 code · #8 the twelve steps of refresh()
92 / 123
Tip

of these six rows, the two worth rehearsing are BeanDefinitionOverrideException and ConflictingBeanDefinitionException. Both messages are long, but only two pieces of them carry information: the name that collided and the two places it was registered from. Search the stack for defined in and There is already and you have the files in two glances.

93 / 123

Row one is this article's most typical missing-file scene. Do not read the answer first — click the frame you believe is guilty in this real stack:

94 / 123
Triage
Error triageNoSuchBeanDefinitionException

mvn spring-boot:run starts cleanly on your machine; the integration test fails with 'no bean of this type', even though the class is plainly sitting in the project.

APPLICATION FAILED TO START
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.legacy.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1862)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1425)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1346)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredFieldElement.java:693)
at com.example.report.ReportJob.<init>(ReportJob.java:24)
... 58 more
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
95 / 123
Section
13. Practice in three levels
96 / 123
Section
Level 1 · Follow along
97 / 123

Goal: put two definitions into the registry yourself — one singleton, one prototype — and verify both halves of the story at once: what the definition fields say, and how many objects exist.

98 / 123

Step one, pom.xml needs a single dependency:

99 / 123
xml
<dependencies>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-context</artifactId>        <version>6.1.8</version>    </dependency></dependencies>
100 / 123

Step two, a minimal class at src/main/java/com/example/demo/HelloServiceImpl.java:

101 / 123
java
package com.example.demo;public class HelloServiceImpl {    private final String greeting;    public HelloServiceImpl(String greeting) {        this.greeting = greeting;        System.out.println("   constructing HelloServiceImpl@" + Integer.toHexString(hashCode()));    }    public String sayHello(String name) {        return greeting + ", " + name;    }}
102 / 123

Step three, the driver at src/main/java/com/example/demo/DefinitionLab.java:

103 / 123
java
package com.example.demo;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.support.BeanDefinitionBuilder;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.context.support.GenericApplicationContext;public class DefinitionLab {    public static void main(String[] args) {        GenericApplicationContext ctx = new GenericApplicationContext();        // 1. a hand-written SINGLETON definition, constructor arg "Hi"        RootBeanDefinition singletonBd = new RootBeanDefinition(HelloServiceImpl.class);        singletonBd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hi");        ctx.registerBeanDefinition("hiSingleton", singletonBd);        // 2. a PROTOTYPE definition through the builder, constructor arg "Hello"        BeanDefinition prototypeBd = BeanDefinitionBuilder                .genericBeanDefinition(HelloServiceImpl.class)                .setScope(BeanDefinition.SCOPE_PROTOTYPE)     // <- the only difference is this line                .addConstructorArgValue("Hello")                .getBeanDefinition();        ctx.registerBeanDefinition("hiPrototype", prototypeBd);        // 3. refresh first: registering is the paperwork, refresh starts production        ctx.refresh();        // 4. read the definitions back to prove the fields really were stored        BeanDefinition s = ctx.getBeanDefinition("hiSingleton");        BeanDefinition p = ctx.getBeanDefinition("hiPrototype");        System.out.println("hiSingleton  scope=[" + s.getScope() + "] isSingleton=" + s.isSingleton() + " isLazy=" + s.isLazyInit());        System.out.println("hiPrototype  scope=[" + p.getScope() + "] isSingleton=" + p.isSingleton() + " isLazy=" + p.isLazyInit());        System.out.println("definition count = " + ctx.getBeanDefinitionCount());        // 5. now check identity: the singleton never changes, the prototype always does        Object a1 = ctx.getBean("hiSingleton", HelloServiceImpl.class);        Object a2 = ctx.getBean("hiSingleton", HelloServiceImpl.class);        System.out.println("singleton == ? " + (a1 == a2));        Object b1 = ctx.getBean("hiPrototype", HelloServiceImpl.class);        Object b2 = ctx.getBean("hiPrototype", HelloServiceImpl.class);        System.out.println("prototype == ? " + (b1 == b2));        System.out.println(((HelloServiceImpl) a1).sayHello("Manual"));        ctx.close();    }}
104 / 123

Run main. Expected output (the @xxxxxx parts are hash addresses and will differ on your machine; the singleton must log one construction line, and the two prototype addresses must differ):

105 / 123
text
   constructing HelloServiceImpl@5f2108b5      <- this line lands inside refresh(): the singleton was pre-instantiatedhiSingleton  scope=[] isSingleton=true isLazy=falsehiPrototype  scope=[prototype] isSingleton=false isLazy=falsedefinition count = 2singleton == ? true                            <- both lookups hit the cache, no new construction line   constructing HelloServiceImpl@71bbf57e   constructing HelloServiceImpl@2f92e0f4prototype == ? false                           <- one extra construction line per lookup   (delete the setScope line and rerun: the two lines disappear and this becomes true)Hi, Manual
106 / 123

Three readings of that output: ① the first construction line appears inside refresh(), not after any getBean — which is "registering is paperwork, refresh is production" proven on your own console; ② scope=[] is an empty string — the default singleton is not written into the definition as the word "singleton", only an explicit setting appears there, which is why you must ask isSingleton() instead of comparing strings; ③ the definition count is exactly 2, because GenericApplicationContext inserts no built-ins, unlike AnnotationConfigApplicationContext, which gives you a dozen internal bean names the moment you refresh.

107 / 123

Done when: ① you deleted .setScope(SCOPE_PROTOTYPE), saw prototype == ? turn into true — you just edited a definition and watched behaviour follow; ② you can say at which step hiSingleton was created (Section 4 plus step twelve in the next article); ③ you registered one beanName twice with two different classes and wrote down the exception verbatim.

108 / 123
Section
Level 2 · Variants
109 / 123

Change one thing per run, and predict before you execute:

110 / 123
  1. Add singletonBd.setLazyInit(true). You will observe the construction line for hiSingleton move out of refresh() to the moment of the first getBean — @Lazy / lazyInit changes only timing, never scope; it remains a singleton.
  2. Call ctx.registerBeanDefinition("late", bd) after ctx.refresh(). You will observe no IllegalStateException from a GenericApplicationContext, but that definition is never pre-instantiated and can only be reached by an explicit getBean — this is why the Section 4 trap insists on BeanDefinitionRegistryPostProcessor for late registrations.
  3. Register hiPrototype2, same HelloServiceImpl type but a different beanName, then call ctx.getBean(HelloServiceImpl.class). You will observe NoUniqueBeanDefinitionException: ... expected single matching bean but found 3, matching row two of the table above; add singletonBd.setPrimary(true) and it recovers instantly.
  4. Grab the definition with ctx.getBeanFactory().getBeanDefinition("hiSingleton") and read getResourceDescription(). You will observe null for your hand-registered bean, while XML or scanned definitions carry a file name or class path — the single most useful trick when asking "who registered this bean?".
111 / 123

Tip: after variant 3, rerun the bd lab from Section 8 with the multi argument. The framework's view and your manual reproduction should agree exactly; where they don't, you have found something worth writing down.

112 / 123
Section
Level 3 · Build something
113 / 123

Write DefinitionDumper, a mini registry visualizer for any project — plausibly the first framework-grade tool you own.

114 / 123

Requirements:

115 / 123
  • A static dump(ConfigurableApplicationContext ctx) that walks getBeanDefinitionNames() and prints one line per bean: beanName | scope | lazyInit | primary | beanClassName | origin (resourceDescription)
  • A summary block: total definitions, singleton / prototype / other-scope counts, how many are lazy, how many declare a parent (getParentName() != null)
  • A filter argument listing only definitions whose class name contains a keyword, e.g. dump(ctx, "Tx")
  • Bonus: flag suspicious same-name definitions coming from different origins by comparing resourceDescription — that is exactly the pre-emptive check for a BeanDefinitionOverride
116 / 123

Done when: ① running dump(ctx) in an empty Spring Boot project prints hundreds of lines including auto-configured definitions; ② --filter=DataSource narrows it to single digits; ③ you can manufacture a name collision and the tool warns before startup fails; ④ nothing in it depends on private API beyond reflection — every call comes from public ConfigurableListableBeanFactory methods.

117 / 123
Section
14. Self-check
118 / 123
Self-check

in one sentence, what separates a BeanDefinition from a Bean, and at which moment does the container really call a constructor?

119 / 123
Self-check

which class reads XML, which reads annotation scanning, which reads @Bean methods — and at what point do all three pipelines merge?

120 / 123
Self-check

to bulk-change which beans exist, which interface do you implement and at which refresh() step does it run? To change what an object looks like, what do you use instead?

121 / 123
Self-check

why does a misspelled scope string pass unnoticed? Which single line of code prints the raw value back out of the definition?

122 / 123
Self-check

why must registerBeanDefinition happen before refresh()? And when you genuinely need to add beans at runtime, what is the correct instrument?

123 / 123
Mnemonic

the container reads the card, not the dish — beanName is the drawer, BeanDefinition is the card; edit cards with a BFPP (step five), edit dishes with a BPP (after birth), and two cards in one drawer throws Override.