KubeMQ
ConnectorsHow-to guidesMigration

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.

ConnectorAMQP 1.0
Ports5672 (plain / SASL), 5671 (TLS)
Canonical clientApache Qpid JMS 2.x (jakarta namespace) — org.apache.qpid:qpid-jms-client:2.x
Enable defaultOpt-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:next

The 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.

DimensionJMS (Qpid JMS over AMQP 1.0)Notes
Drop-in levelclient-swapSwap the ConnectionFactory; JMS API code unchanged
Point-to-point queuesQueuequeues/<channel>
Pub/sub (non-durable)Topicevents/<channel>
Durable subscriptions✅¹Topicevents-store/<channel> (persistence-backed)
Request / reply (RPC)QueueRequestor / TemporaryQueue → Commands / Queries via dynamic nodes
Ordering guarantee⚠️ node-localQueue ordering within a single node; no cluster-wide guarantee
TransactionsSESSION_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 modelPLAIN (JWT) / EXTERNALSASL PLAIN with a KubeMQ JWT as password; EXTERNAL via mTLS
TLS / mTLS✅ 5671Active when the server Security block is configured
Top unsupportedJMS 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/audit

Direct 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 conceptQpid JMS addressKubeMQ patternChannel name
Queue (point-to-point)queues/ordersQueuesorders
Topic (non-durable pub/sub)events/notificationsEventsnotifications
Durable Topic subscriptionevents-store/auditEvents Storeaudit
QueueRequestor / TemporaryQueue replycommands/status + dynamic reply nodeCommandsstatus
Virtual Topic (ActiveMQ Classic)events-store/<channel> + group propertyEvents 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 (or connectors.amqp10.enable = true in 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 ClientID claim 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 the transport.trustStoreLocation / transport.trustStorePassword URI 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

MechanismWhen offeredCredential
PLAINalwayspassword = KubeMQ JWT (auth enabled) or anything (auth disabled)
ANONYMOUSauthentication disabledClientID = sanitized container-id
EXTERNALmTLS with verified client certClientID = certificate CN

What Does NOT Migrate / Deviations

Not supported

JMS featureStatusNotes
SESSION_TRANSACTED❌ not supportedJMS 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 supportedJMS XA (XASession, XAConnectionFactory) require two-phase AMQP transactions. Not available.
Client-settable DLQ❌ no client DLQWhen 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 onlyunsubscribe() 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 supportedTwo-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 supportedNo WebSocket binding; use raw TCP / TLS on 5672 / 5671.
Selectors on Queues❌ not supportedSelector 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 expressibleQpid 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

AreaBehavior
RPC reply shape differs by patternA 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 0events/ 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 creditA 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-localAttaching 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-sequenceRound-trips losslessly but provides no ordering or consumer-affinity guarantee in KubeMQ.
header.priorityRound-trips as the amqp10.priority tag but drives no priority scheduling.
released increments receive countReleasing 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_ACKNOWLEDGE and CLIENT_ACKNOWLEDGE modes.
  • Queue (point-to-point), Topic (fan-out), and durable Topic subscriptions.
  • TextMessage, BytesMessage, MapMessage, and ObjectMessage body types (round-trip lossless for standard properties).
  • Selectors on Events / Events Store.
  • JMSReplyTo + JMSCorrelationID for request / reply over a TemporaryQueue.
  • JMSPriority, JMSTimestamp, JMSMessageID, JMSType, and JMSCorrelationID headers.

Verification Smoke Test

Use the point-to-point block from the Canonical Client Example as a send / receive confirmation:

  1. Enable the AMQP 1.0 connector on the target KubeMQ server (CONNECTORS_AMQP10_ENABLE=true) and confirm port 5672 is reachable.
  2. Produce one message — run the snippet's "Point-to-point (Queues)" block to publish a TextMessage to queues/orders.
  3. Consume it — run the createConsumer(ordersQueue).receive(5000) block and confirm the text matches.
  4. 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.
  5. Confirm zero dropped messageskubemq_amqp10_events_dropped_no_credit_total and kubemq_amqp10_events_store_dropped_stalled_total should remain 0.

See Also

Was this page helpful?

On this page