Изграждане на приложение, което се управлява от свойства с помощта на Spring Boot.

Тази публикация се счита за втора част от моята предишна публикация относно някои много често използвани анотации в рамката на Spring. В тази публикация исках конкретно да се съсредоточа върху това как можем да изграждаме бекенд услуги с помощта на Spring/Spring Boot и да управляваме определени секции от приложението, използвайки свойства, които също могат да бъдат доставени извън кодовата база. Когато казвам, че се управлява от свойства, на много високо ниво имам предвид способността да се променя поведението на приложението само чрез използване на свойства. Например, ако изградим бекенд услуга, която ще позволи функционалност за влизане с помощта на услуги на трети страни (да речем влизане с помощта на услуга X или услуга Y) и във всеки даден момент само една от тях ще бъде налична и искаме възможността да превключваме между двете опции, използвайки свойства извън приложението.

С изискване като това става ясно, че имаме нужда от начин извън приложението да диктуваме на приложението кое влизане да активира. Постигаме това, като използваме поддръжката на spring за свойства (които могат да бъдат .properties или .yaml files). Тези свойства се поставят в директорията src/main/resources и започват с думата приложение, последвана от тире и името на профила (ако искате да прочетете повече за него, моля, вижте Документацията на Spring за профили).

За тази публикация използвам следните версии на инструменти и рамки:

  • Пролетно зареждане: 2.4.1
  • Котлин: 1.4.21
  • JDK: 15
  • Gradle: 6.7.1

Без повече шум, ето как създаваме Spring Boot Kotlin приложение, което може условно да превключва между услуги, използвайки външни свойства:

Стъпка 1: Измислете персонализиран ключ и стойност на свойството

Първото нещо, което правим, е да измислим персонализирано име на свойство, за да посочим услугата за влизане, която искаме да е активна. Можем да го наречем authentication.service (уверете се, че името на свойството вече е заето. Можете да се обърнете към списъка със свойства на Spring, за да се уверите). След това дефинираме възможните стойности за това свойство. В нашия случай, ако трябва да разгледаме само услуга X/услуга Y, можем да дефинираме разрешените стойности като service-xили service-y. Така че, ако искахме да използваме Service X за влизане, бихме добавили следното към нашия файл application.YAML (използвам файловия формат yaml за този пример):

authentication.service: service-x

И за да използвате услуга Y за влизане:

authentication.service: service-y

Стъпка 2: Дефинирайте интерфейс за функционалността

След това дефинираме интерфейс за функционалността за влизане. Този интерфейс може да бъде супер прост и да съдържа само един метод, който основно изпълнява функцията за влизане. Имайте предвид, че всички класове, които изискват функционалност за влизане, ще препращат само към AuthenticationService и НЕ към специфичните реализации на услугата X или услугата Y.

interface AuthenticationService {
    fun login()
}

Стъпка 3: Добавете специфична за услугата функция за влизане

В тази стъпка добавяме клас, който позволява влизане с помощта на услуга X и друг клас, който позволява влизане с помощта на услуга Y. Имайте предвид, че и двата класа ще имплементират интерфейса, който дефинирахме в стъпка 2.

class ServiceXAuthService: AuthenticationService {
    fun login() {    
        // Functionality to execute login using Service X 
    }
}
class ServiceYAuthService: AuthenticationService {
fun login() {    
        // Functionality to execute login using Service Y 
    }
}

Стъпка 4: Прочетете ключовете за услуги за удостоверяване в обект

За да използваме API или SDK от услуга на трета страна, ще трябва да дефинираме клас, който да съдържа тези свойства, които след това могат да бъдат използвани от ServiceXAuthService и ServiceYAuthService за комуникация съответно с услуги X и Y.

Нека направим още няколко предположения тук: услуга X изисква API ключ за удостоверяване, а услуга Y изисква API ключ и API секрет. Така че ще създадем клас, който да държи свойствата на услугата X.

@ConstructorBinding
@ConfigurationProperties(prefix = "service-x")
data class ServiceXProperties(
    val apiKey: String
)

и един за задържане на свойствата на услугата Y:

@ConstructorBinding
@ConfigurationProperties(prefix = "service-y")
data class ServiceYProperties(
    val apiKey: String
    val apiSecret: String
)

Анотациите @ConstructorBinding и @ConfigurationProperties са ключът тук. @ConstructorBinding се използва, за да посочи, че свойствата от свойствата или YAML файла ще трябва да бъдат прочетени в този обект с помощта на неговия конструктор, а @ConfigurationProperties анотацията показва префикса на свойството, което трябва да бъде прочетено от YAML или файла със свойства.

За да сте сигурни, че свойствата са взети и зададени на класовете, както се очаква, добавете следната зависимост към раздела за зависимости във файла build.gradle.kts (за повече информация вижте този документ):

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

След това ще актуализираме специфичните за услугата файлове, за да предоставим идентификационните данни за удостоверяване:

