[Из песочницы] Получаем Spring Bean из сторонних Application Context правильно
Добрый день, хабровчане!
В данной статье предлагаю обсудить одну из проблем, с которой нередко сталкиваются в проектах, использующих фреймворк Spring ввиду неверно составленных spring-конфигураций. Не нужно стараться, чтобы такую ошибку допустить, и поэтому данная ошибка является довольно распространенной.
Формулировка проблемы
Проблема, представленная в данной статье, связана с неправильной конфигурацией beans в текущем application context, которые взяты из других application context. Такая проблема может возникнуть в крупном промышленном приложении, которое состоит из множества jar, у каждого из которых имеется собственный application context, содержащий spring beans.
Как результат неправильной конфигурации, получаем несколько копий beans с непредсказуемым состоянием, даже если они имеют scope singleton. Более того, бездумное копирование beans может привести к тому, что в приложении будет создано более десятка копий всех beans какой-либо jar, что чревато проблемами производительности приложения, увеличению времени запуска приложения.
Пример использования bean из внешнего application context в текущем
Представим, что мы ведем разработку в одном из модулей приложения, в котором множество других модулей и что у каждого из модулей имеется собственный application context. У такого приложения должен быть модуль, в котором создаются экземпляры application context всех модулей приложения.
Допустим, в application context одного из внешних модулей создан экземпляр bean класса NumberGenerator, который мы хотим получить в нашем модуле. Также допустим, что класс NumberGenerator расположен в пакете org.example.kruchon.generators, в котором хранятся какие-либо классы, занимающиеся генерацией значений.
Данный bean имеет состояние — поле int count.
package org.example.kruchon.calculators
public class NumberGenerator {
private int count = 0;
public synchronized int next() {
return count++;
}
}
Экземпляр данного bean создается в подконфигурации GeneratorsConfiguration.
@Configuration
public class GeneratorsConfiguration {
@Bean
public NumberGenerator numberGenerator() {
return new NumberGenerator();
}
...
}
Также во внешнем application context имеется главная конфигурация, в которой импортированы все подконфигурации внешнего модуля.
@Configuration
@Import({GeneratorsConfiguration.class, ...})
public class ExternalContextConfiguration {
...
}
Теперь приведу несколько примеров, в которых singleton bean класса NumberGenerator настроен в конфигурации текущего application context неправильно.
Неправильная конфигурация 1. Импорт главной конфигурации внешнего application context
Самое плохое решение, которое может быть.
@Configuration
@Import(ExternalContextConfiguration.class)
public class CurrentContextConfiguration {
...
}
- В приложении пересоздаются все экземпляры beans из внешнего application context. Другими словами, создается копия всего внешнего модуля, что сказывается на потреблении памяти, производительности, времени запуска приложения.
- Получаем копию NumberGenerator в текущем application context. У копии NumberGenerator имеется собственное значение поля count, несогласованное со первым экземпляром NumberGenerator. Такая несогласованность порождает трудно отлаживаемые ошибки в приложении.
Неправильная конфигурация 2. Импорт подконфигурации внешнего application context
Второй неверный и часто встречающийся на практике вариант.
@Configuration
@Import(GeneratorsConfiguration.class)
public class CurrentContextConfiguration {
...
}
В данном варианте уже не создается полная копия внешнего модуля, тем не менее мы снова получим второй экземпляр bean класса NumberGenerator.
Неправильная конфигурация 3. Look up инъекция непосредственно в bean, где хотим использовать NumberGenerator
public class OrderHandlingService {
private final NumberGenerator numberGenerator;
public OrderCreationService() {
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, имеющего scope singleton. Ведь теперь мы переиспользуем bean из другого application context и никак его не пересоздаем!
Но такой способ:
- Усложняет разработанный класс и его юнит-тестирование.
- Исключает автоматическое внедрение bean класса NumberGenerator в beans текущего модуля.
- Не принято использовать lookUp для инъекции singleton bean в общих случаях.
Поэтому подобное решение больше похоже на неуклюжий workaround, чем на рациональное решение проблемы.
Рассмотрим, как нужно правильно настраивать bean из внешнего application context.
Решение 1. Получить bean из внешего application context в конфигурации
Данный способ очень похож на 3-ий пример неправильной конфигурации с одним отличием: мы получаем bean, делая lookUp из внешнего контекста в конфигурации.
@Configuration
public class CurrentContextConfiguration {
@Bean
public NumberGenerator numberGenerator() {
ApplicationContext externalApplicationContext = getExternalContext();
return externalApplicationContext.getBean(NumberGenerator.class);
}
private ApplicationContext getExternalContext(){
...
}
}
Теперь мы можем автоматически внедрить данный bean в beans из собственного модуля.
Решение 2. Сделать внешний application context родительским
Есть вероятность, что функциональность текущего модуля расширяет функциональность внешнего. Может быть такой случай, когда в одном из внешних модулей разработаны общие для всего приложения вспомогательные beans, а в других модулях эти beans используются. В этом случае логично указать, что внешний модуль является родительским по отношению к предыдущему. В этом случае все bean из родительского модуля возможно использовать в текущем модуле и тогда bean родительского модуля не требуется настраивать в конфигурации текущего application context.
Указать родительскую связь возможно при создании экземпляра контекста, используя конструктор с параметром parent:
public AbstractApplicationContext(ApplicationContext parent) { ... }
Либо использовать сеттер:
public void setParent(ApplicationContext parent) { ... }
В случае, если application context объявлен в xml, можем воспользоваться конструктором:
public ClassPathXmlApplicationContext(String[] configLocations,
ApplicationContext parent)
throws BeansException { ... }
Заключение
Таким образом, будьте внимательны при конфигурации spring beans, следуйте приведенным в статье рекомендациям и старайтесь не допускать копирования beans, у которых scope singleton. Буду рад ответить на возникшие вопросы!