Обработка запросов от пользователя #
Васильев Андрей Михайлович, 2023
Версии презентации
Архитектура приложения и её проектирование #
Процесс проектирования архитектуры приложения может быть
- выделеным, когда происходит разработка подходов к решению задач
- оперативным, когда создаются отдельные файлы, функции и классы
Архитектура приложения — это комбинация выбранных подходов к формированию приложения, а также результирующая структура исходного кода
Обычно архитектура проявляется в подходе к разделению зон ответственности исходного кода приложения на части
Подходы к разделению #
Деление по функциям предметной области #
Приложение разделяется по логически связным частям предметной области, для каждой функции выделяются отдельные части приложения
- Управление комнатами и списками постояльцев
- Управление персоналом гостиницы
- Управление автопарком гостиницы
Для каждой из этих областей можно выделить класс или набор классов в выделенном пакете
Деление по техническим аспектам #
Выделение кода, описывающего предметную область #
Любое достаточно сложное приложение содержит нетривиальную логику по модификации данных, эту логику удобно отделить от технической части приложения
- Минимизация зависимостей позволяет легче тестировать классы предметной области
- Каждая часть приложения будет достаточно сложной
- В каждой части можно будет решать одновременно меньше задач
Выделение различных технических частей приложения #
Относительно веб-приложения можно рассмотреть следующие части
- Обработчики HTTP-запросов
- Фильтры
- Слой валидации данных
- Слой визуализации данных в HTML-документы или JSON-документы
Вариант разделения частей для веб-приложения #
Можно предложить следующий подход к разделению небольшого приложения на части:
- Описание предментной области, пакет
.domain
- Подсистема хранения данных, пакет
.domain.storage
или.domain.database
- Подсистема изменения данных, пакет
.domain.operations
- Подсистема хранения данных, пакет
- Статические данные приложения, пакет
.public
- Логика обработки HTTP-запросов, пакет
.web
- Фильтры по обработке HTTP-запросов, пакет
.web.filters
- Обработчики HTTP-запросов, пакет
.web.handlers
- Шаблоны для отображения информации, пакет
.web.models
- Подсистема валидации входящих запросов от пользователя
.web.validation
- Фильтры по обработке HTTP-запросов, пакет
Базовый цикл работы веб-приложения #
Вход — запрос от клиента, клиентом может выступать:
- Веб-браузер
- Клиентские приложения curl, wget, HTTPie
- Скрипты и собственные приложения
- Специализированные приложения для тестирования приложения
- Автоматические тесты приложения
Выход — ответ сервера
- Статус
- Заголовки
- Тело ответа
Клиентское приложение должно всегда получать корректный ответ. Если был составлен некорректный запрос, то приложение должно оповещать об этом
Данные клиентского запроса #
- Маршрут к документу
- HTTP-заголовки
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Путь к документу с переменными
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Параметры запроса
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Содержимое полей формы
- Наличие: установлен или нет
- Содержимое: корректно или нет
Для каждого случая необходимо подготовить разумную стратегию обработки ошибок
Неразумные стратегии обработки ошибок #
- Обработка только успешного сценария, отказ от обработки некорректных сценариев
- «Техническая обработка»:
- возвращение исключительно HTTP-статусов
- возвращение единого ответа на все некорректные запросы
- Потеря некорректных данных, переданных пользователем
Обработка некорректного маршрута #
Источники некорректных маршрутов #
- Логические ошибки внутри приложения
- Ошибки, вызванные данными, которые ввели пользователи
- Некорректная ссылка, сформированная вне приложения
- Корректная ранее ссылка
Подход к обработке #
- Необходимо показать страницу, с которой он сможет открыть другие части сайта
- Необходимо показать информацию по пришедшему запросу, чтобы пользователь мог сообщить о проблеме
- Если это возможно, перенаправить запрос на другую страницу с корректным содержимым
Фильтры http4k #
fun interface Filter : (HttpHandler) -> HttpHandler
Фильтры позволяют выполнить действия, независимые от конкретного запроса:
- Проверить авторизацию пользователя
- Выполнить обработку внутренних ошибок сервера
- Выполнить обработку ошибочных запросов от пользователя
- Выполнить журналирование запросов
- Настроить общие политики для предоставления содержимого, установка CORS-заголовков
- Выполнить кеширование ответов на запросы
Реализация собственного фильтра #
Фильтр принимает в качестве аргумента HttpHandler и должен вернуть HttpHandler
val handler = { _: Request -> Response(OK) }
val myFilter = Filter {
next: HttpHandler -> {
request: Request ->
val start = System.currentTimeMillis()
val response = next(request)
val latency = System.currentTimeMillis() - start
println("I took $latency ms")
response
}
}
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth(
"my realm", "user", "password")
.then(myFilter)
val app: HttpHandler = latencyAndBasicAuth.then(handler)
Стандартные фильтры http4k #
Пакет org.http4k.filter содержит описание фильтров, поставляемых с помощью http4k
- DebuggingFilters — фильтры, помогающие в отладке работы приложения
- PrintRequest — отображать запрос, PrintRequest — отображать ответ
- PrintRequestAndResponse — отображать и запрос и ответ
- ServerFilters — основные серверные фильтры
- BasicAuth — реализация базовой HTTP-авторизации
- CatchAll — перехватывание всех внутренних исключений, возникающих при обработке сообщений
- Cors — установка CORS заголовков
- HandleRemoteRequestFailed — обработка ошибок от HTTP-запросов к внешним ресурсам
- RequestTracing — структурированное отслеживание запросов в формате Zipkin
Обработка внутренних ошибок #
- Маршрутизатор по умолчанию возвращает статус 404 при обращении к несуществующему маршруту
- Стандартные обработчики тоже могут возвращать неуспешные ответы
Подход к реализации:
- Надо выбрать уровень обработки сообщений. Либо только динамические маршруты, либо все маршруты приложения
- Необходимо реализовать фильтр для обработки неуспешного запроса, отслеживать состояние ответа можно с помощью Status.successful
Наивный подход к реализации
val errorFilter = Filter { next: HttpHandler ->
{ request: Request ->
val response = next(request)
if (response.successfull) {
response
} else {
response.body(renderer(ErrorMessage(request)))
}
}
}
val application = errorFilter.then(router)
Обработка параметров #
Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:
- Переданы ли данные
- Являются ли они корректными
Подходы к обработке данной ситуации:
- Вручную обработать ситуацию наличия или отсутствия данных
- Использовать специализированный механизм основной библиотеки
- Реализовать собственную подсистему для обработки запросов
Ручная обработка переменных внутри обработчика #
Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение сообщения, что искомый элемент не был найден
fun handler(
renderer: TemplateRenderer,
getEntityByData: (Int) -> Entity?,
): HttpHandler = handler@{ request ->
val paramemter: String? = request.path("parameter")?.toIntOrNull()
if (parameter == null) {
return@handler Response(UNSATISFIABLE_PARAMETERS).body(
renderer(EmptyParameterRequestFoundVM())
)
}
val entity: Entity? = getEntityByData(parameter)
if (entity == null) {
return@handler Response(NOT_FOUND).body(
renderer(EntityNotFoundVM())
)
}
Response(OK).body(renderer(ShowEntityVM(entity)))
}
Или с помощью составления цепочки по обработке данных, которые могут
стать null
:
fun handler(
renderer: Templaterenderer,
getEntityByData: (Int) -> Entity?,
): HttpHandler = { request ->
request
.path("parameter")
?.let { parameter ->
getEntityByParameter(parameter)
}?.let { entity ->
Response(OK).body(renderer(ShowEntityVM(entity)))
} ?: Response(NOT_FOUND).body(renderer(EntityNotFoundVM()))
}
Или путём обработки данной ситуации на уровне шаблона Pebble
Разрешаем null-состояние для модели
data class ShowEntityVM(entity: Entity?)
Передаём неопределённое состояние в модель
fun handler(
renderer: Templaterenderer,
getEntityByData: (Int) -> Entity?,
): HttpHandler = { request ->
val entity request
.path("parameter")
?.let { parameter ->
parameter.toIntOrNull()
}?.let { number ->
getEntityByParameter(parameter)
}
Response(OK).body(renderer(ShowEntityVM(entity)))
}
Проверка на null в шаблоне Pebble:
{% if model.entity is null %}
...
{% else %}
...
{% endif %}
Однако такой подход не следует активно использовать:
- Шаблонизатор исполняет код медленнее компилированного кода приложения
- Шаблонизатору приходится работать в рамках враждебного окружения, что значительно повышает сложность восприятия, следовательно повышается риск ошибок
В рамках курса такой подход запрещён