Apache ActiveMQ Artemis JMS Bridge
Nachrichten zwischen unterschiedlichen Message-Brokern zuverlässig austauschen – genau dafür ist die JMS Bridge da. In diesem Beitrag zeigt Christoph, wie sich mit Apache ActiveMQ Artemis und einer schlanken Spring Boot Anwendung verschiedenste Systeme verbinden lassen. Erhalte einen praxisnahen Überblick über die Konfiguration, Einsatzszenarien und Vorteile, egal ob IBM MQ, AMQ Broker oder ein anderer JMS Broker.
Apache ActiveMQ ist ein weit verbreiteter Open Source Message Broker für verschiedene Protokolle, basierend auf Java. Dabei unterstützt die JMS-Infrastruktur gängige Sprachen und Plattformen, um Clients in JavaScript, C, C++, Python, .NET, Java und mehr zu entwickeln.
Apache unterscheidet zwischen der klassischen, herkömmlichen Lösung ActiveMQ Classic und der hochperformanten, neuen Lösung ActiveMQ Artemis für die nächste Generation von Messaging. Um zwischen verschiedenen Systemen und Plattformen Messages austauschen zu können, wird eine weitere Komponente notwendig: die JMS Bridge. Diese konsumiert Messages von einer Source Destination und sendet diese an eine bestimmte Target-Destination, welche typischerweise auf einem anderen Server liegen. Apache ActiveMQ Artemis Bridges unterstützen dabei Filter-, Transformations- oder Routing-Möglichkeiten.
In diesem Blogpost wollen wir uns die Möglichkeiten der JMS Bridge als Standalone Application ansehen. Die JMS Bridge ist nicht zu verwechseln mit der Core Bridge, welche es erlaubt, innerhalb von ActiveMQ Brokern Messages auszutauschen. Die Core Bridge und das Core-Protokoll sind auf hohe Performance ausgelegt und optimiert. Die JMS Bridge hingegen ist auf Interoperabilität ausgelegt und benötigt spezifische Implementierungen für unterschiedliche Message Broker-Hersteller und Plattformen.
Application Integration Pattern
Grundsätzlich lassen sich vier verschiedene Szenarien unterscheiden, wie zwei Applikationen über eine JMS Bridge integriert werden können:
1) Source und Target System stellen beide eine Destination (Queue), typischerweise von einem Message-Broker, zur Verfügung. Die JMS Bridge liest aus der Source Destination und schreibt in die Target Destination.
2) Das Source System stellt eine Destination (Queue), typischerweise von einem Message-Broker, zur Verfügung,. Die Artemis-Infrastruktur stellt die Target Destination bereit, und ein JMS-Client auf dem Zielsystem kann die Messages auslesen.
3) Die JMS Clients von Source und Target System verwenden die Destinations, welche von der Artemis Infrastruktur zur Verfügung gestellt werden.
4) Der JMS Client des Source System schreibt in die Destination, welche die Artemis Infrastruktur zur Verfügung stellt. Die JMS Bridge leitet die Messages an die Target-Destination weiter, typischerweise von einem Message-Broker.
Je nach Integration Pattern muss auf der Artemis Infrastruktur neben der JMS Bridge auch ein Message-Broker betrieben werden, welcher allenfalls die Destination(s) zur Verfügung stellt.
JMS Bridge Application
Um die Anbindung an verschiedene Systeme und Plattformen zu ermöglichen, müssen Entwickler:innen spezifische Implementierungen von Connection Factories und Destinations bereitstellen. JMS definiert dabei den Standard, den die unterschiedlichen Message-Broker-Hersteller implementieren und unterstützen.
In diesem Beispiel implementieren wir eine Spring Boot Application, welche verschiedene Lösungen von Connection Factories sowie deren Konfiguration beinhaltet. Das mag auf den ersten Blick als Overkill erscheinen, der Codieraufwand hält sich jedoch in Grenzen und der Betrieb der Spring Boot Application bietet all die angenehmen Vorteile von einfachen und bekannten Prozessen wie Build, Deploy & Run inkl. Logging, Monitoring etc.
Starten wir mit der Connection Factory für Artemis; dazu sind folgende Properties für die Protokolle Core und AMQP notwendig:
record ArtemisConnectionFactoryProperties(
String brokerUrl, String username, String password, String queue) {
}
Nutzer:innen können diese Konfiguration entweder in der Konfigurationsdatei definieren oder beim Instanzieren der Connection Factory anwenden. Im Fall von Artemis Core:
public ConnectionFactory createConnectionFactory() {
return new org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory(
getBrokerUrl(), getUsername(), getPassword());
}
Ähnlich sieht die Instanzierung von Artemis AMQP aus – mit einer spezifischen JMS ConnectionFactory für AMQP (auch QPID Client):
public ConnectionFactory createConnectionFactory() {
return new org.apache.qpid.jms.JmsConnectionFactory(
getUsername(), getPassword(), getBrokerUrl());
}
Ein IBM MQ Client verlangt etwas andere Parameter, die man im Konstruktor der Factory-Klasse setzt:
public IbmMqConnectionFactory(Map<String, Object> config) {
this.host = (String) config.get("host");
this.port = (int) config.get("port");
this.queueManager = (String) config.get("queue-manager");
this.channel = (String) config.get("channel");
this.queue = (String) config.get("queue");
this.username = (String) config.get("username");
this.password = (String) config.get("password");
}
Damit lässt sich die Connection Factory für den IBM MQ Client instanziieren und konfigurieren:
public ConnectionFactory createConnectionFactory() throws JMSException {
MQConnectionFactory factory = new MQConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setQueueManager(queueManager);
factory.setChannel(channel);
factory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
return factory;
}
In allen aufgeführten Beispielen initialisiert man jeweils nur eine Connection Factory. Die eigentliche Connection sowie die Destination (Source oder Target) entstehen erst im nächsten Schritt – bei der Instanzierung der JMS Bridge.
JMSBridge jmsBridge = new JMSBridgeImpl(); jmsBridge.setBridgeName(jmsBridgeProperties.name()); jmsBridge.setSourceConnectionFactoryFactory(() -> sourceConnectionFactory); jmsBridge.setSourceDestinationFactory(() -> sourceQueue); jmsBridge.setTargetConnectionFactoryFactory(() -> targetConnectionFactory); jmsBridge.setTargetDestinationFactory(() -> targetQueue); jmsBridge.setSelector(jmsBridgeProperties.selector()); jmsBridge.setFailureRetryInterval(jmsBridgeProperties.getFailureRetryIntervalOrDefault()); jmsBridge.setMaxRetries(jmsBridgeProperties.getMaxRetriesOrDefault()); jmsBridge.setQualityOfServiceMode(jmsBridgeProperties.getQualityOfServiceModeOrDefault()); jmsBridge.setMaxBatchSize(jmsBridgeProperties.getMaxBatchSizeOrDefault()); jmsBridge.setMaxBatchTime(jmsBridgeProperties.getMaxBatchTimeOrDefault()); jmsBridge.setSubscriptionName(jmsBridgeProperties.subName()); jmsBridge.setClientID(jmsBridgeProperties.clientId()); jmsBridge.setAddMessageIDInHeader(jmsBridgeProperties.getAddMessageIdInHeaderOrDefault());
Nebst den spezifischen Connection Factories für die Anbindung von Source und Target Destination sind eine Reihe von JMS Bridge Parametern notwendig, welche das Verhalten der JMS Bridge beeinflussen. Die JMS Bridge kann z.B. so konfiguriert werden, dass sie nach einem Verbindungsunterbruch zum Source oder Target System automatisch die Verbindung wieder aufbaut. Zudem können Authentication und / oder Verschlüsselung für die sichere und geschützte Übertragung von Messages eingesetzt werden.
Nachdem wir alle JMS Bridges erfolgreich instanziert haben, starten wir sie explizit. Beim Shutdown der Applikation stoppen wir sie kontrolliert. Spring Boot eignet sich dafür besonders gut: Über einen Application Lifecycle Service lassen sich Start (@PostConstruct) und Stopp (@PreDestroy) der JMS Bridges gezielt steuern.
Ziel ist es, anhand einer Liste von JMS Bridge-Konfigurationen die jeweiligen Connection Factories und Destinations aufzusetzen. Damit ist es zu einem späteren Zeitpunkt sehr einfach möglich, weitere JMS Bridges zu konfigurieren oder bestehende anzupassen. D.h. nach der erfolgreichen Einführung der Applikation kann diese an das Business Team übergeben werden, welches selbständig die Konfiguration warten kann.
Beispiel einer möglichen JMS Bridge-Konfiguration:
app:
jmsBridges:
- name: JMS_SOURCE_TARGET_TEST
failure-retry-interval: 600
max-retries: 10
quality-of-service-mode: DUPLICATES_OK
add-message-id-in-header: true
source:
type: IBM_MQ_CLIENT
config:
host: 192.193.194.195
port: 1414
queue-manager: QMPUZZLE
channel: COM.PUZZLE
queue: SOURCE.QUEUE.TEST
username: USER_NAME
password: changeit
target:
type: ARTEMIS_CORE
config:
broker-url: tcp://jms-broker.puzzle.ch:61617?sslEnabled=true
username: USER_NAME
password: changeit
queue: TARGET_QUEUE_TEST
- name: JMS_RESPONSE_TEST
quality-of-service-mode: DUPLICATES_OK
add-message-id-in-header: true
source:
type: ARTEMIS_CORE
config:
broker-url: tcp://jms-broker.puzzle.ch:61617?sslEnabled=true
username: USER_NAME
password: changeit
queue: SOURCE_RESPONSE_QUEUE_TEST
target:
type: IBM_MQ_CLIENT
config:
host: 192.193.194.195
port: 1414
queue-manager: QMPUZZLE
channel: COM.PUZZLE
queue: TARGET.RESPONSE.QUEUE.TEST
username: USER_NAME
password: changeit
Das Root-Attribut jmsBridges definiert die Liste von JMS Bridges. Jede JMS Bridge kann eine Reihe von Attributen sowie eine Source- und Target-Konfiguration beinhalten. Source- und Target-Konfigurationen werden anhand des Attributes type spezifisch konfiguriert, d.h. können wiederum unterschiedliche Attribute enthalten – je nach Message Broker-Hersteller. In obigem Beispiel werden je ein IBM MQ Client und ein Artemis Core Client konfiguriert für eine «bidirektionale» Verbindung, d.h. jeweils eine ein- und ausgehende Verbindung. Ein Artemis AMQP Client lässt sich analog konfigurieren.
Fazit
| Ansatz / Option | Vorteil | Nachteil / Einschränkung | Technischer Hinweis | |
|---|---|---|---|---|
| Interne Bridges in AMQ Artemis | Keine zusätzlichen Komponenten nötig | Nur Core-Protokoll unterstützt → nur Artemis↔Artemis | – | |
| Spring Boot Applikation | Unterstützung verschiedener Protokolle & Broker durch standardisierte Interfaces | Zusätzliche Komponente (Spring Boot App) nötig, die betrieben und gewartet werden muss | Hohe Flexibilität durch einfache Erweiterbarkeit | |
| JMS Bridge im Application Server (z. B. JBoss EAP) | Kein Core-Protokoll-Zwang, breitere Integration möglich | Wartungsaufwändige und schwergewichtigere Komponente (EAP) | – | |
- Die JMS Bridge von Apache basiert auf JMS Version 2.0 (Package javax.jms). Dies bedingt, dass neuere Spring Boot Applications, z.B. 3.2.x auf JMS 2.0 downgraded werden müssen, was bisher im Betrieb der Applikation zu keinem Nachteil geführt hat. Trotzdem können neuere Features von Spring Boot verwendet werden.
- In diesem Artikel wird grundsätzlich mit den Begriffen «Destination» bzw. konkret «Queue» gearbeitet. Selbstverständlich kann anstelle von «Queue» auch ein «Topic» konfiguriert werden.
- Beim Weiterleiten der JMS Message werden die Properties der Message übernommen. Artemis unterstützt dabei nur Property Keys, welche einem Java Identifier entsprechen, z.B. ist TRACE_ID oder traceId gültig, hingegen trace-id nicht (AMQ139012: The property name ‹trace-id› is not a valid java identifier.)! Diese Restriktion ist etwas unschön, weil sie erst zur Laufzeit auftreten wird. Falls dieses Problem auftritt, müsste bereits bei der Source Destination korrigiert werden (falls möglich) bzw. ein Pre-Message Processing forciert werden.
Quellen und weitere Informationen
Das Beispiel stammt aus dem Mandat bei der Bank Vontobel AG, wo wir die Möglichkeiten der JMS Bridge analysieren und für verschiedene Message Broker-Anbindungen entwickeln und konfigurieren konnten. Vielen Dank an die Bank Vontobel, welche diese Publikation ermöglicht und unterstützt hat!