298 lines
16 KiB
Markdown
298 lines
16 KiB
Markdown
---
|
||
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 与微服务]]
|