Bootstrap

解决 Spring 单例 Bean 注入原型 Bean 被共享的问题

在使用 Spring 开发应用时,Bean 的作用域(Scope) 决定了 Bean 实例的生命周期。Spring 提供了多种作用域,其中最常见的是 单例(singleton)原型(prototype) 作用域。

  • 单例作用域(singleton):容器中只有一个实例,整个应用上下文中的 Bean 共享同一个实例。
  • 原型作用域(prototype):每次请求都会创建一个新的实例,每个请求返回一个不同的对象。

理论上,原型 Bean 的每次请求都应返回一个新的实例,而单例 Bean 应该只存在一个实例。问题发生在当 原型 Bean 注入到单例 Bean 中时,原型 Bean 的实例化行为可能并不像预期的那样,每次都返回新的实例。

一、问题描述

假设我们有一个 单例 Bean SingletonBean,它需要使用一个 原型 Bean PrototypeBean 来完成某些功能。通常,我们期望每次调用 SingletonBean 中的 PrototypeBean 时,都能获得一个新的实例。然而,由于 Spring 的默认行为,PrototypeBeanSingletonBean 中可能会被共享,这与我们希望原型 Bean 每次创建新实例的设计相冲突。

示例代码
@Component
@Scope("singleton")  // SingletonBean 是单例作用域
public class SingletonBean {

    @Autowired
    private PrototypeBean prototypeBean; // 注入原型 Bean

    public void doSomething() {
        prototypeBean.doSomething();  // 每次都会调用同一个实例
    }
}

@Component
@Scope("prototype")  // PrototypeBean 是原型作用域
public class PrototypeBean {

    public void doSomething() {
        System.out.println("PrototypeBean doing something");
    }
}

在这个示例中:

  • SingletonBean 是单例作用域的,每次访问都会返回相同的实例。
  • PrototypeBean 是原型作用域的,本应每次请求时创建新的实例,但由于它被注入到 SingletonBean 中,Spring 只会在容器启动时实例化一次 PrototypeBean,并将该实例注入到 SingletonBean 中。

问题的本质是:尽管 PrototypeBean 是原型作用域的,Spring 会在 SingletonBean 创建时注入它的实例,并且在 SingletonBean 的整个生命周期内使用同一个 PrototypeBean 实例,而不会每次都创建新的实例。

二、问题产生的原因

问题的根源在于 Spring 的依赖注入机制

  1. Spring 容器在启动时创建所有的 Bean。对于单例作用域的 Bean,Spring 会在容器初始化时创建实例,并且在整个应用生命周期内保持该实例。而原型作用域的 Bean 每次被请求时都会创建新实例。

  2. 单例 Bean 中注入原型 Bean 时,Spring 会在容器初始化时就实例化 PrototypeBean,并将其注入到 SingletonBean 中。即使 PrototypeBean 是原型作用域的,它的实例也会在容器初始化时就被创建,并且这个实例会在 SingletonBean 的整个生命周期内被共享。

  3. 没有延迟加载机制:单例 Bean 中的 PrototypeBean 注入后,Spring 并不会每次重新创建一个新的实例,而是使用已经注入的原型 Bean 实例。因此,每次访问 SingletonBean 时,得到的 PrototypeBean 实例是同一个,而不是一个新的实例。

三、解决方案

为了确保 单例 Bean 中注入的原型 Bean 每次都能返回新的实例,可以通过以下几种方法来解决这个问题。

1. 使用 ObjectProvider

ObjectProvider 是 Spring 提供的一种 延迟加载机制。通过 ObjectProvider,我们可以在需要时动态地获取原型 Bean,确保每次调用时都返回一个新的实例。

@Component
public class SingletonBean {

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public void doSomething() {
        // 每次调用 getObject() 时都会获取新的 PrototypeBean 实例
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.doSomething();
    }
}
  • ObjectProvider<PrototypeBean>PrototypeBean 以延迟加载的方式实例化,每次调用 getObject() 时都会返回新的实例。
  • 这种方法可以确保每次调用时都获得一个新的原型 Bean 实例,而不是复用已注入的实例。
2. 使用 ApplicationContext 获取原型 Bean

ApplicationContext 提供了一个方法 getBean(),可以动态获取原型作用域的 Bean。通过这种方式,我们可以确保每次获取原型 Bean 时,都会得到一个新的实例。

@Component
public class SingletonBean {

    @Autowired
    private ApplicationContext applicationContext;

    public void doSomething() {
        // 每次调用 getBean() 都会获取新的 PrototypeBean 实例
        PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
        prototypeBean.doSomething();
    }
}
  • applicationContext.getBean(PrototypeBean.class) 每次调用时都会返回新的 PrototypeBean 实例,而不是使用已经注入的实例。
  • 这种方法适用于需要通过 ApplicationContext 动态获取原型 Bean 的场景。
3. 结合 @Scope("prototype")@Lazy 注解

@Lazy 注解可以延迟 Bean 的初始化,确保原型 Bean 只在第一次需要时才会被创建。这结合 @Scope("prototype") 注解可以确保每次请求时都会创建新的实例。

package com.nbsaas.boot.controller.web;

import com.nbsaas.boot.rest.response.ResponseObject;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StateController {

    private Integer account=0;

    @Resource
    private  ObjectProvider<StateHandle> stateHandles;

    @Lazy
    @Resource
    private StateHandle stateHandle;


    @RequestMapping("/state")
    public ResponseObject<String> state(){
        ResponseObject<String> result=new ResponseObject<>();
        account++;
        stateHandles.getObject().handle();

        stateHandle.handle();
        return result;
    }

    @RequestMapping("/account")
    public String account(){
        return "account:"+account;
    }


    @PostConstruct
    public void init(){
       System.out.println("init");
    }


}

当我们在 Spring 中遇到 单例 Bean 注入原型 Bean 被共享的问题时,问题的根源在于 Spring 默认会在容器启动时实例化原型 Bean,并将同一个实例注入单例 Bean。为了解决这个问题,我们可以使用以下几种方法:

  1. 使用 ObjectProvider:延迟加载原型 Bean,每次调用时返回新的实例。
  2. 使用 ApplicationContext:通过 getBean() 动态获取原型 Bean,每次获取新的实例。
  3. 结合 @Scope("prototype")@Lazy 注解:延迟初始化原型 Bean,确保每次请求时创建新的实例。

这些解决方案能确保单例 Bean 中的原型 Bean 每次都创建新的实例,而不是共享同一个实例,从而解决了原型 Bean 被共享的问题。

;