«1. Обзор

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

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

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

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

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

@Configuration
public class AppConfig {

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

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

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

Теперь давайте внедрим bean-компонент с областью действия прототипа в singleton, а затем предоставим его с помощью метода 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 и дважды получим singleton bean-компонент: ~ ~~

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

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

3. Внедрение ApplicationContext

Мы также можем внедрить 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. javax.inject API

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

Вот бин-одиночка:

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

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

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

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

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

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

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

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

По умолчанию Spring использует библиотеку CGLIB для прямого подкласса объектов. Чтобы избежать использования CGLIB, мы можем настроить режим прокси с помощью ScopedProxyMode.INTERFACES, чтобы вместо этого использовать динамический прокси JDK.

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

Spring предоставляет интерфейс ObjectFactory\u003cT\u003e для создания объектов заданного типа по запросу:

public class SingletonObjectFactoryBean {

    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

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

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

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

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

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

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

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

    //...   
}

«

public class SingletonFunctionBean {
    
    @Autowired
    private Function<String, PrototypeBean> beanFactory;
    
    public PrototypeBean getPrototypeInstance(String name) {
        PrototypeBean bean = beanFactory.apply(name);
        return bean;
    }

}

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

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

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

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

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

Теперь давайте напишем простой тест JUnit для проверки случая с интерфейсом ObjectFactory:

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

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

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