Migrating from JMS
Swap your JMS ConnectionFactory to Apache Qpid JMS over KubeMQ's AMQP 1.0 connector: quick start, destination mapping, a runnable test, 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. Port 5672 is already bound by the AMQP 0-9-1 connector (on by default); without AMQP 1.0 enabled a client connects and is closed during protocol negotiation; on Kubernetes, set spec.amqp10.enabled: true |
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.
Quick start: repoint one client
Try the switch on your own machine before you plan the migration.
The Docker tab points one client at the KubeMQ you started with Try KubeMQ, turning its connector on where needed and keeping your evaluation or license and your data. The Kubernetes tab works on a cluster you installed with Install on Kubernetes. Use it to try the switch, not for production; see Plans compared for staging and production licenses. If your current broker runs on this machine, stop it first, because two programs cannot publish the same port. The kmq command runs natively on Windows; the other commands are bash, so on Windows run them in WSL (Windows Subsystem for Linux).
You started KubeMQ with Try KubeMQ or Install with Docker.
Turn the AMQP 1.0 connector on
The connector is off by default. This command turns it on and publishes port 5672 on the same container and volume. If you started KubeMQ with the Docker command, skip it and use Without kmq below.
kmq deploy update --installation kubemq --connector amqp10 --deadline 10mCheck that installation_id is the same as before and data_retained is true. You should see:
{
"installation_id": "…",
"state": "…",
"target": "…",
"data_retained": true,
"next_action": "…"
}Point your client at KubeMQ
// Any other JMS provider, for example ActiveMQ
ConnectionFactory cf = new ActiveMQConnectionFactory("tcp://old-broker:61616");// Qpid JMS over KubeMQ's AMQP 1.0 connector
ConnectionFactory cf = new JmsConnectionFactory("amqp://localhost:5672");Send and receive one message
You need Java and JBang. Save the test and run it; JBang downloads Qpid JMS on the first run. The test uses the javax.jms line (Qpid JMS 1.x); for jakarta.jms, use Qpid JMS 2.x and change the imports.
//DEPS org.apache.qpid:qpid-jms-client:1.16.0
import javax.jms.Connection;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.Session;
import org.apache.qpid.jms.JmsConnectionFactory;
public class SmokeTest {
public static void main(String[] args) throws Exception {
// The only KubeMQ-specific line: Qpid JMS pointed at the AMQP 1.0 connector.
JmsConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:5672");
try (Connection connection = factory.createConnection()) {
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("queues/smoke-test");
session.createProducer(queue).send(session.createTextMessage("hello kubemq"));
Message received = session.createConsumer(queue).receive(10_000);
if (received == null) {
throw new IllegalStateException("no message within 10 seconds");
}
System.out.println("received: " + received.getBody(String.class));
}
}
}jbang SmokeTest.javaYou should see, as the last line:
received: hello kubemqTo turn the connector off again, run the same kmq deploy update command without --connector. Your messages and your evaluation or license stay.
You installed KubeMQ with Install on Kubernetes.
Turn the connector on
Put the connector's key in cluster-values.yaml. If Helm installed your cluster, add it to the file you created on Install on Kubernetes; if kmq installed it, create the file with only this key.
amqp10:
enabled: trueIf kmq installed your cluster, apply the file with kmq:
kmq deploy update --installation messaging --kube-context YOUR_KUBE_CONTEXT --values cluster-values.yaml --deadline 10mReplace:
YOUR_KUBE_CONTEXT— your cluster's kubectl context;kubectl config get-contextslists them.
Check that data_retained is true. You should see:
{
"installation_id": "…",
"state": "…",
"target": "…",
"data_retained": true,
"next_action": "…"
}If Helm installed your cluster, apply the file with the command in Apply a settings change.
Either way, check that the connector's Service exists:
kubectl --context YOUR_KUBE_CONTEXT -n kubemq get svc messaging-amqp10Check that the row lists 5672/TCP. You should see:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
messaging-amqp10 ClusterIP … <none> 5672/TCP,… …Reach the connector from your machine
Keep this running in a second terminal:
kubectl --context YOUR_KUBE_CONTEXT -n kubemq port-forward svc/messaging-amqp10 5672:5672You should see:
Forwarding from 127.0.0.1:5672 -> 5672
Forwarding from [::1]:5672 -> 5672Send and receive one message
Run steps 2 and 3 of the Docker tab unchanged. The port-forward serves the connector at the same localhost:5672 address.
Before you move production traffic
- Read What does not migrate before you commit.
- Plan how traffic moves and how you roll back: run both brokers, move one queue or topic at a time, and keep your JMS provider until you have verified the move.
- Install KubeMQ for real: Choose your path.
Compatibility Matrix
This is the JMS column of the cross-protocol matrix on Migrate from another broker, 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
The examples on this page use qpid-jms-client 2.6.0 (jakarta) or 1.16.0 (javax); newer releases in the same line are expected to work.
<!-- 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.16.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.16.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.x (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. Port 5672 is open regardless, because the AMQP 0-9-1 connector sharing it is on by default — so an open port does not by itself mean AMQP 1.0 is enabled. -
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
For the basic round trip, see the quick start; this section adds deeper checks.
- 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
Migrate from another broker
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?