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

Обратите внимание , 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);
}

Теперь я мог бы создать Factory, например:

public interface ThingFactory {
    public Thing createThing(String name);
}

Но это лишает смысла использование Spring для замены шаблонов проектирования ServiceLocator и Factory , которые были бы идеальными для этого варианта использования.

Если бы Spring Java Config мог это сделать, я бы смог избежать:

  • определение интерфейса Factory
  • определение реализации Factory
  • написание тестов для реализации Factory

Это тонна работы (условно говоря) для чего-то столь тривиального, что Spring уже поддерживает через конфигурацию XML.

Les Hazlewood
источник
15
Отличный вопрос.
Сотириос Делиманолис, 03
Однако есть ли причина, по которой вы не можете просто создать экземпляр класса самостоятельно и получить его из Spring? Есть ли у него зависимости от других bean-компонентов?
Сотириос Делиманолис, 03
@SotiriosDelimanolis: да, Thingреализация на самом деле более сложная и имеет зависимости от других bean-компонентов (я просто опустил их для краткости). Таким образом, я не хочу, чтобы реализация обработчика запросов знала о них, поскольку это жестко связывает обработчик с API / bean-компонентами, которые ему не нужны. Я обновлю вопрос, чтобы отразить ваш (отличный) вопрос.
Les Hazlewood,
Я не уверен, разрешает ли Spring это в конструкторе, но я знаю, что вы можете указать @Qualifierпараметры для установщика с помощью @Autowiredсамого установщика.
CodeChimp 03
2
В Spring 4 ваш пример с @Beanworks. @BeanМетод вызывается с соответствующими аргументами , переданными getBean(..).
Сотириос Делиманолис

Ответы:

94

В @Configurationклассе такой @Beanметод

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

используется для регистрации определения bean-компонента и предоставления фабрики для создания bean-компонента . Бин, который он определяет, создается только по запросу с использованием аргументов, которые определяются либо напрямую, либо путем его сканирования ApplicationContext.

В случае prototypebean-компонента каждый раз создается новый объект и, следовательно, @Beanтакже выполняется соответствующий метод.

Вы можете получить компонент из метода ApplicationContextчерез его BeanFactory#getBean(String name, Object... args)метод, который указывает

Позволяет указать явные аргументы конструктора / аргументы метода фабрики, переопределив указанные аргументы по умолчанию (если есть) в определении bean-компонента.

Параметры:

args аргументы, которые необходимо использовать при создании прототипа с использованием явных аргументов статического фабричного метода. Недопустимо использовать ненулевое значение args в любом другом случае.

Другими словами, для этого prototypebean-компонента с заданной областью действия вы предоставляете аргументы, которые будут использоваться не в конструкторе класса bean-компонента, а при @Beanвызове метода.

По крайней мере, это верно для версий Spring 4+.

Сотириос Делиманолис
источник
12
Моя проблема с этим подходом заключается в том, что вы не можете ограничить @Beanметод ручным вызовом. Если вы когда - либо метод будет вызван , вероятно , умирает от невозможности вводить параметр. То же самое, если вы . Я нашел это немного хрупким. @Autowire Thing@Bean@Autowire List<Thing>
Ян Зыка,
@JanZyka, есть ли способ, которым я могу выполнить автоматическое подключение Thing, кроме того, что указано в этих ответах (которые, по сути, одинаковы, если вы прищурились). В частности, если я заранее знаю аргументы (во время компиляции / конфигурации), могу ли я как-нибудь выразить эти аргументы в какой-нибудь аннотации, с которой я могу квалифицировать @Autowire?
М. Прохоров
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(однако вам придется написать собственный, @FunctionalInterfaceчтобы передать более двух параметров).

Роман Голышев
источник
1
Я считаю этот подход очень полезным и чистым. Спасибо!
Alex Objelean
1
Что такое ткань? Я понимаю ваше использование ... но не терминологию ... не думаю, что я слышал о "тканевом узоре"
AnthonyJClink
1
@AnthonyJClink, я думаю, я просто использовал fabricвместо factory, мой плохой :)
Роман Голышев
1
@AbhijitSarkar о, понятно. Но вы не можете передать параметр в Providerили в ObjectFactory, или я ошибаюсь? И в моем примере вы можете передать ему строковый параметр (или любой параметр)
Роман Голышев
2
Если вы не хотите (или не должны) использовать методы жизненного цикла компонентов Spring (которые отличаются от прототипов компонентов ...), вы можете пропустить @Beanи Scopeаннотации к Thing thingметоду. Более того, этот метод можно сделать частным, чтобы скрыть себя и оставить только factory.
m52509791 06
17

