Spring Java Config: как создать прототип-область @Bean с аргументами времени выполнения?
используя Java Config Spring, мне нужно получить / создать экземпляр компонента с прототипом с аргументами конструктора, которые доступны только во время выполнения. Рассмотрим следующий пример кода (упрощенный для краткости):
@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
}
где класс вещи определяется следующим образом:
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 config? Следующее не работает:
@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}
Я может создать фабрику, например:
public interface ThingFactory {
public Thing createThing(String name);
}
а то побеждает всю точку использования пружины для замены ServiceLocator и Заводская модель дизайна, что было бы идеально для этого случая использования.
если бы Spring Java Config мог это сделать, я бы смог избежать:
- определение Заводского интерфейса
- определение Заводской реализации
- написание тестов для заводской реализации
это тонна работы (относительно говоря) для чего-то настолько тривиального, что Spring уже поддерживает через XML config.
4 ответов:
на
@Configurationкласса,@Beanспособ Вот так@Bean @Scope("prototype") public Thing thing(String name) { return new Thing(name); }используется для регистрации определение Боба и предоставить завод для создания Боба. Компонент, который он определяет, создается только по запросу с использованием аргументов, которые определяются либо непосредственно, либо путем сканирования that
ApplicationContext.в случае a
prototypebean, каждый раз создается новый объект и, следовательно, соответствующий@Beanметод также выполняется.вы можете получить Боб из
ApplicationContextчерезBeanFactory#getBean(String name, Object... args)метод, который государства -позволяет указать явные аргументы конструктора / заводской метод аргументы, переопределяющие указанные аргументы по умолчанию (если таковые имеются определение Боба.
параметры:
args аргументы для использования при создании прототипа с использованием явных аргументов к статическому заводскому методу. Это допускается использовать ненулевое значение аргумента в любом другом случае.
другими словами, для этого
prototypescoped bean, вы предоставляете аргументы, которые будут использоваться, не в конструкторе класса bean, а в@Beanвызов метода.это по крайней мере верно для весенних версий 4+.
С 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); // ... }так что теперь вы можете получить свой боб во время выполнения. Это заводской шаблон, конечно, но вы можете сэкономить некоторое время на написании конкретного класса, как
ThingFactory(однако вам придется написать custom@FunctionalInterfaceдля передачи более двух параметров).
обновлено за комментарий
во-первых, я не уверен, почему вы говорите "это не работает" для чего-то, что работает просто отлично весной 3.х. Я подозреваю, что что-то должно быть не так в конфигурации где-то.
это работает:
-- 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 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таким образом, Боб "Синглтон" запрашивается дважды. Однако, как и следовало ожидать, Весна создает его только один раз. Во второй раз он видит, что у него есть этот боб и просто возвращает существующий объект. Конструктор (метод@Bean) не вызывается во второй раз. В связи с этим, когда компонент 'Prototype' запрашивается из одного и того же объекта контекста дважды, мы видим, что ссылка изменяется в выходных данных и что конструктор (метод@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. Если вам нужны компоненты 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;
С весны 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.
Comments