简体   繁体   English

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

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

Using Spring's Java Config, I need to acquire/instantiate a prototype-scoped bean with constructor arguments that are only obtainable at runtime.使用 Spring 的 Java Config,我需要使用只能在运行时获得的构造函数参数来获取/实例化一个原型范围的 bean。 Consider the following code example (simplified for brevity):考虑以下代码示例(为简洁起见进行了简化):

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

where the Thing class is defined as follows:其中 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;
    }
}

Notice name is final : it can only be supplied via a constructor, and guarantees immutability.注意namefinal :它只能通过构造函数提供,并保证不变性。 The other dependencies are implementation-specific dependencies of the Thing class, and shouldn't be known to (tightly coupled to) the request handler implementation.其他依赖项是Thing类的特定于实现的依赖项,请求处理程序实现不应该知道(紧密耦合到)。

This code works perfectly well with Spring XML config, for example:此代码与 Spring XML 配置完美配合,例如:

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

How do I achieve the same thing with Java config?如何使用 Java 配置实现相同的目标? The following does not work using Spring 3.x:以下不适用于 Spring 3.x:

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

Now, I could create a Factory, eg:现在,我可以创建一个工厂,例如:

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

But that defeats the entire point of using Spring to replace the ServiceLocator and Factory design pattern , which would be ideal for this use case.但这违背了使用 Spring 替换 ServiceLocator 和 Factory 设计模式的全部意义,这对于这个用例来说是理想的。

If Spring Java Config could do this, I would be able to avoid:如果 Spring Java Config 可以做到这一点,我将能够避免:

  • defining a Factory interface定义工厂接口
  • defining a Factory implementation定义工厂实现
  • writing tests for the Factory implementation为工厂实现编写测试

That's a ton of work (relatively speaking) for something so trivial that Spring already supports via XML config.对于 Spring 已经通过 XML 配置支持的微不足道的事情,这是大量的工作(相对而言)。

In a @Configuration class, a @Bean method like so@Configuration类中,像这样的@Bean方法

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

is used to register a bean definition and provide the factory for creating the bean .用于注册bean 定义并提供用于创建 bean 的工厂 The bean that it defines is only instantiated upon request using arguments that are determined either directly or through scanning that ApplicationContext .它定义的 bean 仅根据请求使用直接或通过扫描ApplicationContext确定的参数实例化。

In the case of a prototype bean, a new object is created every time and therefore the corresponding @Bean method is also executed.prototype bean 的情况下,每次都会创建一个新对象,因此也会执行相应的@Bean方法。

You can retrieve a bean from the ApplicationContext through its BeanFactory#getBean(String name, Object... args) method which states您可以通过其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.允许指定显式构造函数参数/工厂方法参数,覆盖 bean 定义中指定的默认参数(如果有)。

Parameters:参数:

args arguments to use if creating a prototype using explicit arguments to a static factory method.如果使用静态工厂方法的显式参数创建原型,则使用args参数。 It is invalid to use a non-null args value in any other case.在任何其他情况下使用非空 args 值都是无效的。

In other words, for this prototype scoped bean, you are providing the arguments that will be used, not in the constructor of the bean class, but in the @Bean method invocation.换句话说,对于这个prototype作用域 bean,您提供将使用的参数,而不是在 bean 类的构造函数中,而是在@Bean方法调用中。 (This method has very weak type guarantees since it uses a name lookup for the bean.) (此方法具有非常弱的类型保证,因为它对 bean 使用名称查找。)

Alternatively, you can use the typed BeanFactory#getBean(Class requiredType, Object... args) method which looks up the bean by type.或者,您可以使用按类型查找 bean 的类型化BeanFactory#getBean(Class requiredType, Object... args)方法。

This is at least true for Spring versions 4+.至少对于 Spring 版本 4+ 来说是这样。

Note that, if you don't want to start with the ApplicationContext or BeanFactory for your bean retrieval, you can inject an ObjectProvider (since Spring 4.3).请注意,如果您不想从ApplicationContextBeanFactory进行 bean 检索,您可以注入一个ObjectProvider (自 Spring 4.3 起)。

A variant of ObjectFactory designed specifically for injection points, allowing for programmatic optionality and lenient not-unique handling.专为注入点设计的ObjectFactory变体,允许编程可选和宽松的非唯一处理。

and use its getObject(Object... args) method并使用它的getObject(Object... args)方法

Return an instance (possibly shared or independent) of the object managed by this factory.返回此工厂管理的对象的实例(可能是共享的或独立的)。

Allows for specifying explicit construction arguments, along the lines of BeanFactory.getBean(String, Object) .允许按照BeanFactory.getBean(String, Object)指定显式构造参数。

For example,例如,

@Autowired
private ObjectProvider<Thing> things;

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

With Spring > 4.0 and Java 8 you can do this more type-safely:使用 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);
    }

}

Usage:用法:

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

    // ...
}

