Редактирование данных. Пути приложения #
Васильев Андрей Михайлович, 2024
Версии презентации
Архитектура приложения #
Жёлтым обозначены физические (виртуальные) машины, зелёным — составляющие веб-приложения, синим — важные компоненты системы
- Веб-сервер обслуживает множество клиентов
- Клиентам необходимо множество документов, для получения которых выполняется несколько HTTP-запросов
- HTML-документ с данными для отображения
- CSS-документы с описанием классов для стилизации HTML
- JS-документы для выполнения динамических действий на стороне клиента
- Для формирования документов серверу недостаточно данных, которые хранятся в его оперативной памяти
- Необходимо обратиться к хранилищу данных (СУБД или другая система хранения) за динамическими данными: список товаров, их количество, данные пользователя
- Необходимо обратиться к внутренним ресурсам для получения статической информации: CSS/JS-документы, шаблоны страниц
- Клиентам необходимо обратиться за документами, которые были загружены или сформированы приложением: изображения, архивы, PDF-документы
Максимальное использование локальных ресурсов #
- Современные системы предоставляют множество процессоров, способных независимо обрабатывать информацию
- HTTP-запросы хорошо масштабируются, т.к. каждый запрос не зависит от других запросов
- HTTP-сервера обычно запускают множество процессов (и потоков), которые используют разные процессоры
- Потоки имеют доступ к общим данным:
- Файловая система
- Ресурсы приложения
- Данные в оперативной памяти
- Для изменяемых данных необходимо аккуратно подходить к задаче редактирования общих данных: данные в оперативной памяти и на файловой системе
- Библиотека http4k по умолчанию запускает несколько процессов
Балансировка запросов между компьютерами #
- Для обработки большого числа запросов от клиентов мощностей одного компьютера может не хватать
- Можно организовать обработку запросов с помощью множества одинаковых приложений, запущенных на нескольких компьютерах
- Выводы из данной архитектуры:
- Файловая система не может служить подходящим средством для хранения общих бинарных данных
- Все данные приложения должны находится в единых системах хранения: кластере СУБД-серверов, серверах хранения больших данных и т.д.
- Приложение должно считывать конфигурацию из сетевой службы.
- Невозможно организовать доступ к общим данным в оперативной памяти, необходимо использовать специализированные инструменты
- При разработке локального приложения стоит изначально рассматривать сложности перехода к запуску нескольких приложений на нескольких серверах
Локальные данные приложения #
Рассмотрим следующие источники данных, доступные JVM-приложениям:
- Ресурсы приложения
- Файловая система
Общие свойства #
- Предоставляют данные в виде файлов
- Предоставляют данные в иерархической структуре каталогов
- Для построения путей к файлам можно использовать абсолютные и относительные пути
Отличия #
- Данные внутри ресурсов доступны только на чтение
- Ресурсы поставляются вместе с исполняемым кодом приложения
- Файлы поставляются отдельно
- Абсолютные пути внутри ресурсов будут работать на любом компьютере для файлов стоит использовать относительные
Работа с данными без СУБД #
СУБД решает много вопросов при разработке веб-приложений: проблемы надёжного хранения данных, проблемы совместного доступа, проблемы работы с большим объёмом данных
Без СУБД будем считывать и записывать все данные приложения с файловой системы
- При старте приложение считывает информацию из файловой системы
- Во время работы приложение изменяет данные в оперативной памяти
- При завершении работы приложение сохраняет данные на файловую систему
Ключевые проблемы:
- Возникновение коллизий в данных в оперативной памяти в случае редактирования в нескольких потоках
- Надёжное сохранение данных в случае завершения работы приложения (приложению могут не дать корректно завершить работу)
Жизненный цикл JVM-приложения #
- Создаётся процесс внутри операционной системы, в рамках которой запускается первый JVM-процесс
- Запускается функция main(), порождающая множество JVM-процессов
- По завершении работы последнего JVM-процесса выполняется вызов обработчиков завершения работы
- После выполнения всех задач по обработке запроса основной процесс ОС завершает свою работу
Жизненный цикл http4k-приложения #
- Функция start() создаёт столько JVM-процессов, сколько есть процессоров в системе
- Во время инициализации приложения функции по созданию фильтров вызываются 1 раз на каждый процессор
- Есть функция stop(), которая завершит работу всех потоков
Написание обработчика завершения работы приложения #
public void addShutdownHook(Thread hook)
Обработчик завершения работы JVM-приложения — это интерфейс Thread, описывающий отдельный поток выполнения
Kotlin предоставляет удобную функцию для создания объектов, реализующих поток:
fun thread(
start: Boolean = true,
...
block: () -> Unit
): Thread
- Аргумент
start
указывает надо ли сразу запускать данный поток на исполнение - Аргумент
block
содержит код, который надо выполнять в рамках процесса
Обработчики для http4k-приложений не должны запускаться автоматически, а в блоке должна быть логика по сохранению данных
Ручная корректная остановка работы приложения #
Ввиду того, что обработчик завершения JVM-процесса может не всегда сработать (например при остановке приложения из IDEA в Windows), можно реализовать остановку приложения по отправке команды из командного интерфейса
- При старте приложения выполняется считывание данных в оперативную память
- Выполняется создание обработчиков HTTP-запроса
- Создание обработчика остановки JVM-процесса, чтобы можно было обработать остановку JVM-процесса
- Запуск HTTP-сервера
- Блокировка основного потока приложения на считывание данных из стандартного потока ввода
- Остановка сервера (данный шаг выполнится, если пользователь ввёл данные и нажал Enter)
- Автоматическое выполнение обработчика остановки приложения
Работа со сложными структурами данных #
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
Рассмотрим следующие структуры данных
data class Class(val id: Int, val name: String, val teacher: String)
data class Course(val id: Int, val name: String, val clasess: List<Class>)
data class Speciality(val id: Int, val courses: List<Course>)
val speciality: Speciality = ...
Модификация структур данных #
Для получения информации по предмету необходимо найти курс в списке, найти предмет, обратиться к полям класса для получения:
val class = speciality.courses[1].classes[5]
Для редактирования потребуется выполнить несколько копирований:
val newClass = speciality.courses[1].classes[5]
.copy(name = "Безопасность жизнедеятельности")
val newClasses = speciality.courses[1].classes.toMutableList()
.apply { set(5, newClass) }
val newCourse = speciality.courses[1].copy(classes = newClasses)
val newCourses = speciality.courses.toMutableList()
.apply { set(1, newCourse) }
val newSpeciality = speciality.copy(courses = newCourses)
Можно создать новую версию неизменяемых данных с нужным для нас состоянием
Сложные структуры сложны #
Сильная вложенность структур данных несёт следующие проблемы:
- Надо корректно указать путь к данным несколько раз
- Необходимо написать много кода
- Возможны технические ошибки
- Код, которому нужен доступ только к конечным данным, начинает зависеть от всех промежуточных объектов, что усложняет изменение структур данных в приложении
Решение с помощью функций #
Мы можем ввести функции, позволяющие решить данные задачи:
fun getName(speciality: Speciality, courseId: Int, classId: Int): String
fun setName(name: String, speciality: Speciality,
courseId: Int, classId: Int): Speciality
Код будет зависеть только от класса Speciality и одной из указанных функций
Моделирование связных списков #
В рамках приложения данные зачастую представлены в виде связных списков:
- Список групп
- Список студентов
- Список предметов
При моделировании связей между объектами в приложении кажется удобным подход с хранением ссылок на связные объекты:
class Group(
val id: Int,
val name: Sting,
val students: List<Student>
)
При такой организации удобно получить доступ ко всем студентам, т.е. оптимизировано под одну операцию, а что делать если потребуется получить список студентов из разных групп с какой-то фамилией?
Подход к моделированию списка #
- Каждый элемент хранится в своём хранилище
- Для связи элемента одного списка с элементом другого списка надо сохранить соответствующий идентификатор
В примере выше:
- У сущности студент добавляется идентификатор группы, в которой он состоит
- У предмета добавляется идентификатор группы, на котором он преподаётся
Для выполнения операций над несколькими списками вводите операции, которые обращаются с несколькими хранилищами
HTML-формы #
Для передачи данных от пользователя к веб-приложению используются HTML-формы
GET и POST-запросы #
POST-запросы обычно используется для создания нового элемента в данных сервера или для изменения существующих элементов
- GET-запросы должны быть безопасными, не должны менять видимое состояние
- Безопасные запросы можно повторять несколько раз, можно кешировать
- POST-запросы не являются безопасными
- Результат работы POST-запроса не кешируется на прокси-серверах
Если POST-запрос приводит к изменению данных на стороне сервера запрещено возвращать HTML-документ пользователю, т.к. он легко сможет повторить такой запрос
Правильное решение — перенаправить пользователя на адрес, где он с помощью GET-запроса сможет просмотреть новое состояние сервера
Форма на добавление данных #
Формы на редактирование данных не отличаются от форм для поиска и фильтрации
<form method="POST">
<div class="mb-3">
<label for="email" class="form-label">Адрес электронной почты</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password"
name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="check" name="check">
<label class="form-check-label" for="check">Проверить меня</label>
</div>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
Единственное отличие — передача данных осуществляется POST-запросом
Кодирование данных формой #
POST-запрос может содержать тело, а HTML-формы могут туда помещать данные с использованием следующих кодировок:
application/x-www-form-urlencoded
— данные кодируются в форме данных URL-запроса, но в отличие от последнего нет ограничения на длинуmultipart/form-data
— данные передаются в бинарном виде, подходит для отправки файлов на сервер, требуют отдельной обработки в http4ktext/plain
— подходит для низкоуровневой отладки, почти не используется
Тип кодировки формы задаётся с помощью атрибута формы enctype
В рамках данной лекции рассмотрим обработку первого типа кодировок данных
Обработка данных от формы #
От клиента к серверу все данные передаются в виде пар ключ-значение, схожим образом с параметрами URI-запроса: может быть несколько одинаковых ключей
Класс Request
предоставляет следующие методы для получения данных из тела формы:
fun Request.form(name: String): String?
— получить значение поля по названию ключа, при каждом вызове происходит разбор всей строки (не эффективный)fun Request.form(): Form
— получить весь набор параметровfun Request.form(name: String, value: String): Request
— указать новое значение для поля формы внутри объекта-запроса, подходит для тестированияfun Request.formAsMap(): Map<String, List<String?>>
— получить список значений в форме словаря
Особенности указанных функций
- Тип данных
Form
является псевдонимом типаParameters
:typealias Form = Parameters
, т.е. поддерживает все соответствующие функции - Для работы со словарём предоставляется метод
getFirst(key: String)
, позволяющий извлечь первый элемент из списка значений ключа
Перенаправление HTTP-клиента #
HTML-формы зачастую направлены на изменение данных на стороне сервера:
- Добавление новых элементов
- Редактирование существующих элементов коллекции
- Удаление существующих элементов коллекций
Для этих случаев используются не-GET-запросы, GET-запрос не должен менять состояние сервера
В случае успеха POST-запроса, в случае изменения данных, HTTP-сервер должен вернуть ответ с указанием адреса, куда следует сделать GET-запрос для получения HTML-документа с результатом
Параметры ответа:
- Статус ответа — 302, FOUND
- Заголовок
Location
с ссылкой на страницу для просмотра результата
Принципиальная схема работы #
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")
}
Данный обработчик не проверяет переданные данные, это неверный способ решения задачи!
Обработка HTML-форм #
Принципиальная схема не удовлетворяет требованиям по обработке данных от пользователя:
- Работает только в случае передачи корректных данных
- Падает с сообщением внутренних проблем при получении данных
- Теряет данные, которые были введены пользователем
В случае возникновения проблемы POST-запрос должен вернуть HTML-документ с формой, на которой:
- Показаны все данные, которые ввёл пользователь
- Показаны сообщения о проблемах с теми данными, которые указал пользователь
Полный сценарий обработки формы #
Пример формы #
Форма для отправки обратной связи
<form method="POST">
<div class="mb-3">
<label for="age" class="form-label">Возраст</label>
<input type="number" class="form-control" id="age" name="age" required>
</div>
<div class="mb-3">
<label for="name" class="form-label">Имя</label>
<input type="password" class="form-control" id="password"
name="password" required>
</div>
<div class="mb-3 form-check">
<label for="feedback" class="form-label">Комментарии</label>
<textarea id="feedback" name="feedback" rows="10"></textarea>
</div>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
Отображение результатов проверки в форме #
- При первом показе форма должна отображать пустые поля ввода
- В случае ошибки форма должна отображать данные, введённые пользователем и сообщения об ошибках
Для описания полей формы можно воспользоваться либо:
- Специальным объектом, в котором предоставлены все поля
- Удобно для написания кода (везде есть подсказки от компилятора)
- Полями объекта должны быть значения String?, т.к. данные пользователя надо сохранить
- Списком параметров
- Усложняется вытаскивание данных для
Редактирование данных в приложении #
Сценарий схож с добавлением, отличается только начальное состояние - данные на странице должны быть заполнены
Логичным подходом к сокращению своих издержек является выделение формы в отдельный Pebble-файл, который подключать в шаблонах форм страниц на добавление и редактирование данных
Шаг извлечения данных из шаблонов формы можно тоже объединить
Удаление данных в приложении #
Процедура удаления не может происходит путём только одного нажатия. Пользователю необходимо выполнить несколько операций по работе
Формирование путей к ресурсам, REST #
Проблема - количество путей начинает возрастать, как с этим можно выживать в приложении
REST как подход к решению задачи
Передача JSON между приложениями #
При формировании JSON-запросов учитываем:
- Метод
- Путь
- Данные
Можно всё запихнуть в данные, но исследовать работу с таким интерфейсом будет достаточно сложно
REST #
Idemponent requests & other things
- Ноль, один и много
Описание путей для работы с одним элементом #
- GET /element
- PUT /element
Описание путей для работы со множеством элементов #
- GET /elements
- PUT /elements
- GET /elements/1
- POST /elements/new
- PUT /elements/1
- DELETE /elements/1