Введение в 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
 
Шаги по созданию проекта с помощью генератора #
- Project core
- Kind of app writing: Server
 - Server-side WebSocket: No
 - Server engine: Undertow
 - HTTP client library: OkHttp
 
 - Functional modules
- JSON serialization library: Jackson
 - Templating library: Pebble
 - Other messaging formats: None
 - Inetgrations: OpenApi3
 
 - Testing: Kotest
 - Application identity:
 - Build tooling:
- Build tool: Gradle
 - Packaging type: zip
 
 
Созданный проект: https://toolbox.http4k.org/stack/dD1BQU1BWlFES0FTOEI5Z0pjQXlVRGhBUG9BLWtFc0FVWCZjPVdlYkFwcGxpY2F0aW9uJnA9cnUuYWMudW5peWFy
Важные особенности проекта Gradle #
gradle.properties
junitVersion=5.8.2
http4kVersion=4.20.2.0
kotlinVersion=1.6.10build.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-сервера необходимо:
- Создать обработчик HTTP-запросов
 - Связать обработчик с 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") },
)- Описание маршрута с помощью шаблонной строки
 - Вызов инфиксного метода 
String.bind - Указание названия HTTP-метода, который надо обработать
 - Вызов инфиксного метода 
PathMethod.to - Передача обработчика 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.pathfun 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 #
Для использования шаблонизатора необходимо:
- Подключить соответствующую библиотеку к проекту в build.gradle
 - Создать объект шаблонизатора
 - Для каждой отдельной страницы
- Создать модель для отображения данных
 - Создать файл шаблона в ресурсах, преобразующий данные в текст
 
 
Варианты обновления шаблонов #
- Активное кеширование шаблона из ресурсов 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-запросов
 - Реализация классов предметной области
 - Шаблонизаторы
 
Все компоненты соединяются между собой внутри маршрутизатора