Введение в 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: Multipart formats
- Inetgrations: None
- Testing: Kotest
- Application identity
- Main class name: WebApplication
- Base package name: ru.ac.uniyar
- Build tooling:
- Build tool: Gradle
- 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-сервера необходимо:
- Создать обработчик 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.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
Принципиальная схема работы шаблонизатора
- Шаблонизатор — это компонент, который формирует текстовый документ на основании шаблона и данных для отображения этого шаблона
- Шаблон — это размеченный специальным образом текст, в котором указаны места вставки данных
- Шаблон может быть как одним файлом, так и включать в себя другие файлы
Использование шаблонизатора в 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-формы
Отображение форм #
Формы являются частью языка 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-запросов
- Реализация классов предметной области
- Шаблонизаторы
Все компоненты соединяются между собой внутри маршрутизатора