Динамический Java-код
Модуль ru.bgcrm.dynamic компилирует Java-исходники во время работы ERP и загружает полученные классы без пересборки приложения.
Он используется для клиентской логики, которая поставляется отдельно от ядра: обработчиков событий, задач планировщика, обработчиков модуля Task, провайдеров и других реализаций интерфейсов ERP.
Динамический код не является отдельным процессом и не изолирован от приложения. Он выполняется в JVM ERP, видит classpath текущей поставки и получает те же права на память, файловую систему и внешние ресурсы, что и сервер.
Исходники и настройки
Исходники находятся в каталоге, заданном относительно рабочей директории приложения:
dynamic.src.dir=dyn
dynamic.src.encoding=UTF-8
dynamic.src.dir по умолчанию равен dyn.
Если новая настройка отсутствует, поддерживается устаревшее имя dyn.src.dir.
Кодировка по умолчанию — UTF-8.
Путь к файлу должен соответствовать полному имени класса.
Например, класс ru.bgcrm.dyn.example.ProcessHandler размещается в файле:
<dynamic.src.dir>/ru/bgcrm/dyn/example/ProcessHandler.java
Платформенный метод DynamicClassManager.getClass(…) считает динамическими только имена с префиксом ru.bgcrm.dyn..
Поэтому прикладные классы размещаются в этом пакете и пишутся на Java.
Каталог должен присутствовать на каждом инстансе ERP. Модуль не хранит исходники или скомпилированный байткод в базе данных и не распространяет их между узлами.
Запуск сервера
При старте выполняется следующая последовательность:
-
DynamicClassManagerрекурсивно находит все.java-файлы вdynamic.src.dir; -
CompilerWrapperзапускает системныйjavacиз JDK; -
успешный результат загружается отдельным classloader;
-
публикуется
DyncodeRecompiledEventсо списком запрошенных классов; -
выполняются классы
Runnableиз настройкиrunOnStart; -
создаются классы из
createOnStart, которые обычно регистрируют listeners.
Для запуска требуется JDK, а не JRE: CompilerWrapper получает компилятор через ToolProvider.getSystemJavaCompiler().
Classpath берётся из системного свойства java.class.path, а каталог исходников передаётся javac как -sourcepath.
Предупреждения о deprecated API включены параметром -Xlint:deprecation.
Ошибка компиляции записывается в журнал и в состояние сервера, но сама по себе не останавливает запуск ERP. На свежем процессе классы с ошибками недоступны, поэтому зависящие от них listeners, задачи и провайдеры также не загрузятся.
Полная и выборочная компиляция
Полная компиляция
DynamicClassManager.recompileAll() собирает весь каталог в новый набор классов.
Текущая карта классов и актуальный classloader заменяются только после успешной компиляции.
Если сервер до этого уже работал и полная сборка завершилась ошибкой, ранее загруженный набор остаётся доступен.
Полная перекомпиляция через административный экран дополнительно вызывает EventProcessor.subscribeDynamicClasses().
Он удаляет старые подписки классов из createOnStart и создаёт их новые экземпляры.
Выборочная компиляция
DynamicClassManager.recompile(classNames) передаёт javac выбранные исходники.
Компилятор может также собрать найденные через -sourcepath зависимые исходники; все полученные .class-файлы, включая вложенные и анонимные классы, помещаются в карту загруженных классов.
При успешной сборке выбранные классы заменяются, а остальные записи карты сохраняются.
Уже созданные объекты автоматически не меняют свой класс: их необходимо создать заново.
В частности, выборочная компиляция не перерегистрирует listeners из createOnStart.
Событие DyncodeRecompiledEvent содержит имена, которые запросили перекомпилировать, а не полный список файлов, дополнительно собранных javac.
Потребитель события должен сравнивать именно эти имена со своими зависимостями.
Загрузка классов
Java не позволяет заменить определение уже загруженного класса.
Поэтому каждая успешная компиляция создаёт новый DatabaseClassLoader, а DynamicClassManager обновляет ссылки на новые объекты Class<?>.
Название DatabaseClassLoader историческое.
Текущая реализация не обращается к БД: на вход classloader получает Map<String, byte[]> с байткодом только что выполненной компиляции.
Временный каталог с .class-файлами удаляется после чтения байткода в память.
Файл ReadWriteClassManager.java целиком закомментирован и не участвует в работе приложения.
Основные способы получить класс:
| API | Поведение |
|---|---|
|
Для имени |
|
Находит класс тем же способом и вызывает публичный конструктор без аргументов. |
|
Ищет точное имя только среди загруженных динамических классов и возвращает |
|
Возвращает загруженные классы, совместимые с заданным интерфейсом или базовым классом. Так формируются, например, списки обработчиков Task и задач планировщика. |
|
При каждом запросе делегирует загрузку текущему динамическому classloader. Используется там, где библиотеке нужно один раз передать объект |
После выборочной компиляции актуальный classloader содержит байткод, полученный именно этой операцией, тогда как полная карта DynamicClassManager также сохраняет ранее загруженные классы.
Поэтому DynamicClassLoaderFacade может не найти несвязанный динамический класс, который не попал в выборочную сборку.
Если компонент загружает произвольные классы через facade, после изменения выполняйте полную компиляцию каталога.
Не сохраняйте экземпляр динамического класса или его Class<?> в долгоживущем кеше, если компонент должен подхватывать обновления.
Храните имя класса и разрешайте его через DynamicClassManager перед созданием объекта либо явно очищайте кеш по DyncodeRecompiledEvent.
Событие перекомпиляции
После успешной полной или выборочной компиляции менеджер синхронно публикует DyncodeRecompiledEvent.
Событие позволяет компонентам пересоздать объекты, которые удерживают старые версии классов.
Пример подписки:
EventProcessor.subscribe(this::onRecompile, DyncodeRecompiledEvent.class);
private void onRecompile(DyncodeRecompiledEvent event, ConnectionSet connectionSet)
{
if (event.getClasses().contains(handlerClassName))
{
reloadHandler();
}
}
Обработчик должен завершаться быстро.
Если обновление требует долгой работы после успешного commit, используйте AsyncWorker.
Административные операции
Экран Администрирование → Динамический код обслуживает DynamicAction.
Права назначаются отдельно на каждую операцию: просмотр дерева и файла, сохранение, компиляцию, удаление и запуск.
| Операция | Результат |
|---|---|
|
Компилирует весь каталог и повторно создаёт классы из |
|
Временно сохраняет текст, компилирует выбранный класс и при диагностической ошибке возвращает прежнее содержимое файла. |
|
Только сохраняет исходник. Загруженная версия не меняется до компиляции. |
|
Компилирует выбранный исходник и запускает его через |
|
Без параметра |
Операция /admin/dynamic.do?action=runDynamicClass является внутренним административным входом, а не публичным API интеграции.
Она требует авторизованную сессию и отдельное право ERP.
Редактор изменяет файлы непосредственно в dynamic.src.dir текущего приложения.
Правка через экран не заменяет поставку исходников из репозитория и может быть потеряна при следующем развёртывании.
Глобальные скрипты
GlobalScriptInvoker поддерживает два контракта.
Класс, реализующий IGlobalScript, получает Setup и JDBC-соединение:
public class RepairScript extends GlobalScriptBase
{
@Override
public void execute(Setup setup, Connection connection) throws Exception
{
print("Скрипт запущен");
}
}
Аргументы из административной формы доступны через getParams().
Вывод print(…) и error(…) возвращается пользователю и одновременно записывается в серверный лог.
После успешного execute(…) соединение фиксируется вызовом commit().
Класс без IGlobalScript должен иметь статический метод main(String[] args).
Для новых служебных операций предпочтительны типизированный action, команда сервиса или задача планировщика: они дают явные права, транзакционные границы и наблюдаемость.
Использование в прикладном коде
Динамическая реализация должна реализовывать контракт, объявленный в ядре или плагине.
Типичный потребитель хранит имя класса в конфигурации и создаёт объект через DynamicClassManager:
ProcessHandler handler = DynamicClassManager.newInstance(className);
handler.handle(processId);
Учитывайте совместимость при изменении контрактов ядра. Динкод компилируется вместе с текущей поставкой, поэтому удаление метода, изменение сигнатуры или Kotlin default-метод в интерфейсе могут сломать Java-реализации вне этого репозитория. Перед поставкой изменения найдите реализации в dyn-проектах и соберите их против новой версии ядра.
Не размещайте в динамическом коде платформенную функциональность, нужную всем установкам. Такой код должен находиться в обычном модуле и проходить общую сборку и тесты.
Диагностика
На экране Статус сервера блок Динамический код показывает:
-
число найденных исходников;
-
число загруженных классов, включая вложенные и анонимные;
-
число исходников, для которых не найден загруженный класс;
-
число компиляций после старта;
-
время, длительность, тип и инициатора последней компиляции;
-
ошибки и предупреждения
javacс файлом, строкой и колонкой.
При диагностике проверяйте в таком порядке:
-
фактическое значение
dynamic.src.dirи наличие каталога на нужном инстансе; -
соответствие пути исходника полному имени и пакету класса;
-
ошибки последней компиляции в Статусе сервера и серверном логе;
-
наличие класса среди загруженных и совместимость с ожидаемым интерфейсом;
-
перерегистрацию или пересоздание объекта после обновления;
-
настройки
createOnStart,runOnStart, планировщика или Task, которые запускают класс.
Если исходников меньше ожидаемого, проверьте рабочую директорию процесса: относительный dynamic.src.dir разрешается от неё.
Если исходников и загруженных классов разное число, учитывайте, что один .java может создать несколько .class; признаком недобора служит отдельный счётчик нескомпилированных исходников.
Проверка перед поставкой
Минимальная проверка изменения динамического кода:
-
полная компиляция каталога проходит на JDK 21 без ошибок;
-
предупреждения компилятора разобраны, особенно
deprecation; -
проверен фактический вход: событие, Task, планировщик, provider или административный вызов;
-
после повторной компиляции потребитель использует новый класс, а не старый объект из кеша;
-
операции записи проверены в ожидаемой транзакции;
-
для нескольких инстансов исходники доставлены и скомпилированы на каждом из них;
-
секреты и персональные данные не попадают в вывод глобального скрипта и серверный лог.
Система событий и таймаут динамических listeners описаны в документации системы событий. Общие правила выбора механизма расширения приведены в разделе «Расширение функционала».