So now you can get your bean at runtime.所以现在你可以在运行时获取你的 bean。 This is a factory pattern of course, but you can save some time on writing specific class like ThingFactory (however you will have to write custom @FunctionalInterface to pass more than two parameters).这当然是一种工厂模式,但是您可以节省一些时间来编写像ThingFactory这样的特定类(但是您必须编写自定义@FunctionalInterface来传递两个以上的参数)。

Since Spring 4.3, there is new way to do it, which was sewed for that issue.从 Spring 4.3 开始,有一种新的方法可以做到这一点,这是针对该问题缝制的。

ObjectProvider - It enables you just to add it as a dependency to your "argumented" Prototype scoped bean and to instantiate it using the argument. ObjectProvider - 它使您可以将它作为依赖项添加到“有参数的”Prototype 范围的 bean 中,并使用参数实例化它。

Here is a simple example of how to use it:这是一个如何使用它的简单示例:

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

This will of course print hello string when calling usePrototype.这当然会在调用 usePrototype 时打印 hello 字符串。

UPDATED per comment每条评论更新

First, I'm not sure why you say "this does not work" for something that works just fine in Spring 3.x.首先,我不确定你为什么说“这不起作用”对于在 Spring 3.x 中运行良好的东西。 I suspect something must be wrong in your configuration somewhere.我怀疑您的配置中某处一定有问题。

This works:这有效:

-- Config File: -- 配置文件:

@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 File to execute: -- 要执行的测试文件:

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

Using Spring 3.2.8 and Java 7, gives this output:使用 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

So the 'Singleton' Bean is requested twice.所以'Singleton' Bean 被请求两次。 However as we would expect, Spring only creates it once.然而正如我们所料,Spring 只创建一次。 The second time it sees that it has that bean and just returns the existing object.第二次它看到它有那个 bean 并且只返回现有的对象。 The constructor (@Bean method) is not invoked a second time.构造函数(@Bean 方法)不会被第二次调用。 In deference to this, when the 'Prototype' Bean is requested from the same context object twice we see that the reference changes in the output AND that the constructor (@Bean method) IS invoked twice.考虑到这一点,当从同一个上下文对象两次请求“原型”Bean 时,我们看到输出中的引用发生了变化,并且构造函数(@Bean 方法)被调用了两次。

So then the question is how to inject a singleton into a prototype.那么问题是如何将单例注入到原型中。 The configuration class above shows how to do that too!上面的配置类也展示了如何做到这一点! You should pass all such references into the constructor.您应该将所有此类引用传递给构造函数。 This will allow the created class to be a pure POJO as well as making the contained reference objects immutable as they should be.这将允许创建的类成为纯 POJO,并使包含的引用对象保持不变。 So the transfer service might look something like:因此,传输服务可能类似于:

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;
    }
    ....
}

If you write Unit Tests you will be ever so happy you created the classes this without all the @Autowired.如果你编写单元测试,你会很高兴你创建了没有所有@Autowired 的类。 If you do need autowired components keep those local to the java config files.如果您确实需要自动装配组件,请将这些组件保留在 java 配置文件中。

This will call the method below in the BeanFactory.这将在 BeanFactory 中调用下面的方法。 Note in the description how this is intended for your exact use case.请在说明中注意这如何适用于您的确切用例。

/**
 * 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;

If you need to create a qualified bean you can do it this way:如果你需要创建一个合格的 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");

You can achieve a similar effect just by using an inner class :您可以通过使用内部类来实现类似的效果:

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

Nice solutions until now.到目前为止,很好的解决方案。 But I want to post yet another alternative.但我想发布另一种选择。 Spring has the @Lookup annotation. Spring 有@Lookup注释。 From javadoc:来自 javadoc:

An annotation that indicates 'lookup' methods, to be overridden by the container to redirect them back to the BeanFactory for a getBean call.指示“查找”方法的注释,将被容器覆盖以将它们重定向回 BeanFactory 以进行 getBean 调用。

you can declare your Thing as prototype bean:您可以将您的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;
    }
}

then you can create instances by creating a method like createThing below in any other bean:然后您可以通过在任何其他 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.");
    }
}

Late answer with a slightly different approach.迟到的答案略有不同。 That is a follow up of this recent question that refers this question itself.这是对这个最近的问题的跟进,该问题涉及这个问题本身。

Yes, as that was said you can declare the prototype bean that accepts a parameter in a @Configuration class that allows to create a new bean at each injection.是的,正如前面所说,您可以在@Configuration类中声明接受参数的原型 bean,该类允许在每次注入时创建一个新 bean。
That will make this @Configuration class a factory and to not give this factory too much responsibilities, this should not include other beans.这将使这个@Configuration类成为一个工厂,并且不给这个工厂太多的责任,这不应该包括其他 bean。

@Configuration    
public class ServiceFactory {

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

}

But you can also inject that configuration bean to create Thing s :但是您也可以注入该配置 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
    // ...    
}

It is both type-safe and concise.它既是类型安全的又是简洁的。

在你的 bean xml 文件中使用属性scope="prototype"

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM