Skip to main content

Message-Listener

Überblick​

Message-Listener ermöglichen es Integratoren, Aktionen asynchron auf der Serverseite auszuführen und den Client über den Fortschritt und das Ergebnis zu informieren. Anwendungsfälle sind beispielsweise das Speichern von clientseitigen Änderungen an Annotationen, der Export bzw. Druck eines Dokuments, das Auswerten von Attributen der serverseitigen HttpSession oder beliebige eigene Backend-Aufrufe.

Ein Message-Listener besteht aus drei Bestandteilen:

  • einem Nachrichtennamen, über den Client und Server die Nachricht identifizieren (z.B. EXPORT),
  • einem DTO, das die Parameter der Anfrage transportiert, und
  • einer Implementierung des Interfaces com.levigo.jadice.web.transport.server.messaging.MessageListener, die die Anfrage verarbeitet.

Hinweis: Message-Listener ersetzen die frühere ServerOperation-API. Die Interfaces ServerOperation, ServerOperationParameters, ServerOperationMessage sowie die ServerOperationRegistry stehen nicht mehr zur Verfügung.

Funktionsweise​

Der Client eröffnet eine Konversation: Er sendet eine Nachricht mit einem Nachrichtennamen und einer Payload an den Server. Jede Konversation besitzt eine ID, über die alle zugehörigen Nachrichten einander zugeordnet werden.

Serverseitig wird die Payload anhand des generischen Typparameters des Listeners (MessageListener<T>) deserialisiert und an die Methode consume(T, IncomingMessageContext) übergeben. Diese Methode wird auf dem Thread aufgerufen, der die eingehende Nachricht verarbeitet. Länger laufende Arbeit ist daher in einen ExecutorService auszulagern; zurückgegeben wird ein CancelHandler, über den eine laufende Verarbeitung abgebrochen werden kann.

