Создание микросервиса на Quarkus, Kotlin и Gradle

от автора

Введение

В предыдущей статье было приведено краткое описание процесса создания микросервиса на современных JVM фреймворках, а также их сравнение. В этой статье будет более детально рассмотрен недавно вышедший Quarkus на примере создания микросервиса с использованием упомянутых технологий и в соответствии с требованиями, указанными в основной статье. Полученное приложение станет частью следующей микросервисной архитектуры:

target architecture

Исходный код проекта, как обычно, доступен на GitHub.

Перед началом работы с проектом должны быть установлены:

Создание нового проекта

Для генерации нового проекта используйте web starter или Maven (для создания Maven проекта или Gradle проекта). Стоит отметить, что фреймворк поддерживает языки Java, Kotlin и Scala.

Зависмости

В этом проекте в качестве системы сборки используется Gradle Kotlin DSL. Скрипт сборки должен содержать:

  • плагины
    Листинг 1. build.gradle.kts

    plugins {     kotlin("jvm")     kotlin("plugin.allopen")     id("io.quarkus") }

    Разрешение версий плагинов выполняется в settings.gradle.kts.

  • зависимости
    Листинг 2. build.gradle.kts

    dependencies {     ...     implementation(enforcedPlatform("io.quarkus:quarkus-bom:$quarkusVersion"))     implementation("io.quarkus:quarkus-resteasy-jackson")     implementation("io.quarkus:quarkus-rest-client")     implementation("io.quarkus:quarkus-kotlin")     implementation("io.quarkus:quarkus-config-yaml")     testImplementation("io.quarkus:quarkus-junit5")     ... }

    Информация по поводу импорта Maven BOM доступна в документации Gradle.

Также требуется сделать некоторые Kotlin классы open (по умолчанию они final; больше информации по конфигурированию Gradle в Quarkus Kotlin guide:

Листинг 3. build.gradle.kts

allOpen {     annotation("javax.enterprise.context.ApplicationScoped") }

Конфигурирование

Фреймворк поддерживает конфигурирование с использованием properties и YAML файлов
(подробнее в Quarkus config guide). Конфигурационный файл располагается в папке resources и выглядит так:

Листинг 4. application.yaml

quarkus:   http:     host: localhost     port: 8084  application-info:   name: quarkus-service   framework:     name: Quarkus     releaseYear: 2019

В этом фрагменте кода конфигурируются стандартные и кастомные параметры микросервиса. Последние могут быть прочитаны так:

Листинг 5. Чтение кастомных параметров приложения (исходный код)

import io.quarkus.arc.config.ConfigProperties  @ConfigProperties(prefix = "application-info") class ApplicationInfoProperties {      lateinit var name: String      lateinit var framework: FrameworkConfiguration      class FrameworkConfiguration {         lateinit var name: String         lateinit var releaseYear: String     } }

Бины

Перед тем как начать работать с кодом, надо отметить, что в исходном коде приложения на Quarkus нет main метода (хотя, возможно, появится).

Внедрение @ConfigProperties бина с предыдущего листинга в другой бин выполняется с использованием аннотации @Inject:

Листинг 6. Внедрение @ConfigProperties бина (исходный код)

@ApplicationScoped class ApplicationInfoService(     @Inject private val applicationInfoProperties: ApplicationInfoProperties,     @Inject private val serviceClient: ServiceClient ) {     ... }

ApplicationInfoService бин, аннотированный @ApplicationScoped, в свою очередь, внедряется так:

Листинг 7. Внедрение @ApplicationScoped бина (исходный код)

class ApplicationInfoResource(     @Inject private val applicationInfoService: ApplicationInfoService )

Больше информации по Contexts and Dependency Injection в Quarkus CDI guide.

REST контроллер

В REST контроллере нет ничего необычного для тех, кто работал с Spring Framework или Java EE:

Листинг 8. REST контроллер (исходный код)

@Path("/application-info") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) class ApplicationInfoResource(     @Inject private val applicationInfoService: ApplicationInfoService ) {      @GET     fun get(@QueryParam("request-to") requestTo: String?): Response =         Response.ok(applicationInfoService.get(requestTo)).build()      @GET     @Path("/logo")     @Produces("image/png")     fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build() }

REST клиент

Для работы в микросервисной архитектуре Quarkus сервис должен уметь выполнять запросы к другим сервисам. Т. к. все сервисы имеют одинаковый API, имеет смысл создать единый интерфейс для общего кода и пачку наследующих его REST клиентов:

Листинг 9. REST клиенты (исходный код)

@ApplicationScoped @Path("/") interface ExternalServiceClient {     @GET     @Path("/application-info")     @Produces("application/json")     fun getApplicationInfo(): ApplicationInfo }  @RegisterRestClient(baseUri = "http://helidon-service") interface HelidonServiceClient : ExternalServiceClient  @RegisterRestClient(baseUri = "http://ktor-service") interface KtorServiceClient : ExternalServiceClient  @RegisterRestClient(baseUri = "http://micronaut-service") interface MicronautServiceClient : ExternalServiceClient  @RegisterRestClient(baseUri = "http://quarkus-service") interface QuarkusServiceClient : ExternalServiceClient  @RegisterRestClient(baseUri = "http://spring-boot-service") interface SpringBootServiceClient : ExternalServiceClient

Как видите, создание REST клиента к другим сервисам представляет собой всего лишь создание интерфейса с соответствующими JAX-RS и MicroProfile аннотациями.

Service Discovery

