- PVSM.RU - https://www.pvsm.ru -

В настоящее время нет недостатка во фреймворках для создания микросервисов на Java и Kotlin.
В статье рассматриваются следующие:
| Название | Версия | Год первого релиза | Разработчик |
|---|---|---|---|
| Helidon SE [1] | 1.1.1 | 2019 | Oracle |
| Ktor [2] | 1.2.1 | 2018 | JetBrains |
| Micronaut [3] | 1.1.3 | 2018 | Object Computing |
| Spring Boot [4] | 2.1.5 | 2014 | Pivotal |
На их основе созданы четыре сервиса, которые могут взаимодействовать друг с другом посредством HTTP API с использованием паттерна Service Discovery, реализованного с помощью Consul [5]. Таким образом они формируют гетерогенную (на уровне фреймворков) микросервисную архитектуру (далее МСА):

Определим набор требований к каждому сервису:
GET /application-info{?request-to=some-service-name}GET /application-info/logoДалее рассматривается реализация микросервиса на каждом из фреймворков и сравниваются параметры полученных приложений.
Каркас разработки был создан в Oracle для внутреннего использования, впоследствии став open-source’ным. Существует две модели разработки на основе этого фреймворка: Standard Edition (SE) и MicroProfile (MP). В обоих случаях сервис будет обычной Java SE программой. Подробнее о различиях можно узнать на этой [6]странице.
Если коротко, то Helidon MP — это одна из реализаций Eclipse MicroProfile [7], что даёт возможность использования множества API, как ранее известных разработчикам на Java EE, так и более новых (Health Check, Metrics, Fault Tolerance и т. д.). В варианте Helidon SE разработчики руководствовались принципом “No magic”, что выражается, в частности, в меньшем количестве или полном отсутствии аннотаций, необходимых для создания приложения.
Для разработки микросервиса выбран Helidon SE. Помимо прочего в нём отсутствуют средства для реализации Dependency Injection, поэтому для внедрения зависимостей использован Koin [8]. Далее приведён класс, содержащий main-метод. Для реализации Dependency Injection класс наследуется от KoinComponent. Сначала стартует Koin, далее инициализируются требуемые зависимости и вызывается метод startServer(), где создаётся объект типа WebServer, которому предварительно передаётся конфигурация приложения и настройка роутинга; после старта приложение регистрируется в Consul:
object HelidonServiceApplication : KoinComponent {
@JvmStatic
fun main(args: Array<String>) {
val startTime = System.currentTimeMillis()
startKoin {
modules(koinModule)
}
val applicationInfoService: ApplicationInfoService by inject()
val consulClient: Consul by inject()
val applicationInfoProperties: ApplicationInfoProperties by inject()
val serviceName = applicationInfoProperties.name
startServer(applicationInfoService, consulClient, serviceName, startTime)
}
}
fun startServer(
applicationInfoService: ApplicationInfoService,
consulClient: Consul,
serviceName: String,
startTime: Long
): WebServer {
val serverConfig = ServerConfiguration.create(Config.create().get("webserver"))
val server: WebServer = WebServer
.builder(createRouting(applicationInfoService))
.config(serverConfig)
.build()
server.start().thenAccept { ws ->
val durationInMillis = System.currentTimeMillis() - startTime
log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port())
// register in Consul
consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port()))
}
return server
}
Роутинг настраивается следующим образом:
private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder()
.register(JacksonSupport.create())
.get("/application-info", Handler { req, res ->
val requestTo: String? = req.queryParams()
.first("request-to")
.orElse(null)
res
.status(Http.ResponseStatus.create(200))
.send(applicationInfoService.get(requestTo))
})
.get("/application-info/logo", Handler { req, res ->
res.headers().contentType(MediaType.create("image", "png"))
res
.status(Http.ResponseStatus.create(200))
.send(applicationInfoService.getLogo())
})
.error(NotFoundException::class.java) { req, res, ex ->
log.error("NotFoundException:", ex)
res.status(Http.Status.BAD_REQUEST_400).send()
}
.error(Exception::class.java) { req, res, ex ->
log.error("Exception:", ex)
res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send()
}
.build()
В приложении используется конфиг в формате HOCON [9]:
webserver {
port: 8081
}
application-info {
name: "helidon-service"
framework {
name: "Helidon SE"
release-year: 2019
}
}
Для конфигурирования возможно также использовать файлы в форматах JSON, YAML и properties (подробнее здесь [10]).
Фреймворк написан на Kotlin. Новый проект можно создать несколькими способами: используя систему сборки, start.ktor.io [11]или плагин к IntelliJ IDEA (подробнее здесь [12]).
Как и в Helidon SE, в Ktor отсутствует DI “из коробки”, поэтому перед стартом сервера с помощью Koin осуществляется внедрение зависимостей:
val koinModule = module {
single { ApplicationInfoService(get(), get()) }
single { ApplicationInfoProperties() }
single { MicronautServiceClient(get()) }
single { Consul.builder().withUrl("http://localhost:8500").build() }
}
fun main(args: Array<String>) {
startKoin {
modules(koinModule)
}
val server = embeddedServer(Netty, commandLineEnvironment(args))
server.start(wait = true)
}
Необходимые приложению модули указываются в конфигурационном файле (возможно использование только формата HOCON; подробнее о конфигурировании Ktor-сервера здесь [13]), содержимое которого представлено ниже:
ktor {
deployment {
host = localhost
port = 8082
watch = [io.heterogeneousmicroservices.ktorservice]
}
application {
modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module]
}
}
application-info {
name: "ktor-service"
framework {
name: "Ktor"
release-year: 2018
}
В Ktor и Koin используется термин “модуль”, обладающий при этом разными значениями. В Koin модуль — это аналог контекста приложения в Spring Framework. Модуль Ktor — это определённая пользователем функция, которая принимает объект типа Application и может осуществлять конфигурирование пайплайна, установку фич (features), регистрацию роутов, обработку
запросов и т. д.:
fun Application.module() {
val applicationInfoService: ApplicationInfoService by inject()
if (!isTest()) {
val consulClient: Consul by inject()
registerInConsul(applicationInfoService.get(null).name, consulClient)
}
install(DefaultHeaders)
install(Compression)
install(CallLogging)
install(ContentNegotiation) {
jackson {}
}
routing {
route("application-info") {
get {
val requestTo: String? = call.parameters["request-to"]
call.respond(applicationInfoService.get(requestTo))
}
static {
resource("/logo", "logo.png")
}
}
}
}
В этом фрагменте кода настраивается роутинг запросов, в частности, статический ресурс logo.png. Ktor-сервис может содержать фичи. Фича — это функциональность, встраиваемая в пайплайн запрос-ответ (DefaultHeaders, Compression и другие в примере кода выше). Возможна реализация собственных фич, например, ниже приведён код, имплементирующий паттерн Service Discovery в сочетании с клиентской балансировкой нагрузки на основе алгоритма Round-robin:
class ConsulFeature(private val consulClient: Consul) {
class Config {
lateinit var consulClient: Consul
}
companion object Feature : HttpClientFeature<Config, ConsulFeature> {
var serviceInstanceIndex: Int = 0
override val key = AttributeKey<ConsulFeature>("ConsulFeature")
override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient)
override fun install(feature: ConsulFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.Render) {
val serviceName = context.url.host
val serviceInstances =
feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response
val selectedInstance = serviceInstances[serviceInstanceIndex]
context.url.apply {
host = selectedInstance.service.address
port = selectedInstance.service.port
}
serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size
}
}
}
}
Основная логика находится в методе install: во время фазы запроса Render (которая выполняется перед фазой Send) сначала определяется название вызываемого сервиса, далее у consulClient запрашивается список инстансов этого сервиса, после чего вызывается инстанс, определённый с помощью алгоритма Round-robin. Таким образом становится возможным следующий вызов:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {
httpClient.get<ApplicationInfo>("http://$serviceName/application-info")
}
Micronaut разрабатывается создателями фреймворка Grails [14] и вдохновлён опытом построения сервисов с использованием Spring, Spring Boot и Grails. Фреймворк является полиглотом, поддерживая языки Java, Kotlin и Groovy; возможно [15], будет поддержка Scala. Внедрение зависимостей в Micronaut осуществляется на этапе компиляции, что приводит к меньшему потреблению памяти и более быстрому запуску приложения по сравнению со Spring Boot.
Main-класс имеет следующий вид:
object MicronautServiceApplication {
@JvmStatic
fun main(args: Array<String>) {
Micronaut.build()
.packages("io.heterogeneousmicroservices.micronautservice")
.mainClass(MicronautServiceApplication.javaClass)
.start()
}
}
Некоторые компоненты приложения на основе Micronaut похожи на свои аналоги в приложении на Spring Boot, например, ниже приведён код контроллера:
@Controller(
value = "/application-info",
consumes = [MediaType.APPLICATION_JSON],
produces = [MediaType.APPLICATION_JSON]
)
class ApplicationInfoController(
private val applicationInfoService: ApplicationInfoService
) {
@Get
fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)
@Get("/logo", produces = [MediaType.IMAGE_PNG])
fun getLogo(): ByteArray = applicationInfoService.getLogo()
}
Поддержка Kotlin в Micronaut реализована на основе плагина компилятора kapt [16] (подробнее здесь [17]). Сборочный скрипт при этом конфигурируется так:
plugins {
...
kotlin("kapt")
...
}
dependencies {
kapt("io.micronaut:micronaut-inject-java")
...
kaptTest("io.micronaut:micronaut-inject-java")
...
}
Далее показано содержимое конфигурационного файла:
micronaut:
application:
name: micronaut-service
server:
port: 8083
consul:
client:
registration:
enabled: true
application-info:
name: ${micronaut.application.name}
framework:
name: Micronaut
release-year: 2018
Конфигурирование микросервиса возможно также файлами форматов JSON, properties и Groovy (подробнее здесь [18]).
Фреймворк был создан с целью упростить разработку приложений, использующих экосистему Spring Framework. Это достигается посредством механизмов автоконфигурации при подключении библиотек. Ниже приведён код контроллера:
@RestController
@RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE])
class ApplicationInfoController(
private val applicationInfoService: ApplicationInfoService
) {
@GetMapping
fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)
@GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE])
fun getLogo(): ByteArray = applicationInfoService.getLogo()
}
Микросервис конфигурируется файлом формата YAML:
spring:
application:
name: spring-boot-service
server:
port: 8084
application-info:
name: ${spring.application.name}
framework:
name: Spring Boot
release-year: 2014
Также для конфигурирования возможно использовать файлы формата properties (подробнее здесь [19]).
Проект работает на JDK 12, хотя, вероятно, и на 11-й версии тоже, требуется только соответствующим образом поменять в сборочных скриптах параметр jvmTarget:
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "12"
...
}
}
Перед запуском микросервисов нужно установить [20] Consul и запустить [21] агент — например, так: consul agent -dev.
Запуск микросервисов возможен из:
java -jar helidon-service/build/libs/helidon-service-all.jar
java -jar ktor-service/build/libs/ktor-service-all.jar
java -jar micronaut-service/build/libs/micronaut-service-all.jar
java -jar spring-boot-service/build/libs/spring-boot-service.jar
После старта всех микросервисов на http://localhost:8500/ui/dc1/services вы увидите:

В качестве примера приведены результаты тестирования API Helidon service:
GET http://localhost:8081/application-info
{
"name": "helidon-service",
"framework": {
"name": "Helidon SE",
"releaseYear": 2019
},
"requestedService": null
}
GET http://localhost:8081/application-info?requestTo=ktor-service
{
"name": "helidon-service",
"framework": {
"name": "Helidon SE",
"releaseYear": 2019
},
"requestedService": {
"name": "ktor-service",
"framework": {
"name": "Ktor",
"releaseYear": 2018
},
"requestedService": null
}
}
GET http://localhost:8081/application-info/logoПротестировать API произвольного микросервиса можно с помощью Postman [22] (коллекция [23] запросов), IntelliJ IDEA HTTP client [24] (коллекция [25] запросов), браузера или другого инструмента. В случае использования первых двух клиентов требуется указать порт вызываемого микросервиса в соответствующей переменной (в Postman она находится в меню коллекции -> Edit -> Variables, а в HTTP Client — в переменной среды, указываемой в этом [26] файле), а при тестировании метода 2) API также нужно указать название запрашиваемого “под капотом” микросервиса. Ответы при этом будут аналогичны приведённым выше.
Размер артефакта
C целью сохранения простоты настройки и запуска приложений в сборочных скриптах не были исключены какие-либо транзитивные зависимости, поэтому размер uber-JAR сервиса на Spring Boot значительно превышает размеры аналогов на других фреймворках (т. к. при использовании стартеров импортируются не только нужные зависимости; при желании размер можно существенно уменьшить):
| Микросервис | Размер артефакта, Мбайт |
|---|---|
| Helidon service | 16,6 |
| Ktor service | 20,9 |
| Micronaut service | 16,5 |
| Spring Boot service | 42,7 |
Время запуска
Время запуска каждого приложения непостоянно и попадает в некоторое “окно”; в таблице ниже приведено время запуска артефакта без указания каких-либо дополнительных параметров:
| Микросервис | Время запуска, секунды |
|---|---|
| Helidon service | 2,2 |
| Ktor service | 1,4 |
| Micronaut service | 4,0 |
| Spring Boot service | 10,2 |
Стоит отметить, что если “почистить” приложение на Spring Boot от ненужных зависимостей и уделить внимание настройке запуска приложения (например, сканировать только нужные пакеты и использовать ленивую инициализацию бинов), то можно значительно сократить время запуска.
Нагрузочное тестирование
Для проведения тестирования были использованы Gatling [27] и скрипт [28] на Scala. Генератор нагрузки и тестируемый сервис были запущены на одной машине (Windows 10, четырёхъядерный процессор 3,2 ГГц, 24 Гбайт RAM, SSD). Порт этого сервиса указывается в Scala-скрипте.
Для каждого микросервиса определяется:
-Xmx), необходимый для запуска работоспособного (отвечающего на запросы) микросервисаПод прохождением нагрузочного теста понимается то, что микросервис ответил на все запросы за любое время.
| Микросервис | Минимальный объём heap-памяти, Мбайт | ||
|---|---|---|---|
| Для запуска сервиса | Для нагрузки 50 * 1000 |
Для нагрузки 500 * 1000 |
|
| Helidon service | 9 | 9 | 11 |
| Ktor service | 11 | 11 | 13 |
| Micronaut service | 13 | 13 | 17 |
| Spring Boot service | 22 | 23 | 25 |
Стоит заметить, что все микросервисы используют HTTP-сервер Netty.
Поставленную задачу — создание простого сервиса с HTTP API и возможностью функционировать в МСА — удалось выполнить на всех рассматриваемых фреймворках. Пришло время подвести итоги и рассмотреть их плюсы и минусы.
Helidоn
Standard Edition
@JvmStatic — для интеропа Java-Kotlin).
MicroProfile
Микросервис на этом фреймворке реализован не был, поэтому отмечу лишь пару известных мне пунктов:
Ktor
Позволяет подключать только те функции, которые непосредственно нужны для выполнения поставленной задачи;
С другой, непохожесть на “классические” Spring и Java EE позволяет взглянуть на процесс разработки под другим углом, возможно, более осознанно.
Micronaut
Spring Boot
Также можно выделить общие проблемы, связанные с новыми фреймворками и отсутствующие у Spring Boot:
Рассмотренные фреймворки принадлежат к разным весовым категориям: Helidon SE и Ktor — это микрофреймворки [33], Spring Boot — full-stack фреймворк, Micronaut, скорее, тоже full-stack; ещё одна категория — MicroProfile (например, Helidon MP). В микрофреймворках функциональность ограничена, что может замедлить выполнение задач; для уточнения возможности реализации той или иной функциональности на основе какого-либо каркаса разработки рекомендую ознакомиться с его документацией.
Не берусь судить о том, “выстрелит” ли тот или иной фреймворк в ближайшем будущем, поэтому, на мой взгляд, пока лучше продолжить наблюдать за развитием событий, используя имеющийся каркас разработки для решения рабочих задач.
В то же время, как было показано в статье, новые фреймворки выигрывают у Spring Boot по рассмотренным параметрам полученных приложений. Если для какого-то из ваших микросервисов критически важны какие-либо из этих параметров, то, возможно, стоит обратить внимание на фреймворки, показавшие по ним лучшие результаты. Однако, не стоит забывать, что Spring Boot, во-первых, продолжает совершенствоваться, во-вторых, имеет огромную экосистему и с ним знакомы значительное количество Java-программистов. Есть и другие фреймворки, не освещённые в настоящей статье: Javalin, Quarkus и т. д.
С кодом проекта вы можете ознакомиться на GitHub [34]. Благодарю за внимание!
P.S.: Спасибо artglorin [35] за помощь в подготовке статьи.
Автор: Roman Kudryashov
Источник [36]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/321180
Ссылки в тексте:
[1] Helidon SE: https://helidon.io
[2] Ktor: https://ktor.io
[3] Micronaut: https://micronaut.io/
[4] Spring Boot: https://spring.io/projects/spring-boot
[5] Consul: https://www.consul.io/
[6] этой : https://github.com/oracle/helidon/wiki/FAQ
[7] MicroProfile: https://microprofile.io/
[8] Koin: https://insert-koin.io/
[9] HOCON: https://en.wikipedia.org/wiki/HOCON
[10] здесь: https://helidon.io/docs/latest/#/config/01_introduction
[11] start.ktor.io : https://start.ktor.io/
[12] здесь: https://ktor.io/quickstart/#set-up-a-ktor-project
[13] здесь: https://ktor.io/servers/configuration.html
[14] Grails: https://grails.org/
[15] возможно: https://github.com/micronaut-projects/micronaut-core/issues/675
[16] kapt: https://kotlinlang.org/docs/reference/kapt.html
[17] здесь: https://docs.micronaut.io/latest/guide/index.html#kotlin
[18] здесь: https://docs.micronaut.io/latest/guide/index.html#config
[19] здесь: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html
[20] установить: https://www.consul.io/docs/install/index.html
[21] запустить: https://learn.hashicorp.com/consul/getting-started/agent
[22] Postman: https://www.getpostman.com/
[23] коллекция: https://github.com/rkudryashov/heterogeneous-microservices/blob/master/_misc/api_testing/postman/Test.postman_collection.json
[24] HTTP client: https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html
[25] коллекция: https://github.com/rkudryashov/heterogeneous-microservices/blob/master/_misc/api_testing/idea_http_client/http-request.http
[26] этом: https://github.com/rkudryashov/heterogeneous-microservices/blob/master/_misc/api_testing/idea_http_client/http-client.env.json
[27] Gatling: https://gatling.io/
[28] скрипт: https://github.com/rkudryashov/heterogeneous-microservices/blob/master/_misc/load_testing/load-test.scala
[29] MicroProfile Starter: https://start.microprofile.io/
[30] Micronaut for Spring: https://micronaut-projects.github.io/micronaut-spring/latest/guide/index.html
[31] TIOBE: https://www.tiobe.com/tiobe-index/
[32] Spring Fu: https://github.com/spring-projects/spring-fu
[33] микрофреймворки: https://en.wikipedia.org/wiki/Microframework
[34] GitHub: https://github.com/rkudryashov/heterogeneous-microservices
[35] artglorin: https://habr.com/en/users/artglorin/
[36] Источник: https://habr.com/ru/post/456376/?utm_source=habrahabr&utm_medium=rss&utm_campaign=456376
Нажмите здесь для печати.