В 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 и всего, что с ним связано.