Files
cs-note/hhs/MQ/03-协议与标准/7-JMS-与-OpenMessaging.md
T
2026-06-08 23:08:57 +08:00

298 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 与微服务]]