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 .