Migrating from JMS
Swap your JMS ConnectionFactory to Apache Qpid JMS over KubeMQ's AMQP 1.0 connector — destination mapping, selectors, a Java example, and the XA gaps.
JMS (Jakarta / Java Message Service) is a Java API specification, not a wire protocol.
Any conforming JMS provider can be swapped for another without touching application code —
your program calls Session.createProducer, Session.createConsumer, and so on regardless
of the underlying transport. The migration path is therefore a ConnectionFactory swap:
replace your current provider's factory with Apache Qpid JMS, which speaks AMQP 1.0 over
the wire, and point it at KubeMQ's AMQP 1.0 connector. The JMS calls you already wrote stay
exactly as they are.
This guide covers the connection-factory migration, the concept-to-pattern mapping, a self-contained Java snippet, the security posture, and the documented gaps. For the AMQP 1.0 wire contract behind Qpid JMS — frame-level semantics, link options, message translation, selector grammar, and settlement rules — see the Migrating from AMQP 1.0 guide and the AMQP 1.0 connector capabilities reference.
Overview
KubeMQ's AMQP 1.0 connector is the substrate. Qpid JMS connects to it over AMQP 1.0 on port 5672 (plain / SASL) or 5671 (TLS), sharing the listener with AMQP 0.9.1.
| Connector | AMQP 1.0 |
| Ports | 5672 (plain / SASL), 5671 (TLS) |
| Canonical client | Apache Qpid JMS 2.x (jakarta namespace) — org.apache.qpid:qpid-jms-client:2.x |
| Enable default | Opt-in — disabled by default. Set CONNECTORS_AMQP10_ENABLE=true (or connectors.amqp10.enable = true in TOML) to turn it on |
Qpid JMS 2.x vs 1.x. The jakarta-namespace release
(org.apache.qpid:qpid-jms-client:2.x, artifact classifier jakarta) requires
jakarta.jms:jakarta.jms-api:3.x. If your codebase still targets the javax.jms
namespace (JMS 2.0), use the 1.x line (org.apache.qpid:qpid-jms-client:1.x) instead.
The Qpid URI format, the AMQP addressing, and all JNDI properties are identical between
the two lines — only the JMS API namespace differs.
The AMQP 1.0 connector is disabled by default — a stock kubemq-server does not bind the AMQP 1.0 listener until you turn it on. Enable it with its enable variable:
docker run -d \ --name kubemq \ -p 5672:5672 \ -p 5671:5671 \ -p 50000:50000 \ -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \ -e CONNECTORS_AMQP10_ENABLE=true \ europe-docker.pkg.dev/kubemq/images/kubemq:nextThe enable variable is CONNECTORS_AMQP10_ENABLE — the literal 10 stays attached to
AMQP with no underscore. CONNECTORS_AMQP_1_0_ENABLE and CONNECTORS_AMQP10ENABLE do
not bind. For Kubernetes, set spec.amqp10.enabled: true in the KubemqCluster CR.
Compatibility Matrix
This is the JMS column of the cross-protocol matrix in the migration hub, restated here so this guide stands on its own.
| Dimension | JMS (Qpid JMS over AMQP 1.0) | Notes |
|---|---|---|
| Drop-in level | client-swap | Swap the ConnectionFactory; JMS API code unchanged |
| Point-to-point queues | ✅ | Queue → queues/<channel> |
| Pub/sub (non-durable) | ✅ | Topic → events/<channel> |
| Durable subscriptions | ✅¹ | Topic → events-store/<channel> (persistence-backed) |
| Request / reply (RPC) | ✅ | QueueRequestor / TemporaryQueue → Commands / Queries via dynamic nodes |
| Ordering guarantee | ⚠️ node-local | Queue ordering within a single node; no cluster-wide guarantee |
| Transactions | ❌ | SESSION_TRANSACTED and XA are not supported |
| Dead-letter / redrive | ❌ no client DLQ² | Poison messages are dropped after MaxReceiveCount; no consumable dead-letter address |
| Selectors / filtering | ✅ selectors (SQL92 subset) | createConsumer(dest, selector) works on Events / Events Store |
| Auth model | PLAIN (JWT) / EXTERNAL | SASL PLAIN with a KubeMQ JWT as password; EXTERNAL via mTLS |
| TLS / mTLS | ✅ 5671 | Active when the server Security block is configured |
| Top unsupported | JMS transactions / XA; node-local unsubscribe() | See What Does NOT Migrate |
¹ Durable subscription is backed by the persistence engine. session.unsubscribe() is
node-local — see What Does NOT Migrate for the full
explanation.
² No client-settable DLQ — see What Does NOT Migrate for the full explanation.
Connection / Endpoint Migration
As shown in the Compatibility Matrix (Drop-in level: client-swap),
the only required change is the ConnectionFactory URL and its class name. The JMS API
calls (createSession, createProducer, createConsumer, and so on) remain unchanged.
Using JNDI properties
# jndi.properties — before (ActiveMQ / Artemis example)
# java.naming.factory.initial=org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory
# connectionFactory.kubemq=tcp://old-broker:61616
# jndi.properties — after (Qpid JMS over KubeMQ AMQP 1.0)
java.naming.factory.initial=org.apache.qpid.jms.jndi.JmsInitialContextFactory
connectionfactory.kubemq=amqp://kubemq.example.com:5672
# Optional TLS endpoint
# connectionfactory.kubemq=amqps://kubemq.example.com:5671
# Declare destinations (prefix drives pattern selection)
queue.ordersQueue=queues/orders
topic.ordersTopic=events/orders
topic.auditTopic=events-store/auditDirect instantiation (no JNDI)
import org.apache.qpid.jms.JmsConnectionFactory;
// Before (example from any other provider):
// ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://old-broker:61616");
// After (Qpid JMS):
ConnectionFactory cf = new JmsConnectionFactory("amqp://kubemq.example.com:5672");For TLS, use "amqps://kubemq.example.com:5671".
Maven / Gradle dependency
<!-- Maven — jakarta namespace (JMS 3.x) -->
<dependency>
<groupId>org.apache.qpid</groupId>
<artifactId>qpid-jms-client</artifactId>
<version>2.6.0</version>
<classifier>jakarta</classifier>
</dependency>
<!-- javax namespace (JMS 2.0), if still on javax.jms -->
<!-- <artifactId>qpid-jms-client</artifactId><version>1.12.0</version> -->// Gradle — jakarta namespace (JMS 3.x)
implementation 'org.apache.qpid:qpid-jms-client:2.6.0:jakarta'
// javax namespace (JMS 2.0), if still on javax.jms
// implementation 'org.apache.qpid:qpid-jms-client:1.12.0'Concept & Destination Mapping
Qpid JMS resolves destination names against KubeMQ by the address prefix — the prefix is
stripped and the remainder becomes the channel name. JMS Queue / Topic objects carry the
full address, not just the channel name.
| JMS concept | Qpid JMS address | KubeMQ pattern | Channel name |
|---|---|---|---|
Queue (point-to-point) | queues/orders | Queues | orders |
Topic (non-durable pub/sub) | events/notifications | Events | notifications |
Durable Topic subscription | events-store/audit | Events Store | audit |
QueueRequestor / TemporaryQueue reply | commands/status + dynamic reply node | Commands | status |
| Virtual Topic (ActiveMQ Classic) | events-store/<channel> + group property | Events Store | — |
JMS 2.0 shared subscriptions. createSharedConsumer does not automatically enable
consumer-group load balancing. The AMQP 1.0 connector activates group load-balancing through
a wire-level AMQP link property set on the ATTACH frame — which is not expressible through the
standard JMS API. The createSharedConsumer(dest, groupName) call by itself does not wire
the group; to load-balance Events across a group with Qpid JMS, the link property must be set
at the wire level.
Bare address (no prefix). Qpid JMS advertises a JMS capability hint (queue or topic)
in the AMQP ATTACH. The connector uses this to resolve Queue("orders") → Queues and
Topic("orders") → Events automatically, without requiring the prefix. If you use addresses
that lack the prefix and the capability hint is absent, the connector falls back to the
configured default pattern (queues). Prefer the explicit prefix so resolution is never
ambiguous.
Selectors. Pass the selector expression as the second argument to createConsumer or
createDurableConsumer. The connector evaluates a SQL92 subset (comparisons, AND / OR /
NOT, BETWEEN, IN, LIKE, IS NULL) against application properties and JMS headers
(JMSPriority, JMSType, JMSCorrelationID, JMSMessageID, JMSTimestamp). Selectors are
supported on Events and Events Store links only; a selector on a Queue (queues/) link
fails the ATTACH with amqp:not-implemented.
Canonical Client Example
Client: Apache Qpid JMS 2.6.0 (jakarta). API symbols:
JmsConnectionFactory, Connection.createSession, Session.AUTO_ACKNOWLEDGE,
Session.createQueue, Session.createTopic, Session.createProducer,
Session.createConsumer, Session.createDurableConsumer,
Session.createTemporaryQueue, MessageProducer.send, MessageConsumer.receive.
import jakarta.jms.*;
import org.apache.qpid.jms.JmsConnectionFactory;
public class KubeMQJmsExample {
public static void main(String[] args) throws JMSException {
// 1. ConnectionFactory — the only change from your previous provider
ConnectionFactory cf = new JmsConnectionFactory(
"amqp://kubemq.example.com:5672");
// When authentication is enabled, pass the KubeMQ JWT as the password.
// The username is recorded for audit only and does not affect identity.
Connection conn = cf.createConnection("svc-orders", System.getenv("KUBEMQ_JWT"));
conn.start();
Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);
// ── Point-to-point (Queues) ──────────────────────────────────────────
Queue ordersQueue = session.createQueue("queues/orders");
// Produce
TextMessage msg = session.createTextMessage("order-payload");
session.createProducer(ordersQueue).send(msg);
// Consume (competing consumer — AUTO_ACKNOWLEDGE maps to AMQP ACCEPTED)
TextMessage received = (TextMessage) session.createConsumer(ordersQueue).receive(5000);
System.out.println("Received: " + received.getText());
// ── Non-durable pub/sub (Events) ─────────────────────────────────────
Topic eventsTopic = session.createTopic("events/notifications");
session.createProducer(eventsTopic).send(
session.createTextMessage("event-payload"));
// Subscribers on events/notifications receive a copy (fan-out).
// ── Durable subscription (Events Store) ──────────────────────────────
Topic auditTopic = session.createTopic("events-store/audit");
// Durable identity derived from (container-id, subscription-name) — survives reconnect.
MessageConsumer durableConsumer =
session.createDurableConsumer(auditTopic, "audit-sub");
// ── Selector on Events Store ──────────────────────────────────────────
// SQL92 subset evaluated in the connector before delivery.
MessageConsumer filtered = session.createConsumer(
session.createTopic("events-store/audit"),
"JMSPriority > 4 AND region = 'EU'");
// ── Request / reply (Commands) ────────────────────────────────────────
// Use a TemporaryQueue as the reply destination; the connector backs it
// with a dynamic AMQP node (/responses/<id>).
Queue statusCmd = session.createQueue("commands/status");
TemporaryQueue replyQ = session.createTemporaryQueue();
Message req = session.createTextMessage("get-status");
req.setJMSReplyTo(replyQ);
req.setJMSCorrelationID("req-001");
session.createProducer(statusCmd).send(req);
Message reply = session.createConsumer(replyQ).receive(5000);
System.out.println("Reply: " + ((TextMessage) reply).getText());
conn.close();
}
}Acknowledge modes and AMQP settlement. AUTO_ACKNOWLEDGE → AMQP accepted (AckRange).
CLIENT_ACKNOWLEDGE → deferred accepted on msg.acknowledge(). SESSION_TRANSACTED is
not supported — see What Does NOT Migrate.
Security
Authentication
-
Opt-in connector. The AMQP 1.0 connector is disabled by default. Set
CONNECTORS_AMQP10_ENABLE=true(orconnectors.amqp10.enable = truein TOML) to start it. -
No auth (development). When authentication is disabled (the server default), the connector accepts SASL PLAIN with any credentials and also accepts SASL ANONYMOUS. If the server is reachable from untrusted networks, enable authentication or firewall ports 5672 / 5671.
-
With auth enabled. Pass a KubeMQ JWT as the SASL PLAIN password:
Connection conn = cf.createConnection("any-username", kubemqJWT);The username is recorded for audit; the JWT's
ClientIDclaim becomes the connection identity for authorization checks.
Authorization
With authorization enabled, the ClientID derived from the JWT is checked against the policy on the resolved channel at ATTACH time:
- Producers (sender links) need Write on the channel.
- Consumers (receiver links) need Read on the channel.
A denied ATTACH fails with amqp:unauthorized-access; the JMS client throws a
JMSSecurityException.
TLS / mTLS
-
TLS (server auth). Change the connection URL to
amqps://kubemq.example.com:5671. The Qpid JMS client performs the standard TLS handshake; trust the server certificate using a JVM truststore or thetransport.trustStoreLocation/transport.trustStorePasswordURI options:amqps://kubemq.example.com:5671?transport.trustStoreLocation=/path/to/truststore.jks -
mTLS (client auth). When the server requests a client certificate, the connector offers SASL EXTERNAL; the certificate CN becomes the ClientID (no JWT needed):
amqps://kubemq.example.com:5671?transport.keyStoreLocation=/path/to/keystore.jks&transport.keyStorePassword=secret
The TLS listener on port 5671 is active only when the server Security block is configured.
See Auth & Security for the shared connector security
model.
SASL mechanisms
| Mechanism | When offered | Credential |
|---|---|---|
PLAIN | always | password = KubeMQ JWT (auth enabled) or anything (auth disabled) |
ANONYMOUS | authentication disabled | ClientID = sanitized container-id |
EXTERNAL | mTLS with verified client cert | ClientID = certificate CN |
What Does NOT Migrate / Deviations
Not supported
| JMS feature | Status | Notes |
|---|---|---|
SESSION_TRANSACTED | ❌ not supported | JMS transacted sessions rely on an AMQP transaction coordinator (declare / discharge), which the connector does not implement. Fall back to AUTO_ACKNOWLEDGE or CLIENT_ACKNOWLEDGE with idempotent message design. |
| XA transactions | ❌ not supported | JMS XA (XASession, XAConnectionFactory) require two-phase AMQP transactions. Not available. |
| Client-settable DLQ | ❌ no client DLQ | When a queue message exceeds MaxReceiveCount (default 1024), it is silently dropped — not delivered to a dead-letter address. The AMQP 1.0 connector never routes poison messages to a consumable dead-letter channel. Design for idempotency and monitor kubemq_messages_dropped_total (CounterVec; label cause="dropped"). |
session.unsubscribe() (cluster-wide) | ⚠️ node-local only | unsubscribe() removes the durable Events Store subscription registration on the node where it is called. If the durable subscription is active on another cluster node, that registration is not removed. Use the REST / dashboard API to delete durable subscriptions cluster-wide. |
rcv-settle-mode=second | ❌ not supported | Two-phase receiver settlement (exactly-once) is not supported. The model is at-least-once (queue / durable Events Store) or at-most-once (Events fire-hose). |
| AMQP-over-WebSocket | ❌ not supported | No WebSocket binding; use raw TCP / TLS on 5672 / 5671. |
| Selectors on Queues | ❌ not supported | Selector expressions on queues/ links fail the ATTACH with amqp:not-implemented. Selectors work on Events and Events Store links. |
| JMS start-position from Qpid JMS | ❌ not expressible | Qpid JMS exposes no API for the x-opt-kubemq-start link property — you cannot set an arbitrary Events-Store start position from JMS. Use a native durable consumer with new-only, or a native client (Go / .NET / Python / Rust / JS) for first / last / sequence: / time:. |
Behavioral deviations
| Area | Behavior |
|---|---|
| RPC reply shape differs by pattern | A JMS request / reply over a dynamic reply node behaves differently depending on the target pattern: a commands/ reply carries executed / error properties, while a queries/ reply carries body + metadata only. Read the reply accordingly — do not expect executed/error flags on a Queries response. |
| Pub/sub events at credit 0 | events/ subscribers are fire-hose: if the consumer grants no link credit, events are dropped (kubemq_amqp10_events_dropped_no_credit_total). Grant credit continuously with an async consumer. |
| Events Store stalled credit | A per-link buffer (MaxUnsettledPerLink, default 1024) fronts the durable subscription. If it fills while credit stays at 0, the link is detached (amqp:resource-limit-exceeded) and the buffered window is dropped. A durable re-attach resumes after the dropped window — those messages are lost. Size MaxUnsettledPerLink accordingly. |
| Durable-subscription registry is node-local | Attaching the same durable subscription (same container-id + subscription name) on two cluster nodes produces a clientID conflict in the persistence engine. One attachment should be active at a time per durable-subscription identity. |
JMSXGroupID / group-sequence | Round-trips losslessly but provides no ordering or consumer-affinity guarantee in KubeMQ. |
header.priority | Round-trips as the amqp10.priority tag but drives no priority scheduling. |
released increments receive count | Releasing a message (rollback / recover()) increments the receive count, which counts toward MaxReceiveCount. A strict reading of AMQP would not count a release as a delivery attempt. |
modified{undeliverable-here=true} | Treated as a delivery failure (NAckRange); there is no per-consumer exclusion, so the message may be redelivered to the same consumer. |
What does migrate cleanly
- JMS
AUTO_ACKNOWLEDGEandCLIENT_ACKNOWLEDGEmodes. Queue(point-to-point),Topic(fan-out), and durableTopicsubscriptions.TextMessage,BytesMessage,MapMessage, andObjectMessagebody types (round-trip lossless for standard properties).- Selectors on Events / Events Store.
JMSReplyTo+JMSCorrelationIDfor request / reply over aTemporaryQueue.JMSPriority,JMSTimestamp,JMSMessageID,JMSType, andJMSCorrelationIDheaders.
Verification Smoke Test
Use the point-to-point block from the Canonical Client Example as a send / receive confirmation:
- Enable the AMQP 1.0 connector on the target KubeMQ server
(
CONNECTORS_AMQP10_ENABLE=true) and confirm port 5672 is reachable. - Produce one message — run the snippet's "Point-to-point (Queues)" block to publish a
TextMessagetoqueues/orders. - Consume it — run the
createConsumer(ordersQueue).receive(5000)block and confirm the text matches. - Check the dashboard — the KubeMQ dashboard AMQP 1.0 page should show one connection, one sender link, one receiver link, and one transfer in / out.
- Confirm zero dropped messages —
kubemq_amqp10_events_dropped_no_credit_totalandkubemq_amqp10_events_store_dropped_stalled_totalshould remain 0.
See Also
Migration hub
Choose the right KubeMQ connector for your existing broker and the cross-protocol matrix.
Migrating from ActiveMQ
Route an ActiveMQ workload by client type — Java/JMS via Qpid JMS, STOMP and MQTT by endpoint.
Migrating from AMQP 1.0
Point a native AMQP 1.0 client at KubeMQ — address mapping, a Go example, RPC, and deviations.
AMQP 1.0 capabilities
Exactly what the AMQP 1.0 connector supports and what it rejects on the wire.
Connector configuration
The Connectors.Amqp10 settings, env-var table, and CRD fields.
Was this page helpful?
Migrating from AMQP 1.0
Point a native AMQP 1.0 client at KubeMQ — address-prefix pattern mapping, a go-amqp example, RPC, and the transaction and settlement deviations.
Auth & Security
JWT Bearer authentication, CORS, origin validation, and TLS/mTLS — the security model shared by every KubeMQ connector.