Асинхронная альтернатива для Kotlin в лице Vert.x

в 12:35, , рубрики: java, kotlin, rest, vertx, web-разработка, Разработка веб-сайтов

Kotlin — популярный инструмент у разработчиков на Android, но, как известно, это не единственное ему применение. Поэтому когда я решился написать простой веб-сервис, показалось разумным сделать это как раз на Kotlin.

Оказывается, Spring Framework — это не единственный вариант. Существует еще одна мощная асинхронная альтернатива — Vert.x, которая почему-то редко упоминается в контексте Kotlin. Об этом тандеме и поговорим в этой статье.

Vert.x + Kotlin

Мотивация

Начиная проект, хотелось невозможного: и прототип написать быстро, и хостить просто на каком-нибудь Heroku, и при надобности расширить прототип до полноценного проекта не переписывая с нуля.

Официальная документация и примеры от добрых блоггеров в один голос рекомендовали Spring Framework, ссылаясь на хорошую совместимость и даже родную поддержку для Kotlin в будущей версии. Но если так подумать, нужна ли какая-то особая совместимость? Язык и так дружит с Java, поэтому выбираешь любой фреймворк, импортируешь стандартную библиотеку и вперед.

Что такое Vert.x?

Vert.x — это асинхронный событийно-ориентированный фреймворк для любых приложений, с модулем для веб. Архитектура схожа с Node.js, настолько, что проект даже начал свое существование в 2011 году под названием "Node.x", а уж потом создатель Тим Фокс посчитал это рисковым и вспомнил другой синоним к слову "node" ("node" и "vertex" — это "узел" в теории графов). В отличие от Node.js, который ограничен на JavaScript, Vert.x поддерживает еще и Java, Groovy, Ruby и Ceylon (в прошлом так же поддерживал Python, Scala и Clojure).

Меня заинтересовали следующие параметры Vert.x:

  • Производительность и асинхронность, в немалой мере благодаря Netty, на котором он базирован
  • Однопоточная модель, которая упрощает подход к разработке
  • Разделение приложения на минимальные ячейки, называемые "вертиклами"
  • Распределенная шина событий позволяющая отдельным вертиклам общаться друг с другом, не смотря на язык, на котором они написаны

На этом завершу описание самого фреймворка, ибо на этом сайте уже была хорошая статья про это. Моя задача показать, как можно использовать все эти удобства в Kotlin.

Задача

Допустим нам нужен веб-сервис, который будет возвращать список островов (например, Котлин) и стран, в которых эти острова находятся, в формате JSON по модели REST.

  • GET /islands
    • Список всех островов и стран
  • GET /countries
    • Список всех стран, в которых есть острова
  • GET /countries/:code
    • Страна по ее ISO 3166 коду

Соглашусь, не особенно полезный веб-сервис, но этого вполне достаточно для демонстрации фреймворка, избегая лишние подробности проекта и других библиотек, которые только отвлекают от основной темы.

Данные

Начнем с данных, которые веб-сервис будет возвращать. Модели нужны всего две: Island и Country.

data class Island(val name: String, val country: Country)

data class Country(val name: String, val code: String)

Благодаря дата классам в Kotlin, больше ни о чем волноваться не надо — методы equals(), hashCode(), геттеры и сеттеры все автоматически зашиты в эту простую конструкцию.

Дальше IslandDao для доступа к данным: в реальном приложении здесь будут запросы в некую базу данных, а у нас простой статичный массив с заготовленными островами.

class IslandsDao {

    companion object {
        private val MOCK_ISLANDS by lazy {
            listOf(
                    Island("Kotlin", Country("Russia", "RU")),
                    Island("Stewart Island", Country("New Zealand", "NZ")),
                    Island("Cockatoo Island", Country("Australia", "AU")),
                    Island("Tasmania", Country("Australia", "AU"))
            )
        }
    }

    fun fetchIslands() = MOCK_ISLANDS

    fun fetchCountries(code: String? = null) =
            MOCK_ISLANDS.map { it.country }
                    .distinct()
                    .filter { code == null || it.code.equals(code, true) }
                    .sortedBy { it.code }

}

Краткий обзор методов:

  • fetchIslands() возвращает весь список островов с их странами
  • fetchCountries(code)
    • map — вытаскивает страны из списка островов
    • distinct — отметает повторные (Австралию)
    • filter — фильтрует по заданному коду (если таковой присутствует)
    • sortedBy — сортирует по кодам

Такого минимального DAO достаточно, чтобы переходить к самому приложению.

Вертикл

Сердце Vert.x приложения — это сами вертиклы. У меня фантазия плохая, поэтому назовем его "MainVerticle".

class MainVerticle : AbstractVerticle()

Начнем с того, что создадим в нем поле для DAO, который уже написали выше.

private val dao = IslandsDao()

Теперь важная часть: маршрутизатор, который будет распределять запросы по типу и пути. Для начала разберем самый простой маршрут.

private val router = Router.router(vertx).apply {
        get("/").handler { ctx ->
            ctx.response().end("Welcome!")
        }
    }

Это рутовый GET маршрут, который возвращает обычный текст "Welcome!".

Но зачем нам текст? Нам бы лучше JSON сериализацию объектов. Для этого в утилях пишем расширение endWithJson(Any), которое заканчивает цепь запроса, только предварительно заполнив заголовок "Content-Type" с JSON форматом и сериализовав любой объект, который ему передали.

fun HttpServerResponse.endWithJson(obj: Any) {
    putHeader("Content-Type", "application/json; charset=utf-8").end(Json.encodePrettily(obj))
}

Теперь можно добавить в маршрутизатор еще пару маршрутов, которые возьмут списки данных из DAO и вернут их в виде JSON.

get("/islands").handler { ctx ->
    val islands = dao.fetchIslands()
    ctx.response().endWithJson(islands)
}

get("/countries").handler { ctx ->
    val countries = dao.fetchCountries()
    ctx.response().endWithJson(countries)
}

Уже интереснее и полезнее, не так ли?

Из поставленной задачи, остался только маршрут для поиска стран по коду.

get("/countries/:code").handler { ctx ->
    val code = ctx.request().getParam("code")
    val countries = dao.fetchCountries(code)

    if (countries.isEmpty()) {
        ctx.fail(404)
    } else {
        ctx.response().endWithJson(countries.first())
    }
}

Все почти так же, как и в предыдущих, только добавился параметр :code к самому пути (который можно извлекать с помощью HttpServerRequest.getParam(String)) и, вдобавок к успешному end(), появился еще и fail() с HTTP кодом ошибки на случай не найденной страны.

Итак, маршрутизатор готов. Осталось только собрать сам сервер. Звучит, признаться, намного грандиознее, чем на самом деле.

В абстрактном классе AbstractVerticle есть метод start(), который вызывается при запуске вертикла. Процедуру запуска веб-сервера помещаем как раз туда.

override fun start(startFuture: Future<Void>?) {
    vertx.createHttpServer()
            .requestHandler { router.accept(it) }
            .listen(Integer.getInteger("http.port", 8080)) { result ->
                if (result.succeeded()) {
                    startFuture?.complete()
                } else {
                    startFuture?.fail(result.cause())
                }
            }
}

Код выше делает следующее:

  1. Создает новый HTTP сервер
  2. Передает запросы нашему маршрутизатору
  3. Слушает запросы через порт, который задается в параметрах (или 8080 по умолчанию)

На этом код самого приложения завершен, теперь магия конфигурации!

Конфигурация

Внимание! В объяснении ниже, я притворюсь, что версия Kotlin 1.1 уже вышла в продакшн, чтобы не засорять код конфигурацией EAP репозитория (где живут не выпущенные версии языка). Ждать осталось уже не долго — на момент написания, 1.1 уже в стадии Release Candidate. В проекте на GitHub все, конечно же, в рабочем состоянии, без всяких упрощений.

