Injecting Prototype Beans into a Singleton Instance in Spring – 在Spring中把原型Bean注入到Singleton实例中

最后修改: 2018年 3月 11日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick article, we’re going to show different approaches of injecting prototype beans into a singleton instance. We’ll discuss the use cases and the advantages/disadvantages of each scenario.

在这篇简短的文章中,我们将展示将原型Bean注入单子实例的不同方法。我们将讨论每个方案的使用情况和优缺点。

By default, Spring beans are singletons. The problem arises when we try to wire beans of different scopes. For example, a prototype bean into a singleton. This is known as the scoped bean injection problem.

默认情况下,Spring Bean是单体的。当我们试图连接不同作用域的Bean时,问题就来了。例如,将一个原型Bean注入一个单子。这就是所谓的作用域Bean注入问题

To learn more about bean scopes, this write-up is a good place to start.

要了解有关Bean作用域的更多信息,这篇文章是一个不错的开始

2. Prototype Bean Injection Problem

2.原型Bean注入问题

In order to describe the problem, let’s configure the following beans:

为了描述这个问题,我们来配置以下Bean。

@Configuration
public class AppConfig {

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

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

Notice that the first bean has a prototype scope, the other one is a singleton.

请注意,第一个Bean有一个原型作用域,另一个则是一个单子。

Now, let’s inject the prototype-scoped bean into the singleton – and then expose if via the getPrototypeBean() method:

现在,让我们将原型范围的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;
    }
}

Then, let’s load up the ApplicationContext and get the singleton bean twice:

然后,让我们加载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();
    
    // get singleton bean instance one more time
    SingletonBean secondSingleton = context.getBean(SingletonBean.class);
    PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

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

Here’s the output from the console:

下面是控制台的输出。

Singleton Bean created
Prototype Bean created
11:06:57.894
// should create another prototype bean instance here
11:06:58.895

Both beans were initialized only once, at the startup of the application context.

这两个Bean只被初始化了一次,在应用程序上下文的启动阶段。

3. Injecting ApplicationContext

3.注入ApplicationContext

We can also inject the ApplicationContext directly into a bean.

我们也可以将ApplicationContext直接注入Bean中。

To achieve this, either use the @Autowire annotation or implement the ApplicationContextAware interface:

要实现这一点,要么使用@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;
    }
}

Every time the getPrototypeBean() method is called, a new instance of PrototypeBean will be returned from the ApplicationContext.

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

However, this approach has serious disadvantages. It contradicts the principle of inversion of control, as we request the dependencies from the container directly.

然而,这种方法有严重的缺点。它与控制倒置的原则相矛盾,因为我们直接向容器请求依赖。

Also, we fetch the prototype bean from the applicationContext within the SingletonAppcontextBean class. This means coupling the code to the Spring Framework.

另外,我们从SingletonAppcontextBean类中的applicationContext获取原型Bean。这意味着将代码与Spring框架耦合

4. Method Injection

4.方法注射

Another way to solve the problem is method injection with the @Lookup annotation:

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

@Component
public class SingletonLookupBean {

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

Spring will override the getPrototypeBean() method annotated with @Lookup. It then registers the bean into the application context. Whenever we request the getPrototypeBean() method, it returns a new PrototypeBean instance.

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

It will use CGLIB to generate the bytecode responsible for fetching the PrototypeBean from the application context.

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

5. javax.inject API

5.javax.inject API

The setup along with required dependencies are described in this Spring wiring article.

在这篇Spring wiring文章中描述了设置以及所需的依赖性。

Here’s the singleton bean:

这里是单子Bean。

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

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

We use Provider interface to inject the prototype bean. For each getPrototypeInstance() method call, the myPrototypeBeanProvider.get() method returns a new instance of PrototypeBean.

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

6. Scoped Proxy

6.范围内的代理

By default, Spring holds a reference to the real object to perform the injection. Here, we create a proxy object to wire the real object with the dependent one.

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

Each time the method on the proxy object is called, the proxy decides itself whether to create a new instance of the real object or reuse the existing one.

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

To set up this, we modify the Appconfig class to add a new @Scope annotation:

为了设置这一点,我们修改Appconfig类,添加一个新的@Scope注解。

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

By default, Spring uses CGLIB library to directly subclass the objects. To avoid CGLIB usage, we can configure the proxy mode with ScopedProxyMode.INTERFACES, to use the JDK dynamic proxy instead.

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

7. ObjectFactory Interface

7.ObjectFactory接口

Spring provides the ObjectFactory<T> interface to produce on demand objects of the given type:

Spring提供了ObjectFactory<T>接口来按需生产指定类型的对象。

public class SingletonObjectFactoryBean {

    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

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

Let’s have a look at getPrototypeInstance() method; getObject() returns a brand new instance of PrototypeBean for each request. Here, we have more control over initialization of the prototype.

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

Also, the ObjectFactory is a part of the framework; this means avoiding additional setup in order to use this option.

另外,ObjectFactory是框架的一部分;这意味着为了使用这个选项,要避免额外的设置。

8. Create a Bean at Runtime Using java.util.Function

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

Another option is to create the prototype bean instances at runtime, which also allows us to add parameters to the instances.

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

To see an example of this, let’s add a name field to our PrototypeBean class:

为了看看这个例子,让我们给我们的PrototypeBean类添加一个名字字段。

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

    //...   
}

Next, we’ll inject a bean factory into our singleton bean by making use of the java.util.Function interface:

接下来,我们将通过使用java.util.Function接口,将一个Bean Factory注入我们的Singleton Bean中。

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

}

Finally, we have to define the factory bean, prototype and singleton beans in our configuration:

最后,我们必须在配置中定义工厂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. Testing

9.测试

Let’s now write a simple JUnit test to exercise the case with ObjectFactory interface:

现在让我们写一个简单的JUnit测试,用ObjectFactory接口来锻炼这个案例。

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

After successfully launching the test, we can see that each time getPrototypeInstance() method called, a new prototype bean instance created.

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

10. Conclusion

10.结论

In this short tutorial, we learned several ways to inject the prototype bean into the singleton instance.

在这个简短的教程中,我们学习了几种将原型Bean注入单子实例的方法。

As always, the complete code for this tutorial can be found on GitHub project.

一如既往,本教程的完整代码可以在GitHub项目上找到。