Введение в http4k

Введение в http4k #

Васильев Андрей Михайлович, 2022

Версии презентации


Обзор библиотеки http4k #

http4k — это набор инструментов для создания серверных и клиентских HTTP-приложений

  • Предоставляет функциональный интерфейс для решения задач
  • Не реализует самостоятельно функции сервера и клиента, а использует промышленные технологии для реализации нужных функций
  • Включает средства для решения следующих задач:
    • Обработка HTTP-запросов
    • Взаимодействие с помощью WebSocket
    • Запуск с собственным сервером, без сервера, встраивается в Jakarta
    • Реализация контрактов OpenApi 3 (Swagger)
    • Работа со множеством шаблонизаторов
    • Поддержка работы со множеством типов данных: JSON, XML, YAML
    • Поддержка всех видов тестирования веб-приложений

Ключевые концепции http4k #

  • Основано на базовых концепциях функциональных языков программирования
  • Вся логика по обработке HTTP-запросов описывается одной функцией
  • Не используются техники метапрограммирования для решения задач
  • Минимальный объём зависимостей
  • Поддержка разработки приложения через написание тестов без сложной инфраструктуры
  • Быстрый запуск и остановка приложений
  • Предоставляет средства строгой типизации для обработки HTTP-сообщений
  • Поддержка контрактов: генерация документации OpenApi, логичное поведение «по умолчанию»

Ключевые типы http4k #

Библиотека http4k предоставляет набор функциональных типов, которые позволяют создавать, тестировать и разворачивать HTTP-приложения

HttpMessage #

Данная структура является неизменяемой и описывает запросы и ответы

  • Неизменяемые структуры позволяют избегать множества ошибок
  • Цепочка формирования структур позволяет отследить изменения в приложении
  • Позволяет удобно тестировать логику работы приложения

Тип HttpHandler #

typealias HttpHandler = (Request) -> Response

Данная функция описывает все входящие и исходящие HTTP-запросы

Запуск сервера #

Функция может быть связана с сервером с помощью 1 строки кода. Это позволяет отделить бизнес-логику от реализации сервера:

val app: HttpHandler = // ...
val jettyServer = app.asServer(Jetty(9000)).start()

Разработка клиента #

HTTP-клиент тоже предоставляет тип HttpHandler

val client: HttpHandler = ApacheClient()

Тип Filter #

fun interface Filter : (HttpHandler) -> HttpHandler
  • Данная функция декорирует HttpHandler, чтобы выполнить обработку запроса до или после оригинального обработчика запросов
  • Фильтры могут составлены в цепочки, чтобы обеспечить нужную обработку

Тип Router #

interface Router {
    fun match(request: Request): RouterMatch
}

Обработчик запроса, который пытается сопоставить входящий запрос с соответствующим обработчиком запроса HttpHandler


Создание проекта с помощью генератора #

Проект создать можно с помощью:

  • Инструмента командного интерфейса http4k
  • С помощью генератора проектов https://toolbox.http4k.org/project
  • Создать проект вручную или с помощью генератора gradle

Шаги по созданию проекта с помощью генератора #

  1. Project core
    1. Kind of app writing: Server
    2. Server-side WebSocket: No
    3. Server engine: Undertow
    4. HTTP client library: OkHttp
  2. Functional modules
    1. JSON serialization library: Jackson
    2. Templating library: Pebble
    3. Other messaging formats: Multipart formats
    4. Inetgrations: None
  3. Testing: Kotest
  4. Application identity
    1. Main class name: WebApplication
    2. Base package name: ru.ac.uniyar
  5. Build tooling:
    1. Build tool: Gradle
    2. Packaging type: zip

Созданный проект: https://toolbox.http4k.org/stack/dD1BQU1BWlFES0FTOEJrQUgyQWx3REpRUG9BLWtFc0FVWCZjPVdlYkFwcGxpY2F0aW9uJnA9cnUuYWMudW5peWFy


Важные особенности проекта Gradle #

gradle.properties

junitVersion=5.9.0
http4kVersion=4.30.9.0
kotlinVersion=1.7.10

build.gradle

apply plugin: 'application'
mainClassName = "uniyar.ac.ru.WebApplicationKt"

dependencies {
    implementation "org.http4k:http4k-client-okhttp:${http4kVersion}"
    implementation "org.http4k:http4k-contract:${http4kVersion}"
    implementation "org.http4k:http4k-core:${http4kVersion}"
    implementation "org.http4k:http4k-format-jackson:${http4kVersion}"
    implementation "org.http4k:http4k-server-undertow:${http4kVersion}"
    implementation "org.http4k:http4k-template-pebble:${http4kVersion}"
}

Основа приложения #

Для работы HTTP-сервера необходимо:

  1. Создать обработчик HTTP-запросов
  2. Связать обработчик с HTTP-сервером
val app: HttpHandler = routes(
    "/ping" bind GET to {
        Response(OK).body("pong")
    },
)

val printingApp: HttpHandler = PrintRequest().then(app)
val server = printingApp.asServer(Undertow(9000)).start()

После этого приложение будет успешно обрабатывать запросы по пути http://localhost:9000/ping


Инфиксные функции в Kotlin #

Функция может быть помечена ключевым словом infix, если

  • Должны являться членом другой функции или расширением
  • Они используют только один параметр
  • Параметр не описывает произвольное количество агументов
  • Не должен иметь значения по умолчанию

Описание инфиксной функции и её использование

infix fun Int.shl(x: Int): Int { /*...*/ }
1 shl 2
1.shl(2)

Обязательно указывать как получателя, так и параметра


Описание маршрутизатора #

Основная задача сервера — обработка запросов от клиента. В рамках HTTP-протокола клиент формирует запрос к документу и указывает к нему путь.

При разработке сервера необходимо определить связь между маршрутом и обработчиком

Функция routes позволяет описать данную связь:

val handlerOne: HttpHandler = { Response(OK).body("First response") }
val handlerTwo: HttpHandler = { Response(OK).body("Second response") }
val app = routes (
    "first" bind GET to handlerOne,
    "second" bind GET to handlerTwo,
)
  • handler1 и handler2 являются обработчиками, HttpHandler
  • Функция routes сама возвращает обработчик RoutedHttpHandler

Данная конфигурация обрабатывает маршруты /first и /second


Описание связи маршрута и обработчика #

routes(
    "bob" bind GET to { Response(OK).body("you GET bob") },
    "rita" bind POST to { Response(OK).body("you POST rita") },
    "sue" bind DELETE to { Response(OK).body("you DELETE sue") },
)
  1. Описание маршрута с помощью шаблонной строки
  2. Вызов инфиксного метода String.bind
  3. Указание названия HTTP-метода, который надо обработать
  4. Вызов инфиксного метода PathMethod.to
  5. Передача обработчика HttpHandler в качестве обработчика маршрута

Поддерживаемые HTTP-методы #

http4k поддерживает следующие HTTP-методы: GET, POST, PUT, DELETE, OPTIONS, TRACE, PATCH, PURGE, HEAD. Методы описаны в перечислении org.http4k.core.Method


Динамические маршруты #

В рамках строки-шаблона могут содержаться переменные, которые можно использовать в рамках обработки запроса

routes (
    "/book/{title}" bind GET to { req ->
        Response.invoke(Status.OK).body(req.path("title").orEmpty())
    },
    "/author/{name}/latest" bind GET to { req ->
        Response.invoke(Status.OK).body(req.path("name").orEmpty())
    },
)
  • Шаблоны могут располагаться в любой части строки, их может быть несколько
  • Для получения переданных данных используется метод Request.path
    fun Request.path(name: String): String?
  • Метод String?.orEmpty() возвращает пустую строку, если ссылка содержит null

Написание обработчиков #

Обработчиком HTTP-запроса является является функциональный тип HttpHandler

Он может быть создан либо с использованием лямбда-выражения:

val handler: HttpHandler = { request: Request -> Response(OK) }

Либо с использованием класса, реализующего данный тип:

class SomeHandler : HttpHandler {
    override fun invoke(request: Request) : Response {...}
}
  • В любом случае входными данными является класс Request
  • Выходными данными является объект класса Response

Формирование ответа #

Для описания HTTP-ответа используется класс Response

  • Минимально необходимая информация — HTTP-статус ответа, который описывает класс Status, предоставляющий множество констант
  • Для установки других частей ответа предоставляются соответствующие функции:
    • body — установить новое тело, строку для ответа
    • header — установить новое значение для конкретного заголовка
    • headers — установить новое значение для набора заголовков

Методы класса Response не изменяют объект, но предоставляют его копию, у которого установлены новые значения. В результате каждый отдельный экземпляр класса Response является неизменяемым, но можно добиться создания произвольного ответа.


Статусы ответов #

OK, успешный результат #

  • HTTP код: 200
  • Назначение: устанавливается когда по запросу был найден документ

NOT_FOUND, документ не найден #

  • HTTP код: 404
  • Назначение: устанавливается, когда по запросу не был найден документ
  • Ответ приложения на библиотеке http4k по умолчанию

FOUND, перенаправление #

  • HTTP код: 302
  • Назначение: перенаправить к адресу документа после создания новой сущности

Тело ответа #

Тело ответа передаётся клиенту для дальнейшей обработки

  • Сформировать строку вручную
  • Использовать специальные подсистемы для формирования ответа
    • Предоставление структурированных документов JSON, XML, GraphQL и т.д.
    • Предоставление текстовых HTML-документов
    • Предоставление произвольных текстовых документов

Будем использовать шаблонизатор Pebble для формирования HTML-документов


Шаблонизаторы в http4k #

Библиотека http4k включает в себя поддержку большинства продуктовых шаблонизаторов, доступных для платформы JVM

Принципиальная схема работы шаблонизатора

flowchart LR renderer[Шаблонизатор] template{{Шаблон}} base_template{{Базовый шаблон}} partial{{Частичный шаблон}} data[Данные\nшаблонизатора] html{{HTML-документ}} base_template --> template partial --> template template --> renderer data --> renderer renderer --> html
  • Шаблонизатор — это компонент, который формирует текстовый документ на основании шаблона и данных для отображения этого шаблона
  • Шаблон — это размеченный специальным образом текст, в котором указаны места вставки данных
  • Шаблон может быть как одним файлом, так и включать в себя другие файлы

Использование шаблонизатора в http4k #

Для использования шаблонизатора необходимо:

  1. Подключить соответствующую библиотеку к проекту в build.gradle
  2. Создать объект шаблонизатора
  3. Для каждой отдельной страницы
    1. Создать модель для отображения данных
    2. Создать файл шаблона в ресурсах, преобразующий данные в текст

Варианты обновления шаблонов #

  • Активное кеширование шаблона из ресурсов JVM
  • Активное кеширование шаблона из файловой системы
  • Горячая перезагрузка шаблона из файловой системы

Последний вариант подходит для разработки, остальные — для продуктового использования


Связывание шаблона и объекта для отображения #

data class Person(val name: String, val age: Int) : ViewModel

fun main() {
    // Создать шаблонизатор, горячая перезагрузка из каталога
    val renderer = PebbleTemplates().HotReload("src/test/resources")
    val app: HttpHandler = {
        // Создать модель
        val viewModel = Person("Bob", 45)
        // Отобразить модель в шаблоне src/test/resources/Person.peb
        val renderedView = renderer(viewModel)
        Response(OK).body(renderedView)
    }
    println(app(Request(GET, "/someUrl")))
}

Раздача статических данных #

Для отображения HTML-документов необходимо предоставить пользователю возможность получить CSS и JavaScript-документы. Для этого используется функция static

Предположим, что базовым пакетом приложения является org.example, а в рамках ресурсов в данном пакете находится каталоги

  • public для хранения статических данных
  • models для хранения файлов шаблонов
routes(
    "/templates/pebble" bind GET to {
        val renderer = PebbleTemplates().HotReload("src/main/resources/")
        val viewModel = PebbleViewModel("Hello there!")
        val renderedView = renderer(viewModel)
        Response(OK).body(renderedView)
    },
    static(ResourceLoader.Classpath("/org/example/")),
)

