BeanDefinition in Detail: The Registry Behind Every Bean
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.
Five words, one line each (they recur throughout):
- Container: the "housekeeper" object (
ApplicationContext/BeanFactory) that creates and manages your objects; you ask it for things withgetBeanand 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
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".

After this article you should be able to answer three questions:
- 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?
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.
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.

This buys two crucial capabilities:
- 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,
@Beanand@Importconjure beans out of thin air
BeanDefinition is an interface; the real carrier is AbstractBeanDefinition and its subclasses. The core fields:
| Field | In plain words |
|---|---|
beanClass | The class used to instantiate the bean (or the class holding the factory method) |
scope | singleton / prototype — one instance or many |
lazyInit | When true, skipped during startup pre-instantiation; created on first use |
primary | Preferred among multiple candidates of the same type |
dependsOn | Names that must be created before this bean (ordering only, not injection) |
initMethodName | Init callback method name (the XML init-method) |
destroyMethodName | Destroy callback method name (the XML destroy-method) |
autowireMode | Autowire mode (byName / byType / constructor / no) |
abstract | An abstract definition used only as a parent template, never instantiated |
parentName | The parent definition's beanName, used for merging |
Here is what it looks like in code:
// 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() 才是 trueBeanDefinitionBuilderis the official builder, sparing you from instantiating concrete subclasses directlyaddConstructorArgValue("Hello")mirrors<constructor-arg value="Hello"/>in XML- Note that
setDependsOnonly guarantees creation order; no object is injected — an easy confusion - One more real trap: the
// singletonabove reflects the case where you set it explicitly. If you never did,getScope()returns an empty string, not"singleton"(the default singleton is decided byisSingleton()), so read definitions withisSingleton() / 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.
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:


Where do definitions come from? Three common paths. First, XML:
// XmlBeanDefinitionReader: turns <bean> tags into definitionsDefaultListableBeanFactory factory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);reader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));Second, annotation scanning:
// ClassPathBeanDefinitionScanner: scans packages, reads annotations -> ScannedGenericBeanDefinitionAnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();ctx.scan("com.example"); // internally a ClassPathBeanDefinitionScannerctx.refresh();Third, @Bean methods:
@Configurationpublic class AppConfig { @Bean // parsed by ConfigurationClassBeanDefinitionReader public HelloService helloService() { return new HelloServiceImpl("Hello"); }}- All three produce the same
BeanDefinition; only the subclass differs, and all land in the sameBeanDefinitionRegistry - XML goes through
XmlBeanDefinitionReader, scanning throughClassPathBeanDefinitionScanner, and@Beanmethods throughConfigurationClassBeanDefinitionReader - So "XML or annotations" differs only at the reading stage; registration, merging and instantiation are completely unified afterwards
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:

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.
Since definitions are the core, we can register one manually at runtime — "I can be a container too":
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(); }}The console prints:
Hi, ManualGenericApplicationContextis a "clean" container: no auto-scanning, no auto-loading XML; you register everything explicitlyregisterBeanDefinition(name, bd)still needs arefresh()— registering is only "taking out a file";refreshis "starting production"- The
Hi, Manualoutput 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.
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.
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".
Parent and child definitions are merged into one complete RootBeanDefinition before instantiation — the MergedBeanDefinition:
<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>- 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:
classcannot be overridden (that throws), and property lists override by name rather than merge
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:
@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()); }}ConfigurationClassPostProcessoris the heavyweight one: it scans@Configuration, processes@Beanand@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.
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:

@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.
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:

@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".
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.
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:
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:
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:
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:
Conditional assembly (@Conditional) decides whether a definition enters the registry at all. Thousands of Boot auto-configured beans are discarded precisely this way:
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:
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":
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:
ctx.getBean("userService"); // the only line you writeString beanName = transformedName(name); // aliases and the & prefix are flattened hereObject cached = getSingleton(beanName); // ask the singleton pool: already built?RootBeanDefinition mbd = mergedDefinition(name); // the file gets merged at this momentObject raw = createBeanInstance(mbd, args); // reflection calls the constructor from the filepopulateBean(raw, mbd); // wiring: fill fields and settersinitializeBean(raw, mbd); // callbacks, then publish it to the pool| what you wrote | : one lookup call |
| container state | refresh() already finished |
ApplicationContext.getBeanTime 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:
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.
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:
beanDefinition.scope = "singleton", lazyInit = falseAt startup: preInstantiateSingletons creates 1 instanceThree consecutive getBean(userService) -> == comparison: true true trueLive instances in memory: 1singletonObjects pool: {userService=UserService@1a2b}
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.
A warm-up, straight from the field table in Section 2 and the sandbox above:
The heavier one, tying Section 6 and the decision card together:
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.
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.
| Error text (excerpt) | Real cause | 30-second fix | Dig deeper in |
|---|---|---|---|
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.UserService' available | The 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 loaded | Ask "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 name | Section 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 disambiguate | Add @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 side | The 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 there | Nine 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 up | The 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 letter | Section 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 collide | Name one explicitly, @RestController("apiUserController"), or narrow the @ComponentScan base packages | Section 7 · #17 SpringApplication |
java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext | You looked a bean up before refresh(), or after close(); typically a hand-built container where definitions were registered but refresh() was never called | The order is fixed: register → refresh() → look up. This is the one line beginners omit when writing a GenericApplicationContext by hand | Section 4 code · #8 the twelve steps of refresh() |
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.
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:
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.
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.
Step one, pom.xml needs a single dependency:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.8</version> </dependency></dependencies>Step two, a minimal class at src/main/java/com/example/demo/HelloServiceImpl.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; }}Step three, the driver at src/main/java/com/example/demo/DefinitionLab.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(); }}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):
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, ManualThree 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.
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.
Change one thing per run, and predict before you execute:
- Add
singletonBd.setLazyInit(true). You will observe the construction line forhiSingletonmove out ofrefresh()to the moment of the firstgetBean—@Lazy/lazyInitchanges only timing, never scope; it remains a singleton. - Call
ctx.registerBeanDefinition("late", bd)afterctx.refresh(). You will observe noIllegalStateExceptionfrom aGenericApplicationContext, but that definition is never pre-instantiated and can only be reached by an explicitgetBean— this is why the Section 4 trap insists onBeanDefinitionRegistryPostProcessorfor late registrations. - Register
hiPrototype2, sameHelloServiceImpltype but a different beanName, then callctx.getBean(HelloServiceImpl.class). You will observeNoUniqueBeanDefinitionException: ... expected single matching bean but found 3, matching row two of the table above; addsingletonBd.setPrimary(true)and it recovers instantly. - Grab the definition with
ctx.getBeanFactory().getBeanDefinition("hiSingleton")and readgetResourceDescription(). You will observenullfor 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?".
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.
Write DefinitionDumper, a mini registry visualizer for any project — plausibly the first framework-grade tool you own.
Requirements:
- A static
dump(ConfigurableApplicationContext ctx)that walksgetBeanDefinitionNames()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
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.
in one sentence, what separates a BeanDefinition from a Bean, and at which moment does the container really call a constructor?
which class reads XML, which reads annotation scanning, which reads @Bean methods — and at what point do all three pipelines merge?
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?
why does a misspelled scope string pass unnoticed? Which single line of code prints the raw value back out of the definition?
why must registerBeanDefinition happen before refresh()? And when you genuinely need to add beans at runtime, what is the correct instrument?
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.