Как было видно в предыдущем разделе, значениями параметра baseUri являются названия сервисов. Но пока что в Quarkus нет встроенной поддержки Service Discovery (Eureka) или она не работает (Consul), т. к. фреймворк в перую очередь направлен на работу в облачных средах. Поэтому паттерн Service Discovery реализован с использованием библиотеки Consul Client for Java.

Клиент к Consul включает два метода, register и getServiceInstance (использующий алгоритм Round-robin):

Листинг 10. Клиент к Consul (исходный код)

@ApplicationScoped class ConsulClient(     @ConfigProperty(name = "application-info.name")     private val serviceName: String,     @ConfigProperty(name = "quarkus.http.port")     private val port: Int ) {      private val consulUrl = "http://localhost:8500"     private val consulClient by lazy {         Consul.builder().withUrl(consulUrl).build()     }     private var serviceInstanceIndex: Int = 0      fun register() {         consulClient.agentClient().register(createConsulRegistration())     }      fun getServiceInstance(serviceName: String): Service {         val serviceInstances = consulClient.healthClient().getHealthyServiceInstances(serviceName).response         val selectedInstance = serviceInstances[serviceInstanceIndex]         serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size         return selectedInstance.service     }      private fun createConsulRegistration() = ImmutableRegistration.builder()         .id("$serviceName-$port")         .name(serviceName)         .address("localhost")         .port(port)         .build() }

Сначала надо зарегистрировать приложение:

Листинг 11. Регистрация в Consul (исходный код)

@ApplicationScoped class ConsulRegistrationBean(     @Inject private val consulClient: ConsulClient ) {      fun onStart(@Observes event: StartupEvent) {         consulClient.register()     } }

Далее требуется преобразовать названия сервисов в реальное расположение. Для этого используется класс, расширяющий ClientRequestFilter и аннотированный @Provider:

Листинг 12. Фильтр для работы с Service Discovery (исходный код)

@Provider @ApplicationScoped class ConsulFilter(     @Inject private val consulClient: ConsulClient ) : ClientRequestFilter {      override fun filter(requestContext: ClientRequestContext) {         val serviceName = requestContext.uri.host         val serviceInstance = consulClient.getServiceInstance(serviceName)         val newUri: URI = URIBuilder(URI.create(requestContext.uri.toString()))             .setHost(serviceInstance.address)             .setPort(serviceInstance.port)             .build()          requestContext.uri = newUri     } }

В фильтре всего лишь осуществляется замена URI объекта requestContext на расположение сервиса, полученное от клиента к Consul.

Тестирование

Тесты для двух методов API реализованы с использованием библиотеки REST Assured:

Листинг 13. Тесты (исходный код)

@QuarkusTest class QuarkusServiceApplicationTest {      @Test     fun testGet() {         given()             .`when`().get("/application-info")             .then()             .statusCode(200)             .contentType(ContentType.JSON)             .body("name") { `is`("quarkus-service") }             .body("framework.name") { `is`("Quarkus") }             .body("framework.releaseYear") { `is`(2019) }     }      @Test     fun testGetLogo() {         given()             .`when`().get("/application-info/logo")             .then()             .statusCode(200)             .contentType("image/png")             .body(`is`(notNullValue()))     } }

Во время тестирования нет необходимости регистрировать приложение в Consul, поэтому в исходном коде проекта рядом с тестом находится ConsulClientMock, расширяющий ConsulClient:

Листинг 14. Мок для ConsulClient (исходный код)

@Mock @ApplicationScoped class ConsulClientMock : ConsulClient("", 0) {      // do nothing     override fun register() {     } }

Сборка

Во время Gradle задачи build вызывается задача quarkusBuild. По умолчанию она генерирует runner JAR и папку lib, где находятся зависимости. Для создания uber-JAR задача quarkusBuild долна быть сконфигурирована следующим образом:

Листинг 15. Настройка генерации uber-JAR (исходный код)

tasks {     withType<QuarkusBuild> {         isUberJar = true     } }

Для сборки выполните в корне проекта ./gradlew clean build.

Запуск

Перед запуском микросервиса надо стартовать Consul (описано в основной статье).

Микросервис можно запустить, используя:

  • Gradle задачу quarkusDev
    Выполните в корне проекта:
    ./gradlew :quarkus-service:quarkusDev
    или запустите задачу в IDE
  • uber-JAR
    Выполните в корне проекта:
    java -jar quarkus-service/build/quarkus-service-1.0.0-runner.jar

Теперь можно использовать REST API, например, выполните следующий запрос:

GET http://localhost:8084/application-info

Результатом будет:

Листинг 16. Результат вызова API

{   "name": "quarkus-service",   "framework": {     "name": "Quarkus",     "releaseYear": 2019   },   "requestedService": null }

Совместимость с Spring

Фреймворк обеспечивает совместимость с несколькими технологиями Spring: DI, Web, Security, Data JPA.

Заключение

В статье был рассмотрен пример создания простого REST сервиса на Quarkus с использованием Kotlin и Gradle. В основной статье вы можете видеть, что полученное приложение имеет сопоставимые параметры с приложениями на других современных JVM фреймворках. Таким образом у Quarkus есть серьёзные конкуренты, такие как Helidon MicroProfile, Micronaut и Spring Boot (если речь идёт о fullstack фреймворках). Поэтому, думаю, нас ждёт интересное развитие событий, которое будет полезно для всей экосистемы Java.

Полезные ссылки

P.S. Спасибо vladimirsitnikov за помощь в подготовке статьи.

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *