Васильев Андрей Михайлович, 2024
Версии презентации
Жёлтым обозначены физические (виртуальные) машины, зелёным — составляющие веб-приложения, синим — важные компоненты системы
Рассмотрим следующие источники данных, доступные JVM-приложениям:
СУБД решает много вопросов при разработке веб-приложений: проблемы надёжного хранения данных, проблемы совместного доступа, проблемы работы с большим объёмом данных
Без СУБД будем считывать и записывать все данные приложения с файловой системы
Ключевые проблемы:
public void addShutdownHook(Thread hook)
Обработчик завершения работы JVM-приложения — это интерфейс Thread, описывающий отдельный поток выполнения
Kotlin предоставляет удобную функцию для создания объектов, реализующих поток:
fun thread(
start: Boolean = true,
...
block: () -> Unit
): Thread
start
указывает надо ли сразу запускать данный поток на исполнениеblock
содержит код, который надо выполнять в рамках процессаОбработчики для http4k-приложений не должны запускаться автоматически, а в блоке должна быть логика по сохранению данных
Ввиду того, что обработчик завершения JVM-процесса может не всегда сработать (например при остановке приложения из IDEA в Windows), можно реализовать остановку приложения по отправке команды из командного интерфейса
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
Рассмотрим следующие структуры данных
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-формы
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)
, позволяющий извлечь первый элемент из списка значений ключаHTML-формы зачастую направлены на изменение данных на стороне сервера:
Для этих случаев используются не-GET-запросы, GET-запрос не должен менять состояние сервера
В случае успеха POST-запроса, в случае изменения данных, HTTP-сервер должен вернуть ответ с указанием адреса, куда следует сделать GET-запрос для получения HTML-документа с результатом
Параметры ответа:
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")
}
Данный обработчик не проверяет переданные данные, это неверный способ решения задачи!
Принципиальная схема не удовлетворяет требованиям по обработке данных от пользователя:
В случае возникновения проблемы 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>
Для описания полей формы можно воспользоваться либо:
Сценарий схож с добавлением, отличается только начальное состояние - данные на странице должны быть заполнены
Логичным подходом к сокращению своих издержек является выделение формы в отдельный Pebble-файл, который подключать в шаблонах форм страниц на добавление и редактирование данных
Шаг извлечения данных из шаблонов формы можно тоже объединить
Процедура удаления не может происходит путём только одного нажатия. Пользователю необходимо выполнить несколько операций по работе
Проблема - количество путей начинает возрастать, как с этим можно выживать в приложении
REST как подход к решению задачи
При формировании JSON-запросов учитываем:
Можно всё запихнуть в данные, но исследовать работу с таким интерфейсом будет достаточно сложно
Idemponent requests & other things