Der Server kann eine Konversation mit beliebig vielen Nachrichten beantworten (IncomingMessageContext#reply) und so insbesondere bei lang laufenden Operationen den Fortschritt zurückmelden. Beendet wird eine Konversation mit einer End-of-Conversation-Nachricht (EOC), die über sendSuccess(), sendFail(...) oder sendEOC(...) erzeugt wird.

Pro Nachrichtenname kann genau ein Listener registriert werden. Eine zweite Registrierung desselben Namens wird mit einer IllegalArgumentException abgelehnt.

Der IncomingMessageContext​

MethodeBeschreibung
reply(String messageName, Object payload)Sendet eine Nachricht innerhalb der laufenden Konversation an den Client, z.B. ein Zwischenergebnis oder eine Fortschrittsmeldung.
sendSuccess()Beendet die Konversation und signalisiert dem Client die erfolgreiche Verarbeitung.
sendFail(Throwable) bzw. sendFail(String, Throwable)Beendet die Konversation mit einer Fehlermeldung.
sendEOC(boolean success, String errorMessage, ObjectNode metadata)Beendet die Konversation; erlaubt zusätzlich das Mitsenden von Metadaten.
done()Markiert die Konversation als abgeschlossen und gibt die zugehörigen Referenzen frei. Ein Abbruch ist danach nicht mehr möglich. Wird von den sendEOC-Varianten automatisch aufgerufen.
getActive()Liefert einen BooleanSupplier, über den geprüft werden kann, ob die Konversation noch aktiv ist (z.B. nicht abgebrochen wurde).
getMessageContext()Zugriff auf den MessageContext mit HttpSession, Principal, Client-ID, Request-URI und Query-String der Verbindung.
getClient()Der Client, der die Nachricht gesendet hat.
getConversationId()Die ID der laufenden Konversation.

Implementierung eines eigenen Message-Listeners​

Zunächst wird ein DTO für die Anfrage definiert. Die Serialisierung erfolgt über Jackson, ein Default-Konstruktor sowie Getter und Setter sind daher erforderlich — im Beispiel über Lombok erzeugt:

@Data
@NoArgsConstructor
public final class CustomRequestDTO {
private String name;
}

Anschließend wird der Listener implementiert. Der Nachrichtenname wird über die Annotation @MessageName festgelegt, @Component sorgt in einer Spring-Umgebung für die automatische Registrierung:

@Component
@MessageName("CUSTOM")
public class CustomMessageListener implements MessageListener<CustomRequestDTO> {

@Autowired
private ExecutorService executorService;

@Override
public CancelHandler consume(CustomRequestDTO dto, IncomingMessageContext ctx) { // (1)
final Future<?> future = executorService.submit(() -> this.run(dto, ctx)); // (2)
return () -> future.cancel(true); // (3)
}

private void run(CustomRequestDTO dto, IncomingMessageContext ctx) {
try {
final CustomResponseDTO response = new CustomResponseDTO();
response.setGreeting("Hallo, " + dto.getName() + "!");

ctx.reply("CUSTOM_RESPONSE", response); // (4)
ctx.sendSuccess(); // (5)
} catch (Exception e) {
try {
ctx.sendEOC(false, e.getMessage(), null); // (6)
} catch (Throwable ignored) {
}
} finally {
ctx.done(); // (7)
}
}
}
  1. Diese Methode muss implementiert werden. Sie wird auf dem verarbeitenden Thread der eingehenden Nachricht aufgerufen und sollte daher schnell zurückkehren.
  2. Die eigentliche Verarbeitung wird asynchron ausgeführt. In einer Spring-Umgebung kann der vom jadice web toolkit bereitgestellte ExecutorService injiziert werden.
  3. Der zurückgegebene CancelHandler wird aufgerufen, wenn der Client die Konversation abbricht.
  4. Antwortnachricht an den Client. Der Nachrichtenname ist frei wählbar und wird clientseitig ausgewertet. Auf diesem Weg können auch mehrere Zwischenergebnisse gesendet werden.
  5. Erfolgreicher Abschluss der Konversation.
  6. Im Fehlerfall wird die Konversation mit einer Fehlermeldung beendet. Der Client erhält die Meldung als Fehler der Konversation.
  7. done() gibt die Referenzen der Konversation frei. Die sendEOC-Varianten rufen die Methode bereits selbst auf; ein zusätzlicher Aufruf ist unschädlich.

Enthält eine eingehende Nachricht neben der Payload einen separaten Dokumentteil, wird stattdessen die Überladung consume(T, IncomingMessageContext, Object) aufgerufen. Deren Default-Implementierung wirft eine IllegalArgumentException; Listener für solche Nachrichten müssen sie daher überschreiben.

Registrierung​

In einer Spring-Umgebung genügt die Annotation @Component zusammen mit @MessageName — alle MessageListener-Beans werden beim Start eingesammelt und registriert (siehe Spring Boot / Spring Framework).

Ohne Spring werden die Listener manuell erzeugt, mit ihren Abhängigkeiten versorgt und über JWTServerContext#registerMessageListeners(List) registriert (siehe Beispiel). Auch in diesem Fall ist die Annotation @MessageName erforderlich, da der Nachrichtenname daraus ermittelt wird. Alternativ lässt sich ein Listener auch direkt — ohne die reflektionsbasierte @MessageName-Auswertung — über ServerMessageManager.get().registerMessage(name, descriptor) registrieren; der Nachrichtenname und die DTO-Klasse werden in diesem Fall explizit angegeben.

Werden beide Wege kombiniert, ist die Reihenfolge zu beachten: registerMessageListeners(List) ruft intern ServerMessageManager#reset() auf und verwirft damit alle bereits registrierten Nachrichten. Der Aufruf muss daher vor registerDefaultMessages() und vor eigenen registerMessage(...)-Aufrufen erfolgen. Pro Nachrichtenname ist nur eine Registrierung möglich; eine zweite wird mit einer IllegalArgumentException abgelehnt.

Aufruf vom Client​

Angular / TypeScript​

Über ServerConnection.get().initConversation(...) wird eine Konversation eröffnet. Der Aufruf liefert ein Observable, das die Antwortnachrichten des Servers ausgibt; die EOC-Nachricht beendet den Stream bzw. führt im Fehlerfall zu einem error:

ServerConnection.get()
.initConversation("CUSTOM", { name: "User" })
.subscribe((message) => {
if (message.msgName === "CUSTOM_RESPONSE") {
console.log(message.payload.greeting);
}
});

Wird das Abonnement vor dem Ende der Konversation beendet, sendet der Client automatisch einen Abbruch an den Server; dort wird der vom Listener zurückgegebene CancelHandler aufgerufen.

GWT​

Im GWT-Client wird eine Nachricht über Message.create(...) erzeugt und mit einem ConversationListener versendet. Der Rückgabewert erlaubt den Abbruch der Konversation:

final Message message = Message.create("CUSTOM", requestDTO);

final ServerConnection.Cancelable conversation = ServerConnection.get().send(message, false, response -> {
if ("CUSTOM_RESPONSE".equals(response.getMessageName())) {
final CustomResponseDTO dto = response.getPayload().cast();
// ...
} else if (DefaultMessageNames.EOC.equals(response.getMessageName())) {
final ConversationEndDTO eoc = response.getPayload().cast();
// Auswertung von eoc.wasSuccessful()
}
});

Zugriff auf HttpSession, Principal und Dokument​

Der IncomingMessageContext stellt über getMessageContext() den Kontext der Verbindung bereit. Darüber sind HttpSession (getHTTPSession()), der angemeldete Benutzer (getUserPrincipal()), die Client-ID (getClientID()) sowie Request-URI und Query-String zugänglich. Ein Umweg über eine Factory, wie er in der früheren ServerOperation-API nötig war, entfällt damit.

Übermittelt der Client einen Dokument-Snapshot als Teil der Payload, lässt sich daraus serverseitig ein Document erzeugen. Verwendet wird dafür der DocumentSnapshotConverter (convert(IncomingMessageContext, DocumentSnapshotDTO)), wie es beispielsweise der Export tut.

Instanziierung mittels einer ContextualFactory​

Das Interface ContextualFactory wird weiterhin für DocumentDataProvider verwendet. Es erlaubt, eine Instanz abhängig vom Kontext des Aufrufs zu erzeugen: Die Methode create(InvocationContext) wird auf dem Request-Thread ausgeführt und kann bei einem ServletInvocationContext u.a. auf die HttpSession und den Principal zugreifen.

@Component
public class MyDocumentDataProviderFactory
implements ContextualFactory<DocumentDataProvider<MySource, MyPageSegmentHandle>> {

@Override
public DocumentDataProvider<MySource, MyPageSegmentHandle> create(InvocationContext context) {
final ServletInvocationContext servletContext = (ServletInvocationContext) context;
return new MyDocumentDataProvider(servletContext.getSession().getId());
}
}

Zu beachten ist, dass der InvocationContext nur innerhalb der Methode create gültig ist. Ein späterer Zugriff führt zu einer IllegalStateException; benötigte Werte sind daher innerhalb der Methode auszulesen und dem erzeugten Objekt mitzugeben.

Standard-Nachrichtennamen​

Die vom jadice web toolkit selbst verwendeten Nachrichtennamen sind in com.levigo.jadice.web.transport.shared.messaging.DefaultMessageNames zusammengefasst, u.a.:

NachrichtennameBedeutung
CREATE_DOCUMENTAnlegen eines Dokuments
GET_TEXT_CONTENT, TEXT_CONTENT, TEXT_BLOCK, TEXT_LINESAnfrage und Antworten zum Textinhalt
TEXT_SEARCH, TEXT_SEARCH_RESULT, TEXT_SEARCH_PROGRESSTextsuche inkl. Fortschrittsmeldungen
GET_ANNO_PROFILE, ANNO_PROFILEAnfrage und Antwort zu Annotationsprofilen
GET_INSTRUCTIONS, GET_INSTRUCTIONS_FINISHEDLinks und PDF-Bookmarks
EXPORT, EXPORT_FINISHEDExport eines Dokuments
AUTHENTICATION_INFO, ADDITIONAL_HTTP_HEADERSAuthentifizierungsinformationen und zusätzliche HTTP-Header
LOG_REMOTEÜbermitteln clientseitiger Logausgaben
CANCELAbbruch laufender Konversationen
EOCEnde einer Konversation

Eigene Nachrichtennamen dürfen mit keinem der bereits registrierten Namen kollidieren.

Weiterführende Informationen​

Ein eigenes Kapitel zur Realisierung des Exports und des Drucks findet sich in Export aus der Webanwendung heraus.