在Spring中将原型Bean注入单例实例

2023/05/13

1. 概述

在这篇文章中,我们介绍将原型bean注入单例实例的不同方法。

默认情况下,Spring bean是单例的,当我们尝试注入不同作用域的bean时,就会出现问题。例如,将原型bean注入单例bean。这被称为作用域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()方法公开:


@Slf4j
public class SingletonBean {

    @Autowired
    private PrototypeBean prototypeBean;

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

    public PrototypeBean getPrototypeBean() {
        log.info(String.valueOf(LocalDate.now()));
        return prototypeBean;
    }
}

接下来,我们加载ApplicationContext并获取两次单例bean:


public class BeanInjectionStarter {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        SingletonBean firstSingleton = context.getBean(SingletonBean.class);
        PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();

        SingletonBean secondSingleton = context.getBean(SingletonBean.class);
        PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

        isTrue(firstPrototype.equals(secondPrototype), "The same instance is returned");
    }
}

下面是控制台的输出:

23:11:16.810 [main] INFO  [c.t.t.i.singletone.SingletonBean] >>> Singleton instance created 
23:11:16.823 [main] INFO  [c.t.t.i.prototype.PrototypeBean] >>> Prototype instance created 
23:11:16.864 [main] INFO  [c.t.t.i.singletone.SingletonBean] >>> 23:11:16.864035400 
23:11:16.865 [main] INFO  [c.t.t.i.singletone.SingletonBean] >>> 23:11:16.865034500 

两个bean只在应用程序上下文启动时初始化一次

3. 注入ApplicationContext

我们还可以将ApplicationContext直接注入到一个bean中。

为此,我们可以使用@Autowire注解或实现ApplicationContextAware接口


@Component
public class SingletonAppContextBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

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

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

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

然而,这种方法有严重的缺点,它与控制反转的原则相矛盾,因为我们直接从容器中请求依赖bean

此外,我们从SingletonAppContextBean类中的applicationContext获取原型bean,这意味着将代码耦合到Spring框架。

4. 方法注入

解决问题的另一种方法是使用@Lookup注解的方法注入:


@Component
public class SingletonLookupBean {

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

Spring将重写带有@Lookup注解的getPrototypeBean()方法。 然后它将bean注册到应用程序上下文中,每当我们请求getPrototypeBean()方法时,它都会返回一个新的PrototypeBean实例。

它使用CGLIB生成负责从应用程序上下文中获取PrototypeBean的字节码

5. javax.inject API

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

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

我们使用Provider接口来注入原型bean。 对于每个getPrototypeInstance()方法调用,myPrototypeBeanProvider.get()方法返回一个新的PrototypeBean实例。

6. 作用域代理

默认情况下,Spring持有对真实对象的引用以执行注入。 在这里,我们创建一个代理对象来注入真实对象和依赖对象

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

为此,我们需要修改AppConfig类:

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

默认情况下,Spring使用CGLIB库直接子类化对象。 为了避免使用CGLIB,我们可以使用ScopedProxyMode.INTERFACES配置代理模式,改为使用JDK动态代理。

7. ObjectFactory接口

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

public class SingletonObjectFactoryBean {
    
    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

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

让我们看看getPrototypeInstance()方法;getObject()为每个请求返回一个全新的PrototypeBean实例。在这里,我们可以更好地控制原型的初始化。

此外,ObjectFactory是框架的一部分;这意味着避免为了使用此方法而进行其他设置。

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

另一种选择是在运行时创建原型bean实例,这也允许我们向实例添加参数。

我们在PrototypeBean类中添加一个name字段:

public class PrototypeBean {
    private String name;

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

    // ...
}

接下来,我们通过使用java.util.Function接口将bean工厂注入到我们的单例bean中:

public class SingletonFunctionBean {

    @Autowired
    private Function<String, PrototypeBean> beanFactory;

    public PrototypeBean getPrototypeInstance(String name) {
        return beanFactory.apply(name);
    }
}

最后,我们必须在配置中定义工厂bean、原型和单例bean:


@Configuration
public class AppConfigFunctionBean {

    @Bean
    public Function<String, PrototypeBean> beanFactory() {
        return this::prototypeBeanWithParam;
    }

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

    @Bean
    public SingletonFunctionBean singletonFunctionBean() {
        return new SingletonFunctionBean();
    }
}

9. 测试

现在我们编写一个简单的JUnit测试来测试ObjectFactory接口的使用:


@ExtendWith(SpringExtension.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = AppConfig.class)
class PrototypeBeanInjectionIntegrationTest {

    @Test
    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("New instance expected", firstInstance != secondInstance);
    }
}

成功启动测试后,我们可以看到每次调用getPrototypeInstance()方法时,都会创建一个新的原型bean实例。

10. 总结

在这个教程中,我们学习了几种将原型bean注入单例实例的方法。 与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章