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

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

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 [1], на котором он базирован
  • Однопоточная модель, которая упрощает подход к разработке
  • Разделение приложения на минимальные ячейки, называемые "вертиклами"
  • Распределенная шина событий позволяющая отдельным вертиклам общаться друг с другом, не смотря на язык, на котором они написаны

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

Задача

Допустим нам нужен веб-сервис, который будет возвращать список островов (например, Котлин [3]) и стран, в которых эти острова находятся, в формате 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 [4]" здесь используем для того, чтобы создаваемый 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 документация [5], и радоваться результату.

Заключение

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

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

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

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

Ссылки

Автор: mgouline

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/245678

Ссылки в тексте:

[1] Netty: http://netty.io/

[2] статья: https://habrahabr.ru/post/276771/

[3] Котлин: https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D1%82%D0%BB%D0%B8%D0%BD

[4] shadow: https://github.com/johnrengelman/shadow

[5] Heroku документация: https://devcenter.heroku.com/articles/git

[6] vertx-kotlin-example: https://github.com/mgouline/vertx-kotlin-example

[7] vertx-examples: https://github.com/vert-x3/vertx-examples

[8] Vert.x Core Manual: http://vertx.io/docs/vertx-core/java/

[9] Vert.x-Web: http://vertx.io/docs/vertx-web/java/

[10] What's New in Kotlin 1.1: https://kotlinlang.org/docs/reference/whatsnew11.html

[11] Источник: https://habrahabr.ru/post/322406/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best