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

Исходный код проекта, как обычно, доступен на GitHub.
Перед началом работы с проектом должны быть установлены:
- JDK 13
- Consul
Создание нового проекта
Для генерации нового проекта используйте web starter или Maven (для создания Maven проекта или Gradle проекта). Стоит отметить, что фреймворк поддерживает языки Java, Kotlin и Scala.
Зависмости
В этом проекте в качестве системы сборки используется Gradle Kotlin DSL. Скрипт сборки должен содержать:
-
плагины
Листинг 1. build.gradle.ktsplugins { kotlin("jvm") kotlin("plugin.allopen") id("io.quarkus") }Разрешение версий плагинов выполняется в
settings.gradle.kts. -
зависимости
Листинг 2. build.gradle.ktsdependencies { ... 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/
Добавить комментарий