1. 概述

本文将探讨在Spring框架中向单例实例注入原型Bean的几种实现方式。我们将分析每种方案的使用场景及其优缺点。

Spring默认将Bean创建为单例模式。当需要混合不同作用域的Bean时(例如将原型Bean注入单例Bean),就会产生问题——这就是著名的作用域Bean注入问题

关于Bean作用域的基础知识,建议参考Spring Bean作用域详解

2. 原型Bean注入问题

先通过配置两个Bean来描述这个问题:

@Configuration
public class AppConfig {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public PrototypeBean prototypeBean() {
        return new PrototypeBean();
    }

    @Bean
    public SingletonBean singletonBean() {
        return new SingletonBean();
    }
}

注意:第一个Bean是原型作用域,第二个是单例作用域。

现在将原型Bean注入单例Bean,并通过getPrototypeBean()方法暴露:

public class SingletonBean {

    // ..
    @Autowired
    private PrototypeBean prototypeBean;

    public SingletonBean() {
        logger.info("Singleton instance created");
    }

    public PrototypeBean getPrototypeBean() {
        logger.info(String.valueOf(LocalTime.now()));
        return prototypeBean;
    }
}

测试代码:加载ApplicationContext并获取两次单例Bean:

public static void main(String[] args) throws InterruptedException {
    AnnotationConfigApplicationContext context 
      = new AnnotationConfigApplicationContext(AppConfig.class);
    
    SingletonBean firstSingleton = context.getBean(SingletonBean.class);
    PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();
    
    // 再次获取单例Bean实例
    SingletonBean secondSingleton = context.getBean(SingletonBean.class);
    PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

    isTrue(firstPrototype.equals(secondPrototype), "应该返回相同实例");
}

控制台输出:

Singleton Bean created
Prototype Bean created
11:06:57.894
// 此处应该创建新的原型Bean实例
11:06:58.895

两个Bean都只在应用上下文启动时初始化了一次——这就是问题所在。

3. 注入ApplicationContext

可以直接将ApplicationContext注入到Bean中:

实现方式有两种:使用@Autowire注解或实现ApplicationContextAware接口:

public class SingletonAppContextBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public PrototypeBean getPrototypeBean() {
        return applicationContext.getBean(PrototypeBean.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) 
      throws BeansException {
        this.applicationContext = applicationContext;
    }
}

每次调用getPrototypeBean()方法时,都会从ApplicationContext返回一个新的PrototypeBean实例。

但这种方案存在严重缺陷:

  • ❌ 违背了控制反转原则(直接从容器获取依赖)
  • ❌ 将代码与Spring框架强耦合

4. 方法注入

使用@Lookup注解实现方法注入是另一种解决方案:

@Component
public class SingletonLookupBean {

    @Lookup
    public PrototypeBean getPrototypeBean() {
        return null;
    }
}

Spring会重写被@Lookup注解的方法,将其注册到应用上下文。每次调用getPrototypeBean()时都会返回新的PrototypeBean实例。

底层实现:

  • ✅ 使用CGLIB生成字节码
  • ✅ 从应用上下文动态获取原型Bean

5. javax.inject API

依赖配置参考Spring依赖注入

单例Bean实现:

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

    public PrototypeBean getPrototypeInstance() {
        return myPrototypeBeanProvider.get();
    }
}

使用Provider接口注入原型Bean。每次调用getPrototypeInstance()时,myPrototypeBeanProvider.get()都会返回新的PrototypeBean实例。

6. 作用域代理

核心思想: 创建代理对象来连接实际对象和依赖对象,而非直接引用真实对象。

每次调用代理对象的方法时,代理会自行决定创建新实例还是复用现有实例。

修改AppConfig配置:

@Scope(
  value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, 
  proxyMode = ScopedProxyMode.TARGET_CLASS)

代理模式说明:

  • 默认使用CGLIB直接子类化对象
  • 可设置proxyMode = ScopedProxyMode.INTERFACES改用JDK动态代理(避免CGLIB)

7. ObjectFactory接口

Spring提供的ObjectFactory<T>接口可按需生成指定类型对象:

public class SingletonObjectFactoryBean {

    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

    public PrototypeBean getPrototypeInstance() {
        return prototypeBeanObjectFactory.getObject();
    }
}

关键特性:

  • ✅ 每次调用getObject()返回全新的PrototypeBean实例
  • ✅ 对原型Bean初始化有更多控制权
  • ✅ 属于框架原生接口,无需额外配置

8. 使用java.util.Function运行时创建Bean

此方案支持在运行时创建原型Bean实例,并允许添加参数。

先给PrototypeBean添加name字段:

public class PrototypeBean {
    private String name;
    
    public PrototypeBean(String name) {
        this.name = name;
        logger.info("Prototype instance " + name + " created");
    }

    //...   
}

通过java.util.Function接口注入Bean工厂:

public class SingletonFunctionBean {
    
    @Autowired
    private Function<String, PrototypeBean> beanFactory;
    
    public PrototypeBean getPrototypeInstance(String name) {
        PrototypeBean bean = beanFactory.apply(name);
        return bean;
    }
}

配置类中定义工厂Bean:

@Configuration
public class AppConfig {
    @Bean
    public Function<String, PrototypeBean> beanFactory() {
        return name -> prototypeBeanWithParam(name);
    } 

    @Bean
    @Scope(value = "prototype")
    public PrototypeBean prototypeBeanWithParam(String name) {
       return new PrototypeBean(name);
    }
    
    @Bean
    public SingletonFunctionBean singletonFunctionBean() {
        return new SingletonFunctionBean();
    }
    //...
}

9. 测试验证

ObjectFactory方案为例编写JUnit测试:

@Test
public void givenPrototypeInjection_WhenObjectFactory_ThenNewInstanceReturn() {

    AbstractApplicationContext context
     = new AnnotationConfigApplicationContext(AppConfig.class);

    SingletonObjectFactoryBean firstContext
     = context.getBean(SingletonObjectFactoryBean.class);
    SingletonObjectFactoryBean secondContext
     = context.getBean(SingletonObjectFactoryBean.class);

    PrototypeBean firstInstance = firstContext.getPrototypeInstance();
    PrototypeBean secondInstance = secondContext.getPrototypeInstance();

    assertTrue("应返回新实例", firstInstance != secondInstance);
}

测试结果验证:每次调用getPrototypeInstance()都会创建新的原型Bean实例。

10. 总结

本文介绍了在Spring中向单例实例注入原型Bean的七种方案:

方案 优点 缺点
ApplicationContext注入 实现简单 ❌ 违背IoC原则,强耦合
@Lookup方法注入 代码简洁 需CGLIB支持
javax.inject API 标准JSR-330 需额外依赖
作用域代理 配置简单 代理可能影响性能
ObjectFactory接口 框架原生支持 需理解工厂模式
Function动态创建 支持参数化 配置稍复杂

推荐选择:

  • ✅ 简单场景:@Lookup方法注入
  • ✅ 需参数化:java.util.Function方案
  • ⚠️ 避免使用:直接注入ApplicationContext

完整代码示例见GitHub项目


原始标题:Injecting Prototype Beans into a Singleton Instance in Spring | Baeldung