Spring Java Config: как создать прототип с областью @Bean с аргументами времени выполнения?
используя Java-конфигурацию 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
is final
: он может поставляться только через конструктор и гарантирует неизменность. Другие зависимости являются зависимостями, специфичными для реализации Thing
class, и не должно быть известно (тесно связано с) реализацией обработчика запросов.
этот код отлично работает с конфигурацией Spring XML, например:
<bean id="thing", class="com.whatever.Thing" scope="prototype">
<!-- other post-instantiation properties omitted -->
</bean>
как я могу достичь того же самого с конфигурацией Java? Не работает следующее:
@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);
}
используется для регистрации определение bean и предоставить завод для создания bean. Боб, который он определяет, создается только по запросу с использованием аргументов, которые определяются либо непосредственно, либо путем сканирования that ApplicationContext
.
в случае prototype
bean, каждый раз создается новый объект и, следовательно, соответствующий @Bean
метод также выполняется.
вы можете получить Боб из ApplicationContext
через BeanFactory#getBean(String name, Object... args)
метод, который государства -
позволяет указать явные аргументы конструктора / заводской метод аргументы, отменяя указанное аргументы по умолчанию (если таковые имеются) в определение фасоли.
параметры:
args аргументы для использования при создании прототипа с использованием явных аргументов статический метод фабрики. Это недопустимо использовать ненулевое значение args в любом другом случае.
другими словами, для этого prototype
scoped 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);
// ...
}
теперь вы можете получить свой bean во время выполнения. Конечно, это фабричный шаблон, но вы можете сэкономить время на написании определенного класса, такого как ThingFactory
(однако вам придется написать custom @FunctionalInterface
для передачи более двух параметров).
обновлено за комментарий
во-первых, я не уверен, почему вы говорите "это не работает" для чего-то, что отлично работает весной 3.X. Я подозреваю, что что-то не так в вашей конфигурации.
это работает:
-- 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) не вызывается второй раз. В соответствии с этим, когда "прототип" 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. Если вам нужны компоненты 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 - это позволяет просто добавить его в качестве зависимости от вашего" аргументированного " прототипа с областью действия и создать его экземпляр с помощью аргумента
вот простой пример того, как его использовать:
@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();
}
}
это, конечно, напечатает строку hello при вызове usePrototype.