В Spring Framework 7 появился новый API — BeanRegistry, который упрощает и расширяет возможности по динамической регистрации бинов. Это особенно актуально, когда невозможно заранее предсказать, сколько компонентов потребуется, как в случае со Spring Data. В новой статье от эксперта сообщества Spring АйО, Михаила Поливахи, вы узнаете:
-
Как Spring Data справлялась с динамической регистрацией раньше;
-
Какие подходы регистрации существовали до BeanRegistrar, как они работали;
-
Как новый API связан со Spring AOT.
Привет, Хабр!
В рамках разработки на Spring Framework иногда возникает необходимость регистрировать Bean-ы динамически. И вот в рамках Spring Framework 7 завозят новый API, который как раз это и позволяет сделать. Давайте разберемся, что это за новый зверь, и сделаем мы это на примере Spring Data, ниже будет понятно, почему.
NOTE: Spring Framework 7 еще не имеет GA релиза, иными словами, он еще не вышел. Все, что касается нового API в статье относится к 7-ому Milestone релизу Spring Framework. Вряд ли API будет сильно меняться, но имейте это в виду.
Spring Data. Динамическая Регистрация Репозиториев
Что в общем случае делает 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!
Стоит отметить, что 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 по сей день. Возникает вопрос: “Чем он нам не угодил?”
Новый API. BeanRegistry
Начиная с 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; } } }
И тут есть второй нюанс. И этот нюанс актуален как для BeanRegistrar API, так и для BeanDefinitionRegistryPostProcessor.
С одной стороны — да, мы получим Environment. Но с другой стороны, тот Environment, который мы получим, опять же, в силу того, когда отрабатывают BeanFactoryPostProcessor-ы может быть неполным. Иными словами, если какой-нибудь модуль Spring или ваш самописный код работает с MutablePropertySource и добавляет свойства на определенном этапе lifecycle бина, даже на этапе его создания, допустим, то этих свойств, увы, еще не будет на момент отработки BeanDefinitionRegistryPostProcessor.
Kotlin DSL. Мелочь, а Приятно.
Мы в рамках Spring АйО сообщества уже как-то выпускали пост о том, что Spring Framework и JetBrains запускают стратегическое партнёрство в рамках языка Kotlin. Это, в том числе, означает, что в Spring Framework были и будут появляться новые Kotlin DSL для существующих API. Это коснулось и BeanRegistrar API. Для тех, кто пишет на Kotlin, для нового BeanRegistrar API завезли к тому же BeanRegistrarDsl.
Важно понимать, что это, как большая часть Kotlin DSL в Spring Framework вообще, лишь дополнительная абстракция поверх уже существующего API. Она функционально ничего нового не приносит, просто делает работу с BeanRegistrar в рамках Kotlin более приятной.
Spring AOT. Действительно Важно.
И последний нюанс, который на самом деле довольно важный. О нём я узнал после личного общения с Juergen Hoeller-ом на Spring I/O 2025 Barcelona. Эксклюзив для Вас, так сказать.
Дело в том, что новый API сильно упрощает жизнь проекту Spring AOT. Для тех, кто не в курсе — Spring Framework довольно сильно в последних версиях фокусируется на ускорении времени старта приложения, ускорении ramp up-а приложений на Spring и т.п. Именно эти цели преследует Spring AOT. Это не какой-то новый модуль Spring-а на самом деле, а общее название для AOT активностей в рамках экосистемы Spring Framework.
Одна из задач, которую Spring AOT перед собой ставит, заключается в том, чтобы чётко понять на этапе сборки те бины, которыми Ваше приложение собирается оперировать. Иными словами, Spring AOT будет пытаться на этапе сборки просканировать classpath и сгенерировать некоторую статическую информацию, из которой потом будут созданы Bean Defnition-ы. По сути, происходит некоторая кодогенерация.
Идём дальше. Одно из следствий такого поведения является то, что classpath, если так можно выразиться, статический. То есть при использовании Spring AOT, состояние classpath-а должно быть определено во время билда и уже не должно меняться при жизни приложения в продакшене.
Вернёмся к BeanRegistrar и к тому, как конкретно он помогает Spring AOT. Видите ли, регистрировать BeanDefinition-ы через BeanDefinitionRegistryPostProcessor, конечно, можно. Никто не спорит. Однако, важно понимать, что BeanDefinitionRegistryPostProcessor – это общий API процессинга BeanDefinitionRegistry. Иными словами, использование BeanDefinitionRegistryPostProcessor еще ни о чём не говорит. Это не значит, что там будет происходить регистрация компонента.
Spring AOT работает, в том числе, путём Annotation Processing-а, и теперь представьте, что APT видит использование BeanDefinitionRegistryPostProcessor. Какой он из этого должен сделать вывод? Да никакой, и APT придётся анализировать Ваши исходники, чтобы понять, а что же Вы всё-таки там делаете. Это довольно сложный подход.
А с BeanRegistrar всё гораздо проще! Это же по сути просто функциональный интерфейс, и мы точно знаем, что любые его реализации должны заниматься только созданием компонентов динамически: есть чёткий контракт взаимодействия. Это означает, что работа для APT тула сильно упрощается. Ему лишь нужно сгенерировать код, который у всех BeanRegistrar вызывает метод register() и тем самым, потенциально, кладёт новые бины в контекст.
Выводы
Как итог, новый BeanRegistrar API отличается от своих предшественников тем, что
-
Он более удобный и не требует писать свой
BeanFactoryPostProcessor. -
Имеет специализированный Kotlin DSL
-
Упрощает интеграцию со Spring AOT.
Во многом, из-за последнего пункта, BeanRegistrar является не просто ещё одним подходом, а является целевым для Spring Framework и для всей экосистемы в целом. Для тех же, кто не пользуется Spring AOT, новый подход предоставляет API для динамической регистрации бинов с «человеческим лицом» — более верхнеуровневый и приятный в использовании, особенно в рамках Kotlin.
Код, используемый в проекте, размещён на GitHub в рамках организации spring-aio.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
ссылка на оригинал статьи https://habr.com/ru/articles/915512/
Добавить комментарий