Свой Spring bean (EventServer)
Когда из своего Java-кода нужно обращаться к lsFusion-системе — запустить фоновую задачу, поднять интеграционный сервер, обработать поток событий, выставить наружу RMI-сервис — это делается через Spring bean, наследующийся от иерархии EventServer.
Это второй из двух способов обращения из внутренней системы, параллельный пути через InternalAction.
Базовый класс
Bean наследуется от одного из двух классов:
MonitorServer— для большинства компонентов: планировщиков, socket-listener-ов, message-queue-консьюмеров, периодических задач.RmiServer— если bean должен быть доступен внешним клиентам как RMI-сервис.
Минимальные Java-переопределения: getEventName(), getLogicsInstance() и нужные lifecycle-хуки.
getEventName() должен возвращать константу (как правило, строковый литерал). Он вызывается из field-initializer-а EventServer (new TopExecutionStack(getEventName())) ещё до того, как отрабатывают field-инициализаторы и конструктор подкласса, поэтому опираться на DI-инжектированные значения или поля подкласса нельзя — на момент вызова они ещё не присвоены.
Потоки и stack
Каждый базовый класс задаёт свой thread-контекст: MonitorServer — monitor, RmiServer — RMI. От того, на каком потоке выполняется код, зависит, как получать ExecutionStack:
-
Свой пул задач. Создаётся через
ExecutorFactory.createMonitorThreadService(threads, this)илиcreateMonitorScheduledThreadService(threads, this)дляMonitorServer-bean-а;createRMIThreadService(threads, this)— дляRmiServer. Каждый поток в таком пуле автоматически входит и выходит из нужного thread-контекста; внутри задач работаетgetStack(). -
Внешний callback-поток (рабочие потоки RabbitMQ-клиента, WebSocket-библиотеки, и т.п. — не созданные через
ExecutorFactory). Контекст проставляется и снимается вокруг блока вручную; код обычно находится внутри anonymous class (DefaultConsumerи т.п.), поэтому в качествеMonitorServer-а явно передаётсяMyServer.this(а не простоthis, который сослался бы на anonymous instance):try {
ThreadLocalContext.aspectBeforeMonitorHTTP(MyServer.this);
try (DataSession session = createSession()) {
// getStack() работает здесь
...
}
} finally {
ThreadLocalContext.aspectAfterMonitorHTTP(MyServer.this);
}Внутри обрамления
getStack()валиден. -
Lifecycle-метод или одноразовый вызов (поток не в monitor- и не в RMI-контексте, оборачивать его не нужно). Используется
getTopStack()— top-level execution stack, привязанный к самому bean-у.
Экспорт по RMI
Полный шаблон RMI-сервиса — это RmiServer-bean, реализующий remote-интерфейс, плюс ручной экспорт через RmiManager.
Remote-интерфейс наследуется от lsfusion.interop.server.RmiServerInterface (а не от голого java.rmi.Remote); каждый remote-метод объявляет throws RemoteException:
public interface MyRemoteInterface extends RmiServerInterface {
String doSomething(String arg) throws RemoteException;
}
RmiServer-bean реализует этот интерфейс. Сам подкласс не экспортируется по RMI автоматически — экспорт делается вручную через RmiManager:
- в
onStarted:getLogicsInstance().getRmiManager().bindAndExport(name, this)— экспортирует объект и регистрирует его в RMI-registry под именем<exportName>/<name>, гдеexportNameзадан в платформенной конфигурацииrmiManager-а, аname— короткий идентификатор сервиса ("EquipmentServer"и т.п.). - в
onStopping:getLogicsInstance().getRmiManager().unbindAndUnexport(name, this)— снимает регистрацию и экспорт.
Если RmiManager используется часто, обычно добавляют helper-метод getRmiManager() на самом bean-е (см. EquipmentServer.getRmiManager() в ERP).
RMI-контекст внутри remote-метода обычно устанавливается автоматически — достаточно вызвать getStack() и работать с сессией. Явный ThreadLocalContext.assureRmi(this) можно добавить как защитный вызов в нестандартных путях, но обязательной ручной установки аспекта не требуется.
createSession() бросает SQLException, методы LP / LA / applyException — SQLException и SQLHandledException; в RMI-методе это обычно ловится одним блоком и заворачивается в RemoteException:
@Override
public String doSomething(String arg) throws RemoteException {
BusinessLogics BL = getLogicsInstance().getBusinessLogics();
try (DataSession session = createSession()) {
String result = "..."; // читать/писать свойства, выполнять действия — getStack() валиден
session.applyException(BL, getStack()); // обязательно, если что-то писали
return result;
} catch (SQLException | SQLHandledException e) {
throw new RemoteException("doSomething failed", e);
}
}
RMI-клиент находит сервис в registry по тому же <exportName>/<name>.
Жизненный цикл
Bean инжектируется через Spring (минимально достаточно logicsInstance (LogicsInstance); через него доступны getBusinessLogics(), getDbManager(), getRmiManager()). Стандартные хуки распределяются так:
afterPropertiesSet()(когда bean реализуетInitializingBean) — сразу после Spring DI; здесь толькоAssert.notNull(...)для проверки инжекции. Runtime платформы ещё не готов, открывать сессии нельзя.onInit(LifecycleEvent)— платформа начала инициализацию. Здесь резолвят модуль (getLogicsInstance().getBusinessLogics().getModule("MyModule")) и сохраняютLP/LA-обёртки в полях черезLM.findProperty(...)/LM.findAction(...).onStarted(LifecycleEvent)— платформа полностью поднята. Здесь стартуют фоновые потоки и listener-ы; дляRmiServer-bean-ов делаютbindAndExport.onStopping(LifecycleEvent)— корректно гасят свои потоки, дляRmiServerделаютunbindAndUnexport.
Если компонент должен подняться после основной платформы, в конструкторе передают super(DAEMON_ORDER).
Чтение, запись и выполнение
Внутри методов bean-а работа со свойствами и действиями идёт через DataSession и ExecutionStack (а не ExecutionContext, как у InternalAction). Аргументы-объекты передаются как DataObject (или более общий ObjectValue — DataObject или NullValue); записываемые значения свойств — обычные Java-значения встроенных классов (String, Integer, LocalDateTime и т.п.). Полный каталог классов и методов — в Java API для интеграций. LP / LA-обёртки обычно резолвят один раз в onInit и хранят в полях; здесь для краткости они получаются прямо в месте вызова:
BusinessLogics BL = getLogicsInstance().getBusinessLogics();
LP<?> someProperty = BL.findProperty("name[Class]");
LA<?> someAction = BL.findAction("name[Class]");
ExecutionStack stack = getStack();
DataObject keyObject = new DataObject(keyValue, (ConcreteCustomClass) BL.findClass("Class"));
try (DataSession session = createSession()) {
Object value = someProperty.read(session, keyObject);
someProperty.change(newJavaValue, session, keyObject);
someAction.execute(session, stack, keyObject);
session.applyException(BL, stack);
}
createSession() открывает новую сессию изменений; всё, что в неё записано, копится до явного применения — session.applyException(BL, stack) (бросает исключение при ошибке) или session.applyMessage(BL, stack) (возвращает текст ошибки или null). Если до применения вылетает исключение, try-with-resources откатит всё неприменённое.
Подключение в Spring
Чтобы подключить свой bean, проектный модуль кладёт lsfusion-bootstrap.xml в src/main/resources (он перекрывает платформенный одноимённый файл). В нём импортируется lsfusion.xml, объявляется bean и регистрируется в customLifecycleListeners:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
default-lazy-init="true">
<import resource="classpath:lsfusion.xml"/>
<bean id="dataSyncServer" class="myapp.controller.DataSyncServer">
<property name="logicsInstance" ref="logicsInstance"/>
</bean>
<util:list id="customLifecycleListeners">
<ref bean="dataSyncServer"/>
</util:list>
</beans>
<util:list id="customLifecycleListeners"> подменяет одноимённый пустой список из платформенного lsfusion.xml и перечисляет bean-ы, которые должны получать onInit / onStarted / onStopping.
Если bean дополнительно нужно делать доступным через getLogicsInstance().getCustomObject(MyClass.class) из произвольного места кода, его перечисляют ещё и в <util:list id="customObjects"> — это сервисный реестр платформы (карта Class → Object). На практике такие bean-ы часто регистрируются сразу в обоих списках.
Пример
DataSyncServer — periodic-задача, которая раз в минуту опрашивает внешнюю систему и обновляет статистику в lsFusion-свойствах. Резолвит свойства в onInit, запускает executor в onStarted, гасит executor в onStopping, применяет изменения в DataSession.
package myapp.controller;
import lsfusion.server.base.controller.lifecycle.LifecycleEvent;
import lsfusion.server.base.controller.manager.MonitorServer;
import lsfusion.server.base.controller.thread.ExecutorFactory;
import lsfusion.server.language.ScriptingErrorLog;
import lsfusion.server.language.ScriptingLogicsModule;
import lsfusion.server.language.property.LP;
import lsfusion.server.logics.BusinessLogics;
import lsfusion.server.logics.LogicsInstance;
import lsfusion.server.logics.action.session.DataSession;
import lsfusion.server.physics.admin.log.ServerLoggers;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import java.time.LocalDateTime;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DataSyncServer extends MonitorServer implements InitializingBean {
private static final Logger logger = ServerLoggers.startLogger;
private LogicsInstance logicsInstance;
private ScheduledExecutorService executor;
private LP<?> lastSyncedAt;
private LP<?> totalRecordCount;
public DataSyncServer() {
super(DAEMON_ORDER);
}
public void setLogicsInstance(LogicsInstance logicsInstance) { this.logicsInstance = logicsInstance; }
@Override public LogicsInstance getLogicsInstance() { return logicsInstance; }
@Override public String getEventName() { return "data-sync"; }
@Override
public void afterPropertiesSet() {
Assert.notNull(logicsInstance, "logicsInstance must be specified");
}
@Override
protected void onInit(LifecycleEvent event) {
ScriptingLogicsModule LM = getLogicsInstance().getBusinessLogics().getModule("DataSync");
Assert.notNull(LM, "DataSync module not found");
try {
lastSyncedAt = LM.findProperty("lastSyncedAt[]");
totalRecordCount = LM.findProperty("totalRecordCount[]");
} catch (ScriptingErrorLog.SemanticErrorException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onStarted(LifecycleEvent event) {
executor = ExecutorFactory.createMonitorScheduledThreadService(1, this);
executor.scheduleAtFixedRate(this::syncOnce, 0, 1, TimeUnit.MINUTES);
}
@Override
protected void onStopping(LifecycleEvent event) {
if (executor != null) executor.shutdownNow();
}
private void syncOnce() {
BusinessLogics BL = getLogicsInstance().getBusinessLogics();
try (DataSession session = createSession()) {
int recordCount = fetchExternalCount();
lastSyncedAt.change(LocalDateTime.now(), session);
totalRecordCount.change(recordCount, session);
session.applyException(BL, getStack());
} catch (Exception e) {
// periodic task: log and let the next tick retry
// (any uncaught exception, including runtime ones from fetchExternalCount,
// would suppress further runs of scheduleAtFixedRate)
logger.error("data sync failed", e);
}
}
private int fetchExternalCount() {
// call external system
return 0;
}
}
ExecutorFactory.createMonitorScheduledThreadService(threads, this) поднимает потоки в monitor-контексте, поэтому внутри задач работает getStack(). Исключения внутри задачи не пропускаются наружу — scheduleAtFixedRate подавит дальнейшие запуски, поэтому ошибки логируются.