Описание модульной инфраструктуры

В этой статье:

Техническая инфраструктура

Продукты Юниверс используют Java 9 Jigsaw + собственные расширения для межмодульного взаимодействия. Это решение имеет преимущества:

  • Jigsaw является частью платформы Java.

  • Jigsaw полностью предоставляет то, что требуется: модульность в виде jar-файлов без превнесения дополнительной сложности в виде новых API, которые нужно интегрировать и использовать, и дополнительной инфраструктуры, в которую нужно встраиваться.

  • Собственные расширения не привносят лишних зависимостей, легковесны и просты: в противовес системы для менеджмента межмодульного взаимодействия OSGi, имеющей сложную и громоздкую структуру.

  • Запуск кода модулей in-process избегает проблемы "константной добавленной сложности", присущей микросервисной архитектуре. Это очень сильно помогает в борьбе за производительность.

Управление модулями

Для управления модульной подсистемой в системе используется ModuleService, являющийся регистратором модулей, источником информации о модулях и средством управления модульной подсистемой.

Схема ModuleService

Рисунок 1 - Схема ModuleService

В задачи ModuleService входит загрузка и запуск модулей во время старта системы.

  1. Во время старта автоматически запускается только корневой контекст Spring и сканируется только com.unidata.mdm.core.

  2. После того, как core полностью считан и инициализирован, ModuleService сканирует архивы в поисках классов - дескрипторов, имплементирующих org.unidata.mdm.common.types.Module и указанных в дескрипторе jar-файлов в META-INF/MANIFEST.MF jar-файла (в стиле OSGi, см. ниже).

  3. Для всех найденых модулей выстраивается дерево зависимостей, сканируются пакеты и производится инициализация дочерних Spring контекстов в порядке взаимных зависимостей.

ModuleService сохраняет статус инсталляции каждого модуля в базе данных. В таблице состояния хранится:

  • Версия инсталлированного модуля (MANIFEST.MF или дескриптор)

  • Строковый ID модуля (MANIFEST.MF или дескриптор)

  • Тэг модуля (часть идентификатора, указывающая на возможную кастомизацию для заказчика, MANIFEST.MF или дескриптор)

  • Дата инсталляции

  • Результат инсталляции (успех/неудача)

  • Путь к файлу, из которого запускалась инсталляция

При старте системы модули, которые были переданы для инсталляции и потерпели в ходе инсталляции неудачу, более не стартуют. На UI статус таких модулей отмечен соответственно.

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

Взаимодействие модулей

Между модулями прописываются явные зависимости. Удаление модуля, от которого зависят другие модули, вызывает также удаление зависимых модулей. Наличие неудовлетворенных зависимостей вызывает отказ в инсталляции модуля в систему.

Предполагается поддержка стандартных аннотаций Spring типа @Autowire или JSR330 @Inject, так как они являются необходимым и привычным элементом каждодневной разработки. Речь о том, чтобы модифицировать спринговый ContextFactory таким образом, чтобы сервисы модулей могли инжектится в экземпляры классов других модулей.

Альтернативно, в случае неудачи с подходом, описанным выше, можно использовать динамическое получение экземпляров сервисов из других модулей, основанное на boolean<M extends Module> ModuleService.isActive(Class<M> module) и <T> T Module.getService(Class<T> serviceType), обработкой аннотаций @Module, либо схожими методами по id модуля (строка). Предполагается, что если модуль активен, то все его сервисы тоже активны.

Модули взаимодействуют между собой и с клиентским кодом только через публичные - или, для внутренних нужд системы, - полупубличные (*Ext) интерфейсы.

Описание модуля

Модулем является jar-файл, который можно инсталлировать в систему, и сведения о котором будут видны на UI в разделе «Управление модулями». Отличительной чертой модуля является наличие в архиве модуля реализации интерфейса org.unidata.mdm.common.types.Module, указанного в дескрипторе в META-INF/MANIFEST.MF jar-файла модуля.

Модулем может быть исполняемый код, набор ресурсов, патч модели данных, набор REST- или SOAP-эндпоинтов, или любая другая функциональность. Также это могут быть полностью кастомные модули, написанные для конкретного заказчика.

В исходном коде модуль оформлен отдельным gradle (IDEA / Eclipse) подпроектом, который собирается в отдельный jar -файл. Какой конкретно модуль попадет в сборку для определенного заказчика зависит от конфигурации билда.

Модуль выполняет задачи инсталляции, миграции и обновления окружения в методе install(), который вызывается ModuleService во время процесса инсталляции / обновления.

Модуль выполняет задачи по удалению информации о себе в системе через метод uninstall().

Метод start() выполняется во время старта модуля и может быть нужен для нестандартных инициализаций.

Метод stop() вызывается во время остановки модуля по каким-либо причинам и служит для нестандартного освобождения ресурсов.

Правила для модулей

