引言
在 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 实例。这一过程由 AutowiredAnnotationBeanPostProcessor 和 DefaultListableBeanFactory 完成,具体逻辑在 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 注解的处理主要由 AutowiredAnnotationBeanPostProcessor 和 CglibSubclassingInstantiationStrategy 完成。当 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 的。
总结
- 问题根源:单例 Bean 的
@Autowired字段在初始化时固定,原型 Bean 无法动态更新。 - 解决方案:
- 使用
ApplicationContext动态获取 Bean。 - 使用
@Lookup注解,通过 CGLIB 代理实现动态 Bean 获取。
- 使用
- 核心机制:
@Lookup依赖 CGLIB 生成子类,拦截方法调用,通过BeanFactory获取新实例。 - 注意事项:确保正确配置作用域,避免循环依赖,了解 CGLIB 的性能影响。
通过深入分析源码和案例,开发者可以更好地理解 Spring 中原型 Bean 的行为,避免常见陷阱,并在实际项目中灵活运用 @Lookup 等高级特性。