Перейти к основному содержимому

Внедрение бинов-прототипов в экземпляр синглтона в Spring

· 5 мин. чтения

1. Обзор

В этой быстрой статье мы собираемся показать различные подходы к внедрению прототипов bean-компонентов в экземпляр singleton . Мы обсудим варианты использования и преимущества/недостатки каждого сценария.

По умолчанию bean-компоненты Spring являются синглтонами. Проблема возникает, когда мы пытаемся связать компоненты разных областей видимости. Например, прототип bean-компонента в синглтон. Это известно как проблема внедрения bean -компонентов с ограниченной областью действия .

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

2. Проблема внедрения прототипа компонента

Чтобы описать проблему, давайте настроим следующие bean-компоненты:

@Configuration
public class AppConfig {

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public PrototypeBean prototypeBean() {
return new PrototypeBean();
}

@Bean
public SingletonBean singletonBean() {
return new SingletonBean();
}
}

Обратите внимание, что первый компонент имеет область видимости прототипа, а другой — синглтон.

Теперь давайте добавим bean-компонент с областью действия прототипа в синглтон, а затем предоставим его с помощью метода getPrototypeBean() :

public class SingletonBean {

// ..

@Autowired
private PrototypeBean prototypeBean;

public SingletonBean() {
logger.info("Singleton instance created");
}

public PrototypeBean getPrototypeBean() {
logger.info(String.valueOf(LocalTime.now()));
return prototypeBean;
}
}

Затем давайте загрузим ApplicationContext и дважды получим одноэлементный компонент:

public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);

SingletonBean firstSingleton = context.getBean(SingletonBean.class);
PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();

// get singleton bean instance one more time
SingletonBean secondSingleton = context.getBean(SingletonBean.class);
PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

isTrue(firstPrototype.equals(secondPrototype), "The same instance should be returned");
}

Вот вывод из консоли:

Singleton Bean created
Prototype Bean created
11:06:57.894
// should create another prototype bean instance here
11:06:58.895

Оба компонента были инициализированы только один раз, при запуске контекста приложения.

3. Внедрение контекста приложения

Мы также можем внедрить ApplicationContext непосредственно в bean-компонент.

Для этого либо используйте аннотацию @Autowire , либо реализуйте интерфейс ApplicationContextAware :

public class SingletonAppContextBean implements ApplicationContextAware {

private ApplicationContext applicationContext;

public PrototypeBean getPrototypeBean() {
return applicationContext.getBean(PrototypeBean.class);
}

@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}

Каждый раз, когда вызывается метод getPrototypeBean() , новый экземпляр PrototypeBean будет возвращен из ApplicationContext .

Однако такой подход имеет серьезные недостатки. Это противоречит принципу инверсии управления, так как мы запрашиваем зависимости у контейнера напрямую.

Кроме того, мы извлекаем прототип bean-компонента из applicationContext в классе SingletonAppcontextBean . Это означает привязку кода к Spring Framework .

4. Внедрение метода

Другой способ решения проблемы — внедрение метода с аннотацией @Lookup :

@Component
public class SingletonLookupBean {

@Lookup
public PrototypeBean getPrototypeBean() {
return null;
}
}

Spring переопределит метод getPrototypeBean() с аннотацией @Lookup. Затем он регистрирует компонент в контексте приложения. Всякий раз, когда мы запрашиваем метод getPrototypeBean() , он возвращает новый экземпляр PrototypeBean .

Он будет использовать CGLIB для генерации байт-кода, отвечающего за получение PrototypeBean из контекста приложения.

5. API -интерфейс javax.inject

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

Вот одноэлементный компонент:

public class SingletonProviderBean {

@Autowired
private Provider<PrototypeBean> myPrototypeBeanProvider;

public PrototypeBean getPrototypeInstance() {
return myPrototypeBeanProvider.get();
}
}

