Маршрутизация HTTP-запросов

Маршрутизация HTTP-запросов #

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

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


Описание Uri в http4k #

Библиотека предоставляет класс org.http4k.core.Uri, с помощью которого описываются пути в запросах и ответах

Объекты класса можно создать с помощью:

  • Конструктора, передав туда все части URI
  • Функции из объекта-компаньона of, которой передаётся строка

В обработчиках HTTP-запросов URI находится в свойстве uri объекта-запроса

С помощью Uri-объекта можно получить доступ к следующим частям URI:

  • Путь запроса в свойстве path
  • Параметры запроса в свойстве query
  • Схема доступа в свойстве scheme
  • Адрес сервера в свойстве host

Маршрутизация запросов #

Всю логику по обработке всех возможных запросов можно реализовать в рамках одного обработчика HTTP-запросов, функциональном типе HttpHandler, но

  • Его будет чрезвычайно сложно тестировать
  • Дорабатывать его функциональность будет очень сложно
  • Разрабатывать совместно практически невозможно

Решение проблемы — разделение обработки HTTP-запросов между разными обработчиками, выделение слоя маршрутизации


Выделение маршрутизаторов #

diagram

  • Маршрутизатор является обработчиком HTTP-запросов
  • HTTP-обработчик привязывается к конкретному маршруту
  • Обычным подходом к разделению зоны ответственности является деление маршрута по делителю /
  • Подобный маршрутизатор можно написать самостоятельно (обработать request.uri) или воспользоваться подсистемой используемого инструмента

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

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

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

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

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

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


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

Функция routes позволяет связать HTTP-обработчики с маршрутами

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),
)
  • handlerOne и handlerTwo являются обработчиками, HttpHandler
  • Конфигурация обрабатывает пути /first и /second
  • В переменную app был помещён HTTP-обработчик, его тип RoutingHttpHandler
  • Обработчик вернёт ответ с кодом 404 и пустым телом, если для переданного ему пути нет связи с конкретным вложенным HTTP-обработчиком
  • Структура обрабатываемых маршрутов доступна через свойство description

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

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. Вызов инфиксной функции bind связывает строку с методом по его обработке, возвращая объект типа PathMethod
  3. Указание названия HTTP-метода, который надо обработать
  4. Инфиксная функция PathMethod.to связывает результат работы предыдущего метода с обработчиками запроса, порождая объект типа RoutingHttpHandler
  5. Передача обработчика HttpHandler в качестве обработчика маршрута

Сформированный список объектов RoutingHttpHandler передаётся на вход функции routes, которая сама возвращает обработчик RoutingHttpHandler

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

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


Вложенные маршруты #

Функции routes в качестве аргумента можно передать объект RoutingHttpHandler, созданный в результаты другого вызова функции routes:

val webCourseRouter = routes(
    "topic" bind GET to { Response(OK).body("Веб-разработка") },
    "length" bind GET to { Response(OK).body("1 семестр") }
)
val unixCourseRouter = routes(
    "topic" bind GET to { Response(OK).body("Использование UNIX") },
    "length" bind GET to { Response(OK).body("1 или 2 семсетра")},
)
val coursesApp = routes(
    "test/ping" bind GET to { Response(OK).body("pong") },
    "unix" bind unixCourseRouter,
    "web" bind webCourseRouter,
)

Обрабатываются маршруты:

  • /test/ping
  • /unix/topic, /unix/length
  • /web/topic, /web/length

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

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

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

Данные для формирования ответа #

Зачастую информации из запроса недостаточно для формирования ответа:

  • Необходимо получить доступ к данным из базы данных (хранилища)
  • Необходимо выполнить проверку уровня доступа пользователя
  • Необходимо обратиться к ресурсам

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

// Конструктор в виде класса
class UserListerHandler(private val users: List<String>): HttpHandler {
    override fun invoke(request: Request): Response =
        Response(OK).body(users.joinToString(", "))
}
val userHandler = UserListerhandler(listOf("Иван", "Марья"))
// Констуктор в виде функции-генератора
fun messageResponder(message:String): HttpHandler = {
    Response(OK).body(message)
}
val messageHandler = messageResponder("Привет!")

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


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

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

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

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

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

  • Не надо перегружать внутреннюю логику обработчиков HTTP-запросов
  • Выделяйте специфическую логику в отдельные подсистемы

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