Введение
В статье тыцк был рассмотрен один из возможных способов подключения разделяемого между микросервисами common модуля при помощи ImportBeanDefinitionRegistrar Spring-а. Он обладает существенным недостатком: инструменты статического анализа кода не могут понимать такую “уличную магию”. Поэтому в данной статье рассмотрен другой способ решения этой же задачи при помощи классического Spring starter-а.
Состав common модуля
Для примера предположим, что в вашем common модуле находятся Spring-компоненты, решающие следующие задачи:
-
Конфигурация CORS
-
Логирование и трассировка
-
Обработка ошибок и унификация формата тела ответа сервера при возникновении ошибок
-
Feature flags
Функции 2 и 3, очевидно, будут востребованы в каждом разрабатываемом микросервисе, тогда как функции 1 и 4 - нет. То есть нам также нужна возможность конфигурации используемых функций common-модуля, чтобы не притаскивать в runtime микросервиса ненужные вещи.
Данные функции оформлены в виде отдельного Kotlin-модуля, среди зависимостей которого отсутствует Spring Boot, а также application-сервер. Common модуль в своих зависимостях объявляет только конкретные версии отдельных библиотек экосистемы Spring, необходимых для успешной компиляции разделяемых функций.
Немного теории
Напомним, что в составе Spring Boot есть подсистема автоконфигурации, пожалуй, одна из наиболее удобных разработок всего проекта Spring Framework. Она позволяет подключать стартеры, внутри которых содержится определение конфигурации различных подпроектов Spring-а “по умолчанию”. Если рассматривать внутреннее устройство стартеров, то для создания стартера необходимо:
-
Разработать класс автоконфигурации и пометить его аннотацией @AutoConfiguration (строго говоря, допускается также и обычная @Configuration)
-
Написать служебный файл META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports с указанием полного пути к классу автоконфигурации
При запуске Spring Boot приложения, будут найдены все файлы META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, доступные в classpath приложения (включая внутренности jar архивов зависимостей). После чего будут найдены классы автоконфигураций, указанные в упомянутых файлах. Далее классы автоконфигураций будут обработаны обычным образом, как и любые другие конфигурации.
Дополнительно к возможностям обычных конфигураций используются аннотации @ConditionalOn*, которые позволяют добавить в ApplicationContext бины опционально в зависимости от произвольных условий. Наиболее популярными условиями являются: по отсутствию заданного класса и по значению параметра конфигурации.
Реализация
Для нашего common модуля мы можем реализовать автоконфигурацию на основе следующего принципа. Внутри класса автоконфигурации расположим вложенные классы обычных конфигураций с аннотациями @ConditionalOnProperty, чтобы привязать включение того или иного модуля в ApplicationContext в зависимости от булевых переменных, задаваемых в конфигурации микросервисов.
@AutoConfiguration
@ConditionalOnProperty("gentleman-toolkit.enabled", havingValue = "true", matchIfMissing = false)
@EnableConfigurationProperties(value = [
KafkaProperties::class,
GentlemanToolkitProperties::class
])
class GentlemanToolkitAutoConfiguration {
@ConditionalOnProperty("gentleman-toolkit.provide-cors-config", havingValue = "true", matchIfMissing = false)
@Configuration
class AllowedCorsConfigProvider {
@Bean
fun devAllowedCorsConfig(
@Value("\${cors.allowed-origins}")
allowedOrigins: Array<String>
): WebMvcConfigurer = AllowedCorsConfig(allowedOrigins)
}
@ConditionalOnProperty("gentleman-toolkit.provide-exception-handlers", havingValue = "true", matchIfMissing = false)
@Configuration
class ExceptionHandlerProvider {
@Bean
fun commonExceptionHandler(): CommonExceptionHandler = CommonExceptionHandler()
@Bean
fun defaultExceptionHandler(): DefaultExceptionHandler = DefaultExceptionHandler()
}
@ConditionalOnProperty("gentleman-toolkit.provide-logging-filter", havingValue = "true", matchIfMissing = false)
@Configuration
class LoggingFilterBeanProvider {
@Bean
@Order(1)
fun loggingFilter(): LoggingFilter = LoggingFilter()
}
@ConditionalOnProperty("gentleman-toolkit.provide-feature-flags", havingValue = "true", matchIfMissing = false)
@Configuration
class FeatureFlagsProvider {
@Bean
@Profile("stage", "prod")
fun gitlabFeatureFlagsManager(
environment: Environment
): FeatureFlagsManager = GitLabFeatureFlagsManager(environment)
@Bean
@Profile("dev")
fun stubFeatureFlagsManager(
environment: Environment
): FeatureFlagsManager = StubFeatureFlagsManager(environment)
}
}
Далее остается указать автоконфигурацию в служебном файле Spring-а и подключить сам проект common модуля к микросервису.
org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.newdex.services.common.GentlemanToolkitAutoConfiguration