Привет, Хабр!
В рамках разработки на Spring Framework иногда возникает необходимость регистрировать Bean-ы динамически. И вот в рамках Spring Framework 7 завозят новый API, который как раз это и позволяет сделать. Давайте разберемся, что это за новый зверь, и сделаем мы это на примере Spring Data, ниже будет понятно, почему.
NOTE: Spring Framework 7 еще не имеет GA релиза, иными словами, он еще не вышел. Все, что касается нового API в статье относится к 7-ому Milestone релизу Spring Framework. Вряд ли API будет сильно меняться, но имейте это в виду.
Что в общем случае делает Spring Data:
- Она распознает репоизтории, для которых должна создать реализации. Любой репозиторий обязан наследовать
либо
org.springframework.data.repository.Repository, либо его потомка. - Ну и далее Spring Data должна просканировать classpath для того, чтобы найти наши объявленные репозитории и создать из них бины.
Обратите внимание - Spring Data заранее не знает о том, сколько бинов репозиториев она должна будет создать и
впоследствие зарегистрировать в конексте. Соотвественно, единственный способ решить данную проблему это
динамически регистрировать бины. Под "динамической регситрацией" здесь имеется в виду регистрация бинов "на-лету",
то есть вызывая какой-то API Spring-а, а не через привычные нам аннотации @Component и её производные.
Но подождите, Spring Framework 7 еще не вышел, все что у наc есть это Milestone релиз, но Spring Data JPA, например, существует уже более 10 лет. Как до этого Spring Data справлялась и регистрировала бины динамически, если сама по сбее динамическая регистрация бинов еще даже не вышла?
Давайте разбираться.
В чем же дело. А дело в том, что механизм динамической регистрации бинов он в той или иной степени уже был возможен долгое время (О том, для чего же нам новый API поговорим чуть позже).
В частности, уже долгие годы в Spring Framework существует интерфейс SingletonBeanRegistry. Он позволяет осуществлять
динмаическию регистрацию бинов:
class MyFirst {
private final MySecond mySecond;
MyFirst(MySecond mySecond) {
this.mySecond = mySecond;
}
}
class MySecond { }
class ProgrammaticBeanRegistrationApplicationTest {
@Test
void testRegisterSingletonMethod() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
MySecond second = new MySecond();
applicationContext.getBeanFactory().registerSingleton("mySecond", second);
applicationContext.getBeanFactory().registerSingleton("myFirst", new MyFirst(second));
applicationContext.refresh();
assertThat(applicationContext.containsBean("myFirst")).isTrue();
assertThat(applicationContext.containsBean("mySecond")).isTrue();
}
}
Казалось бы - чего мудрить, вот оно, решение. Но данный мехнизм имеет свои недостатки. В частности, данный API не дает
никакой возможности сконфигурировать BeanDefinition. Для ясности - BeanDefinition представляет собой
некоторую конфигурацию нашего бина, набор его свойств. Например:
- Scope бина. Чаще всего мы работаем с Singleton, но и Session иногда бывает нужен.
- Init методы. В данном случае можно речь про
@PostConstructили методafterPropertiesSet(), хотя и между ними есть небольшая разница. - Primary/Fallback флаги. Их, как правило, проставляют аннотациями
@Primaryи@Fallbackсоотвественно
и т.д.
Проблема интферфейса SingletonBeanRegistry еще и в том, что его реализации работают довольно примитивно. Они производят регистрацию бина
в контексте, при этом не учитывают никакие @PostConstruct/@PreDestroy и другие коллбеки. Иными словами, реализации
делают предположение, что бин уже сконфигурирован и инициализирован. К тому же Singleton, который будет зарегистрирован, не будет принимать
участие в жизненном цикле ApplicationContext:
class WithCallback implements InitializingBean {
private boolean callbackCalled;
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Hey from the callback");
callbackCalled = true;
}
public boolean isCallbackCalled() {
return callbackCalled;
}
}
class ProgrammaticBeanRegistrationApplicationTest {
@Test
void testRegisterSingletonMethod_noCallbacksInvoked() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.getBeanFactory().registerSingleton("mySecond", new WithCallback());
applicationContext.refresh();
assertThat(applicationContext.containsBean("mySecond")).isTrue(); // Бин-то есть
assertThat(applicationContext.getBean("mySecond", WithCallback.class).isCallbackCalled()).isFalse(); // А коллбек не будет вызван
}
}
По сути, единственная хорошая для нас новость в том, что все попытки взять бин из BeanFactory/ApplicationContext-а через getBean() и другие
его перегруженные вариации будут работать.
Ну хорошо. Проблема ясна и понятна. И тут в дверь с ноги вламывается BeanDefinitionRegistry!
Стоит отметить, что BeanDefinitionRegistry также существует в Spring Framework уже довольно давно. И да, если Вам таки очень инетерсно, как же
Spring Data создает свои репозитории, ответ прост - именно через BeanDefinitionRegistry.
Вот конкретный метод в Spring Data Commons,
который занимается регистрацией BeanDefinition-ов. Само создание прокси репозитория происходит
в отдельных реализацияхRepositoryFactorySupport,
но это уже за рамками данной статьи.
Давайте посмотрим, как же выглядит API BeanDefinitionRegistry:
class ProgrammaticBeanRegistrationApplicationTest {
@Test
void testDynamicBeansRegistration_beanDefinitionRegistry() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(ProgrammaticBeanRegistry.class);
WithCallback firstBean = applicationContext.getBean("withCallback", WithCallback.class);
WithCallback secondBean = applicationContext.getBean("withCallback", WithCallback.class);
assertThat(firstBean.isCallbackCalled()).isTrue(); // Коллбеки сработали!
assertThat(secondBean.isCallbackCalled()).isTrue();
assertThat(firstBean).isNotSameAs(secondBean); // И бин нам вернулся не один и тот же, prototype!
}
@Component
static class ProgrammaticBeanRegistry implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
registry.registerBeanDefinition("withCallback",
BeanDefinitionBuilder.genericBeanDefinition(WithCallback.class).setPrimary(true).setLazyInit(false)
.setScope("prototype").getBeanDefinition());
}
}
}
Как мы можем увидеть, здесь уже у нас появляется довольно тонкая настройка BeanDefinition-a, который мы хотим зарегистрировать. Это отлично.
Несмотря на то, что, в теории, досутп к BeanDefinitionRegistry можно получить разными способами, но чаще всего это делают именно через написание своего
собственного BeanDefinitionRegistryPostProcessor. Вот пример видео от Josh-a Long-a,
Spring Framework Developer Advocate-а, где он демонстрирует данный API через BeanDefinitionRegistryPostProcessor.
Хорошие новости в том, что init методы, scope-ы, primary, lazy-init - это все учитывается и работает. Почему? Читайте секцию ниже. И Spring Data использовала и использует этот API по сей день. Вопрос - чем он нам не угодил?
Начиная с Spring Framework 7, у нас появляется новый BeanRegistry API. Для того, чтобы иметь почву для обсуждения, давайте сразу взглянем на использвание BeanRegistry в действии:
class ProgrammaticBeanRegistrationApplicationTest {
@Test
void testDynamicBeansRegistration_beanRegistry() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(
BeanRegistryConfiguration.class);
BeanDefinition beanDefinition = applicationContext.getBeanDefinition("prodBean");
assertThat(beanDefinition.getScope()).isEqualTo("prototype");
assertThat(beanDefinition.isPrimary()).isTrue();
assertThat(beanDefinition.isLazyInit()).isTrue();
}
/**
* application.properties содержит следующую строчку:
* <p>
* <pre class="code">
* spring.profiles.active=prod
* </pre>
*/
@Configuration
@PropertySource("application.properties")
@Import(MyBeanRegistry.class)
static class BeanRegistryConfiguration { }
@Component
static class MyBeanRegistry implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
if (env.matchesProfiles("dev|qa")) {
registry.registerBean("testBean", WithCallback.class, spec -> spec.fallback().lazyInit().order(Ordered.HIGHEST_PRECEDENCE));
} else if (env.matchesProfiles("prod")) {
registry.registerBean("prodBean", WithCallback.class, spec -> spec.primary().prototype().lazyInit());
}
}
}
}
Важно, то, что для осуществления динамической регистрации бинов мы использовали лишь API привычных нам классов. Иными словами, нам не пришлось писать свой
BeanDefinitionRegistryPostProcessor или т.п. К тому же, обратите внимание, у нас здесь есть доступ к инициализированной Environment для того, чтобы получить доступ
к свойствам приложения, которые мы, как правило, инжектим через @Value. Кстати, а можно ли нечто подобное сделать с BeanDefinitionRegistryPostProcessor?
Давайте задаимся вопросом, можно ли прокинуть Environment в BeanDefinitionRegistryPostProcessor, чтобы осуществить такую же динамическую регистрацию
бинов с упопром на значения из application.properties? Да, можно, но есть нюанс... и не один.
Во-первых, мы не можем просто поставить @Autowired Environment env;. Почему? Да потому, что BeanDefinitionRegistryPostProcessor это BeanFactoryPostProcessor. Запомните,
BeanFactoryPostProcessor отрабатывают очень рано (на уровне на уровне создания BeanDefinition-ов), до
создания рядовых бинов, поэтому инжектить через @Autowired нечего - еще ничего Spring не создал. В частности, за счет того, что на этапе работы BeanFactoryPostProcessor-ов еще
никаких бинов нет, Spring может спокойно добавлять дополнительные BeanDefinition-ы к уже существующим и они не будут (ну, почти) отличаться от созданных через @Component, например.
Поэтому и работают коллббеки и все остальное.
Но я же сказал, что заинжектить Environment можно. Это правда, можно. Например вот так:
class ProgrammaticBeanRegistrationApplicationTest {
@Test
void testDynamicBeansRegistration_propertiesInBeanFactoryPostProcessor() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(
BeanDefinitionRegistryPostProcessorConfiguration.class);
BeanDefinition beanDefinition = applicationContext.getBeanDefinition("withCallback");
assertThat(beanDefinition.getScope()).isEqualTo("prototype");
assertThat(beanDefinition.isPrimary()).isTrue();
assertThat(beanDefinition.isLazyInit()).isFalse();
}
@Configuration
@PropertySource("application.properties")
@Import(ProgrammaticBeanRegistryEnvironmentAware.class)
static class BeanDefinitionRegistryPostProcessorConfiguration { }
@Component
static class ProgrammaticBeanRegistryEnvironmentAware implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment environment;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (environment.matchesProfiles("prod")) {
registry.registerBeanDefinition("withCallback",
BeanDefinitionBuilder.genericBeanDefinition(WithCallback.class).setPrimary(true).setLazyInit(false)
.setScope("prototype").getBeanDefinition());
}
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
}
И тут есть второй нюанс. С одной стороны - да, мы получим Environment. Но с другой стороны, тот Environment, который мы получим, опять же, в силу того, когда отрабатывают
BeanFactoryPostProcessor-ы может быть не полным. Иными словами, если какой-нибудь модуль Spring или ваш самописный код работает с MutablePropertySource/ConfigurableEnvironment
и добавляет свойства на опредленном этапе lifecycle бина, даже на этапе его создания допустим, то этих свойств, увы, еще не будет на момент отработки BeanDefinitionRegistryPostProcessor.
Вообще динамичесая регистрация бинов она гораздо сложнее, чем статическая. В корне, это связано с тем, что регистрировать бины динамически мы
можем по сути на любом этапе жизненного цикла ApplicationContext (конечно, если тот еще не закрыт). Например регистрация через
BeanDefinitionRegistryPostProcessor влечет за собой ограничения на auto-wiring и ряд других ограничений. Регистрация бинов через SingletonBeanRegistry.registerSingleton
решает эти проблемы по-другому - новый бин существует вне жизненного цикл приложения.
Новый подход старается дать людям API для динамической регстрации бинов с "человеческим лицом". Он с одной стороны более простой, с другой стороны - дающий больше возможностей по сравнению с традиционными подходами.