skip to content

Because method injection relies on CGLIB subclassing, what constraints and failure modes must you anticipate, and how do you diagnose 'my @Lookup returns the same instance'?

level: principalimportance: should knowfreq 26%

answer

  1. CGLIB subclass -> no final class/method, no private/static
  2. @Bean factory-method beans NOT subclassed -> @Lookup ignored
  3. same-instance? check target scope first
  4. returned bean is $$SpringCGLIB$$ subtype
  5. AOT/native needs hints -> prefer ObjectProvider

basics

~20 s

CGLIB subclasses the bean, so it can't be final, the method can't be final/private/static, and the bean must be instantiable (not from a @Bean factory method). If none of that holds, Spring can't override the method and you get the original body / a stale singleton.

solid answer

~50 s

Method injection (`@Lookup`, `<lookup-method>`, `<replaced-method>`) works by Spring generating a **CGLIB subclass** and overriding the target method. That imposes hard constraints: the **class must not be `final`**, the **method must not be `final`, `private`, or `static`**, and the bean must be **CGLIB-instantiable** — component-scanned classes work, but beans produced by `@Bean` factory methods are **not** subclassed, so the override silently doesn't apply. If you see a `@Lookup` method returning the same object every call, check: (1) is the *target* bean actually prototype-scoped, not singleton? (2) was the bean created by a factory method or is the class/method final, so CGLIB couldn't override it? (3) are you calling it on a hand-constructed instance instead of the container-managed one? Also note the returned bean is a **CGLIB-enhanced subtype**, which matters for `getClass()` checks, and that CGLIB requires a usable constructor.

go deeper

for a junior

Aware CGLIB is involved and the class/method can't be final.

for a middle

Can list the non-final/private/static rules and that factory-method beans don't get the override.

for a senior

Can systematically diagnose stale-instance issues (scope vs override failure) and knows the CGLIB subtype implication.

for a principal

Weighs AOT/native constraints, sets team-wide defaults toward ObjectProvider, and articulates the scope-vs-lookup invariant.

## Why CGLIB, and what it implies Spring cannot rewrite your compiled bytecode in place, so to 'implement' a `@Lookup`/`<lookup-method>`/`<replaced-method>` it generates, at runtime, a **CGLIB subclass** of your bean and **overrides** the target method. Everything that constrains subclass-based overriding therefore constrains method injection. ### Hard constraints - **Class not `final`.** A `final` class can't be subclassed → CGLIB fails, no override. - **Method not `final`, `private`, or `static`.** None of these can be overridden by a subclass. `private`/`static` aren't polymorphic; `final` is sealed. - **Bean must be instantiable by CGLIB.** CGLIB needs to construct the subclass, which requires a usable (typically non-arg or resolvable) constructor. - **Not a `@Bean` factory-method bean.** When a bean is produced by a `@Configuration` factory method, Spring already has the concrete instance from your code and does **not** create a CGLIB subclass for `@Lookup`; the annotation is effectively ignored. Use **component scanning** (`@Component` + stereotype) so Spring controls instantiation. - **The instance is a subtype.** The object you receive is a CGLIB-generated subclass, so `bean.getClass()` is not exactly your class (it has a `$$SpringCGLIB$$`/`$$EnhancerBySpringCGLIB$$`-style name). Identity/`getClass()==` checks and some serialization assumptions can break. ## Diagnosing 'my @Lookup keeps returning the same instance' Work through these in order: 1. **Is the target bean prototype-scoped?** `@Lookup` faithfully returns whatever `getBean` yields. If the returned bean is a **singleton**, every call correctly returns the same shared instance — the bug is the missing `@Scope("prototype")` (or other short scope), not `@Lookup`. 2. **Could CGLIB not override the method?** Check for a `final` class/method, a `private`/`static` method, or a bean created via a **`@Bean` factory method**. In any of these the original method body runs (or the abstract method fails to be implemented), so you see stale behavior. 3. **Are you on the managed instance?** If you `new` the class yourself (e.g., in a test) you bypass the CGLIB subclass entirely and the override never exists. Obtain the bean from the container. 4. **Is the return type/name resolvable?** An unresolvable type or wrong `@Lookup("name")` yields a `NoSuchBeanDefinitionException` / `BeanCurrentlyInCreationException` rather than staleness — but worth confirming. ## Additional failure modes - **`BeanInstantiationException` / CGLIB errors** when the class is final or lacks a constructable path. - **Circular/early-init surprises** if the lookup runs during the container's own bean-creation phase. - **Testing friction:** an abstract bean with a `@Lookup` method can't be instantiated with `new`; either test via an application context or subclass/override the method manually in the test. - **AOT / native images (Spring 6 / GraalVM):** runtime CGLIB proxy generation needs registered hints; method-injection beans may require extra configuration under AOT, which is a reason many teams prefer `ObjectProvider` (no runtime bytecode generation). ## Principal-level guidance - Reserve method injection for cases where a normal method call site is genuinely desirable; otherwise standardize on **`ObjectProvider`/`Provider`**, which avoid CGLIB entirely and behave predictably under AOT/native. - If you must use `@Lookup`, enforce component scanning for those beans, keep classes/methods non-final, and document that instances are CGLIB subtypes. - Remember the root invariant: **method injection controls *when* a lookup happens; the bean's *scope* controls whether that lookup produces a new object.** Both must be right.

  • A @Lookup method returns the same object each call even though everything compiles. Name the two most likely root causes.
    1) The target bean is singleton-scoped, so getBean legitimately returns the same instance — it needs @Scope("prototype"). 2) CGLIB couldn't override the method (final class/method, private/static method, or the bean was created by a @Bean factory method), so the original body runs.
  • Why might method injection need special attention in a GraalVM native image?
    It relies on runtime CGLIB subclass generation. Native/AOT compilation restricts runtime bytecode generation, so proxy hints must be registered. Providers like ObjectProvider avoid this because they need no generated subclass.

saying these in an interview costs you the question

  • Blaming @Lookup when the real issue is a singleton-scoped target
  • Expecting @Lookup to work on @Bean factory-method beans
  • Assuming bean.getClass() equals the declared class after CGLIB enhancement
  • Thinking a final method can be overridden by the container

context