Автоматизация и разработка административной панели для 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». Ниже представлен список созданных нами аннотаций, которые в полной мере удовлетворили потребности реализации задуманного функционала:

  1. MetaDataResource – прописывается внутри модели, чтобы определить его как таблицу для добавления в панель администратора, имеет поля:

               1.1.        localization - локализованное имя таблицы, по умолчанию используется название самого аннотированного класса;

               1.2.        tableGroup - определяет, в какой группе таблиц находится аннотированный класс;

               1.3.        canDelete (по умолчанию true) - определяет, можно ли удалять записи данного класса;

               1.4.        canAdd (по умолчанию true) - определяет, можно ли создавать записи данного класса;

               1.5.        canEdit (по умолчанию true) - определяет, можно ли редактировать записи данного класса;

  1. DefaultValue - начальное значение поля, указывается в скобках;
  2. EnumLocalization - используется для определения класса, по которому отслеживаются значения и локализации перечислений. В аннотацию передается перечисление, реализующее кастомный интерфейс LocalizedEnum с методом «getLabel». Аннотированное поле делается селектом;
  3. LabelField служит для обозначения, поля связанной таблицы, которое следует использовать в админке. По умолчанию - id связанной таблицы;
  4. Localization указывается для определения перевода/локализации аннотированного поля, чтобы подставить вместо названия поля по умолчанию;
  5. Max - для определения значения максимума поля (указывается в скобках);
  6. MetaDataIgnore используется для того, чтобы скрыть поле в панели администратора;
  7. Min - аннотация для определения значения минимума поля (указывается в скобках);
  8. MultipleSelect - сигнализирует о выборе нескольких записей, ставится над полями-списками;
  9. ReadOnly - сигнализирует, что поле доступно только для чтения;
  10. Required – отражает необходимость заполнения свойства;
  11. TableResource - ставится над полем-ссылкой на другую таблицу для определения таблицы/ресурса, из которой надо брать данные;

Для удобства восприятия и гибкости отображения данных на клиентской стороне был разработан наш индивидуальный набор типов. На основе этой выборки клиент имел возможность решить, каким образом эффективно визуализировать поле определённого типа. Мы создали словарь, в котором пары "класс-тип данных—строковое представление" выступали соответственно в роли ключей и значений. Строковые обозначения были организованы в форме перечисления, в котором были чётко определены следующие представления:

  1. STRING - “string” (для типов String, Character, char, UUID)
  2. BOOL - “bool” (для типов Boolean и boolean)
  3. NUMBER - “number” (для типов Byte, Short, Integer, Float, Double, Long, byte, short, int, float, double, long)
  4. RESOURCE - “resource” (Любые другие классы, аннотированные как MetaDataResource)
  5. DATE - “date” (для типов LocalDate и LocalDateTime)
  6. 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. Выбранный нами подход к архитектуре панели администратора значительно ускорил разработку ее клиентской части, так как фактически при реализации основного функционала сосредоточилась к разработке компонентов, которые генерировали свое представление полностью по вышеописанной схеме, а именно:

  1. разводящей страницы, с которой запрашивается схема
  2. меню со списком таблиц, объединенных в группы
  3. самой таблицы для отображения записей
  4. формы создания редактирования

Когда пользователь заходит на страницу администратора, компонент запрашивает ранее сформированную схему у бэкенда, получая необходимые метаданные. Эти данные передаются в компоненты интерфейса, которые на их основе построят своё отображение. Так, исходя из списка доступных моделей данных, формируется навигационное меню, через которое администратор может перейти к управлению таблицами. Сама же структура таблиц, формы для создания и редактирования записей, а также поля ввода автоматически конструируются на основе информации о полях каждой модели.

Схема предоставляет возможности для реализации множества функций на стороне клиента. К примеру, использование аннотаций для локализации таблиц и полей позволяет перевести наименования на русский язык и включить их в схему, делая интерфейс понятным для русскоязычных пользователей. Типы данных, представленные перечислениями (ENUM), также учитываются в схеме с полным перечнем вариантов и их описаний, что позволяет на клиенте представить соответствующие поля как выпадающий список с выбираемыми читаемыми опциями.

Для полей, являющихся внешними ключами, в схеме определяется имя связанной таблицы и поле, которое будет использоваться как отображаемая ссылка на запись. Это позволяет на интерфейсе заменить неинформативные идентификаторы на осмысленные данные, например, имя связанного пользователя. В формах создания и редактирования такие поля реализуются как выбор из списка элементов, полученных через GET-запрос с использованием связанных таблиц, обеспечивая интуитивное и удобное управление данными.

java_auto1.jpg

Рис. 1. Меню и таблицы в админке (для примера оставили только группу авторизации)

На рисунке 1 представлена функция загрузки нового файла через кнопку, демонстрирующую дополнительный пользовательский функционал. Это дает клиенту возможность на свое усмотрение добавлять кастомные элементы интерфейса в таблицы и карточки записей, а также интегрировать в работу административного интерфейса сторонние API, которые не используют Spring Data REST для создания. В контексте работы с таблицей файлов кнопка «загрузить новый файл» вызывает всплывающее модальное окно, в котором пользователь может загрузить файл и задать его тип, что упрощает процесс добавления новых данных в систему.

java_auto2.jpg

Рис. 2. Кастомные действия (загрузка файла)

Среди прочих кастомных действий стоит отметить смену пароля у пользователя, а также форму с обновлением всех связанных записей для одной из таблиц. Это обеспечивает дополнительный уровень управления без необходимости глубокой доработки интерфейса. Для эффективного управления множеством других таблиц, включая операции создания и редактирования записей, достаточно функционала, который определяется по схеме.

java_auto3.jpg

Рис. 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». Дополнив клиентскую часть приложения компонентом для фильтра, мы внедрили автоматически генерируемую систему фильтрации в административную панель.

java_auto4.jpg

Рис. 4 Фильтрация

Вывод

Таким образом, выбранное решение позволяет заключить большую часть логики по работе с сущностями в панели администратора в Java-аннотациях. В эту работу входят как простые CRUD операции над сущностями в базе данных, так и довольно сложные операции по типу фильтрации при их получении. Выбранное решение позволяет быстро адаптировать и расширять нашу панель администратора, так как по клиентской части не требуется никаких доработок. Как следствие, модель в панели администратора будет обладать необходимым функционалом, стоит лишь добавить некоторые аннотации на необходимые сущности. Разработанное нами решение позволяет закрывать большинство потребностей расширения административной панели в кротчайшие сро