Как стать автором
Обновить
83.28

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

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров4.7K

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

Теги:
Хабы:
+10
Комментарии5

Публикации

Информация

Сайт
t.me
Дата регистрации
Численность
11–30 человек