Начиная с Spring 4.3, есть новый способ сделать это, который был вшит для этой проблемы.

ObjectProvider - он позволяет вам просто добавить его как зависимость к вашему «аргументированному» компоненту Prototype с областью видимости и создать его экземпляр с помощью аргумента.

Вот простой пример того, как его использовать:

@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» запрашивается дважды. Однако, как и следовало ожидать, Spring создает его только один раз. Во второй раз он видит, что у него есть этот компонент, и просто возвращает существующий объект. Конструктор (метод @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. Если вам действительно нужны компоненты с автоматическим подключением, сохраните их локально в файлах конфигурации 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;
JoeG
источник
3
Спасибо за ответ! Однако я думаю, что вы неправильно поняли вопрос. Самая важная часть вопроса заключается в том, что значение времени выполнения должно быть предоставлено в качестве аргумента конструктора при получении (создании экземпляра) прототипа.
Les Hazlewood,
Я обновил свой ответ. На самом деле казалось, что обработка значения времени выполнения была выполнена правильно, поэтому я оставил эту часть. Хотя это явно поддерживается, как вы можете видеть из обновлений и выходных данных программы.
JoeG 05
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);
        }
    }
}
pmartycz
источник
0

в вашем XML-файле beans используйте атрибут scope = "prototype"

Manpreet Singh
источник
-1

Поздний ответ с немного другим подходом. Это продолжение этого недавнего вопроса, который касается самого вопроса.

Да, как было сказано, вы можете объявить компонент-прототип, который принимает параметр в @Configurationклассе, который позволяет создавать новый компонент при каждой инъекции.
Это сделает этот @Configuration класс фабрикой, и, чтобы не возлагать на эту фабрику слишком много обязанностей, он не должен включать другие компоненты.

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

Но вы также можете ввести этот компонент конфигурации для создания Things:

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

Это одновременно и типобезопасный, и лаконичный.

davidxxx
источник
1
Спасибо за ответ, но это антипаттерн Spring. Объекты конфигурации не должны «просачиваться» в код приложения - они существуют для настройки графа объектов вашего приложения и взаимодействия с конструкциями Spring. Это похоже на классы XML в компонентах вашего приложения (т. Е. Другой механизм конфигурации). То есть, если Spring поставляется с другим механизмом конфигурации, вам придется провести рефакторинг кода приложения - явный индикатор, который нарушает разделение задач. Лучше, чтобы ваш Config создавал экземпляры интерфейса Factory / Function и вводил Factory - без тесной связи с config.
Les Hazlewood
1) Я полностью согласен с тем, что в общем случае объекты конфигурации не должны просачиваться в виде поля. Но в этом конкретном случае внедрение объекта конфигурации, который определяет один и только один bean-компонент для создания прототипа bean-компонентов, имеет смысл в IHMO: этот класс конфигурации становится фабрикой. Где проблема разделения интересов, если только это делается? ...
davidxxx
... 2) Насчет «То есть, если Spring поставляется с другим механизмом конфигурации», это неверный аргумент, потому что, когда вы решаете использовать фреймворк в своем приложении, вы связываете свое приложение с этим. Так что в любом случае вам также придется реорганизовать любые приложения Spring, которые полагаются на изменение @Configurationэтого механизма.
davidxxx
1
... 3) В принятом вами ответе предлагается использовать BeanFactory#getBean(). Но это намного хуже с точки зрения связывания, поскольку это фабрика, которая позволяет получать / создавать экземпляры любых bean-компонентов приложения, а не только того, который нужен текущему bean-компоненту. При таком использовании вы можете очень легко смешивать обязанности вашего класса, поскольку зависимости, которые он может использовать, неограничены, что на самом деле не рекомендуется, но является исключительным случаем.
davidxxx
@ davidxxx - я принял ответ много лет назад, до того, как JDK 8 и Spring 4 были де-факто. Ответ Романа выше более верен для современного использования Spring. Что касается вашего утверждения «потому что, когда вы решите использовать фреймворк в своем приложении, вы объединяете свое приложение с ним», это совершенно противоположно рекомендациям команды Spring и лучшим практикам конфигурирования Java - спросите Джоша Лонга или Юргена Хеллера, если вы получите возможность поговорить с ними лично (у меня есть, и я могу заверить вас, что они явно советуют не связывать код вашего приложения со Spring, когда это возможно). Приветствия.
Les Hazlewood