Инъекция бобов-прототипов в экземпляр Singleton весной

1. Обзор

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

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

Чтобы узнать больше о сферах применения бобов, перейдите по ссылке:/spring-bean-scopes[эта статья - хорошее место для начала].

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

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

@Configuration
public class AppConfig {

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

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

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

Теперь давайте вставим bean-объект с прототипом в синглтон, а затем выставим if с помощью метода 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

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

  • Чтобы достичь этого, используйте аннотацию @ 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 () , из ApplicationContext будет возвращаться новый экземпляр PrototypeBean .

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

Кроме того, мы получаем прототип 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-annotations-resource-inject-autowire[Spring wiring]article.

Вот синглтон бин:

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

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

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

6. Scoped Proxy

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

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

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

@Scope(
  value = ConfigurableBeanFactory.SCOPE__PROTOTYPE,
  proxyMode = ScopedProxyMode.TARGET__CLASS)

По умолчанию Spring использует библиотеку CGLIB для непосредственного подкласса объектов.

Чтобы избежать использования CGLIB, мы можем настроить режим прокси с _ScopedProxyMode . _ INTERFACES, чтобы использовать вместо него динамический прокси JDK.

7. ObjectFactory Interface

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

   //...
}

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

public class SingletonFunctionBean {

    @Autowired
    private Function<String, PrototypeBean> beanFactory;

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

}

Наконец, мы должны определить фабричный 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 () , создается новый экземпляр компонента-прототипа.

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

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

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