场景题:为何在单例Bean中注入Scope为Prototype的Bean,无法体现原型特征?

225 阅读4分钟

引言

在 Spring 开发中,Bean 的作用域(Scope)决定了其生命周期和实例化行为。原型(Prototype)作用域是常见的一种,旨在每次请求时都创建一个新的 Bean 实例。然而,在实际使用中,开发者可能会发现原型 Bean 的行为与预期不符。本文将通过一个具体案例,分析原型 Bean 注入的问题,探讨其原因,并提供解决方案,同时深入解析 @Lookup 注解的实现机制。


案例描述

我们定义了一个原型作用域的 ServiceImpl Bean,并希望在 HelloWorldController 中通过 @Autowired 注入使用:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}
@RestController
public class HelloWorldController {

    @Autowired
    private ServiceImpl serviceImpl;

    @RequestMapping(path = "/hi", method = RequestMethod.GET)
    public String hi() {
        return "helloworld, service is : " + serviceImpl;
    }
}

运行程序并多次访问 http://localhost:8080/hi,结果始终返回相同的 ServiceImpl 实例,例如:

helloworld, service is : com.spring.puzzle.class1.exampleоку1.example3.error.ServiceImpl@4908af

这与我们期望的原型 Bean 每次请求生成新实例的初衷背道而驰。为什么会这样呢?


问题分析

问题的核心在于 Spring 的依赖注入机制。当 HelloWorldController(默认单例作用域)被创建时,Spring 会通过 @Autowired 注解为 serviceImpl 字段注入一个 ServiceImpl 实例。这一过程由 AutowiredAnnotationBeanPostProcessorDefaultListableBeanFactory 完成,具体逻辑在 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject 方法中:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Field field = (Field) this.member;
    Object value;
    if (this.cached) {
        value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    } else {
        value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    }
    if (value != null) {
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}

serviceImpl 字段在 HelloWorldController 创建时被赋值一次,且由于 HelloWorldController 是单例 Bean,其字段值在整个生命周期内保持不变。因此,即使 ServiceImpl 是原型作用域,注入的实例也不会动态变化。


解决方案

要实现每次请求都获取新的 ServiceImpl 实例,需避免将原型 Bean 固定到单例 Bean 的字段上。以下是两种解决方案:

1. 注入 ApplicationContext 获取动态实例

通过注入 ApplicationContext,在需要时动态获取 ServiceImpl 实例:

@RestController
public class HelloWorldController {

    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(path = "/hi", method = RequestMethod.GET)
    public String hi() {
        return "helloworld, service is : " + getServiceImpl();
    }

    public ServiceImpl getServiceImpl() {
        return applicationContext.getBean(ServiceImpl.class);
    }
}

每次调用 getServiceImpl()ApplicationContext 会创建一个新的 ServiceImpl 实例,符合原型作用域的预期。

2. 使用 @Lookup 注解

通过 @Lookup 注解标记一个方法,Spring 会动态生成该方法的实现,返回新的 Bean 实例:

@RestController
public class HelloWorldController {

    @RequestMapping(path = "/hi", method = RequestMethod.GET)
    public String hi() {
        return "helloworld, service is : " + getServiceImpl();
    }

    @Lookup
    public ServiceImpl getServiceImpl() {
        return null; // 实现无关紧要
    }
}

运行后,每次访问 /hi 接口,getServiceImpl() 都会返回一个新的 ServiceImpl 实例。


@Lookup 注解的实现原理

为什么 @Lookup 注解的方法即使返回 null,也能正确返回新的 Bean 实例?这是因为 Spring 在处理 @Lookup 注解时,使用了 CGLIB 动态代理技术,改写了方法的实现。

关键实现

@Lookup 注解的处理主要由 AutowiredAnnotationBeanPostProcessorCglibSubclassingInstantiationStrategy 完成。当 Spring 检测到 @Lookup 注解时,会为目标 Bean(如 HelloWorldController)生成一个 CGLIB 子类,并将 @Lookup 方法的调用拦截到 LookupOverrideMethodInterceptor 中:

public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
    LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
    Assert.state(lo != null, "LookupOverride not found");
    Object[] argsToUse = (args.length > 0 ? args : null);
    if (StringUtils.hasText(lo.getBeanName())) {
        return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
                this.owner.getBean(lo.getBeanName()));
    } else {
        return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :
                this.owner.getBean(method.getReturnType()));
    }
}

该拦截器通过 BeanFactory 获取指定类型的 Bean(ServiceImpl),而非执行原始方法的逻辑(return null)。因此,方法的实现内容(如日志输出)不会被执行。

CGLIB 子类的生成

CGLIB 子类的生成由 SimpleInstantiationStrategy#instantiate 控制:

public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    if (!bd.hasMethodOverrides()) {
        return BeanUtils.instantiateClass(constructorToUse);
    } else {
        return instantiateWithMethodInjection(bd, beanName, owner);
    }
}

当 Bean 定义中包含 @Lookup 注解(记录在 methodOverrides 属性中),Spring 会选择使用 CGLIB 生成子类,拦截 @Lookup 方法的调用。


验证与扩展

为了验证 @Lookup 的行为,我们可以在方法中添加日志:

@Lookup
public ServiceImpl getServiceImpl() {
    log.info("executing this method");
    return null;
}

运行后发现,日志并未输出,证实了 @Lookup 方法的实现被完全忽略,实际调用由 CGLIB 代理完成。

此外,@Lookup 的使用场景不仅限于原型 Bean,还可用于其他需要动态获取 Bean 的场景。开发者需注意,CGLIB 代理会增加一定的性能开销,且要求目标类不能是 final 的。


总结

  1. 问题根源:单例 Bean 的 @Autowired 字段在初始化时固定,原型 Bean 无法动态更新。
  2. 解决方案
    • 使用 ApplicationContext 动态获取 Bean。
    • 使用 @Lookup 注解,通过 CGLIB 代理实现动态 Bean 获取。
  3. 核心机制@Lookup 依赖 CGLIB 生成子类,拦截方法调用,通过 BeanFactory 获取新实例。
  4. 注意事项:确保正确配置作用域,避免循环依赖,了解 CGLIB 的性能影响。

通过深入分析源码和案例,开发者可以更好地理解 Spring 中原型 Bean 的行为,避免常见陷阱,并在实际项目中灵活运用 @Lookup 等高级特性。