Конфигурация будет жить в Gradle скрипте "build.gradle"

buildscript {
    ext {
        kotlin_version = '1.1.0'
        vertx_version = '3.3.3'
    }

    repositories {
        jcenter()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

Сначала buildscript часть, где задаем версии и плагины (в данном случае только один).

plugins {
    id 'java'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '1.2.4'
}

apply plugin: 'kotlin'

Далее применяем заданные и встроенные плагины.

Первые два, "java" и "application", нужны как скелет Java приложения, на основе которого мы все строим.

Заданный выше "kotlin" — это все, что нужно с точки зрения настройки Kotlin приложения.

Плагин "shadow" здесь используем для того, чтобы создаваемый JAR был "толстым" ("fat jar"), то есть, содержал в себе все используемые библиотеки. Это намного упрощает деплой, но для этого нам понадобится его еще и настроить.

shadowJar {
    baseName = 'app'
    classifier = 'shadow'

    manifest {
        attributes 'Main-Verticle': 'net.gouline.vertxexample.MainVerticle'
    }
    mergeServiceFiles {
        include 'META-INF/services/io.vertx.core.spi.VerticleFactory'
    }
}

Первые два поля "baseName" и "classifier" указывают, как должен называться JAR на выходе (т.е. "app-shadow.jar"), чтобы деплой скрипту можно было легко его найти. Помимо этого настраиваем путь к вертиклу, написанному раннее, и к стандартному VerticleFactory.

repositories {
    jcenter()
}

dependencies {
    compile "io.vertx:vertx-core:$vertx_version"
    compile "io.vertx:vertx-web:$vertx_version"

    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
}

Теперь применяем требуемые библиотеки, в данном случае нам хватит всего трех:

  • vertx-core — основное ядро Vert.x
  • vertx-web — дополнения к Vert.x для работы с веб
  • kotlin-stdlib-jre8 — стандартная библиотека Kotlin (для JRE 8)

sourceCompatibility = '1.8'
mainClassName = 'io.vertx.core.Launcher'

Наконец, устанавливаем совместимость исходника на Java 8 (это минимум для Vert.x) и главный класс при запуске, которым будет встроенный Launcher.

Все, конфигурация готова!

Сборка и хостинг

Сборка на локальном компьютере очень проста: gradle run для запуска на localhost или gradle shadowJar для экспорта JAR файла, который можно залить на веб-сервер.

Но, как я упомянул в самом начале, хотелось бы, чтобы все работало еще и на Heroku. Для этого достаточно создать "Procfile" следующего содержания:

web: java $JAVA_OPTS -Dhttp.port=$PORT -jar build/libs/app-shadow.jar

Эта строчка описывает, как следует запускать приложение: через java, задавая номер порта (который решается самим Heroku) и, наконец, тот самый "app-shadow.jar", который мы прописали в "build.gradle".

Вот и все! Теперь это приложение можно целиком заливать в Git ремоут, как описывает Heroku документация, и радоваться результату.

Заключение

Надеюсь, я убедил кого-то попробовать Kotlin, Vert.x или оба вместе. Документации (официальной и любительской) для обоих проектов предостаточно, так что разобраться, как написать более сложное приложение, не должно составить особого труда.

Хоть в документации Vert.x и нет раздела для Kotlin, он пользуется API для Java, поэтому функции одного языка достаточно тривиально переводятся в другой. Более того, при копировании примеров на Java в Kotlin класс, IntelliJ IDEA сам предложит конвертировать код автоматически.

Полную версию проекта можно найти в "vertx-kotlin-example" на GitHub, которую я поддерживаю со всеми обновлениями и некоторыми расширениями. Эта версия легко запускается после скачки и даже деплоится в Heroku.

Спасибо за внимание!

Ссылки

Автор: mgouline

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js