Автоматизация и разработка административной панели для Spring Boot.
Разрабатывая и обеспечивая поддержку react-приложения с бэкендом на Spring Boot, мы столкнулись с необходимостью эффективного управления содержимым и настройками приложения. Инструментом для решения проблемы была выбрана административная панель, которая позволила бы легко контролировать и пользоваться всеми функциями приложения, от управления пользователями до установки параметров конфигурации. Главными критериями панели администратора были простота поддержки, гибкость расширения функционала и интуитивно понятный интерфейс для пользователей.
Чаще всего в подобных ситуациях прибегают к уже существующим решениям. Например, в проектах с бэкендом на популярном фреймворке Django для подобных целей мы использовали Django Admin – встроенную административную систему, обладающую широкими возможностями кастомизации. Исходя из этого, мы рассчитывали найти аналогичную платформу для работы со Spring Boot.
К нашему удивлению, поиск по запросу "Spring Boot Admin" показал лишь наличие одноименной библиотеки, фокусирующейся больше на мониторинге и аналитике, которая никак не соответствовала нашим нуждам в управлении приложением. Отсутствие подходящего готового инструмента побудило нас к созданию собственного решения, специально адаптированного для интеграции с React-приложением и эффективного взаимодействия с бэкендом на Spring Boot.
Проработка архитектуры
Первым шагом в создании панели администратора стало внедрение интерфейсов для CRUD-операций с необходимыми сущностями в админке. Здесь нам помогла библиотека Spring Data REST – готовое решение, взаимодействующее с JPA, которое упрощает добавление в приложение API-методов для операций выборки, сортировки, пагинации, а также редактирования и удаления данных.
Следующий этап – проработка визуализации созданных API на клиентской стороне, задача которой заключалась в отрисовке таблиц, форм и прочих нужных элементов. Но здесь мы столкнулись с проблемой: как клиент сможет увидеть все возможности административной панели? Как клиент должен определять, какие таблицы добавлены в админку, что можно делать с записями в таблицах, какие имена и типы у полей записей? Переносить полную логику на клиентскую сторону было неприемлемо по многим причинам: это было бы неэффективно, создавало бы излишние расходы при расширении функционала админки и выпадало за рамки его обязанностей. Выбранный метод решения этих вопросов определил бы архитектуру всего административного интерфейса.
Исходя из поставленных задач была реализована концепция, существенно сократившая временные и ресурсные затраты на дальнейшее развитие проекта. Основой идеи стал уникальный API-метод, функционирующий как генератор схемы для административного интерфейса. Этот метод представляет из себя полный набор сведений о функциях административной панели, а также метаданные для каждой встроенной таблицы, включая мультиязычные названия полей, их типы и встроенные правила для проверки данных. На основе этой схемы клиентская часть приложения автоматически создавала необходимые формы и таблицы, внедряя заданные правила валидации и подбирая соответствующие пользовательские интерфейсы. Этот методический подход значительно упростил процесс дальнейшего расширения функционала административной панели, что позволило быстрее начать его внедрение.
Рефлексивный бэкенд
В ходе разработки API-метода для генерации схемы административного интерфейса бэкенду необходимо самостоятельно собирать данные о сущностях, предназначенных для управления в админке. Для решения этой задачи была выбрана рефлексия – мощный инструмент, позволяющий взаимодействовать с классами и объектами, даже если они неизвестны во время компиляции. Несмотря на сложности в освоении и потенциальные трудности в поддержке, рефлексия при правильном использовании может значительно упростить множество задач разработки.
Используя возможности рефлексии, мы решили разработать собственный набор аннотаций, которые проставлялись бы напрямую над сущностями и их полями. Эти аннотации содержали в себе важную информацию, такую как локализация имён полей и указание на статус «readonly». Ниже представлен список созданных нами аннотаций, которые в полной мере удовлетворили потребности реализации задуманного функционала:
- MetaDataResource – прописывается внутри модели, чтобы определить его как таблицу для добавления в панель администратора, имеет поля:
1.1. localization - локализованное имя таблицы, по умолчанию используется название самого аннотированного класса;
1.2. tableGroup - определяет, в какой группе таблиц находится аннотированный класс;
1.3. canDelete (по умолчанию true) - определяет, можно ли удалять записи данного класса;
1.4. canAdd (по умолчанию true) - определяет, можно ли создавать записи данного класса;
1.5. canEdit (по умолчанию true) - определяет, можно ли редактировать записи данного класса;
- DefaultValue - начальное значение поля, указывается в скобках;
- EnumLocalization - используется для определения класса, по которому отслеживаются значения и локализации перечислений. В аннотацию передается перечисление, реализующее кастомный интерфейс LocalizedEnum с методом «getLabel». Аннотированное поле делается селектом;
- LabelField служит для обозначения, поля связанной таблицы, которое следует использовать в админке. По умолчанию - id связанной таблицы;
- Localization указывается для определения перевода/локализации аннотированного поля, чтобы подставить вместо названия поля по умолчанию;
- Max - для определения значения максимума поля (указывается в скобках);
- MetaDataIgnore используется для того, чтобы скрыть поле в панели администратора;
- Min - аннотация для определения значения минимума поля (указывается в скобках);
- MultipleSelect - сигнализирует о выборе нескольких записей, ставится над полями-списками;
- ReadOnly - сигнализирует, что поле доступно только для чтения;
- Required – отражает необходимость заполнения свойства;
- TableResource - ставится над полем-ссылкой на другую таблицу для определения таблицы/ресурса, из которой надо брать данные;
Для удобства восприятия и гибкости отображения данных на клиентской стороне был разработан наш индивидуальный набор типов. На основе этой выборки клиент имел возможность решить, каким образом эффективно визуализировать поле определённого типа. Мы создали словарь, в котором пары "класс-тип данных—строковое представление" выступали соответственно в роли ключей и значений. Строковые обозначения были организованы в форме перечисления, в котором были чётко определены следующие представления:
- STRING - “string” (для типов String, Character, char, UUID)
- BOOL - “bool” (для типов Boolean и boolean)
- NUMBER - “number” (для типов Byte, Short, Integer, Float, Double, Long, byte, short, int, float, double, long)
- RESOURCE - “resource” (Любые другие классы, аннотированные как MetaDataResource)
- DATE - “date” (для типов LocalDate и LocalDateTime)
- ENUM - “enum” (для перечислений)
Далее оставалось лишь расставить аннотации над нужными моделями и полями, после чего с помощью рефлексии получить список аннотированных моделей и их полей, преобразовать в понятную для клиента схему и оправлять по запросу. Полученная схема представляла собой массив дескрипторов, где ключами были имена полей, а значения - соответственно описание поля. Дополнительная информация по таблице передавалась в значении ключа __properties. Ниже приведен пример дескриптора для одной из таблиц - таблицы системных файлов:
Фигура 1. Пример дескриптора таблицы
{
"id": {
"Type": "number",
"ReadOnly": true
},
"activationDate": {
"Type": "date",
"Localization": "Дата активации",
"ReadOnly": true
},
"isActive": {
"Type": "bool",
"Localization": "Активен",
"ReadOnly": true
},
"author": {
"Type": "resource",
"LabelField": "name",
"Localization": "Автор",
"ReadOnly": true,
"TableResource": "appUser"
},
"type": {
"EnumLocalization": [
{
"label": "Руководство",
"value": "MANUAL "
},
{
"label": "Пользовательское соглашение",
"value": "TERMS"
}
],
"Type": "enum",
"Localization": "Тип файла",
"ReadOnly": true
},
"filePath": {
"Type": "string",
"Localization": "Путь до файла",
"ReadOnly": true
},
"__properties": {
"TableGroup": "Авторизация",
"Localization": "Соглашения и руководства",
"CanEdit": false,
"CanDelete": false,
"CanAdd": false
}
}
Клиентская часть как генератор UI
Клиентом может выступать любое фронтовое приложение, в нашем случае - приложение на React. Выбранный нами подход к архитектуре панели администратора значительно ускорил разработку ее клиентской части, так как фактически при реализации основного функционала сосредоточилась к разработке компонентов, которые генерировали свое представление полностью по вышеописанной схеме, а именно:
- разводящей страницы, с которой запрашивается схема
- меню со списком таблиц, объединенных в группы
- самой таблицы для отображения записей
- формы создания редактирования
Когда пользователь заходит на страницу администратора, компонент запрашивает ранее сформированную схему у бэкенда, получая необходимые метаданные. Эти данные передаются в компоненты интерфейса, которые на их основе построят своё отображение. Так, исходя из списка доступных моделей данных, формируется навигационное меню, через которое администратор может перейти к управлению таблицами. Сама же структура таблиц, формы для создания и редактирования записей, а также поля ввода автоматически конструируются на основе информации о полях каждой модели.
Схема предоставляет возможности для реализации множества функций на стороне клиента. К примеру, использование аннотаций для локализации таблиц и полей позволяет перевести наименования на русский язык и включить их в схему, делая интерфейс понятным для русскоязычных пользователей. Типы данных, представленные перечислениями (ENUM), также учитываются в схеме с полным перечнем вариантов и их описаний, что позволяет на клиенте представить соответствующие поля как выпадающий список с выбираемыми читаемыми опциями.
Для полей, являющихся внешними ключами, в схеме определяется имя связанной таблицы и поле, которое будет использоваться как отображаемая ссылка на запись. Это позволяет на интерфейсе заменить неинформативные идентификаторы на осмысленные данные, например, имя связанного пользователя. В формах создания и редактирования такие поля реализуются как выбор из списка элементов, полученных через GET-запрос с использованием связанных таблиц, обеспечивая интуитивное и удобное управление данными.
Рис. 1. Меню и таблицы в админке (для примера оставили только группу авторизации)
На рисунке 1 представлена функция загрузки нового файла через кнопку, демонстрирующую дополнительный пользовательский функционал. Это дает клиенту возможность на свое усмотрение добавлять кастомные элементы интерфейса в таблицы и карточки записей, а также интегрировать в работу административного интерфейса сторонние API, которые не используют Spring Data REST для создания. В контексте работы с таблицей файлов кнопка «загрузить новый файл» вызывает всплывающее модальное окно, в котором пользователь может загрузить файл и задать его тип, что упрощает процесс добавления новых данных в систему.
Рис. 2. Кастомные действия (загрузка файла)
Среди прочих кастомных действий стоит отметить смену пароля у пользователя, а также форму с обновлением всех связанных записей для одной из таблиц. Это обеспечивает дополнительный уровень управления без необходимости глубокой доработки интерфейса. Для эффективного управления множеством других таблиц, включая операции создания и редактирования записей, достаточно функционала, который определяется по схеме.
Рис. 3. Редактирование пользователя.
За исключением кнопки смены пароля (кастомного действия), форма на рисунке 3 сгенерирована автоматически по переданной бэкендом схеме. Подобным образом генерируются и все остальные формы.
Генератор кода для фильтрации
От функционала административной панели ожидается не только сортировка и пагинация, а также возможности поиска по полям и продвинутой фильтрации данных. Сортировку и пагинацию мы легко реализовали благодаря встроенным возможностям Spring Data REST. Однако с реализацией поиска и фильтрации возникли сложности.
Для реализации поиска и фильтрации в контексте Spring Data REST потребовалось бы модифицировать каждый репозиторий — добавляя методы findBy
, интегрируя библиотеку QueryDSL или переопределять методы GET в REST-контроллерах, и это для каждой модели отдельно. Учитывая, что в нашей имелось уже свыше 30 таблиц, повторение однотипного кода для всех них казалось чрезмерно трудоёмким и шло вразрез с нашими принципами простоты и гибкости расширения системы. Нужно было вносить изменения в код при каждом добавлении новой сущности.
Нашей основной целью было найти или разработать решение, которое позволяло бы автоматизировать процесс добавления функций фильтрации к каждой сущности в административной панели без необходимости ручного копирования и адаптации кода.
Если вы знакомы с Lombok в среде разработки IntelliJ IDEA, вы, скорее всего, сталкивались с уведомлением о необходимости включить обработку аннотаций: "Lombok requires enabled annotation processing". Этот инструментарий работает на этапе компиляции Java-приложений, отличаясь, этим от рефлексии, которая применяется в рантайме. Lombok автоматизирует процесс генерации кода: он создаёт файлы в директории target и интегрирует их в итоговую сборку программы. Вспомнив про это, у нас возникла идея с помощью данного инструмента перед запуском приложения генерировать для помеченных нашими аннотациями перегруженные контроллеры, которые имели бы единую реализацию фильтрации и отличались бы лишь именем используемого репозитория и модели.
Мы создали универсальный шаблон для контроллера (.ftl), который имел встроенное API для фильтрации данных. Этот шаблон был настроен переменными, представляющими сущности и репозитории. Поскольку логика фильтрации одинакова для всех моделей, такой подход отлично подошёл. Вопрос был в том, как автоматически генерировать контроллеры для нужных классов. Мы создали аннотацию, размещённую над main классом приложения, и логику сбора информации о классах-сущностях для админ-панели.
Аннотация MetaDataResource, указывающая на включение сущностей в админку, помогала нам получать необходимую информацию для шаблона контроллера. Готовые контроллеры сохранялись как классы в папке «target». Дополнив клиентскую часть приложения компонентом для фильтра, мы внедрили автоматически генерируемую систему фильтрации в административную панель.
Рис. 4 Фильтрация