【Spring】原型 Bean 被固定

打印 上一主题 下一主题

主题 869|帖子 869|积分 2607

问题形貌



  • 在界说 Bean 时,有时间我们会使用原型 Bean,比方界说如下:
    1. @Service
    2. @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    3. public class ServiceImpl {
    4. }
    复制代码
  • 然后我们按照下面的方式去使用它:
    1. @RestController
    2. public class HelloWorldController {
    3.     @Autowired
    4.     private ServiceImpl serviceImpl;
    5.     @RequestMapping(path = "hi", method = RequestMethod.GET)
    6.     public String hi(){
    7.          return "helloworld, service is : " + serviceImpl;
    8.     };
    9. }
    复制代码
  • 效果,我们会发现,不管我们访问多少次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:
    1. protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    2.    Field field = (Field) this.member;
    3.    Object value;
    4.    //寻找“bean”
    5.    if (this.cached) {
    6.       value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    7.    }
    8.    else {
    9.      //省略其他非关键代码
    10.      value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    11.    }
    12.    if (value != null) {
    13.       //将bean设置给成员字段
    14.       ReflectionUtils.makeAccessible(field);
    15.       field.set(bean, value);
    16.    }
    17. }
    复制代码
  • 待我们探求到要主动注入的 Bean 后,即可通过反射设置给对应的 field。这个 field 的执行只发生了一次,以是后续就固定起来了,它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。
  • 以是,当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。
问题修正



  • 通过上述源码分析,我们可以知道要修正这个问题,肯定是不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。以是这里我提供了两种修正方式:
1. 主动注入 Context



  • 即主动注入 ApplicationContext,然后界说 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 范例实例。修正代码如下:
    1. @RestController
    2. public class HelloWorldController {
    3.     @Autowired
    4.     private ApplicationContext applicationContext;
    5.     @RequestMapping(path = "hi", method = RequestMethod.GET)
    6.     public String hi(){
    7.          return "helloworld, service is : " + getServiceImpl();
    8.     };
    9.     public ServiceImpl getServiceImpl(){
    10.         return applicationContext.getBean(ServiceImpl.class);
    11.     }
    12. }
    复制代码
2. 使用 Lookup 注解



  • 类似修正方法 1,也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。修正代码如下:
    1. @RestController
    2. public class HelloWorldController {
    3.     @RequestMapping(path = "hi", method = RequestMethod.GET)
    4.     public String hi(){
    5.          return "helloworld, service is : " + getServiceImpl();
    6.     };
    7.     @Lookup
    8.     public ServiceImpl getServiceImpl(){
    9.         return null;
    10.     }  
    11. }
    复制代码

    • 通过这两种修正方式,再次测试程序,我们会发现效果已经符合预期(每次访问这个接口,都会创建新的 Bean)。

  • 这里我们不妨再拓展下,讨论下 Lookup 是如何生效的。究竟在修正代码中,我们看到 getServiceImpl 方法的实现返回值是 null,这或许很难说服本身。
  • 首先,我们可以通过调试方式看下方法的执行,参考下图



  • 从上图我们可以看出,我们最终的执行因为标记了 Lookup 而走入了 CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept:
    1. private final BeanFactory owner;
    2. public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
    3.    LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
    4.    Assert.state(lo != null, "LookupOverride not found");
    5.    Object[] argsToUse = (args.length > 0 ? args : null);  // if no-arg, don't insist on args at all
    6.    if (StringUtils.hasText(lo.getBeanName())) {
    7.       return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
    8.             this.owner.getBean(lo.getBeanName()));
    9.    }
    10.    else {
    11.       return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :
    12.             this.owner.getBean(method.getReturnType()));
    13.    }
    14. }
    复制代码
  • 我们的方法调用最终并没有走入案例代码实现的 return null 语句,而是通过 BeanFactory 来获取 Bean。以是从这点也可以看出,其实在我们的 getServiceImpl 方法实现中,任意怎么写都行,这不太告急。
  • 比方,我们可以使用下面的实现来测试下这个结论:
    1. @Lookup
    2. public ServiceImpl getServiceImpl(){
    3.     //下面的日志会输出么?
    4.     log.info("executing this method");
    5.     return null;
    6. }  
    复制代码
  • 以上代码,添加了一行代码输出日志。测试后,我们会发现并没有日志输出。这也验证了,当使用 Lookup 注解一个方法时,这个方法的具体实现已并不告急。
  • 再回溯下前面的分析,为什么我们走入了 CGLIB 搞出的类,这是因为我们有方法标记了 Lookup。我们可以从下面的这段代码得到验证,参考 SimpleInstantiationStrategy#instantiate:
    1. @Override
    2. public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    3.    // Don't override the class with CGLIB if no overrides.
    4.    if (!bd.hasMethodOverrides()) {
    5.       //
    6.       return BeanUtils.instantiateClass(constructorToUse);
    7.    }
    8.    else {
    9.       // Must generate CGLIB subclass.
    10.       return instantiateWithMethodInjection(bd, beanName, owner);
    11.    }
    12. }
    复制代码
  • 在上述代码中,当 hasMethodOverrides 为 true 时,则使用 CGLIB。而在本案例中,这个条件的建立在于剖析 HelloWorldController 这个 Bean 时,我们会发现有方法标记了 Lookup,此时就会添加相应方法到属性 methodOverrides 内里去(此过程由 AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors 完成)。
  • 添加后效果图如下:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

万有斥力

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表