Работа с простыми свойствами #

Предположим, что данные для страницы описываются следующей моделью

data class Event(
    val start: LocalDateTime,
    val description: String,
    ) : ViewModel

Для отображения этой информации на странице можно воспользоваться

<p>Событие начнётся в {{model.start}}</p>
<p>Описание события</p>
<p>{{model.description}}</p>

Все свойства класса-модели доступны в рамках шаблона, достаточно обратиться к свойствам по имени

Описание базового синтаксиса шаблонов Pebble доступно в официальной документации


Наследование шаблонов #

Шаблонизатор Pebble поддерживает концепцию иерархических шаблонов

  • Базовый шаблон включает в себя основное содержимое и точки расширения
  • Шаблон-расширение указывает содержимое для точек расширения

Базовый шаблон:

<div id="content">
    {% block content %} {% endblock %}
</div>
<div id="footer">
    {% block footer %} Default content {% endblock %}
</div>

Шаблон-расширение:

{% extends "./layout.peb" %}
{% block content %} Содержимое {% endblock %}

Передача списков данных в шаблоны #

Шаблонизаторы поддерживает работу и со списками данных

data class Event(val start: LocalDateTime, val description: String)
data class EventsList(val events: List<Event>)

Шаблон для отображения списка событий:

{% extends "./layout.peb" %}
{% block content %}
  {% for event in model.events %}
    <p>Начало события: {{ event.start }}</p>
    <p>Описание события:</p>
    <p>{{ event.description }}</p>
  {% endfor %}
{% endblock %}

HTML-формы #

Для данных от пользователя к веб-приложению используются HTML-формы

sequenceDiagram Клиент ->> Сервер: GET-запрос страницы с формой Сервер ->> Клиент: HTML-документ с формой Клиент ->> Сервер: POST-запрос с данными формы Сервер ->> Клиент: перенаправление на страницу с результатами Клиент ->> Сервер: GET-запрос на страницу с результатами

Отображение форм #

Формы являются частью языка HTML, для их описания используются следующие элементы:

  • Form — базовый контейнер для всех полей ввода, расположенных на форме
  • Input — элемент для описания интерактивных элементов
  • Textarea — элемент для ввода многострочного текста

CSS-фреймворк Zurb поддерживает стилизацию форм

<form method="POST">
    <input type="text" name="start">
    <textarea name="description">
    <input type="submit" value="Отправить">
</form>

Обработка данных от формы #

От клиента к серверу все данные передаются в виде строк, причём нет возможности для любого поля гарантировать, что оно будет заполнено

Рассмотрим обработку данных от формы на низком уровне

val parameters: Map<String, List<String?>> = request.formAsMap()
val firstValue: String? = parameters.findSingle("start")
  • Функция formAsMap возвращает Map<String, List<String?>>
  • Согласно стандарту HTML в рамках формы у одного параметра может быть несколько значений
  • Для поиска одного значения в данной структуре данных удобно воспользоваться функцией findSingle

Перенаправление HTTP-клиента #

HTML-формы зачастую направлены на изменение данных на стороне сервера:

  • Добавление новых элементов в коллекци
  • Редактирование существующих элементов коллекции
  • Удаление существующих элементов коллекций

Для этих случаев используется не-GET-запрос. На такие запросы HTTP-сервер должен вернуть Redirect-ответ с указанием, куда следует сделать GET-запрос

val strings =  mutableListOf<String>()
val formHandler: HttpHandler = { request ->
    val form = request.form()
    val newString = form.findSingle("text").orEmpty()
    strings.add(newString)
    Response(FOUND).header("Location", "/strings")
}

Архитектурный взгляд на приложение #

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

Компоненты приложения:

  • Маршрутизатор HTTP-запросов
  • Обработчики HTTP-запросов
  • Реализация классов предметной области
  • Шаблонизаторы

Все компоненты соединяются между собой внутри маршрутизатора

© A. M. Васильев, 2022, CC BY-SA 4.0, andrey@crafted.su