SpringJavaConfig: 如何使用运行时参数创建一个原型范围的@Bean?

使用 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;
}
}

注意,namefinal: 它只能通过构造函数提供,并保证不变性。其他依赖项是 Thing类的特定于实现的依赖项,不应该知道它们与请求处理程序实现(紧密耦合)。

这段代码与 Spring XML 配置工作得非常好,例如:

<bean id="thing", class="com.whatever.Thing" scope="prototype">
<!-- other post-instantiation properties omitted -->
</bean>

如何使用 Java 配置来实现同样的功能呢? 以下内容不适用于 Spring3.x:

@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}

现在,我创建一个 Factory,例如:

public interface ThingFactory {
public Thing createThing(String name);
}

但是那个 打破了使用 Spring 替换 ServiceLocator 和 Factory 设计模式的整个要点,对于这个用例来说是理想的。

如果 Spring Java Config 可以做到这一点,我就可以避免:

  • 定义 Factory 接口
  • 定义 Factory 实现
  • 为 Factory 实现编写测试

对于 Spring 已经通过 XML 配置支持的琐碎事情来说,这是一项繁重的工作(相对而言)。

118019 次浏览

@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 参数 转换为静态工厂方法。使用非空参数值无效 在任何其他情况下。

换句话说,对于这个作用域为 prototype的 bean,您将提供将要使用的参数,不是在 bean 类的构造函数中,而是在 @Bean方法调用中。(这个方法的类型保证非常弱,因为它使用 bean 的名称查找。)

或者,您可以使用类型化的 BeanFactory#getBean(Class requiredType, Object... args)方法,该方法按类型查找 bean。

这至少对于 Spring 版本4 + 是正确的。

请注意,如果您不想从 ApplicationContextBeanFactory开始检索 bean,那么可以注入一个 ObjectProvider(自 Spring 4.3以来)。

ObjectFactory的一种变体,专门为注射点设计, 允许程序的可选性和宽松的非唯一性处理。

并使用它的 getObject(Object... args)方法

返回对象的实例(可能是共享的或独立的) 由这家工厂管理。

允许按以下方式指定显式的构造参数 BeanFactory.getBean(String, Object).

比如说,

@Autowired
private ObjectProvider<Thing> things;


[...]
Thing newThing = things.getObject(name);
[...]

每条评论更新

首先,我不知道为什么你会说“ this doesn’t work”,因为它在 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());
}

使用 Spring3.2.8和 Java7,可以得到以下输出:

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 方法)不会被第二次调用。鉴于此,当从同一个上下文对象请求“ Prototype”Bean 两次时,我们会看到在输出 AND 中的引用发生了变化,而构造函数(@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;

使用 Spring > 4.0和 Java8,您可以更安全地完成这项工作:

@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 -它允许您将其作为依赖项添加到“参数化”的原型范围 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 字符串。

你可以通过使用 内部阶级来达到类似的效果:

@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);
}
}
}

用稍微不同的方式回答迟到的问题。 这是这个 最近的问题的后续,它引用了这个问题本身。

是的,正如上面所说的,您可以声明原型 bean,它接受 @Configuration类中的一个参数,该参数允许在每次注入时创建一个新 bean。
这将使这个 @Configuration类成为一个工厂,为了不给这个工厂太多的责任,这不应该包括其他 bean。

@Configuration
public class ServiceFactory {


@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Thing thing(String name) {
return new Thing(name);
}


}

但是您也可以注入该配置 bean 来创建 Things:

@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 文件中使用属性 范围 = “原型”

如果你需要创建一个 合格的豆子,你可以这样做:

@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");


到目前为止都是不错的解决方案,但是我想发布另一个替代方案。 Spring 有 @Lookup注释:

指示“查找”方法的注释,将由 容器将它们重定向回 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.");
}
}