Spring Java Config:如何使用运行时参数创建原型范围的@Bean?

Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments?

使用Spring的Java配置,我需要获取或实例化一个原型范围的bean,其中只有在运行时才可获得的构造函数参数。考虑下面的代码示例(为了简洁起见简化了代码示例):

1
2
3
4
5
6
7
8
9
10
@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类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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类的特定于实现的依赖项,不应该知道(紧密耦合到)请求处理程序的实现。

此代码与SpringXML配置完美配合,例如:

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

如何用Java配置实现相同的事情?使用弹簧3.x时,以下各项不起作用:

1
2
3
4
5
@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

现在,我可以创建一个工厂,例如:

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

但是,这就破坏了使用Spring来替换服务定位器和工厂设计模式的整个观点,这对于这个用例来说是理想的。

如果Spring JavaCONFIG能够做到这一点,我将能够避免:

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

这是一项繁重的工作(相对而言),因为Spring已经通过XML配置支持了这些琐碎的工作。


@Configuration类中,这样的@Bean方法

1
2
3
4
5
@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

用于注册bean定义并提供用于创建bean的工厂。它定义的bean仅在请求时使用直接确定的参数或通过扫描ApplicationContext来实例化。

对于prototypebean,每次都会创建一个新对象,因此也会执行相应的@Bean方法。

您可以通过它的BeanFactory#getBean(String name, Object... args)方法从ApplicationContext中检索bean,该方法声明

Allows for specifying explicit constructor arguments / factory method
arguments, overriding the specified default arguments (if any) in the
bean definition.

Parameters:

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.

换句话说,对于这个prototype作用域bean,您提供的参数将不会在bean类的构造函数中使用,而是在@Bean方法调用中使用。

对于Spring4+版本,这至少是正确的。


使用Spring>4和Java 8,您可以更安全地进行这类操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);
    }

}

用途:

1
2
3
4
5
6
7
8
9
10
@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来传递两个以上的参数)。


按注释更新

首先,我不知道为什么你说"这不起作用",因为它在Spring3.x中工作得很好。我怀疑你在某个地方的配置肯定有问题。

这工作:

-配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();
    }
}

--要执行的测试文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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,给出了这个输出:

1
2
3
4
5
6
7
8
9
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被请求两次。然而,正如我们所期望的,春天只创造了一次。第二次它看到它有那个bean并返回现有的对象。第二次未调用构造函数(@bean方法)。为此,当两次从同一个上下文对象请求"原型"bean时,我们会看到引用在输出中发生了更改,并且两次调用了构造函数(@bean method)。

那么问题是如何在原型中注入一个单体。上面的配置类也展示了如何做到这一点!您应该将所有这些引用传递到构造函数中。这将允许所创建的类是纯POJO,并使包含的引用对象保持不变。因此,传输服务可能看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中调用下面的方法。在描述中注意这是如何用于您的确切用例的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 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;


从4.3年春开始,就有了新的方法来解决这个问题。

ObjectProvider-它允许您将其作为依赖项添加到"argumented"原型范围bean中,并使用参数实例化它。

下面是一个如何使用它的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@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字符串。


您可以通过使用内部类来实现类似的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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);
        }
    }
}