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 注入到單例 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();
// 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");
}這是控制枱的輸出:
Singleton Bean created
Prototype Bean created
11:06:57.894
// should create another prototype bean instance here
11:06:58.895兩個 Bean 僅在應用程序上下文啓動時初始化一次。
3. 注入 ApplicationContext </h2
我們還可以直接將 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 實例。
然而,這種方法存在嚴重缺點。 它與依賴反轉原則相悖,因為我們直接從容器中請求依賴項。
此外,我們在 SingletonAppcontextBean 類中從 applicationContext 中獲取原型 Bean。 這意味着 將代碼與 Spring 框架耦合。
4. 方法注入
另一種解決該問題的方案是使用方法注入,並結合使用 @Lookup 註解:
@Component
public class SingletonLookupBean {
@Lookup
public PrototypeBean getPrototypeBean() {
return null;
}
}Spring 會覆蓋使用 getPrototypeBean() 方法並帶有 @Lookup 註解的方法。它隨後將該 Bean 註冊到應用程序上下文中。每次我們請求 getPrototypeBean() 方法時,它都會返回一個新的 PrototypeBean 實例。
它將使用 CGLIB 生成用於從應用程序上下文中檢索 PrototypeBean 的字節碼。
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. 範圍代理 (Scoped Proxy)
默認情況下,Spring 會持有對真實對象的引用以進行注入。 在這裏,我們創建一個代理對象,用於將真實對象與依賴對象連接起來。
每次在代理對象上調用方法時,代理對象會自行決定是創建真實對象的實例還是重用現有實例。
要設置此功能,我們修改 Appconfig 類以添加新的 @Scope 註解:
@Scope(
value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.TARGET_CLASS)默認情況下,Spring 使用 CGLIB 庫直接子類化對象。為了避免使用 CGLIB,可以配置代理模式為 ScopedProxyMode.INTERFACES,從而使用 JDK 動態代理。
7. ObjectFactory 接口
Spring 提供了 ObjectFactory
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;
logger.info("Prototype instance " + name + " created");
}
//...
}接下來,我們將通過利用 java.util.Function 接口,將一個 Bean 工廠注入到我們的單例 Bean 中:
public class SingletonFunctionBean {
@Autowired
private Function<String, PrototypeBean> beanFactory;
public PrototypeBean getPrototypeInstance(String name) {
PrototypeBean bean = beanFactory.apply(name);
return bean;
}
}最後,我們需要在我們的配置中定義工廠 Bean、原型 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. 測試
現在,我們來編寫一個簡單的 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);
}在測試成功啓動後,我們可以看到每次調用 getPrototypeInstance() 方法時,都會創建一個新的原型 Bean 實例。
10. 結論
在本教程中,我們學習了多種將原型 Bean 注入到單例實例的方法。