«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.