Добрый день, хабровчане! В этой статье я предлагаю обсудить одну из проблем, которая часто встречается в проектах, использующих фреймворк Spring. Проблема, описанная в этой статье, возникает из-за одной из типичных ошибок конфигурации пружин.
Не нужно пытаться допустить такую ошибку в настройке, а потому эта ошибка встречается довольно часто.
Постановка задачи
Проблема, представленная в этой статье, связана с неправильной настройкой bean-компонентов в текущем контексте приложения, которые берутся из других контекстов приложения.Эта проблема может возникнуть в крупном промышленном приложении, состоящем из множества jar-файлов, каждый из которых имеет собственный контекст приложения, содержащий Spring Beans. В результате неправильной настройки мы получаем несколько копий bean-компонентов с непредсказуемым состоянием, даже если они имеют одноэлементную область видимости.
Более того, бездумное копирование бинов может привести к тому, что в приложении будет создано более десятка копий всех бинов jar, что чревато проблемами с производительностью приложения и увеличением времени запуска приложения.
Пример использования бина из контекста внешнего приложения в текущем
Давайте представим, что мы разрабатываем один из модулей приложения, в котором есть множество других модулей и что каждый из модулей имеет свой контекст приложения.Такое приложение должно иметь модуль, в котором создаются экземпляры контекста приложения всех модулей приложения.
Допустим, в контексте приложения одного из внешних модулей создан экземпляр bean-компонента класса NumberGenerator, который мы хотим получить в нашем модуле.
Предположим также, что класс NumberGenerator находится в пакете org.example.kruchon.generators, в котором хранятся все классы, генерирующие значения.
У этого bean-компонента есть состояние — поле int count.
Экземпляр этого bean-компонента создается в подконфигурации GeneratorsConfiguration.package org.example.kruchon.calculators public class NumberGenerator { private int count = 0; public synchronized int next() { return count++; } }
@Configuration
public class GeneratorsConfiguration {
@Bean
public NumberGenerator numberGenerator() {
return new NumberGenerator();
}
.
}
Также в контексте внешнего приложения существует основная конфигурация, в которую импортируются все подконфигурации внешнего модуля.
@Configuration
@Import({GeneratorsConfiguration.class, .
}) public class ExternalContextConfiguration { .
}
Теперь я приведу несколько примеров, в которых синглтон-бин класса NumberGenerator настроен неправильно в конфигурации текущего контекста приложения.
Неправильная конфигурация 1. Импорт основной конфигурации контекста внешнего приложения.
Худшее решение, которое могло быть.
@Configuration
@Import(ExternalContextConfiguration.class)
public class CurrentContextConfiguration {
.
}
- Приложение воссоздает все экземпляры компонентов из контекста внешнего приложения.
Другими словами, создается копия всего внешнего модуля, что влияет на потребление памяти, производительность и время запуска приложения.
- Мы получаем копию NumberGenerator в контексте текущего приложения.
Копия NumberGenerator имеет собственное значение поля счетчика, несовместимое с первым экземпляром NumberGenerator. Эта несогласованность создает в приложении ошибки, которые трудно отладить.
Неправильная конфигурация 2. Импорт подконфигурации контекста внешнего приложения.
Второй вариант неверен и часто встречается на практике.
@Configuration
@Import(GeneratorsConfiguration.class)
public class CurrentContextConfiguration {
.
}
В этом варианте полная копия внешнего модуля больше не создается, однако мы снова получим второй экземпляр бина класса NumberGenerator.
Неправильная конфигурация 3. Найдите внедрение непосредственно в компонент, где мы хотим использовать NumberGenerator.
public class OrderFactory {
private final NumberGenerator numberGenerator;
public OrderFactory() {
ApplicationContext externalApplicationContext = getExternalContext();
numberGenerator = externalApplicationContext.getBean(NumberGenerator.class);
}
public Order create() {
Order order = new Order();
int id = numberGenerator.next();
order.setId(id);
order.setCreatedDate(new Date());
return order;
}
private ApplicationContext getExternalContext(){
.
}
}
В этом методе проблему дублирования bean-компонента, имеющего одноэлементную область видимости, можно считать решенной.
Ведь теперь мы повторно используем бин из другого контекста приложения и никак не создаем его заново! Но так:
- Усложняет разрабатываемый класс и его модульное тестирование.
- Предотвращает автоматическое внедрение bean-компонента класса NumberGenerator в bean-компоненты текущего модуля.
- В общих случаях использование LookUp для внедрения одноэлементного компонента не является общепринятой практикой.
Давайте посмотрим, как правильно настроить бин из контекста внешнего приложения.
Решение 1. Получите компонент из контекста внешнего приложения в конфигурации.
Этот метод очень похож на третий пример неправильной конфигурации с одним отличием: мы получаем бин, выполняя поиск из внешнего контекста в конфигурации, а не непосредственно в бин.
@Configuration
public class CurrentContextConfiguration {
@Bean
public NumberGenerator numberGenerator() {
ApplicationContext externalApplicationContext = getExternalContext();
return externalApplicationContext.getBean(NumberGenerator.class);
}
private ApplicationContext getExternalContext(){
.
}
}
Теперь мы можем автоматически внедрить этот bean-компонент в bean-компоненты из нашего собственного модуля.
Решение 2. Сделайте контекст внешнего приложения родительским
Есть вероятность, что функциональность текущего модуля расширяет функциональность внешнего.Возможен случай, когда в одном из внешних модулей разрабатываются вспомогательные bean-компоненты, общие для всего приложения, а в других модулях используются эти bean-компоненты.
В этом случае логично указать, что внешний модуль является родительским по отношению к предыдущему.
В этом случае все компоненты родительского модуля можно использовать в текущем модуле, а затем Компонент родительского модуля не требует настройки.
в конфигурации текущего контекста приложения.
Родительское соединение можно указать при создании экземпляра контекста, используя конструктор с родительским параметром: public AbstractApplicationContext(ApplicationContext parent) { .
}
Или используйте сеттер: public void setParent(ApplicationContext parent) { .
}
Если контекст приложения объявлен в xml, мы можем использовать конструктор: public ClassPathXmlApplicationContext(String[] configLocations,
ApplicationContext parent) throws BeansException { .
}
Заключение
Таким образом, будьте осторожны при настройке Spring bean-компонентов, следуйте рекомендациям, приведенным в статье, и старайтесь избегать копирования bean-компонентов, имеющих одноэлементную область видимости.Буду рад ответить на любые ваши вопросы! Теги: #java #spring #spring beans #spring-контекст приложения #spring ioc #spring di #spring-внедрение зависимостей #дублированные Spring Singletons
-
Атомэкспо 2019, Часть 2: Экспозиция
19 Oct, 24