Endpunkte
Enterprise Integration Patterns · Gruppe 7 von 8
Endpunkte sind die Stellen, an denen eine Anwendung mit dem Messaging-System in Berührung kommt. Gruppe 2 hat mit dem Message Endpoint das Grundprinzip gezeigt. Diese Gruppe beschreibt, wie Endpunkte im Detail gebaut werden. Einige Muster halten den Messaging-Code aus der Geschäftslogik heraus, andere regeln, wie ein Empfänger an Nachrichten kommt, wie mehrere Empfänger sich die Last teilen oder ihre Nachrichten auswählen, und wie Empfänger Ausfälle und Duplikate unbeschadet überstehen.
- Anwendung
- Integrationskomponente
- Kanal
- Nachricht
- Datenbank / Speicher
- 47. Messaging Gateway
- 48. Messaging Mapper
- 49. Transactional Client
- 50. Polling Consumer
- 51. Event-Driven Consumer
- 52. Competing Consumers
- 53. Message Dispatcher
- 54. Selective Consumer
- 55. Durable Subscriber
- 56. Idempotent Receiver
- 57. Service Activator
47. Messaging Gateway · Messaging-Zugang
Die Idee
Ein Messaging Gateway verbirgt das Messaging vollständig hinter einer normalen, fachlichen Schnittstelle. Die Geschäftslogik ruft eine Methode wie lager.reserviere(artikel) auf und weiß nicht, dass dahinter eine Nachricht erzeugt, in einen Kanal gelegt und eine Antwort abgewartet wird. Das Gateway ist die konkrete Ausprägung des Message Endpoint aus Gruppe 2.
Beispiel
Im Shop gibt es ein Interface Lager mit den Methoden reserviere() und bestand(). Die Implementierung ist ein Messaging Gateway, das daraus Command Messages bzw. Request-Reply-Anfragen macht. Für Tests wird das Gateway durch eine einfache Attrappe ersetzt, die ohne Broker auskommt. Wechselt die Firma später zu einem anderen Messaging-System, bleibt die Geschäftslogik unverändert.
Worauf achten
Das Gateway kann synchron sein, die Methode wartet also auf die Antwort, oder asynchron, dann liefert sie sofort ein CompletableFuture oder nimmt einen Rückruf entgegen. Die synchrone Variante ist bequemer, verdeckt aber, dass ein Aufruf langsam sein oder ausfallen kann. Messaging-typische Fehler wie Zeitüberschreitungen sollte das Gateway in fachliche Ausnahmen übersetzen, statt Broker-Ausnahmen bis in die Geschäftslogik durchzureichen.
In der Praxis
@MessagingGateway in Spring Integration, das aus einem Interface automatisch ein Gateway erzeugt, ProducerTemplate und Proxys über @Produce in Camel. Das Gateway verbindet die GoF-Muster Facade und Proxy mit der Messaging-Welt.
48. Messaging Mapper · Nachrichtenabbildung
Die Idee
Im Programm sind Daten als Objekte organisiert, die aufeinander verweisen: Eine Bestellung kennt ihren Kunden und ihre Positionen. Eine Nachricht dagegen ist ein flaches, in sich abgeschlossenes Dokument. Der Messaging Mapper übersetzt zwischen beiden Welten: Er baut aus Domänenobjekten eine Nachricht und aus einer empfangenen Nachricht wieder Objekte. Die Domänenklassen selbst wissen nichts von Nachrichten.
Beispiel
Die Klasse Bestellung hat eine Referenz auf ein Kunde-Objekt und eine Liste von Position-Objekten. Der Mapper entscheidet, was in die Nachricht gehört: vom Kunden nur die Nummer, von den Positionen Artikel und Menge. Daraus entsteht ein JSON-Dokument. Auf der Empfängerseite baut ein Mapper daraus wieder passende Objekte.
Worauf achten
Das Muster ist eng verwandt mit dem Data Mapper aus Fowlers PoEAA, der dasselbe für Datenbanken leistet. Die Kernfragen sind ähnlich: Wie tief folgt man Verweisen? Was passiert mit Zyklen? Was wird mitgeschickt, was nur als ID? Wichtig ist, die Nachricht als eigenen Vertrag zu behandeln. Wer interne Klassen einfach automatisch serialisiert, macht jede Änderung an einer internen Klasse zu einer Änderung der Schnittstelle nach außen.
In der Praxis
Jackson und JSON-B in Java, MapStruct für die Abbildung zwischen Domänenobjekten und eigenen Nachrichtenklassen, Protobuf und Avro mit generierten Nachrichtenklassen. In Java 21 sind Records ideal als eigene, unveränderliche Nachrichtenklassen, getrennt von den Domänenklassen.
49. Transactional Client · Transaktionaler Client
Die Idee
Ein Transactional Client fasst mehrere Messaging-Schritte, und oft auch Datenbankzugriffe, zu einer Transaktion zusammen. Entweder gelingen alle Schritte, oder keiner gilt als ausgeführt. Eine empfangene Nachricht wird erst dann endgültig aus dem Kanal entfernt, wenn die Verarbeitung vollständig erfolgreich war.
Beispiel
Das Lager empfängt eine Bestellung, bucht den Bestand in seiner Datenbank und schickt eine Bestätigung. Stürzt das Programm nach dem Buchen ab, aber vor dem Senden, wäre ohne Transaktion der Bestand verringert, aber niemand erführe davon. Mit Transaktion wird alles zurückgerollt: Die Buchung wird verworfen, die Bestellung liegt wieder im Kanal und wird erneut zugestellt.
Worauf achten
Eine Transaktion nur über Messaging-Schritte ist meist einfach, weil der Broker sie unterstützt. Schwierig wird es, wenn Broker und Datenbank gemeinsam in einer Transaktion stehen sollen. Verteilte Transaktionen (XA, Zwei-Phasen-Commit) sind langsam und fehleranfällig und werden heute meist vermieden. Verbreitete Alternativen: das Outbox-Muster, bei dem die ausgehende Nachricht zunächst in derselben Datenbanktransaktion gespeichert und danach verschickt wird, und idempotente Empfänger, die doppelte Zustellung verkraften (Muster 56).
In der Praxis
Transaktionale Sessions in JMS (Session.SESSION_TRANSACTED), @Transactional zusammen mit JmsTransactionManager in Spring, Transaktionen und „exactly once“-Semantik in Kafka. Für die Kombination mit der Datenbank in der Praxis meist das Outbox-Muster, etwa mit Debezium.
50. Polling Consumer · Abfragender Empfänger
Die Idee
Ein Polling Consumer holt Nachrichten aktiv ab: Er fragt den Kanal, ob etwas da ist, und nimmt sich die nächste Nachricht, wenn er dazu bereit ist. Wie jemand, der ab und zu in den Briefkasten schaut. Der Empfänger steuert damit selbst sein Tempo.
Beispiel
Das Lager fragt alle fünf Sekunden nach neuen Bestellungen und nimmt sich jeweils bis zu 50 auf einmal. In der Mittagspause, wenn wenig Personal da ist, wird das Intervall verlängert. Nachts verarbeitet ein Stapellauf alle Rückstände in einem Rutsch. Der Shop merkt davon nichts: Er legt Bestellungen einfach in den Kanal.
Worauf achten
Der Vorteil ist die Kontrolle: Der Empfänger wird nie mit mehr Arbeit überschüttet, als er bewältigen kann. Man spricht auch von Backpressure. Der Nachteil: Häufiges Nachfragen kostet Ressourcen, auch wenn nichts da ist, und seltenes Nachfragen verzögert die Verarbeitung. Abhilfe schafft Long Polling: Der Empfänger fragt und wartet eine gewisse Zeit, bis etwas kommt. Das Gegenstück ist der Event-Driven Consumer (Muster 51).
In der Praxis
MessageConsumer.receive(timeout) in JMS, KafkaConsumer.poll(), das Kafka grundsätzlich nach diesem Prinzip arbeitet, ReceiveMessage mit Long Polling in Amazon SQS. In Java ist BlockingQueue.poll(timeout, unit) das Gleiche im Kleinen.
51. Event-Driven Consumer · Ereignisgesteuerter Empfänger
Die Idee
Ein Event-Driven Consumer wartet passiv. Er meldet beim Messaging-System eine Rückrufmethode an, und sobald eine Nachricht eintrifft, ruft das System diese Methode auf. Wie eine Türklingel statt regelmäßig in den Briefkasten zu schauen.
Beispiel
Das Lager registriert einen Listener für den Kanal bestellungen. Trifft eine Bestellung ein, wird onMessage() sofort aufgerufen, und die Kommissionierung beginnt ohne Verzögerung. Ist nichts los, verbraucht der Listener keine Rechenzeit.
Worauf achten
Der Vorteil ist die sofortige Reaktion ohne unnötige Abfragen. Der Nachteil: Der Empfänger hat weniger Kontrolle über sein Tempo. Kommen plötzlich tausend Nachrichten auf einmal, muss die Infrastruktur dafür sorgen, dass er nicht überlastet wird, etwa über eine begrenzte Zahl gleichzeitiger Aufrufe oder einen Puffer. In der Praxis verwischt die Grenze zum Polling Consumer: Viele Listener-Frameworks fragen im Hintergrund selbst ab und rufen dann die Anwendung auf.
In der Praxis
MessageListener.onMessage() in JMS, @JmsListener, @KafkaListener und @RabbitListener in Spring, Message-Driven Beans in Jakarta EE, Cloud-Funktionen wie AWS Lambda, die durch Nachrichten ausgelöst werden.
52. Competing Consumers · Konkurrierende Empfänger
Die Idee
Mehrere gleichartige Empfänger lesen aus demselben Point-to-Point Channel und konkurrieren um die Nachrichten. Jede Nachricht bekommt genau einer von ihnen, nämlich der, der gerade frei ist. So verteilt sich die Last automatisch, und die Verarbeitung lässt sich einfach durch weitere Empfänger beschleunigen.
Beispiel
Im Weihnachtsgeschäft kommen zehnmal mehr Bestellungen als sonst. Statt das Lagersystem umzubauen, startet der Betrieb zusätzliche Worker, die alle aus derselben Queue lesen. Im Januar werden sie wieder abgeschaltet. Fällt ein Worker aus, übernehmen die anderen seine Arbeit, denn eine Nachricht, deren Verarbeitung nicht bestätigt wurde, wird erneut zugestellt.
Worauf achten
Competing Consumers sind die einfachste Form der Skalierung im Messaging, mit zwei typischen Nebenwirkungen. Erstens geht die Reihenfolge verloren: Worker 2 kann vor Worker 1 fertig werden. Ist die Reihenfolge wichtig, etwa für alle Nachrichten eines Kunden, müssen zusammengehörige Nachrichten immer beim selben Worker landen. Kafka erreicht das über Partitionen. Zweitens müssen die Worker ohne gemeinsamen Zustand im Speicher auskommen, weil jede Nachricht bei einem anderen Worker landen kann.
In der Praxis
Mehrere Listener auf einer JMS- oder RabbitMQ-Queue, eine Kafka-Consumer-Group mit mehreren Mitgliedern, automatische Skalierung von Containern anhand der Queue-Länge, etwa mit KEDA in Kubernetes.
53. Message Dispatcher · Nachrichtenverteiler
Die Idee
Ein Message Dispatcher ist ein einzelner Empfänger am Kanal, der Nachrichten innerhalb einer Anwendung an mehrere Bearbeiter weiterreicht. Für das Messaging-System gibt es nur einen Konsumenten. Intern entscheidet der Dispatcher, welcher Bearbeiter, etwa welcher Thread oder welche Klasse, eine Nachricht übernimmt.
Beispiel
Die Lageranwendung liest aus einem Kanal und hat drei interne Bearbeiter für Bücher, Technik und Frischware. Der Dispatcher holt jede Bestellung und übergibt sie dem passenden Bearbeiter, jeweils in einem eigenen Thread. So lassen sich die Bearbeiter getrennt entwickeln und unterschiedlich viele Threads für jeden einplanen.
Worauf achten
Der Unterschied zu Competing Consumers: Dort konkurrieren mehrere Empfänger direkt am Kanal, und das Messaging-System verteilt. Beim Dispatcher verteilt die Anwendung selbst. Das gibt mehr Kontrolle, etwa über die Zahl der Threads oder die Zuordnung nach Inhalt, und belastet den Broker mit nur einer Verbindung. Der Unterschied zum Content-Based Router: Der Dispatcher verteilt innerhalb einer Anwendung an Objekte, der Router zwischen Kanälen.
In der Praxis
Listener-Container in Spring mit einem Thread-Pool (concurrency), ein ExecutorService, an den ein Empfänger Aufgaben weitergibt, und in Java 21 besonders einfach mit virtuellen Threads: ein virtueller Thread pro empfangener Nachricht.
54. Selective Consumer · Wählerischer Empfänger
Die Idee
Ein Selective Consumer gibt beim Empfang ein Auswahlkriterium an, einen Selektor. Er erhält nur die Nachrichten aus dem Kanal, die dazu passen. Mehrere Empfänger mit unterschiedlichen Selektoren können sich so einen Kanal teilen, ohne sich gegenseitig Nachrichten wegzunehmen, die nicht für sie bestimmt sind.
Beispiel
Die Lager in Köln und Wien lesen aus einem gemeinsamen Bestellkanal. Köln gibt den Selektor land = 'DE' an, Wien land = 'AT'. Das Messaging-System wertet den Selektor aus und stellt jedem Lager nur seine Bestellungen zu.
Worauf achten
Selektoren arbeiten meist nur auf Header-Feldern, nicht auf dem Inhalt, weil der Broker den Body nicht versteht. Das ist ein Grund, routingrelevante Angaben in den Header zu schreiben. Vorsicht bei Nachrichten, auf die kein Selektor passt: Sie bleiben im Kanal liegen und können ihn verstopfen. Oft sind getrennte Kanäle (Datatype Channel) oder ein Content-Based Router die übersichtlichere Lösung. Der Selective Consumer verlagert dieselbe Entscheidung in den Empfänger.
In der Praxis
Message Selectors in JMS, ein SQL-ähnlicher Ausdruck wie land = 'DE' AND betrag > 100 beim Anlegen des Consumers, Subscription-Filter in Azure Service Bus, Filter-Policies für SQS-Abonnements bei Amazon SNS.
55. Durable Subscriber · Dauerhafter Abonnent
Die Idee
Bei einem normalen Publish-Subscribe Channel erhält ein Abonnent nur Nachrichten, die eintreffen, während er verbunden ist. Was in seiner Abwesenheit verschickt wurde, verpasst er. Ein Durable Subscriber dagegen hat ein dauerhaftes Abonnement: Das Messaging-System bewahrt seine Nachrichten auf, solange er offline ist, und stellt sie zu, sobald er sich wieder verbindet.
Beispiel
Ein Preisvergleichsdienst abonniert das Topic preise. Während eines Updates ist er eine halbe Stunde offline. In dieser Zeit ändern sich 300 Preise. Als dauerhafter Abonnent erhält er nach dem Neustart alle 300 Änderungen und ist wieder auf dem neuesten Stand. Das Shop-Frontend, das immer läuft, hat sie in der Zwischenzeit direkt bekommen.
Worauf achten
Dauerhafte Abonnements kosten Speicher: Meldet sich ein Abonnent nie wieder, sammeln sich seine Nachrichten unbegrenzt an. Abonnements müssen deshalb gepflegt und nicht mehr benötigte gelöscht werden, oder die Nachrichten erhalten eine Ablaufzeit. Ein Abonnent braucht eine feste Kennung, damit das System ihn nach dem Neustart wiedererkennt. Für Daten, bei denen nur der aktuelle Stand zählt, wie bei Preisen, ist es manchmal einfacher, nach dem Start den aktuellen Zustand neu abzufragen.
In der Praxis
createDurableSubscriber() bzw. createSharedDurableConsumer() in JMS, durable Queues pro Abonnent hinter einem Fanout-Exchange in RabbitMQ. In Kafka ist jeder Konsument im Grunde dauerhaft: Nachrichten werden ohnehin für eine einstellbare Zeit aufbewahrt, und jede Consumer Group merkt sich, wie weit sie gelesen hat.
56. Idempotent Receiver · Idempotenter Empfänger
Die Idee
In verteilten Systemen kann eine Nachricht mehrfach ankommen, zum Beispiel wenn der Sender nach einem Verbindungsabbruch nicht weiß, ob sie angekommen ist, und sie zur Sicherheit noch einmal schickt. Ein Idempotent Receiver ist so gebaut, dass eine wiederholte Nachricht keinen zusätzlichen Effekt hat. Ob er sie einmal oder fünfmal erhält, das Ergebnis ist dasselbe.
Beispiel
Die Buchhaltung erstellt für jede Bestellung eine Rechnung. Bestellung 17 kommt wegen eines Netzwerkfehlers zweimal an. Die Buchhaltung merkt sich die IDs aller verarbeiteten Nachrichten, erkennt die 17 wieder und verwirft das Duplikat. Der Kunde bekommt nur eine Rechnung.
Worauf achten
Es gibt zwei Wege zur Idempotenz. Erstens: Duplikate erkennen, indem man sich verarbeitete Nachrichten-IDs merkt. Das braucht einen dauerhaften Speicher und eine Regel, wann alte IDs gelöscht werden. Zweitens: Operationen von Natur aus idempotent gestalten. „Setze den Bestand auf 12“ ist idempotent, „verringere den Bestand um 2“ nicht. „Markiere Bestellung 17 als versandt“ ist idempotent, „lege eine Rechnung an“ nicht. Da Guaranteed Delivery in der Praxis fast immer „mindestens einmal“ bedeutet, ist Idempotenz eine der wichtigsten Eigenschaften robuster Empfänger.
In der Praxis
idempotentConsumer() in Camel mit einem Speicher für bereits gesehene IDs (im Speicher, JDBC, Redis), IdempotentReceiverInterceptor in Spring Integration, Datenbank-Constraints auf der Nachrichten-ID, Idempotency Keys in Zahlungs-APIs wie bei Stripe.
57. Service Activator · Dienstaktivierer
Die Idee
Ein Service Activator verbindet einen gewöhnlichen Dienst, also eine Methode oder Klasse ohne jede Messaging-Logik, mit einem Kanal. Er empfängt eine Nachricht, liest die Parameter heraus, ruft den Dienst auf und schickt das Ergebnis bei Bedarf als Antwortnachricht zurück. Der Dienst selbst bleibt unverändert und kann weiterhin auch direkt aufgerufen werden.
Beispiel
Das Lager hat eine Methode bestandPruefen(artikel), die von der Web-Oberfläche direkt aufgerufen wird. Damit auch der Shop sie per Messaging nutzen kann, wird ein Service Activator davorgesetzt. Er liest die Artikelnummer aus der Anfrage, ruft die Methode auf und schickt den Bestand als Antwort zurück. Am Lagerdienst ändert sich nichts.
Worauf achten
Der Service Activator ist das Gegenstück zum Messaging Gateway (Muster 47): Das Gateway macht aus einem Methodenaufruf eine Nachricht, auf der Seite des Senders. Der Service Activator macht aus einer Nachricht einen Methodenaufruf, auf der Seite des Empfängers. Mit beiden zusammen kann die Geschäftslogik auf beiden Seiten komplett frei von Messaging-Code bleiben. Fehler des Dienstes muss der Activator sinnvoll übersetzen, etwa in eine Fehlerantwort oder durch Verschieben in den Invalid Message Channel.
In der Praxis
@ServiceActivator in Spring Integration, bean() und to("bean:...") in Camel. Auch @JmsListener oder @KafkaListener an einer Methode, die nur einen fachlichen Dienst aufruft, ist im Kern ein Service Activator.
Die Endpunkte im Überblick
| Frage | Muster | Kurz gesagt |
|---|---|---|
| Wie bleibt die Geschäftslogik sauber? | Messaging Gateway | Senderseite: Methodenaufruf wird zur Nachricht |
| Service Activator | Empfängerseite: Nachricht wird zum Methodenaufruf | |
| Messaging Mapper | Objekte werden zu Nachrichten und zurück | |
| Wie bleibt alles konsistent? | Transactional Client | Empfangen, Verarbeiten und Senden als eine Einheit |
| Wie kommt die Nachricht zum Empfänger? | Polling Consumer | Empfänger holt ab, im eigenen Tempo |
| Event-Driven Consumer | System ruft den Empfänger auf, sofort | |
| Wie wird die Last verteilt? | Competing Consumers | mehrere Empfänger am selben Kanal |
| Message Dispatcher | ein Empfänger verteilt intern | |
| Wer bekommt was? | Selective Consumer | Empfänger wählt per Selektor aus |
| Wie übersteht man Ausfälle? | Durable Subscriber | Abonnent verpasst nichts, wenn er offline ist |
| Idempotent Receiver | doppelte Nachrichten richten keinen Schaden an |