Для всех модулей действуют несколько правил:

  • Модуль имеет строковый id, совпадающий с именем корневого пакета модуля (например, org.unidata.mdm.data). По этому id он доступен из ModuleService.

  • Модуль должен уметь вернуть сведения о себе — свою версию, состоящую из major.minor.revision, локализованное имя, локализованное описание, тэг, потенциально указывающий на специфичность сборки, точки интеграции, которые он поддерживает (Pipelines).

  • Если модуль использует базу данных, он ведет свои данные в полностью изолированной схеме (например, org_unidata_mdm_data или org_unidata_mdm_core), имя которой совпадает с id модуля, где точки заменены знаком подчеркивания.

  • Если модуль пользуется базой данных, он держит свой пул соединений, который, однако, доступен для конфигурации из backend.properties в стиле org.unidata.mdm.data.datasource.minPoolSize = 10.

  • Модуль заполняет MANIFEST.MF в схожем со спецификацией OSGi стиле. Поддерживаются следующие атрибуты метаданных в META-INF/MANIFEST.MF:

Manifest-Version: 1.0
Unidata-Module-Class: org.unidata.mdm.data.module.DataModule
  • Модуль ведет свои миграции схемы данных самостоятельно. Инсталляция / обновление схемы производится при инсталляции / обновлении модуля самим модулем (предлагается перейти на database-migrator).

  • Модуль самостоятельно ведет свои i18n ресурсы.

  • Модуль определяет свои классы исключений (потребуется рефакторинг Unidata Exception API).

  • Модуль публикует поддерживаемые особенности (ModuleFeature), активность или неактивность которых регулируется действующей лицензией. Для этого модуль при старте взаимодействует с LicenseService, который считает из лицензии нужную информацию.

  • Модуль публикует поддерживаемые точки интеграции (Segment(s) в Pipelines).

  • Модуль хранит свои приватные настройки в module.properties, которые недоступны в других контекстах для других модулей и недоступны для редактирования во время выполнения.

  • Все расширения PostgreSQL устанавливаются в схему public, которая автоматически добавляется в search_path конфигурации всех датасорсов всех модулей.

Оформление модуля

Все модули должны придерживаться единых стандартов в структурировании кода.

  • Имена проектов модулей начинаются с имени корневого пакета модуля org.unidata.mdm.* или com.unidata.mdm.* (org.unidata.mdm.core, org.unidata.mdm.data, org.unidata.mdm.dq, org.unidata.mdm.data.rest, org.unidata.mdm.data.soap, etc.).

  • Все проекты размещают код в пакетах с общим префиксом — com.unidata.mdm для коммерческих модулей и org.unidata.mdm для модулей с открытым исходным кодом.

  • Следующим элементом пути должен быть подпакет детализации (core, model, dq, search, data, matching, job, classifier etc.), указывающий на назначение модуля. Этот сегмент имени пакета является корневым пакетом модуля и образует id модуля.

  • Приватные классы реализаций сервисов, DAO и подобного кода должны находится в подпакетах impl следующего уровня — com.unidata.mdm.dq.service.impl, com.unidata.mdm.dq.dao.impl, etc.

  • Следующей частью пути должны быть подпакеты категорий классов. См. таблицу ниже:

Таблица 1 - Описание подпакетов категорий классов

type

Представление данных в среднем слое, промежуточные и служебные типы, не предназначенные для транспортировки наверх или вниз. Ниже могут следовать подпакеты детализации (org.unidata.mdm.core.type.calculables, org.unidata.mdm.core.type.module, org.unidata.mdm.core.type.data, etc.). Иными словами, это место доменной модели.

dao

Если модуль взаимодействует с БД.

po

Если модуль взаимодействует с БД. Ниже могут следовать подпакеты детализации (org.unidata.mdm.core.po.security, org.unidata.mdm.core.po.module, etc.)

service

Интерфейсы публичных сервисов. Имплементации находятся в подпакете impl и имеют соответствующий суффикс.

dto result

Трансфер — объекты (возвращаемые типы). Ниже могут следовать подпакеты детализации (org.unidata.mdm.core.dto.security, org.unidata.mdm.core.dto.module, etc.). Классы должны заканчиваться на *Result и иметь общий префикс модуля.

util

Некатегоризированные статические утилиты

сonvert

Утилиты конвертации представления данных

module

Типы, относящиеся к метаданным самого модуля

exception

ID исключений модуля + локальные типы

context

Контексты (параметры вызовов сервисов). Классы должны заканчиваться на *Context и иметь общий префикс модуля.

configuration

Все, что относится к конфигурации модуля, в том числе, классы аннотированные @Configuration

migration

Миграции схемы данных

serialization

Для объектов, используемых при сериализации

Цель такой организации — сосредоточить все детали имплементации модуля внутри самого модуля, сделав его герметичным, и определить общий шаблон, позволяя быстрое вхождение каждого участника команды в любую область системы, в том числе, в ранее неизвестную.

Модули имеют несколько характеристик:

  • Модули бывают обязательными и опциональными. Обязательные модули всегда присутствуют в системе в той или иной реализации. Опциональные могут отсутствовать. Их работоспособность регулируется лицензией продукта.

  • Модули бывают стандартными или нестандартными (редакция). Это также регулируется метаданными лицензии.

  • На кастомизацию модуля может указывать тэг, когда он имеет значение, отличное от «standard».

  • Модули могут хранить приватные метаданные в пространстве данных лицензии.