当前位置: 首页 > article >正文

【Spring】原型 Bean 被固定

问题描述

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

    @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,访问的结果都是不变的,如下:

    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 {
    
        @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);
        }
    
    }
    
2. 使用 Lookup 注解
  • 类似修正方法 1,也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。修正代码如下:

    @RestController
    public class HelloWorldController {
     
        @RequestMapping(path = "hi", method = RequestMethod.GET)
        public String hi(){
             return "helloworld, service is : " + getServiceImpl();
        };
    
        @Lookup
        public 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 all
       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()));
       }
    }
    
  • 我们的方法调用最终并没有走入案例代码实现的 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 完成)。

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


http://www.kler.cn/a/511872.html

相关文章:

  • Agent AI: 强化学习,模仿学习,大型语言模型和VLMs在智能体中的应用
  • vue3切换路由后页面不报错显示空白,刷新后显示正常
  • 当PHP遇上区块链:一场奇妙的技术之旅
  • Centos7将/dev/mapper/centos-home磁盘空间转移到/dev/mapper/centos-root
  • -bash: /java: cannot execute binary file
  • 20250118-读取并显示彩色图像以及提取彩色图像的 R、G、B 分量
  • 【25】Word:林涵-科普文章❗
  • yum和vim的使用
  • 【Elasticsearch入门到落地】6、索引库的操作
  • Matlab自学笔记四十五:日期时间型和字符、字符串以及double型的相互转换方法
  • React 中hooks之 React useCallback使用方法总结
  • Java 基于微信小程序的原创音乐小程序设计与实现(附源码,部署,文档)
  • Centos7搭建PHP项目,环境(Apache+PHP7.4+Mysql5.7)
  • ubuntu系统文件查找、关键字搜索
  • 2024:成长、创作与平衡的年度全景回顾
  • RabbitMQ---事务及消息分发
  • 【Redis】5种基础数据结构介绍及应用
  • 【MCU】CH591用软件 I2C 出现的 bug
  • 我的创作纪念日——我与CSDN一起走过的365天
  • 从Windows通过XRDP远程访问和控制银河麒麟ukey v10服务器,以及多次连接后黑屏的问题
  • 无数据库开源Wiki引擎WikiDocs
  • Spring的Bean:Bean的生命周期(包括实践)
  • CSS实现实现票据效果 mask与切图方式
  • uniapp——App 监听下载文件状态,打开文件(三)
  • RabbitMQ---应用问题
  • 回顾2024年度 - 挑战之旅:学习、生活与成长的华丽蜕变