От конфигурации к динамике. Новый API по созданию бинов в Spring

от автора

В 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:

  1. Она распознает репозитории, для которых должна создать реализации. Любой репозиторий обязан наследовать либо org.springframework.data.repository.Repository, либо его потомка.

  2. Ну и далее 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 представляет собой некоторую конфигурацию нашего бина, набор его свойств. Например:

  1. Scope бина. Чаще всего мы работаем с Singleton, но и Session иногда бывает нужен.

  2. Init методы. В данном случае можно речь про @PostConstruct или метод afterPropertiesSet(), хотя и между ними есть небольшая разница.

  3. 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 отличается от своих предшественников тем, что

  1. Он более удобный и не требует писать свой BeanFactoryPostProcessor.

  2. Имеет специализированный Kotlin DSL

  3. Упрощает интеграцию со Spring AOT.

Во многом, из-за последнего пункта, BeanRegistrar является не просто ещё одним подходом, а является целевым для Spring Framework и для всей экосистемы в целом. Для тех же, кто не пользуется Spring AOT, новый подход предоставляет API для динамической регистрации бинов с «человеческим лицом» — более верхнеуровневый и приятный в использовании, особенно в рамках Kotlin.

Код, используемый в проекте, размещён на GitHub в рамках организации spring-aio.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.


ссылка на оригинал статьи https://habr.com/ru/articles/915512/