Spring Lookup-Method: Deep Dive into Source Code Execution Flow

public class RedApple extends Produce {
    private YellowBanana banana;

    public RedApple() {
        System.out.println("Fresh red apple acquired");
    }

    public YellowBanana getBanana() {
        return banana;
    }

    public void setBanana(YellowBanana banana) {
        this.banana = banana;
    }
}
public class YellowBanana extends Produce {
    public YellowBanana() {
        System.out.println("Fresh yellow banana acquired");
    }
}
public class Produce {
    public Produce() {
        System.out.println("Base produce instance created");
    }
}
public abstract class ProduceBasket {
    public abstract Produce getFreshProduce();
}
 <bean id="redApple" class="com.example.spring.lookup.RedApple" >
        <property name="banana" ref="yellowBanana"></property>
    </bean>
    <bean id="yellowBanana" class="com.example.spring.lookup.YellowBanana" >
    </bean>
    <bean id="produceBasket1" class="com.example.spring.lookup.ProduceBasket">
        <lookup-method name="getFreshProduce" bean="redApple"></lookup-method>
    </bean>
    <bean id="produceBasket2" class="com.example.spring.lookup.ProduceBasket">
        <lookup-method name="getFreshProduce" bean="yellowBanana"></lookup-method>
    </bean>
ApplicationContext context = new ClassPathXmlApplicationContext("lookup-context.xml");
RedApple apple1 = context.getBean(RedApple.class);
System.out.println(apple1.getBanana());
RedApple apple2 = context.getBean(RedApple.class);
System.out.println(apple2.getBanana());

ProduceBasket basket1 = (ProduceBasket) context.getBean("produceBasket1");
basket1.getFreshProduce();

ProduceBasket basket2 = (ProduceBasket) context.getBean("produceBasket2");
basket2.getFreshProduce();
public void prepareMethodOverrides() throws BeanDefinitionValidationException {
    if (hasMethodOverrides()) {
        getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride);
    }
}

For beans with lookup-method configurations (like ProduceBasket), hasMethodOverrides() returns true, initiating validation for each registered method override.

protected void prepareMethodOverride(MethodOverride override) throws BeanDefinitionValidationException {
    int methodCount = ClassUtils.getMethodCountForName(getBeanClass(), override.getMethodName());
    if (methodCount == 0) {
        throw new BeanDefinitionValidationException(
            "Invalid method override: no method named '" + override.getMethodName() +
            "' found on class [" + getBeanClassName() + "]");
    } else if (methodCount == 1) {
        override.setOverloaded(false);
    }
}

This step validates that the target method exists in the bean class. If exactly one matching method is found, it marks the override as non-overloaded to skip unnecessary argument type checks during instantiation.

During the bean factory initialization phase, finishBeanFactoryInitialization() triggers preInstantiateSingletons() to initialize all non-lazy singleton beans. For each bean, merged bean definition are retrieved, and non-abstract, singleton, non-lazy beans are processed via getBean(), which delegates to doGetBean().

In doGetBean(), if the bean isn't cached, createBean() is called to instantiate the bean. Within createBean(), the bean class is resolevd, method overrides are validated again, and since method overrides exist, Spring uses CGLIB instead of direct reflection instantiation.

@Override
public Object instantiate(RootBeanDefinition mergedBeanDef, @Nullable String beanId, BeanFactory owner) {
    if (!mergedBeanDef.hasMethodOverrides()) {
        // Direct reflection instantiation logic
        Constructor<?> constructorToUse = resolveDefaultConstructor(mergedBeanDef);
        return BeanUtils.instantiateClass(constructorToUse);
    } else {
        return instantiateWithMethodInjection(mergedBeanDef, beanId, owner);
    }
}

Since our ProduceBasket bean has lookup-method overrides, instantiateWithMethodInjection() is called, which uses CglibSubclassCreator to generate an enhanced subclass.

public Object instantiate(@Nullable Constructor<?> ctor, Object... args) {
    Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
    Object instance;
    if (ctor == null) {
        instance = BeanUtils.instantiateClass(subclass);
    } else {
        Constructor<?> subclassCtor = subclass.getConstructor(ctor.getParameterTypes());
        instance = subclassCtor.newInstance(args);
    }

    Factory factory = (Factory) instance;
    factory.setCallbacks(new Callback[] {
        NoOp.INSTANCE,
        new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
        new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)
    });
    return instance;
}

The createEnhancedSubclass() method uses CGLIB's Enhancer to generate a subclass of the target bean, configuring a callback filter to map methods to their respective interceptors.

When getFreshProduce() is called on the ProduceBasket proxy, the LookupOverrideMethodInterceptor intercepts the call:

@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    LookupOverride lookupOverride = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
    Assert.state(lookupOverride != null, "LookupOverride not found for method");
    Object[] targetArgs = (args.length > 0 ? args : null);

    if (StringUtils.hasText(lookupOverride.getBeanName())) {
        return (targetArgs != null ? this.owner.getBean(lookupOverride.getBeanName(), targetArgs) :
                this.owner.getBean(lookupOverride.getBeanName()));
    } else {
        return (targetArgs != null ? this.owner.getBean(method.getReturnType(), targetArgs) :
                this.owner.getBean(method.getReturnType()));
    }
}

This interceptor fetches the specified bean from the Spring container on each call, ensuring that prototype-scoped beans return new instances every time—solving the problem where singleton beans would cache prototype beans when using direct injection.

Tags: Spring Framework Lookup-Method CGLIB Proxy Bean Lifecycle source code analysis

Posted on Mon, 22 Jun 2026 16:50:06 +0000 by coffejor