Мы используем интерфейс Provider для внедрения bean-компонента-прототипа. Для каждого вызова метода getPrototypeInstance() файл myPrototypeBeanProvider. g et() возвращает новый экземпляр PrototypeBean . ``

6. Прокси с заданной областью действия

По умолчанию Spring содержит ссылку на реальный объект для выполнения инъекции. Здесь мы создаем прокси-объект, чтобы связать реальный объект с зависимым.

Каждый раз, когда вызывается метод прокси-объекта, прокси сам решает, создавать ли новый экземпляр реального объекта или повторно использовать существующий.

Чтобы настроить это, мы модифицируем класс Appconfig , чтобы добавить новую аннотацию @Scope :

@Scope(
value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,
proxyMode = ScopedProxyMode.TARGET_CLASS)

По умолчанию Spring использует библиотеку CGLIB для прямого подкласса объектов. Чтобы избежать использования CGLIB, мы можем настроить режим прокси с помощью ScopedProxyMode. ИНТЕРФЕЙСЫ, чтобы вместо этого использовать динамический прокси-сервер JDK.

7. Интерфейс ObjectFactory

Spring предоставляет интерфейс ObjectFactory<T> для создания объектов заданного типа по требованию:

public class SingletonObjectFactoryBean {

@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

public PrototypeBean getPrototypeInstance() {
return prototypeBeanObjectFactory.getObject();
}
}

Давайте посмотрим на метод getPrototypeInstance() ; getObject() возвращает новый экземпляр PrototypeBean для каждого запроса. Здесь у нас больше контроля над инициализацией прототипа.

Кроме того, ObjectFactory является частью фреймворка; это означает отсутствие дополнительной настройки для использования этой опции.

8. Создайте компонент во время выполнения с помощью java.util.Function

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

Чтобы увидеть пример этого, давайте добавим поле имени в наш класс PrototypeBean :

public class PrototypeBean {
private String name;

public PrototypeBean(String name) {
this.name = name;
logger.info("Prototype instance " + name + " created");
}

//...
}

Затем мы добавим фабрику компонентов в наш одноэлементный компонент, используя интерфейс java.util.Function :

public class SingletonFunctionBean {

@Autowired
private Function<String, PrototypeBean> beanFactory;

public PrototypeBean getPrototypeInstance(String name) {
PrototypeBean bean = beanFactory.apply(name);
return bean;
}

}

Наконец, мы должны определить фабричный компонент, прототип и одиночный компонент в нашей конфигурации:

@Configuration
public class AppConfig {
@Bean
public Function<String, PrototypeBean> beanFactory() {
return name -> prototypeBeanWithParam(name);
}

@Bean
@Scope(value = "prototype")
public PrototypeBean prototypeBeanWithParam(String name) {
return new PrototypeBean(name);
}

@Bean
public SingletonFunctionBean singletonFunctionBean() {
return new SingletonFunctionBean();
}
//...
}

9. Тестирование

Давайте теперь напишем простой тест JUnit, чтобы проверить случай с интерфейсом ObjectFactory :

@Test
public void givenPrototypeInjection_WhenObjectFactory_ThenNewInstanceReturn() {

AbstractApplicationContext context
= new AnnotationConfigApplicationContext(AppConfig.class);

SingletonObjectFactoryBean firstContext
= context.getBean(SingletonObjectFactoryBean.class);
SingletonObjectFactoryBean secondContext
= context.getBean(SingletonObjectFactoryBean.class);

PrototypeBean firstInstance = firstContext.getPrototypeInstance();
PrototypeBean secondInstance = secondContext.getPrototypeInstance();

assertTrue("New instance expected", firstInstance != secondInstance);
}

После успешного запуска теста мы видим, что каждый раз, когда вызывается метод getPrototypeInstance() , создается новый экземпляр bean-прототипа.

10. Заключение

В этом коротком руководстве мы узнали о нескольких способах внедрения bean-компонента-прототипа в экземпляр singleton.

Как всегда, полный код этого туториала можно найти на GitHub project .