[英]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.