--- tags: [MQ, JMS, OpenMessaging, Java, 协议] create time: 2026-05-24 19:52 update time: 2026-06-07 17:30 --- # JMS 与 OpenMessaging ## 概述 JMS(Java Message Service)是 Java 生态中消息中间件的接口规范,它不实现任何消息传输逻辑,而是定义了一套统一的 API,让应用代码可以在不同 MQ 产品之间切换。OpenMessaging 则是阿里巴巴发起的下一代开放标准,试图突破 JMS 的 Java 绑定和厂商碎片化问题,构建跨语言、跨平台、云原生的消息规范。 ## 正文 ### JMS 是什么? JMS 不是一个 MQ 产品,而是一份"接口契约"。就像 JDBC 定义了数据库访问的标准接口,JMS 定义了消息通信的标准接口。你的代码面向 JMS API 编写,底层可以换成 ActiveMQ、IBM MQ、TIBCO 等任何 JMS 实现,理论上不需要改一行业务代码。 这个设计思路在 Java 一统天下的企业级开发时代非常有吸引力——应用服务器(如 WebLogic、WebSphere)内置 JMS 实现,开发者只需要注入 `ConnectionFactory` 就能开始收发消息。 ### 两种消息模型 JMS 定义了两种经典的消息通信模型: **Point-to-Point(队列模型)** 一条消息只被一个消费者消费。生产者把消息发到 Queue,消费者从 Queue 拉取。消费后消息从 Queue 中移除(或标记已消费)。典型场景:订单处理、任务分发。 **Pub/Sub(发布订阅模型)** 一条消息可以被多个订阅者同时接收。生产者发布到 Topic,所有订阅该 Topic 的消费者都会收到副本。典型场景:事件广播、通知推送。 ```mermaid graph LR P1["Producer"] -->|"send"| Q["Queue"] Q -->|"consume"| C1["Consumer A"] Q -->|"consume"| C2["Consumer B"] P2["Publisher"] -->|"publish"| T["Topic"] T -->|"deliver"| S1["Subscriber X"] T -->|"deliver"| S2["Subscriber Y"] T -->|"deliver"| S3["Subscriber Z"] style Q fill:#4A90D9,color:#fff style T fill:#F5A623,color:#fff ``` > [!question] Queue 模型中,如果多个消费者监听同一个 Queue,消息会被谁收到? > JMS 规范本身没有规定负载均衡策略,具体由实现厂商决定。但通常的行为是:消息在多个消费者之间轮询(round-robin)分发,每条消息只投递给其中一个消费者。这和 Kafka Consumer Group 的行为类似。 > [!info] JMS 1.1 的统一域模型 > 早期 JMS 1.0 中,PTP 和 Pub/Sub 是两套**完全独立**的 API:`QueueConnectionFactory` / `TopicConnectionFactory`、`QueueSession` / `TopicSession`、`QueueSender` / `TopicPublisher`……写一套代码同时支持两种模型很痛苦。JMS 1.1(2002)统一了域模型——`ConnectionFactory`、`Session`、`MessageProducer`、`MessageConsumer` 同时适用于 Queue 和 Topic,通过创建 Session 时传入不同参数来切换模式。这就是为什么你在下面的核心接口表里看到的是统一的 `MessageProducer` / `MessageConsumer`,而不是 JMS 1.0 时代的 `QueueSender` / `TopicPublisher` 这样分裂的类型。 ### JMS API 核心接口 JMS 的编程模型围绕以下几个核心接口展开: | 接口 | 职责 | |------|------| | **ConnectionFactory** | 创建 Connection 的工厂,通常由 JMS 实现提供,配置了 Broker 地址和认证信息 | | **Connection** | 到 Broker 的 TCP 连接,支持异步消息分发和连接恢复 | | **Session** | 单线程上下文,用于创建 Producer/Consumer 和管理事务 | | **MessageProducer** | 发送消息到 Queue 或 Topic | | **MessageConsumer** | 从 Queue 或 Topic 接收消息,支持同步 `receive()` 和异步 `setMessageListener()` | | **Message** | 消息本体,包含 Header(JMSDestination、JMSDeliveryMode 等)、Properties(自定义键值对)和 Body | ### 消息类型 JMS 规范定义了五种消息体类型,覆盖了常见的数据传输需求: | 类型 | 说明 | 适用场景 | |------|------|----------| | **TextMessage** | 字符串文本(通常为 XML/JSON) | 最常用,Web 服务间通信 | | **ObjectMessage** | 序列化的 Java 对象 | Java 生态内部传输,但有安全风险 | | **MapMessage** | 键值对集合(类似 Map) | 结构化但不需要完整对象的场景 | | **BytesMessage** | 原始字节数组 | 二进制数据、跨语言场景 | | **StreamMessage** | 顺序读写的原始类型流 | 需要流式处理的场景 | > [!question] ObjectMessage 看起来很方便,为什么生产中反而不推荐? > 因为 ObjectMessage 依赖 Java 序列化,存在严重的安全漏洞(反序列化攻击链)。而且它和 Java 绑死,跨语言几乎不可能。现代实践更倾向用 TextMessage + JSON/Protobuf 编码。 ### 消息确认与持久化 JMS 通过两个维度控制消息的可靠性:**投递模式**(消息是否持久化)和**确认模式**(消费者何时确认已消费)。 **投递模式(DeliveryMode)** | 模式 | 说明 | |------|------| | **PERSISTENT**(默认) | Broker 在收到消息后将其写入持久存储(如磁盘),保证 Broker 崩溃后消息不丢 | | **NON_PERSISTENT** | Broker 不保证持久化,性能更高但可能丢消息。适用于可以容忍丢失的场景(如监控指标) | 生产者可以通过 `producer.setDeliveryMode(DeliveryMode.PERSISTENT)` 设置,也可以在发送单条消息时指定。 **确认模式(AcknowledgeMode)** 确认模式决定了消费者告诉 Broker "我已成功处理消息"的时机,直接影响消息的可靠性语义: | 模式 | 行为 | 适用场景 | |------|------|----------| | **AUTO_ACKNOWLEDGE** | Session 在 `receive()` 返回或 `onMessage()` 方法正常结束时自动确认 | 简单场景,允许极小概率丢消息 | | **CLIENT_ACKNOWLEDGE** | 消费者显式调用 `message.acknowledge()` 确认,确认时会批量确认该 Session 中所有已消费的消息 | 需要精确控制确认时机 | | **DUPS_OK_ACKNOWLEDGE** | Session 延迟确认(惰性确认),可能投递重复消息 | 吞吐优先、允许重复的场景 | | **SESSION_TRANSACTED** | 事务模式,Session 提交时自动确认,回滚时消息重新入队 | 需要原子性:消息消费 + 数据库操作要么全成功要么全回滚 | > [!question] JMS 的 AUTO_ACKNOWLEDGE 和 AMQP 的 autoAck 有什么区别? > 思路上类似,但 JMS 的 AUTO_ACKNOWLEDGE 是在方法**正常返回**时确认(异常则不确认),比 AMQP 的 autoAck 更安全一些。不过两者都不推荐在生产环境使用——最可靠的做法都是手动确认。参见 [[5-AMQP-协议]] 中 Prefetch 和手动 ACK 的讨论。 ### 消息选择器(Message Selector) 当一个 Topic 有多个消费者,但每个消费者只关心部分消息时怎么办?JMS 提供了 **Message Selector**——消费者可以在创建时指定一个基于 SQL-92 子集的过滤表达式,Broker 会在投递前过滤掉不匹配的消息。 ```java // 只接收 priority > 5 且 region 为华东的消息 String selector = "priority > 5 AND region = 'east'"; consumer = session.createConsumer(queue, selector); ``` 选择器作用于消息的 **Properties**(自定义键值对)和部分 **Header** 字段(如 `JMSPriority`、`JMSCorrelationID`),不涉及 Body 内容。 > [!warning] 选择器的性能陷阱 > 选择器在 Broker 端执行,实质上是对每条消息做条件匹配。如果选择器表达式复杂、消息量又大,会显著增加 Broker 的 CPU 开销。部分实现(如 ActiveMQ)的选择器性能在高吞吐场景下可能成为瓶颈。如果你发现选择器性能不够用,通常意味着应该用不同的 Queue/Topic 来拆分消息流,而不是在一个队列上做过滤。 ### 请求-应答模式(Request-Reply) JMS 原生支持一种常见的同步通信模式:生产者发送请求消息,并期望收到一条应答消息。这通过两个标准 Header 字段实现: - **`JMSReplyTo`**:请求消息中指定应答消息应发往的 Destination(通常是临时 Queue)。 - **`JMSCorrelationID`**:应答消息携带此 ID,与原始请求关联。 ```mermaid sequenceDiagram participant Client as "Client" participant ReqQ as "Request Queue" participant Server as "Server" participant ReplyQ as "Reply Queue" Client->>ReqQ: "send request" ReqQ->>Server: "deliver" Server->>ReplyQ: "send response with CorrelationID" ReplyQ->>Client: "deliver" ``` ```go // Go 伪代码展示 JMS 请求-应答的核心思路 // 请求方:发消息时指定 ReplyTo 和 CorrelationID req := NewMessage(body) req.SetReplyTo(replyQueue) req.SetCorrelationID(requestID) producer.Send(requestQueue, req) // 应答方:从请求中取出 ReplyTo,应答时回填 CorrelationID resp := NewMessage(result) resp.SetCorrelationID(request.GetCorrelationID()) producer.Send(request.GetReplyTo(), resp) ``` > [!question] 既然可以做请求-应答,那 JMS 能替代 RPC 吗? > 技术上可以,但通常不这么做。请求-应答模式适合**异步、解耦**的场景(如订单处理后通知),但它的延迟比直接 RPC 高(至少经过两次 Broker 投递),且需要处理超时、应答丢失等边界情况。如果你的场景需要同步、低延迟的调用,用 gRPC 或 HTTP 更合适。 ### JMS 的局限性 JMS 在 Java 企业级开发中统治了十多年,但它有几个根深蒂固的问题: 1. **Java 绑定**:JMS 是纯 Java API,无法直接被 Go、Python、C++ 等语言使用。在微服务和多语言架构兴起后,这个限制变得越来越致命。 2. **缺乏跨语言互操作**:不同 JMS 实现之间的有线协议(wire protocol)不统一,ActiveMQ 用 OpenWire,IBM MQ 用 MQI,互相不能直连。 3. **厂商实现碎片化**:虽然有统一规范,但各厂商在扩展特性(如延迟消息、事务消息、死信队列)上的实现差异很大,迁移成本并不低。 4. **规范更新缓慢**:JMS 2.0(2013 年)才引入简化 API,JMS 3.0/3.1 随 Jakarta EE 改名进一步跟进,但整体迭代节奏远落后于社区需求(详见下文 JMS 2.0 改进)。 ### JMS 2.0 的改进 JMS 1.1 的 API 以"繁琐"著称——每次发消息都需要经历 `创建连接 → 创建会话 → 创建生产者 → 创建消息 → 发送 → 关闭` 的完整流程。JMS 2.0(JSR 343,2013 年发布)对此做了大幅简化: **1. JMSContext 合并 Connection + Session** JMS 1.1 中需要分别管理 `Connection` 和 `Session` 两个对象的生命周期。JMS 2.0 引入 `JMSContext`,将二者合并为一个统一入口: ```java // JMS 1.1:需要管理两个对象 Connection conn = factory.createConnection(); Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE); // ... 业务代码 ... session.close(); conn.close(); // JMS 2.0:一个 JMSContext 搞定 try (JMSContext context = factory.createContext()) { context.createProducer().send(queue, "Hello"); } // 自动关闭 ``` **2. 简化消息发送** 不再需要显式创建 `MessageProducer` 和 `TextMessage`: ```java // JMS 1.1 TextMessage msg = session.createTextMessage("Hello"); MessageProducer producer = session.createProducer(queue); producer.send(msg); producer.close(); // JMS 2.0 context.createProducer().send(queue, "Hello"); // 一行搞定 ``` **3. 新增特性** | 特性 | 说明 | |------|------| | **Delivery Delay** | 消息延迟投递:`producer.setDeliveryDelay(30_000)` 表示 30 秒后才投递 | | **Async Send** | 异步发送:`producer.setAsync(completionListener)` 发送后不阻塞,通过回调通知完成 | | **Shared Subscription** | 多个消费者共享同一个 Topic 订阅,消息在消费者间负载均衡(类似 Queue 行为,但作用于 Topic) | | **@JMSDestinationDefinition** | 通过注解声明式定义 JMS Destination,配合 Java EE 容器自动创建队列/主题 | > [!tip] JMS 2.0 的实际影响 > 尽管 JMS 2.0 的 API 改进确实降低了开发门槛,但它发布得太晚了(2013 年)。彼时 Kafka(2011)、RabbitMQ(2007)已各自建立了庞大的生态,新一代开发者更倾向于直接使用 MQ 原生 SDK 而非 JMS 抽象层。JMS 2.0 更多是在存量 Java EE 系统中发挥作用,而非吸引新用户。 ### OpenMessaging:面向云原生的开放标准 2017 年,阿里巴巴在 Linux Foundation 下发起了 OpenMessaging 项目(Apache RocketMQ 是其参考实现之一),目标很明确:定义一个厂商中立、语言无关、云原生的消息和流标准规范。该项目吸引了 Huawei、Yahoo 等公司的参与。 OpenMessaging 的核心设计原则: - **跨语言**:规范不限定编程语言,提供 Java、Go、C++ 等多语言 Binding。 - **跨平台**:不绑定任何特定 MQ 产品,Kafka、RocketMQ、Pulsar 等都可以实现 OpenMessaging 接口。 - **云原生**:原生支持分区、事务、消息追踪、延迟消息等现代需求,而不是事后打补丁。 - **标准化扩展**:通过 Namespace、Region 等概念支持多租户和跨地域部署。 ### OpenMessaging vs JMS 对比 | 维度 | JMS | OpenMessaging | |------|-----|---------------| | 发起方 | Sun Microsystems / Oracle | 阿里巴巴 + 社区 | | 语言绑定 | Java Only | 多语言(Java、Go、C++) | | 消息模型 | Queue + Topic | Partitioned Queue + Topic + Streaming | | 有线协议 | 不统一(厂商各自实现) | 标准化 RPC 协议 | | 事务支持 | JTA 事务集成 | 原生分布式事务 | | 云原生 | 无原生支持 | Namespace、多租户、弹性伸缩 | | 生态成熟度 | 非常成熟,20+ 年积累 | 仍在发展中,落地项目较少 | | 典型实现 | ActiveMQ、IBM MQ | 尚无主流产品完整实现(活跃度低) | > [!question] OpenMessaging 提出了很好的愿景,为什么它的落地进展相对缓慢? > 一方面,Kafka 和 RocketMQ 各自已经形成了庞大生态,应用层集成稳定,切换标准的动力不足。另一方面,消息协议不像 HTTP 那样有极强的网络效应——MQ 的抽象层通常在 SDK 内部,业务开发者感知不到底层协议差异。标准化的收益没有数据库连接池(如 HikariCP 替换 DBCP)那么直接。 > [!warning] OpenMessaging 当前状态 > 截至 2026 年,OpenMessaging 项目的活跃度已大幅下降。GitHub 仓库的提交频率极低,核心维护者基本来自阿里云 RocketMQ 团队,社区生态远未形成。**目前没有主流 MQ 产品完整实现了 OpenMessaging 规范**。它更多地被视为一个概念验证(proof of concept)而非可落地的标准。如果你在做技术选型,不应将 OpenMessaging 作为考量因素——直接评估各 MQ 产品的原生 SDK 更务实。 ### Go 代码:JMS 思路的对等实现 JMS 没有官方 Go SDK,但我们可以用 Go 的接口抽象来实现类似 JMS 的分层设计。以下示例展示如何用面向接口的方式构建消息客户端,底层可插拔不同 MQ 实现。 ```go package mq // Message 对应 JMS 的 Message 接口 type Message interface { Topic() string Body() []byte Properties() map[string]string } // Producer 对应 JMS 的 MessageProducer type Producer interface { Send(topic string, body []byte, props map[string]string) error Close() error } // Consumer 对应 JMS 的 MessageConsumer type Consumer interface { Subscribe(topic string, handler func(Message)) error Close() error } // ClientFactory 对应 JMS 的 ConnectionFactory // 不同 MQ 产品实现这个接口即可插拔替换 type ClientFactory interface { NewProducer() (Producer, error) NewConsumer(group string) (Consumer, error) } ``` 这段代码的核心思想和 JMS 一模一样:面向接口编程。业务代码依赖 `Producer` / `Consumer` 接口,不关心底层是 Kafka、RocketMQ 还是 NATS。替换实现只需要换一个 `ClientFactory`,这正是 JMS 当年设计的初衷——只不过 JMS 把它限定在了 Java 生态里。 --- > [!tip] 实践建议 > - 如果你的系统是纯 Java 栈,JMS 仍然是成熟可靠的选择,配合 Spring JMS Template 能大幅简化开发。 > - 跨语言微服务架构下,优先考虑 Kafka 或 RocketMQ 的原生 SDK,它们的 Go/Python/Node.js 支持都很成熟。 > - 不要为了"标准化"而标准化。协议的价值在于互操作性——如果你的系统不需要对接多个 MQ 产品,直接用原生 SDK 反而更简单。 ## 关联笔记 - [[03-协议与标准/4-MQ-消息协议总览|MQ 消息协议总览]] - [[03-协议与标准/5-AMQP-协议|AMQP 协议]] - [[03-协议与标准/6-MQTT-协议|MQTT 协议]] - [[12-架构与实战/47-MQ-与微服务|MQ 与微服务]]