Spring Java Config:如何创建带有运行时参数的原型作用域@Bean?


134

使用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配置实现同一目的?以下内容在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配置支持了很多工作(相对而言)。


15
很好的问题。
Sotirios Delimanolis 2014年

但是,是否有一个原因,您不能仅自己实例化该类并必须从Spring获取它?它对其他bean有依赖性吗?
Sotirios Delimanolis 2014年

@SotiriosDelimanolis是的,Thing实现实际上更复杂,并且确实依赖于其他bean(为简便起见,我省略了它们)。因此,我不希望Request处理程序实现了解它们,因为这会将处理程序紧密耦合到不需要的API / bean。我将更新问题以反映您的(优秀)问题。
Les Hazlewood 2014年

我不确定Spring是否允许在构造函数上使用它,但是我知道您可以在setter本身上为setter设置@Qualifier参数@Autowired
CodeChimp

2
在春季4,您的示例@Bean工作。将@Bean使用传递给的适当参数调用该方法getBean(..)
Sotirios Delimanolis 2014年

Answers:


94

@Configuration课堂上,@Bean像这样的方法

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

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

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

您可以ApplicationContext通过的BeanFactory#getBean(String name, Object... args)方法检索Bean,该方法指出

允许指定显式构造函数自变量/工厂方法自变量,覆盖Bean定义中指定的默认自变量(如果有)。

参数:

如果使用静态工厂方法的显式参数创建原型,则使用args参数。在任何其他情况下,使用非null的args值都是无效的。

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

对于Spring 4+版本至少是这样。


12
这种方法的问题是您不能将@Bean方法限制为手动调用。如果您曾经调用过@Autowire Thing@Bean方法,可能会死于无法注入参数。如果你也一样@Autowire List<Thing>。我发现这有点脆弱。
Jan Zyka

@JanZyka,除了这些答案中概述的内容外,我是否可以通过其他方式自动为其连接(如果您斜视的话,它们基本上是相同的)。更具体地说,如果我预先知道参数(在编译/配置时),是否有任何方法可以在一些我可以限定@Autowire使用的注释中表达这些参数?
M. Prokhorov

52

使用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(但是您必须编写custom @FunctionalInterface来传递两个以上的参数)。


1
我发现这种方法非常有用且干净。谢谢!
Alex Objelean '17

1
什么面料?我了解您的用法..但不是专业术语。.不要以为我听说过“织物图案”
AnthonyJClink

1
@AnthonyJClink我想我只是用它fabric代替了factory我的坏人:)
Roman Golyshev

1
@AbhijitSarkar哦,我知道了。但是您不能将参数传递给ProviderObjectFactory,或者我错了吗?在我的示例中,您可以向其传递字符串参数(或任何参数)
Roman Golyshev

2
如果您不想(或不需要)使用Spring bean生命周期方法(对于原型bean来说是不同的...),则可以跳过@BeanScope注释该Thing thing方法。此外,可以将该方法设为私有以隐藏自身,并仅保留工厂。
m52509791 '18

17

从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时,这当然会打印问候字符串。


15

每个评论已更新

首先,我不确定您为什么对在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;

3
谢谢回复!但是,我认为您误解了这个问题。这个问题最重要的部分是,在获取(实例化)原型时,必须将运行时值作为构造函数参数提供。
Les Hazlewood 2014年

我更新了我的回复。实际上,似乎好像正确地处理了运行时值,所以我确实省略了那部分。尽管您从程序的更新和输出中可以看到,但它得到显式支持。
JoeG 2014年

0

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

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


-1

回答较晚的方法略有不同。这是一个后续这个高达最近的问题是指这个问题本身。

是的,正如您所说的,您可以在@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

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

它既安全又简洁。


1
感谢您的答复,但这是Spring的反模式。Config对象不应“泄漏”到应用程序代码中-它们存在是为了配置您的应用程序对象图和与Spring构造的接口。这类似于您的应用程序Bean中的XML类(即另一种配置机制)。也就是说,如果Spring附带了另一种配置机制,则您将需要重构您的应用程序代码-一个明确的指标,这违反了关注点分离。最好让您的Config创建一个Factory / Function接口的实例并注入Factory-与配置没有紧密的联系。
莱·哈兹伍德

1)我完全同意一般情况下,配置对象不必作为字段泄漏。但是,在这种特定情况下,注入一个定义一个且只有一个bean的配置对象以生成原型bean,IHMO完全有意义:此配置类成为工厂。如果只有这样,关注点分离问题又在哪里呢?...
davidxxx

... 2)关于“也就是说,如果Spring附带了另一种配置机制”,这是一个错误的论点,因为当您决定在应用程序中使用框架时,您将应用程序与之耦合。因此,无论如何,@Configuration如果该机制发生了变化,您还必须重构所有依赖的Spring应用程序。
davidxxx

1
... 3)您接受的答案建议使用BeanFactory#getBean()。但这在耦合方面更糟,因为这是一个工厂,它允许获取/实例化应用程序的任何bean,而不仅仅是当前bean需要的一个。通过这种用法,您可以轻松混合类的职责,因为它可能拉扯的依赖项是无限的,这实际上不是建议,而是例外情况。
davidxxx

@ davidxxx-多年前,在JDK 8和Spring 4成为事实之前,我接受了答案。上面的Roman答案对于现代Spring用法更为正确。关于您的声明“因为当您决定在应用程序中使用框架时,您将应用程序与之耦合”与Spring团队的建议和Java Config最佳实践完全相反-请询问Josh Long或Jeurgen Hoeller,有机会亲自与他们交谈(我有,我可以向您保证,他们明确建议不要在可能的情况下将您的应用程序代码耦合到Spring)。干杯。
莱·哈兹伍德
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.