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

Это и типо-безопасный и краткий.