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.


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;

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


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

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


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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.