Spring Конфигурация Java: как вы создаете прототип @Bean с аргументами времени исполнения?
Используя 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;
}
}
name
уведомления является final
: оно может быть предоставлено только через конструктор и гарантирует неизменность. Другие зависимости являются зависимыми от реализации зависимостями класса 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 мог это сделать, я бы смог избежать:
- определение фабричного интерфейса
- определение реализации Factory
- написание тестов для реализации Factory
Это тонна работы (условно говоря) для чего-то настолько тривиального, что Spring уже поддерживает через XML-конфигурацию.
Ответы
Ответ 1
В @Configuration
класс, @Bean
метод, как так
@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}
используется для регистрации определения компонента и создания фабрики для создания компонента. Компонент, который он определяет, создается только по запросу, используя аргументы, которые определяются либо напрямую, либо путем сканирования этого ApplicationContext
.
В случае компонента- prototype
каждый раз создается новый объект, и поэтому также выполняется соответствующий метод @Bean
.
Вы можете извлечь компонент из ApplicationContext
помощью BeanFactory#getBean(String name, Object... args)
который гласит:
Позволяет указать аргументы конструктора/аргументы фабричного метода, переопределяя указанные аргументы по умолчанию (если они есть) в определении компонента.
Параметры:
аргументы args для использования при создании прототипа с использованием явных аргументов статическому заводскому методу. Недопустимо использовать ненулевое значение args в любом другом случае.
Другими словами, для этого компонента с prototype
вы используете аргументы, которые будут использоваться, а не в конструкторе класса bean, но в @Bean
метода @Bean
.
Это, по крайней мере, верно для версий Spring 4+.
Ответ 2
С 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 во время выполнения. Это, конечно, шаблон factory, но вы можете сэкономить некоторое время на написании определенного класса, например ThingFactory
(однако вам нужно будет написать пользовательский @FunctionalInterface
для передачи более двух параметров).
Ответ 3
ОБНОВЛЕНО за комментарий
Во-первых, я не уверен, почему вы говорите "это не работает" для чего-то, что отлично работает в 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
[email protected]8692d
[email protected]8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
[email protected]
value of count:3
Using name value of: simulated Dynamic Parameter Two
[email protected]
Таким образом, "Singleton" Bean запрашивается дважды. Однако, как и следовало ожидать, Spring создает его только один раз. Во второй раз он видит, что он имеет Bean и просто возвращает существующий объект. Конструктор (метод @Bean) не вызывается во второй раз. В знак уважения к этому, когда запрос 'Prototype' 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;
}
....
}
Если вы напишете Unit Tests, вы будете так счастливы, что создали классы без всякого @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
С весны 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.
Ответ 5
Вы можете добиться аналогичного эффекта, просто используя внутренний класс:
@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);
}
}
}
Ответ 6
в вашем XML файле beans используйте атрибут scope = "prototype"
Ответ 7
Поздний ответ с немного другим подходом.
Это продолжение этого недавнего вопроса, которое касается самого вопроса.
Да, как уже было сказано, вы можете объявить компонент-прототип, который принимает параметр в классе @Configuration
, который позволяет создавать новый компонент при каждой инъекции.
Это сделает этот класс @Configuration
фабрикой, и чтобы не давать этой фабрике слишком много ответственности, это не должно включать другие бобы.
@Configuration
public class ServiceFactory {
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Thing thing(String name) {
return new Thing(name);
}
}
Но вы также можете добавить этот компонент конфигурации для создания 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
// ...
}
Это и типо-безопасный и краткий.