Данные приложения. Использование шаблонизаторов #
Васильев Андрей Михайлович, 2023
Версии презентации
Архитектура приложения #
- Веб-сервер обслуживает множество клиентов
- Клиентам необходимо множество документов, для получения которых выполняется несколько HTTP-запросов
- HTML-документ с данными для отображения
- CSS-документы с описанием классов для стилизации HTML
- JS-документы для выполнения динамических действий на стороне клиента
- Для формирования документов серверу недостаточно данных, которые хранятся в его оперативной памяти
- Необходимо обратиться к хранилищу данных (СУБД или другая система хранения) за динамическими данными: список товаров, их количество, данные пользователя
- Необходимо обратиться к внутренним ресурсам для получения статической информации: CSS/JS-документы, шаблоны страниц
- Клиентам необходимо обратиться за документами, которые были загружены или сформированы приложением: изображения, архивы, PDF-документы
Максимальное использование локальных ресурсов #
- Современные системы предоставляют множество процессоров, способных независимо обрабатывать информацию
- HTTP-запросы хорошо масштабируются, т.к. каждый запрос не зависит от других запросов
- HTTP-сервера обычно запускают множество процессов (и потоков), которые используют разные процессоры
- Потоки имеют доступ к общим данным:
- Файловая система
- Ресурсы приложения
- Данные в оперативной памяти
- Для изменяемых данных необходимо аккуратно подходить к задаче редактирования общих данных: данные в оперативной памяти и на файловой системе
- Библиотека http4k по умолчанию запускает несколько процессов
Балансировка запросов между компьютерами #
- Для обработки большого числа запросов от клиентов мощностей одного компьютера может не хватать
- Можно организовать обработку запросов с помощью множества одинаковых приложений, запущенных на нескольких компьютерах
- Выводы из данной архитектуры:
- Файловая система не может служить подходящим средством для хранения бинарных данных
- Все данные приложения должны находится в единых системах хранения: кластере СУБД-серверов, серверах хранения больших данных и т.д.
- Приложение должно считывать конфигурацию из сетевой службы.
- Невозможно организовать доступ к общим данным в оперативной памяти, необходимо использовать специализированные инструменты
- При разработке локального приложения стоит изначально рассматривать сложности перехода к запуску нескольких приложений на нескольких серверах
Локальные данные приложения #
Рассмотрим следующие источники данных, доступные JVM-приложениям:
- Ресурсы приложения
- Файловая система
Общие свойства #
- Предоставляют данные в виде файлов
- Предоставляют данные в иерархической структуре каталогов
- Для построения путей к файлам можно использовать абсолютные и относительные пути
Отличия #
- Данные внутри ресурсов доступны только на чтение
- Ресурсы поставляются вместе с исполняемым кодом приложения
- Файлы поставляются отдельно
- Абсолютные пути внутри ресурсов будут работать на любом компьютере для файлов стоит использовать относительные
Добавление ресурсов приложения #
В рамках системы сборки Gradle для добавления ресурсов внутрь приложения необходимо их разместить в каталоге src/main/resources
- Данный каталог является корневым каталогом для ресурсов
- Структура каталогов в ресурсах может совпадать со структурой каталогов в Java и Kotlin-проектах, что в свою очередь соотносится с пакетами
Результатом компиляции приложения на Kotlin является Jar-файл, включающий в себя все бинарные компоненты приложения. Jar-файлы являются Zip-архивами
Сборка приложения #
Для создания дистрибутива приложения с использованием системы сборки Gradle достаточно выполнить одну из задач: distZip
, distTar
, installDist
Например, в результате выполнения задачи distZip
будут выполнены следующие задачи
- Внутри Zip-архива будут находится: Jar-файл приложения, Jar-файлы библиотек, скрипты для запуска приложения под тремя целевыми операционными системами
- Все команды
dist*
создают архивы в каталогеbuild/distributions
- Команда
installDist
создаёт структуру каталоговbuild/install
Доступ к ресурсам приложения #
Для доступа к ресурсам можно воспользоваться классами ClassLoader и Class, которые в Kotlin можно получить следующим образом
- ClassLoader:
SomeClass::class.java.classLoader
- Class:
SomeClass::class.java
Данный код может быть запущен только на платформе JVM
Можно воспользоваться следующими методами класса Class
для чтения данных из ресурсов:
getResource(name: String): URL
getResourceAsStream(name: String): InputStream
В качестве аргумента можно указать относительный путь к файлу относительно пакета, в котором содержится указанный класс
Интерфейс класса ClassLoader предоставляет большое количество методов для поиска файлов среди ресурсов, их загрузки и т.д. Рекомендуется для самостоятельного изучения
Работа с файловой системой #
В рамках стандартной библиотеки языка Kotlin предоставляется пакет kotlin.io
, включающий собственные классы и функции-расширения для стандартных классов Java
Например для считывания содержимого файла можно воспользоваться функцией
fun File.readText(charset: Charset = Charsets.UTF_8): String
Построение пути к файлам #
При работе с файловой системой необходимо обеспечить работу приложения на множестве компьютеров, для чего можно:
- Использовать известные стандартные пути, например
C:/Program Files/
- Использовать относительные пути
- Относительно рабочего каталога приложения
- Относительно исполняемого файла приложения (относительно Jar-файла приложения)
Рабочий каталог приложения #
При запуске приложения с использованием системы сборки Gradle рабочим каталогом приложения становится каталог, в котором
- Находится конфигурационный файл
build.gradle
- Внутри которого находится конфигурация плагина
application
, который определил задачуrun
Таким образом для считывания содержимого текстового файла, расположенного в каталоге data/info.txt
внутри каталога с приложением достаточно:
val text = File("data/info.txt").readText()
Стоит обратить внимание, что данные файлы по умолчанию не будут включены в дистрибутив приложения, который формируют команды distZip
и distTar
. Если они потребуются пользователю приложения для его корректной работы, то необходимо их добавить в конфигурацию Gradle для дистрибуции, однако автоматически определить путь к установочному каталогу достаточно сложно
Использование файловой системы #
- Хранение временных файлов
- Каждая ОС предоставляет средства для поиска каталога со временными файлами
- Хранение данных, загруженных пользователем
- Хранение постоянных данных в структурированном формате, например для встраиваемых БД
- Хранение журналов работы приложения
Как было ранее сказано, файловая система не является подходящим решением для распределённых серверных систем. Вместо файловой системы можно воспользоваться:
- Сервер СУБД вместо встраиваемой БД
- Сервер для хранения и обработки файлов вместо каталога хранения
- Сервер для обработки журналов работы приложения
Однако их тоже кто-то должен настроить и обеспечить работоспособность
При использовании файловой системы предоставьте пользователю возможность указания путей до всех каталогов, которое приложение будет использовать
Использование стандартных форматов данных #
На заре разработки информационных технологий можно было оправдать использование собственных форматов данных. В настоящее время в первую очередь стоит рассмотреть один из существующих форматов:
- CSV, Comma-separated values, для базовых табличных данных
- JSON, JavaScript Object Notation, для произвольных данных, которые можно описать с помощью массивов и ассоциативных массивов
- YAML, YAML Ain’t Markup Language, для аналогичных задач, для описания настроек
- TOML, Tom’s Obvious Minimal Language, для описания настроек
- XML, Extensible Markup Language, для описания сложных структурированных документов, лежит в основе docx
- ProtoBuf, Protocol Buffers, для обмена жёстко структурированными сообщениями
- Ещё 1000+ разных форматов обмена данными
Для чтения и записи данных форматов существует множество библиотек. Каждый из форматов имеет как свои плюсы, так минусы
Всегда можно предложить свой формат, но для этого необходимо чётко понимать отличия от существующих: плюсы, минусы, ограничения
Раздача статических данных #
С точки зрения кода HTTP-кода все данные, которые не проходят обработку перед передачей их клиенту считаются статическими
К ним можно отнести:
- CSS-файлы, JS-файлы, изображения для оформления документов и т.п.
- Файлы, загруженные пользователем и не обрабатываемые с точки зрения приложения
Библиотека http4k предоставляет реализацию HTTP-обработчика, способного дать доступ к этим файлам
fun static(...): RoutingHttpHandler
В качестве аргумента ей необходимо передать объект интерфейса ResourceLoader
- Classpath для предоставление данных из ресурсов
- Directory для предоставления данных из файловой системы
Статические данные в ресурсах #
Функция Classpath принимает в качестве аргумента путь к пакету, данные из которого будут доступны клиенту
Предположим, что CSS и JS-файлы расположены в пакете ru.yarsu.public
, который располагается по пути src/main/resources/ru/yarsu/public
src
└── main
└── resources
└── ru
└── yarsu
└── public
├── css
└── js
Следующая конфигурация ClassPath обеспечит доступ клиентам доступ к этим каталогам:
routes(
...
static(ResourceLoader.Classpath("/ru/yarsu/public")),
)
При выполнении запроса
GET /css/foundation.css HTTP/1.1
В качестве ответа будет передан файл, находящийся внутри ресурсов по пути /ru/yarsu/public/css/foundation.css
Некорректная настройка Classpath #
Рассмотрим структуру каталогов, которая была представлена на предыдущем слайде
Предположим, что разработчик сформулировал настройки следующим образом:
routes(
...
static(ResourceLoader.Classpath("/ru/yarsu/")),
)
Пользователи всё-равно смогут получить доступ к статическим файлам, но по другому пути:
GET /public/css/foundation.css HTTP/1.1
Основная проблема такого решения — пользователь сможет считать не публичную информацию тоже:
- class-файлы приложения:
GET /domain/Prices.class HTTP/1.1
- Приватные данные:
GET /data/certificate.pem HTTP/1.1
Необходимо предоставлять неконтролируемый доступ только для пакетов, содержащих публичную информацию
Раздача данных с файловой системы #
В отличие от Classpath, функция Directory принимает в качестве аргумента путь к данным внутри файловой системы
fun Directory(baseDir: String = "."): ResourceLoader
По умолчанию указывается текущий рабочий каталог, через который можно неконтролируемо получить доступ ко множеству приватных данных. Необходимо указывать путь только к публичным данным
Один из вариантов применения — обеспечение доступа к файлам из каталога ресурсов. Это позволит вносить изменения в статические файлы без перекомпиляции приложения
routes(
...
static(ResourceLoader.Directory("src/main/resources/ru/yarsu/")),
)
Это становится возможным, т.к. система сборки Gradle использует корневой каталог приложения для запуска при использовании команды run
Формирование динамических данных #
Помимо статических данных пользователю необходимо предоставить документы на основании хранилища динамических данных (СУБД и другие средства хранения)
- Данные могут быть представлены в машиночитаемых форматах: CSV, JSON, XML и т.д. Для формирования их строкового представления следует использовать специализированные библиотеки
- Данные могут быть ориентированы на пользователя: HTML-документы, изображения и т.д. Для создания HTML-документов можно:
- Выполнять формирование «в ручную» средствами обычного языка программирования
- Использовать специализированные инструменты — шаблонизаторы
Задача шаблонизатора — формирование сложных текстовых документов с использованием специализированного синтаксиса, т.е. с помощью специализированного языка программирования
Шаблонизаторы в http4k #
Библиотека http4k включает в себя поддержку большинства продуктовых шаблонизаторов, доступных для платформы JVM
Принципиальная схема работы шаблонизатора
- Шаблонизатор — это компонент, который формирует текстовый документ на основании шаблона и данных для отображения этого шаблона
- Шаблон — это размеченный специальным образом текст, в котором указаны места вставки данных
- Шаблон может быть как одним файлом, так и быть связанным с другими файлами
Использование шаблонизатора в http4k #
Для использования шаблонизатора необходимо:
- Подключить соответствующую библиотеку к проекту в build.gradle
- Создать объект шаблонизатора
- Для каждой отдельной страницы
- Создать модель для отображения данных
- Создать файл шаблона в ресурсах, преобразующий данные в текст
Шаблонизаторы TemplateRenderer #
Для описания шаблонизатора в рамках http4k используется тип TemplateRenderer
typealias TemplateRenderer = (ViewModel) -> String
Функциональный тип, выполняющий преобразование данных типа ViewModel в строковое представление
Создание объекта шаблонизатора #
fun main() {
val renderer = PebbleTemplates().HotReload("src/main/resources")
}
-
HotReload — горячая перезагрузка шаблонов из директории, которая указана в качестве аргумента
fun HotReload(baseTemplateDir: String): TemplateRenderer
-
Caching — загрузка шаблонов из каталога с ускорением путём кеширования
fun Caching(baseTemplateDir: String): TemplateRenderer
-
CachingClasspath — загрузка шаблонов из ресурсов с кешированием
fun CachingClasspath(baseClasspathPackage: String): TemplateRenderer
Первый вариант подходит для разработки, два других ориентированы на продуктовое использование
Формирование данных для отображения #
Для описания данных шаблона необходимо создать класс-наследник интерфейса ViewModel
package ru.yarsu.domain
class PersonVM(val name: String, val birthDate: LocalDate) : ViewModel
Интерфейс ViewModel не требует реализации никаких методов, он только предоставляет реализацию метода
open fun template(): String
Данный метод позволяет определить путь к шаблону, который может быть использован для формирования строки на основании этих данных
Для класса из примера ru.yarsu.domain.PersonVM
путём будет ru/yarsu/domain/PersonVM
- Базой для пути является полное имя класса, реализующего интерфейс ViewModel
- Шаблонизатор добавит к пути расширение, для PebbleTemplates — это
.peb
- Если данные в шаблон передавать не надо, то можно создать объект без данных
Расположение Pebble-шаблонов #
После создания класса-модели с данными необходимо создать Pebble-файл, учитывая:
- Название класса-модели, описывающее относительный путь
- Тип шаблонизатора, описывающий расширение для файла
- Конфигурацию шаблонизатора, описывающего логику для поиска файлов-шаблонов
Рекомендуется размещать шаблоны в ресурсах приложения, т.к. ресурсы будут доступны приложению после создания дистрибутива приложения
- В условиях, показанных на предыдущих слайдах следует создать файл по пути
src/main/resources/ru/yarsu/domain/Person.peb
- Для работы шаблонизатора необходимо описать класс-модель и файл шаблона для её отображения
Использование Pebble-шаблона #
val renderer: TemplateRenderer = PebbleTemplates().HotReload("src/main/resources")
val model = PersonVM("Марина", LocalDate.of(2000, 01, 01))
val text = renderer(model)
Наполнение Pebble-шаблона #
Шаблонизатор Pebble реализует язык, похожий на шаблонизаторы TWIG из PHP и Jinja2 из Python
<html>
<head>
<title>Пример шаблона</title>
</head>
<body>
<p>Имя: {{ model.name }}</p>
<p>День рождения: {{ model.birthDate | date("yyyy-MM-dd") }}
</body>
</html>
- Объект модели доступен через переменную
model
- Возможен вызов любых методов модели
- Для вывода содержимого выражения в результирующий текст используется конструкция
{{ }}
- Шаблонизатор предоставляет множество фильтров, которые можно применить к данным
{{ SOURCE | FILTER-1 | FILTER-2 }}
Доступ к переменным #
При обращении к полю переменной model.data
внутри шаблона Pebble попытается у переданного объекта вызвать следующие методы:
- Если
model
является словарём (наследникомMap
), то будет вызван методmodel.get("data")
model.getData()
model.isData()
model.hasData()
model.data()
model.data
Если в переменной list
содержится список, то к его полям можно обращаться с помощью list[0]
вместо list.get(0)
Если возвращённое значение на каком-то этапе будет null
, то шаблонизатор вернёт пустую строку
Передача списков данных в шаблоны #
Шаблонизаторы поддерживает работу и со списками данных
class Event(val start: LocalDateTime, val description: String)
class EventsList(val events: List<Event>) : ViewModel
Шаблон для отображения списка событий:
{% for event in model.events %}
<h3>Событие {{ event.description }}</h3>
<p>Начало события: {{ event.start | date("yyyy-MM-dd HH:mm",
timeZone="UTC") }}</p>
{% else %}
<p>События не описаны</p>
{% endfor %}
Управляющие конструкции, вывод которых не должен попасть в текст документа, помещаются внутри {% %}
Управляющие конструкции #
{% if category == "news" %}
{{ news }}
{% elseif category == "sports" %}
{{ sports }}
{% else %}
<p>Пожалуйста укажите категорию</p>
{% endif %}
- С помощью условных конструкций необходимо адаптировать данные для отображения
- Запрещено помещать логику по обработке, группировке, сортировке и т.п. внутри шаблона. Все эти действия необходимо произвести внутри основной логики приложения, а в шаблон необходимо передать уже подготовленные данные
- Условные выражения поддерживают логическое объединение выражений:
and
— логическое Иor
— логическое ИЛИnot
— логическое отрицание()
— группировка
Проверки #
В рамках условных выражений Pebble позволяет вызывать операцию is
и связывать её с проверками:
{% if 3 is odd %}
3 является нечётным числом
{% endif %}
При использовании оператора можно проверить отрицание проверки:
{% if name is not null %}
Имя не указано!
{% endif %}
Наследование шаблонов #
Шаблонизатор Pebble поддерживает концепцию иерархических шаблонов
- Базовый шаблон включает в себя основное содержимое и точки расширения
- Шаблон-расширение указывает содержимое для точек расширения
Базовый шаблон, например layout.peb
:
<html>
<body>
<div id="content">
{% block content %} {% endblock %}
</div>
<div id="footer">
{% block footer %} Default content {% endblock %}
</div>
</body>
</html>
Шаблон-расширение:
{% extends "./layout.peb" %}
{% block content %} Содержимое {% endblock %}
Включение шаблонов #
Помимо иерархического разделения Pebble поддерживает включение одних шаблонов в другие
- Шаблоны с описанием макросов
- Шаблоны для описания типовых компонентов
- Подменяемые статические конструкции страниц
<div class="sidebar">
{% include "advertisement.html" %}
</div>
Архитектура приложения и её проектирование #
Процесс проектирования архитектуры приложения может быть
- выделеным, когда происходит разработка подходов к решению задач
- оперативным, когда создаются отдельные файлы, функции и классы
Архитектура приложения — это комбинация выбранных подходов к формированию приложения, а также результирующая структура исходного кода
Обычно архитектура проявляется в подходе к разделению зон ответственности исходного кода приложения на части
Подходы к разделению #
Деление по функциям предметной области #
Приложение разделяется по логически связным частям предметной области, для каждой функции выделяются отдельные части приложения
- Управление комнатами и списками постояльцев
- Управление персоналом гостиницы
- Управление автопарком гостиницы
Для каждой из этих областей можно выделить класс или набор классов в выделенном пакете
Деление по техническим аспектам #
Выделение кода, описывающего предметную область #
Любое достаточно сложное приложение содержит нетривиальную логику по модификации данных, эту логику удобно отделить от технической части приложения
- Минимизация зависимостей позволяет легче тестировать классы предметной области
- Каждая часть приложения будет достаточно сложной
- В каждой части можно будет решать одновременно меньше задач
Выделение различных технических частей приложения #
Относительно веб-приложения можно рассмотреть следующие части
- Обработчики HTTP-запросов
- Фильтры
- Слой валидации данных
- Слой визуализации данных в HTML-документы или JSON-документы
Вариант разделения частей для веб-приложения #
Можно предложить следующий подход к разделению небольшого приложения на части:
- Описание предментной области, пакет
.domain
- Подсистема хранения данных, пакет
.domain.storage
или.domain.database
- Подсистема изменения данных, пакет
.domain.operations
- Подсистема хранения данных, пакет
- Статические данные приложения, пакет
.public
- Логика обработки HTTP-запросов, пакет
.web
- Фильтры по обработке HTTP-запросов, пакет
.web.filters
- Обработчики HTTP-запросов, пакет
.web.handlers
- Шаблоны для отображения информации, пакет
.web.models
- Подсистема валидации входящих запросов от пользователя
.web.validation
- Фильтры по обработке HTTP-запросов, пакет