欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 维修 > 【Spring】原型 Bean 被固定

【Spring】原型 Bean 被固定

2025/3/25 7:16:30 来源:https://blog.csdn.net/purplesky95/article/details/145271198  浏览:    关键词:【Spring】原型 Bean 被固定

问题描述

  • 在定义 Bean 时,有时候我们会使用原型 Bean,例如定义如下:

    @Service
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class ServiceImpl {
    }
    
  • 然后我们按照下面的方式去使用它:

    @RestController
    public class HelloWorldController {@Autowiredprivate ServiceImpl serviceImpl;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + serviceImpl;};
    }
    
  • 结果,我们会发现,不管我们访问多少次http://localhost:8080/hi,访问的结果都是不变的,如下:

    helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af

  • 很明显,这很可能和我们定义 ServiceImpl 为原型 Bean 的初衷背道而驰,如何理解这个现象呢?

案例分析

  • 当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员(装配方法参考 AbstractAutowireCapableBeanFactory#populateBean)。

  • 具体到执行过程,它会使用很多 BeanPostProcessor 来做完成工作,其中一种是 AutowiredAnnotationBeanPostProcessor,它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean,然后设置给对应的属性(即 serviceImpl 成员)。

  • 关键执行步骤可参考 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject:

    protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Field field = (Field) this.member;Object value;//寻找“bean”if (this.cached) {value = resolvedCachedArgument(beanName, this.cachedFieldValue);}else {//省略其他非关键代码value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);}if (value != null) {//将bean设置给成员字段ReflectionUtils.makeAccessible(field);field.set(bean, value);}
    }
    
  • 待我们寻找到要自动注入的 Bean 后,即可通过反射设置给对应的 field。这个 field 的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。

  • 所以,当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。

问题修正

  • 通过上述源码分析,我们可以知道要修正这个问题,肯定是不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。所以这里我提供了两种修正方式:
1. 自动注入 Context
  • 即自动注入 ApplicationContext,然后定义 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 类型实例。修正代码如下:

    @RestController
    public class HelloWorldController {@Autowiredprivate ApplicationContext applicationContext;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};public ServiceImpl getServiceImpl(){return applicationContext.getBean(ServiceImpl.class);}}
    
2. 使用 Lookup 注解
  • 类似修正方法 1,也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。修正代码如下:

    @RestController
    public class HelloWorldController {@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};@Lookuppublic ServiceImpl getServiceImpl(){return null;}  }
    
    • 通过这两种修正方式,再次测试程序,我们会发现结果已经符合预期(每次访问这个接口,都会创建新的 Bean)。
  • 这里我们不妨再拓展下,讨论下 Lookup 是如何生效的。毕竟在修正代码中,我们看到 getServiceImpl 方法的实现返回值是 null,这或许很难说服自己。

  • 首先,我们可以通过调试方式看下方法的执行,参考下图

在这里插入图片描述

  • 从上图我们可以看出,我们最终的执行因为标记了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept:

    private final BeanFactory owner;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 no-arg, don't insist on args at allif (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()));}
    }
    
  • 我们的方法调用最终并没有走入案例代码实现的 return null 语句,而是通过 BeanFactory 来获取 Bean。所以从这点也可以看出,其实在我们的 getServiceImpl 方法实现中,随便怎么写都行,这不太重要。

  • 例如,我们可以使用下面的实现来测试下这个结论:

    @Lookup
    public ServiceImpl getServiceImpl(){//下面的日志会输出么?log.info("executing this method");return null;
    }  
    
  • 以上代码,添加了一行代码输出日志。测试后,我们会发现并没有日志输出。这也验证了,当使用 Lookup 注解一个方法时,这个方法的具体实现已并不重要。

  • 再回溯下前面的分析,为什么我们走入了 CGLIB 搞出的类,这是因为我们有方法标记了 Lookup。我们可以从下面的这段代码得到验证,参考 SimpleInstantiationStrategy#instantiate:

    @Override
    public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {// Don't override the class with CGLIB if no overrides.if (!bd.hasMethodOverrides()) {//return BeanUtils.instantiateClass(constructorToUse);}else {// Must generate CGLIB subclass.return instantiateWithMethodInjection(bd, beanName, owner);}
    }
    
  • 在上述代码中,当 hasMethodOverrides 为 true 时,则使用 CGLIB。而在本案例中,这个条件的成立在于解析 HelloWorldController 这个 Bean 时,我们会发现有方法标记了 Lookup,此时就会添加相应方法到属性 methodOverrides 里面去(此过程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)。

  • 添加后效果图如下:
    在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词