Remember @Lookup? A Hand‑crafted Advanced Implementation for Spring Boot 3

This article demonstrates how to recreate Spring's @Lookup method injection by writing a custom BeanFactoryPostProcessor and CGLIB proxy, enabling singleton beans to dynamically obtain prototype or named beans at runtime, with a complete payment‑service example and full source code.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Remember @Lookup? A Hand‑crafted Advanced Implementation for Spring Boot 3

In a Spring application, a singleton‑scoped bean that depends on a prototype bean or needs to select an implementation at runtime cannot use @Autowired or @Resource because those injections are fixed during singleton initialization. Spring solves this with the official @Lookup method injection.

This article manually reproduces that mechanism by defining a custom @Lookup annotation (without attributes) and a BeanFactoryPostProcessor that scans all bean definitions, finds methods annotated with @Lookup, and generates an enhanced subclass using CGLIB. The processor sets an instanceSupplier that creates a proxy instance with two callbacks: NoOp.INSTANCE and a custom LookupMethodInterceptor.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Lookup {}
@Component
public class LookupBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  private static final Class<?>[] CALLBACK_TYPES = new Class<?>[]{NoOp.class, LookupMethodInterceptor.class};
  private final CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    for (String beanName : beanFactory.getBeanDefinitionNames()) {
      BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
      if (!(beanDefinition instanceof ScannedGenericBeanDefinition sbd)) continue;
      try { processLookupComponent(beanFactory, sbd); } catch (Exception ignored) {}
    }
  }

  private void processLookupComponent(ConfigurableListableBeanFactory beanFactory, ScannedGenericBeanDefinition sbd) throws Exception {
    MetadataReader metadataReader = factory.getMetadataReader(sbd.getBeanClassName());
    String className = metadataReader.getClassMetadata().getClassName();
    Class<?> clazz = Class.forName(className);
    Set<Method> methods = MethodIntrospector.selectMethods(clazz, method -> method.isAnnotationPresent(Lookup.class));
    if (methods.isEmpty()) return;
    Class<?> proxyBeanClass = createEnhancedSubclass(beanFactory, clazz);
    sbd.setInstanceSupplier(() -> {
      Object instance = BeanUtils.instantiateClass(proxyBeanClass);
      Factory factory = (Factory) instance;
      factory.setCallbacks(new Callback[]{NoOp.INSTANCE, new LookupMethodInterceptor(beanFactory)});
      return instance;
    });
  }

  private static Class<?> createEnhancedSubclass(ConfigurableListableBeanFactory beanFactory, Class<?> beanClass) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(beanClass);
    enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
    enhancer.setAttemptLoad(false);
    if (beanFactory instanceof ConfigurableBeanFactory cbf) {
      enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cbf.getBeanClassLoader()));
    }
    enhancer.setCallbackFilter(new LookupMethodCallbackFilter());
    enhancer.setCallbackTypes(CALLBACK_TYPES);
    return enhancer.createClass();
  }
}
public class LookupMethodInterceptor implements MethodInterceptor {
  private final ConfigurableBeanFactory beanFactory;
  public LookupMethodInterceptor(ConfigurableBeanFactory beanFactory) { this.beanFactory = beanFactory; }
  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    if (!method.isAnnotationPresent(Lookup.class)) throw new RuntimeException("@Lookup not present");
    Class<?> returnType = method.getReturnType();
    return args.length > 0 ? beanFactory.getBean(args[0].toString(), returnType) : beanFactory.getBean(returnType);
  }
}
public class LookupMethodCallbackFilter implements CallbackFilter {
  @Override
  public int accept(Method method) {
    return method.isAnnotationPresent(Lookup.class) ? 1 : 0;
  }
}

The interceptor redirects calls to methods annotated with @Lookup to the Spring container, retrieving either a bean by type or, when a name argument is supplied, a bean by name and type.

Example usage: a PaymentService interface with three implementations ( AlipayPaymentService, BankCardPaymentService, WeChatPaymentService) each annotated with @Component("ali"), @Component("bank"), and @Component("wexin"). OrderService defines a @Lookup method getPayment(String type) that returns the appropriate PaymentService based on the supplied name.

public interface PaymentService {
  PayResult pay(String orderId, long amount);
}

@Component("ali")
public class AlipayPaymentService implements PaymentService { /* ... */ }

@Component("bank")
public class BankCardPaymentService implements PaymentService { /* ... */ }

@Component("wexin")
public class WeChatPaymentService implements PaymentService { /* ... */ }
@Service
public class OrderService {
  public void create(String type) {
    PaymentService paymentService = getPayment(type);
    paymentService.pay("S-0001", 6660000);
  }

  @Lookup
  public PaymentService getPayment(String type) { return null; }
}

A CommandLineRunner invokes orderService.create("ali"), orderService.create("bank"), and orderService.create("wexin"), producing console output that shows each payment implementation being called. The article also notes that the same approach works for prototype‑scoped beans.

Overall, the tutorial provides a step‑by‑step walkthrough of recreating Spring's @Lookup functionality, illustrating the problem, the custom solution (annotation, bean processor, CGLIB subclassing, interceptor, and callback filter), and a concrete use case with complete source code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootCGLIBBeanFactoryPostProcessorPrototype Bean@LookupMethod InjectionDynamic Bean Retrieval
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.