Васильев Андрей Михайлович, 2022
Версии презентации
http4k — это набор инструментов для создания серверных и клиентских HTTP-приложений
Библиотека http4k предоставляет набор функциональных типов, которые позволяют создавать, тестировать и разворачивать HTTP-приложения
Данная структура является неизменяемой и описывает запросы и ответы
typealias HttpHandler = (Request) -> Response
Данная функция описывает все входящие и исходящие HTTP-запросы
Функция может быть связана с сервером с помощью 1 строки кода. Это позволяет отделить бизнес-логику от реализации сервера:
val app: HttpHandler = // ...
val jettyServer = app.asServer(Jetty(9000)).start()
HTTP-клиент тоже предоставляет тип HttpHandler
val client: HttpHandler = ApacheClient()
fun interface Filter : (HttpHandler) -> HttpHandler
interface Router {
fun match(request: Request): RouterMatch
}
Обработчик запроса, который пытается сопоставить входящий запрос с соответствующим обработчиком запроса HttpHandler
Проект создать можно с помощью:
Созданный проект: https://toolbox.http4k.org/stack/dD1BQU1BWlFES0FTOEJrQUgyQWx3REpRUG9BLWtFc0FVWCZjPVdlYkFwcGxpY2F0aW9uJnA9cnUuYWMudW5peWFy
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-сервера необходимо:
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
Функция может быть помечена ключевым словом 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
являются обработчиками, HttpHandlerroutes
сама возвращает обработчик 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
PathMethod.to
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 {...}
}
Для описания HTTP-ответа используется класс Response
Status
, предоставляющий множество константbody
— установить новое тело, строку для ответаheader
— установить новое значение для конкретного заголовкаheaders
— установить новое значение для набора заголовковМетоды класса Response
не изменяют объект, но предоставляют его копию, у которого установлены новые значения. В результате каждый отдельный экземпляр класса Response
является неизменяемым, но можно добиться создания произвольного ответа.
OK
, успешный результат
#
NOT_FOUND
, документ не найден
#
FOUND
, перенаправление
#
Тело ответа передаётся клиенту для дальнейшей обработки
Будем использовать шаблонизатор Pebble для формирования HTML-документов
Библиотека http4k включает в себя поддержку большинства продуктовых шаблонизаторов, доступных для платформы 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, для их описания используются следующие элементы:
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?>>
findSingle
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")
}
Под архитектурой подразумевается вопрос грамотного разделения приложения на компоненты, каждый из которых решает чётко поставленную задачу. Т.е. так, чтобы из небольших компонентов составлять полнофункциональное сложное приложение
Компоненты приложения:
Все компоненты соединяются между собой внутри маршрутизатора