class ServiceXAuthService(
    private val authProperties: ServiceXProperties
): AuthenticationService {
    fun login() {    
        // Functionality to execute login using Service X 
    }
}
class ServiceYAuthService(
    private val authProperties: ServiceYProperties
): AuthenticationService {
    fun login() {    
        // Functionality to execute login using Service Y 
    }
}

Стъпка 5: Предоставяне на стойности на свойства

Сега, когато разполагаме с всички класове, предоставяме стойности за тези свойства във файла application.yaml (или application.properties. Ще използвам yaml файлове като примери тук).

Ако искахме да използваме услугата за вход на Service X:

authentication.service: service-x
service-x:
  apiKey: X-Key
  apiSecret: X-Secret

Вместо това, ако искаме да използваме услуга Y като доставчик на услуга за влизане, файлът application.yaml ще изглежда подобно на:

authentication.service: service-y
service-y.apiKey: Y-Key

Стъпка 6: Магия на конфигурацията

Това е същината на този пост. След като целият код за функционалността е добавен, сега пишем конфигурационен клас, който ще реши кой bean (ServiceXAuthService или ServiceYAuthService) ще бъде достъпен за всички класове, които изискват използването на функционалността за влизане. Наборът от анотации Conditional на Spring ни дава възможност да прочетем свойството authentication.service и да създадем ServiceXAuthService, ако стойността е service-x или ServiceYAuthService, ако стойността е service-y.

// For Service X Auth Service
@Configuration
@EnableConfigurationProperties(ServiceXProperties::class)
@ConditionalOnProperty(
    prefix = "authentication", 
    name = ["service"], 
    havingValue = "service-x"
)
class ServiceXAuthConfiguration(
    private val serviceXProperties: ServiceXProperties
) {
@Bean
   fun xAuthService(): AuthenticationService =  ServiceXAuthService(serviceXProperties)
}

И за да свържете функционалността за влизане на услугата Y

// For Service Y Auth Service
@Configuration
@EnableConfigurationProperties(ServiceYProperties::class)
@ConditionalOnProperty(
    prefix = "authentication", 
    name = ["service"], 
    havingValue = "service-y"
)
class ServiceYAuthConfiguration(
    private val serviceYProperties: ServiceYProperties
) {
@Bean
   fun yAuthService(): AuthenticationService =    ServiceYAuthService(serviceYProperties)
}

@EnableConfigurationProperties във връзка с @Configuration позволява създаване на bean от типа, определен от @EnableConfigurationProperties (в този случай това ще бъде или ServiceXProperties, или ServiceYProperties).

Също така е възможно потребителите да пропуснат конфигурационния клас и директно да поставят тези анотации заедно с @Component анотация в ServiceXAuthService и ServiceYAuthService класовете. Това е въпрос на лични предпочитания. Обичам да използвам конфигурационен клас поради няколко причини:

  1. Много е очевидно кои класове са изложени като зърна в приложението и тази информация се съхранява на централно място.
  2. Ако има допълнителни компоненти/класове, които може би са необходими за специфични условия, например ако имаме нужда от друг клас, който би служил като полезен клас, наречен ServiceXAuthLoginHelper и имаме нужда от това само когато имаме authentication.service зададено на service-x, можем да създадем два конфигурационни класове един за услуга X и друг за услуга Y и класът на конфигурация специално за влизане в услуга X може да съдържа кода за създаване и излагане на двата класа като компоненти и условната анотация може да се приложи към целия клас.
  3. Благодарение на готовата поддръжка на Spring за тестване на конфигурационни класове е много лесно да се тестват модулно конфигурационни класове с условна логика, за да сте сигурни, че нещата работят според очакванията. Тази публикация в блога обяснява много ясно как можем да пишем тестове за конфигурационни класове.

Има множество варианти на анотацията ConditionalX, предоставена от Spring, която може да се основава на компоненти, класове и т.н. Обърнете се към Annotation Types Summary на тази документация за повече информация.

Стъпка 7: Превръщане на свойството в променлива на средата

Вероятно вече се чудите как можем да превключваме между предпочитани услуги за влизане без промяна на кода. В края на краищата трябва да променим стойността authentication.service, за да работи цялата условна магия. Е, това вероятно е втората най-важна стъпка от тази публикация. Ние правим стойността на authentication.service променлива на средата, като по този начин я правим инжектируема извън приложението!

authentication.service: ${authentication.service.name}

Сега можем да превключваме между входове за услуги X и Y, като предоставим service-x или service-y като стойност за променливата на средата authentication.service.name и предоставим свойствата, които всяка от тези услуги може да се нуждае, и стартираме приложението, за да видим правилната услуга, създадена и изложена за инжектиране на зависимости . Bean на другата услуга няма да бъде създаден!

За да прочетете повече за екстернализираната конфигурация с Spring Boot, моля, вижте техния документ по тази тема.

Заключение

Надявам се, че сте намерили тази публикация в блога за проницателна за това как да постигнете управлявано от свойства поведение на условно приложение с помощта на Spring Boot!