[英]How do you create a prototype-scoped @Bean with runtime arguments? With getBean(String name, Object... args)?
[英]Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments?
使用 Spring 的 Java Config,我需要使用只能在運行時獲得的構造函數參數來獲取/實例化一個原型范圍的 bean。 考慮以下代碼示例(為簡潔起見進行了簡化):
@Autowired
private ApplicationContext appCtx;
public void onRequest(Request request) {
//request is already validated
String name = request.getParameter("name");
Thing thing = appCtx.getBean(Thing.class, name);
//System.out.println(thing.getName()); //prints name
}
其中 Thing 類定義如下:
public class Thing {
private final String name;
@Autowired
private SomeComponent someComponent;
@Autowired
private AnotherComponent anotherComponent;
public Thing(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
注意name
是final
:它只能通過構造函數提供,並保證不變性。 其他依賴項是Thing
類的特定於實現的依賴項,請求處理程序實現不應該知道(緊密耦合到)。
此代碼與 Spring XML 配置完美配合,例如:
<bean id="thing", class="com.whatever.Thing" scope="prototype">
<!-- other post-instantiation properties omitted -->
</bean>
如何使用 Java 配置實現相同的目標? 以下不適用於 Spring 3.x:
@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}
現在,我可以創建一個工廠,例如:
public interface ThingFactory {
public Thing createThing(String name);
}
但這違背了使用 Spring 替換 ServiceLocator 和 Factory 設計模式的全部意義,這對於這個用例來說是理想的。
如果 Spring Java Config 可以做到這一點,我將能夠避免:
對於 Spring 已經通過 XML 配置支持的微不足道的事情,這是大量的工作(相對而言)。
在@Configuration
類中,像這樣的@Bean
方法
@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}
用於注冊bean 定義並提供用於創建 bean 的工廠。 它定義的 bean 僅根據請求使用直接或通過掃描ApplicationContext
確定的參數實例化。
在prototype
bean 的情況下,每次都會創建一個新對象,因此也會執行相應的@Bean
方法。
您可以通過其BeanFactory#getBean(String name, Object... args)
方法從ApplicationContext
檢索一個 bean,該方法聲明
允許指定顯式構造函數參數/工廠方法參數,覆蓋 bean 定義中指定的默認參數(如果有)。
參數:
如果使用靜態工廠方法的顯式參數創建原型,則使用args參數。 在任何其他情況下使用非空 args 值都是無效的。
換句話說,對於這個prototype
作用域 bean,您提供將使用的參數,而不是在 bean 類的構造函數中,而是在@Bean
方法調用中。 (此方法具有非常弱的類型保證,因為它對 bean 使用名稱查找。)
或者,您可以使用按類型查找 bean 的類型化BeanFactory#getBean(Class requiredType, Object... args)
方法。
至少對於 Spring 版本 4+ 來說是這樣。
請注意,如果您不想從ApplicationContext
或BeanFactory
進行 bean 檢索,您可以注入一個ObjectProvider
(自 Spring 4.3 起)。
專為注入點設計的
ObjectFactory
變體,允許編程可選和寬松的非唯一處理。
並使用它的getObject(Object... args)
方法
返回此工廠管理的對象的實例(可能是共享的或獨立的)。
允許按照
BeanFactory.getBean(String, Object)
指定顯式構造參數。
例如,
@Autowired
private ObjectProvider<Thing> things;
[...]
Thing newThing = things.getObject(name);
[...]
使用 Spring > 4.0 和 Java 8,您可以更安全地執行此操作:
@Configuration
public class ServiceConfig {
@Bean
public Function<String, Thing> thingFactory() {
return name -> thing(name); // or this::thing
}
@Bean
@Scope(value = "prototype")
public Thing thing(String name) {
return new Thing(name);
}
}
用法:
@Autowired
private Function<String, Thing> thingFactory;
public void onRequest(Request request) {
//request is already validated
String name = request.getParameter("name");
Thing thing = thingFactory.apply(name);
// ...
}
所以現在你可以在運行時獲取你的 bean。 這當然是一種工廠模式,但是您可以節省一些時間來編寫像ThingFactory
這樣的特定類(但是您必須編寫自定義@FunctionalInterface
來傳遞兩個以上的參數)。
從 Spring 4.3 開始,有一種新的方法可以做到這一點,這是針對該問題縫制的。
ObjectProvider - 它使您可以將它作為依賴項添加到“有參數的”Prototype 范圍的 bean 中,並使用參數實例化它。
這是一個如何使用它的簡單示例:
@Configuration
public class MyConf {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public MyPrototype createPrototype(String arg) {
return new MyPrototype(arg);
}
}
public class MyPrototype {
private String arg;
public MyPrototype(String arg) {
this.arg = arg;
}
public void action() {
System.out.println(arg);
}
}
@Component
public class UsingMyPrototype {
private ObjectProvider<MyPrototype> myPrototypeProvider;
@Autowired
public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
this.myPrototypeProvider = myPrototypeProvider;
}
public void usePrototype() {
final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
myPrototype.action();
}
}
這當然會在調用 usePrototype 時打印 hello 字符串。
每條評論更新
首先,我不確定你為什么說“這不起作用”對於在 Spring 3.x 中運行良好的東西。 我懷疑您的配置中某處一定有問題。
這有效:
-- 配置文件:
@Configuration
public class ServiceConfig {
// only here to demo execution order
private int count = 1;
@Bean
@Scope(value = "prototype")
public TransferService myFirstService(String param) {
System.out.println("value of count:" + count++);
return new TransferServiceImpl(aSingletonBean(), param);
}
@Bean
public AccountRepository aSingletonBean() {
System.out.println("value of count:" + count++);
return new InMemoryAccountRepository();
}
}
-- 要執行的測試文件:
@Test
public void prototypeTest() {
// create the spring container using the ServiceConfig @Configuration class
ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
Object singleton = ctx.getBean("aSingletonBean");
System.out.println(singleton.toString());
singleton = ctx.getBean("aSingletonBean");
System.out.println(singleton.toString());
TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
System.out.println(transferService.toString());
transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
System.out.println(transferService.toString());
}
使用 Spring 3.2.8 和 Java 7,給出以下輸出:
value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2
所以'Singleton' Bean 被請求兩次。 然而正如我們所料,Spring 只創建一次。 第二次它看到它有那個 bean 並且只返回現有的對象。 構造函數(@Bean 方法)不會被第二次調用。 考慮到這一點,當從同一個上下文對象兩次請求“原型”Bean 時,我們看到輸出中的引用發生了變化,並且構造函數(@Bean 方法)被調用了兩次。
那么問題是如何將單例注入到原型中。 上面的配置類也展示了如何做到這一點! 您應該將所有此類引用傳遞給構造函數。 這將允許創建的類成為純 POJO,並使包含的引用對象保持不變。 因此,傳輸服務可能類似於:
public class TransferServiceImpl implements TransferService {
private final String name;
private final AccountRepository accountRepository;
public TransferServiceImpl(AccountRepository accountRepository, String name) {
this.name = name;
// system out here is only because this is a dumb test usage
System.out.println("Using name value of: " + this.name);
this.accountRepository = accountRepository;
}
....
}
如果你編寫單元測試,你會很高興你創建了沒有所有@Autowired 的類。 如果您確實需要自動裝配組件,請將這些組件保留在 java 配置文件中。
這將在 BeanFactory 中調用下面的方法。 請在說明中注意這如何適用於您的確切用例。
/**
* Return an instance, which may be shared or independent, of the specified bean.
* <p>Allows for specifying explicit constructor arguments / factory method arguments,
* overriding the specified default arguments (if any) in the bean definition.
* @param name the name of the bean to retrieve
* @param args arguments to use if creating a prototype using explicit arguments to a
* static factory method. It is invalid to use a non-null args value in any other case.
* @return an instance of the bean
* @throws NoSuchBeanDefinitionException if there is no such bean definition
* @throws BeanDefinitionStoreException if arguments have been given but
* the affected bean isn't a prototype
* @throws BeansException if the bean could not be created
* @since 2.5
*/
Object getBean(String name, Object... args) throws BeansException;
如果你需要創建一個合格的 bean,你可以這樣做:
@Configuration
public class ThingConfiguration {
@Bean
@Scope(SCOPE_PROTOTYPE)
public Thing simpleThing(String name) {
return new Thing(name);
}
@Bean
@Scope(SCOPE_PROTOTYPE)
public Thing specialThing(String name) {
Thing thing = new Thing(name);
// some special configuration
return thing;
}
}
// Usage
@Autowired
private ApplicationContext context;
AutowireCapableBeanFactory beanFactory = context.getAutowireCapableBeanFactory();
((DefaultListableBeanFactory) beanFactory).getBean("specialThing", Thing.class, "name");
您可以通過使用內部類來實現類似的效果:
@Component
class ThingFactory {
private final SomeBean someBean;
ThingFactory(SomeBean someBean) {
this.someBean = someBean;
}
Thing getInstance(String name) {
return new Thing(name);
}
class Thing {
private final String name;
Thing(String name) {
this.name = name;
}
void foo() {
System.out.format("My name is %s and I can " +
"access bean from outer class %s", name, someBean);
}
}
}
到目前為止,很好的解決方案。 但我想發布另一種選擇。 Spring 有@Lookup
注釋。 來自 javadoc:
指示“查找”方法的注釋,將被容器覆蓋以將它們重定向回 BeanFactory 以進行 getBean 調用。
您可以將您的Thing
聲明為原型 bean:
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Thing {
@Autowired
private SomeComponent someComponent;
@Autowired
private AnotherComponent anotherComponent;
public Thing(String name) {
this.name = name;
}
}
然后您可以通過在任何其他 bean 中創建類似createThing
的方法來創建實例:
@Controller
public class MyController {
@Autowired
private ApplicationContext appCtx;
public void onRequest(Request request) {
//request is already validated
String name = request.getParameter("name");
Thing thing = createThing(name);
//System.out.println(thing.getName()); //prints name
}
//or public. And can be put in any @Component (including @Configuration)
@Lookup
protected Thing createThing(String name) {
throw new UnsupportedOperationException("Method implemented by Spring.");
}
}
遲到的答案略有不同。 這是對這個最近的問題的跟進,該問題涉及這個問題本身。
是的,正如前面所說,您可以在@Configuration
類中聲明接受參數的原型 bean,該類允許在每次注入時創建一個新 bean。
這將使這個@Configuration
類成為一個工廠,並且不給這個工廠太多的責任,這不應該包括其他 bean。
@Configuration
public class ServiceFactory {
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Thing thing(String name) {
return new Thing(name);
}
}
但是您也可以注入該配置 bean 來創建Thing
s:
@Autowired
private ServiceFactory serviceFactory;
public void onRequest(Request request) {
//request is already validated
String name = request.getParameter("name");
Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
// ...
}
它既是類型安全的又是簡潔的。
在你的 bean xml 文件中使用屬性scope="prototype"
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.