Введение


В предыдущей статье было приведено краткое описание процесса создания микросервиса на современных 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 за помощь в подготовке статьи.