JMS 规范中定义了两种消息传递域
点对点(point-to-point )消息传递域 和发布 / 订 阅 消息传递域(publish/subscribe)简单理解就是:有点类似于我们通过 qq 聊天的时候,在群里面发消息和给其中一个同学私聊消息。在群里发消息,所有群成员都能收到消息。私聊消息只能被私聊的学员能收到消息。
点对点消息传递域
- 每个消息只能有一个消费者
- 消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,都可以提取消息。
发布订阅消息传递域
- 每个消息可以有多个消费者
- 生产者和消费者之间有时间上的相关性。订阅一个主题的消费者只能消费自它订阅之后发布的消息。JMS 规范允许客户创建持久订阅,这在一定程度上降低了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。
消息结构组成
JMS 消息由及部分组成:消息头、属性、消息体
-
消息头
消息头(Header) - 消息头包含消息的识别信息和路由信息,消息头包含一些标准的属性如:JMSDestination 消息发送的目的地,queue 或者 topic)JMSDeliveryMode 传送模式。持久模式和非持久模式JMSPriority 消息优先级(优先级分为 10 个级别,从 0(最低)到 9(最高). 如果不设定优先级,默认级别是 4。需要注意的是,JMS provider 并不一定保证按照优先级的顺序提交消息)JMSMessageID 唯一识别每个消息的标识。
-
属性
按类型可以分为应用设置的属性,标准属性和消息中间件定义的属性
- 应用程序设置和添加的属性,比如
Message.setStringProperty(“key”,”value”);
通过下面的代码可以获得自定义属性的,在接收端的代码中编写在发送端,定义消息属性。
message.setStringProperty("Mic","Hello World");
在接收端接收数据
Enumeration enumeration=message.getPropertyNames();
while(enumeration.hasMoreElements()){
String name=enumeration.nextElement().toString();
System.out.println("name:"+name+":"+message.getStringProperty(name));
System.out.println();
}
- JMS 定义的属性
使用“JMSX”作为属性名的前缀,通过下面这段代码可以返回所有连接支持的 JMSX 属性的名字。
Enumeration names = connection.getMetaData().getJMSXPropertyNames();
while (names.hasMoreElements()){
String name = (String)names.nextElement();
System.out.println(name);
}
- JMS provider 特定的属性
消息体就是我们需要传递的消息内容,JMS API 定义了 5 种消息体格式,可以使用不同形式发送接收数据,并可以兼容现有的消息格式,其中包括
类型 | 格式 |
---|---|
TextMessage | java.lang.String 对象,如 xml 文件内容 |
MapMessage | 名/值对的集合,名是 String 对象,可以是 Java 任何基本类型 |
BytesMessage | 字节流 |
StreamMessage | Java 中的输入输出流 |
ObjectMessage | Java 中的可序列化对象 |
Message | 没有消息体,只有消息头和属性。 |
绝大部分的时候,我们只需要基于消息体进行构造
持久订阅
持久订阅的概念,也很容易理解,比如还是以 QQ 为例,我们把 QQ 退出了,但是下次登录的时候,仍然能收到离线的消息。持久订阅就是这样一个道理,持久订阅有两个特点:
- 持久订阅者和非持久订阅者针对的 Domain 是 Pub/Sub,而不是 PTP
- 当 Broker 发送消息给订阅者时,如果订阅者处于未激活状态状态:持久订阅者可以收到消息,而非持久订阅者则收不到消息。当然这种方式也有一定的影响:当持久订阅者处于未激活状态时,Broker 需要为持久订阅者保存消息;如果持久订阅者订阅的消息太多则会溢出。
服务端
package com.adviser.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.util.Enumeration;
public class JMSTopicProducer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory
("tcp://192.168.0.128:61616");
Connection connection = null;
try {
connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession
(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建队列
Destination destination = session.createTopic("myTopic");
//创建发送者
MessageProducer producer = session.createProducer(destination);
//持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//创建需要发送的消息
TextMessage message = session.createTextMessage("周四晚上上线");
producer.send(message);
session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if (connection!=null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
消费端
package com.adviser.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSPersistentTopicConsumer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory
("tcp://192.168.0.128:61616");
Connection connection = null;
try {
connection = connectionFactory.createConnection();
connection.setClientID("lizhe-001");
connection.start();
Session session = connection.createSession
(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
//创建队列
Topic destination = session.createTopic("myTopic");
//创建发送者
MessageConsumer consumer = session.createDurableSubscriber(destination,"lizhe-001");
//接收消息
TextMessage textMessage = (TextMessage) consumer.receive();
System.out.println(textMessage.getText());
session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if (connection!=null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
先启动消费端去注册一个持久订阅。持久订阅时,客户端向 JMS 服务器注册一个自己身份的 ID,当这个客户端处于离线时,JMS Provider 会为这个 ID 保存所有发送到主题的消息,当客户再次连接到 JMS Provider 时,会根据自己的 ID 得到所有当自己处于离线时发送到主题的消息。这个身份ID,在代码中的体现就是 connection的 ClientID,这个其实很好理解,你要想收到朋友发送的 qq 消息,前提就是你得先注册个 QQ 号,而且还要有台能上网的设备,电脑或手机。设备就相当于是 clientId 是唯一的;qq 号相当于是订阅者的名称,在同一台设备上,不能用同一个 qq号挂 2 个客户端。连接的 clientId 必须是唯一的,订阅者的名称在同一个连接内必须唯一。这样才能唯一的确定连接和订阅者。
activeMQ 控制台的截图
设置持久订阅以后,在控制台能看到下图的变化
JMS 消息的可靠性机制
- 理论上来说,我们需要保证消息中间件上的消息,只有被消费者确认过以后才会被签收,相当于我们寄一个快递出去,收件人没有收到快递,就认为这个包裹还是属于待签收状态,这样才能保证包裹能够安全达到收件人手里。消息中间件也是一样。
- 消息的消费通常包含 3 个阶段:客户接收消息、客户处理消息、消息被确认首先,来简单了解 JMS 的事务性会话和非事务性会话的概念JMS
Session 接口提供了 commit 和 rollback 方法。 - 事务提交意味着生产的所有消息被发送,消费的所有消息被确认;事务回滚意味着生产的所有消息被销毁,消费的所有消息被恢复并重新提交,除非它们已经过期。事务性的会话总是牵涉到事务处理中,commit 或 rollback方法一旦被调用,一个事务就结束了,而另一个事务被开始。关闭事务性会话将回滚其中的事务。
在事务型会话中
必须保证发送端和接收端都是事务性会话
- 在事务状态下进行发送操作,消息并未真正投递到中间件,而只有进行 session.commit操作之后,消息才会发送到中间件,再转发到适当的消费者进行处理。
- 如果是调用rollback 操作,则表明,当前事务期间内所发送的消息都取消掉。通过在创建 session 的时候使用 true or false 来决定当前的会话是事务性还是非事务性。
Session session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
在事务性会话中,消息的确认是自动进行,通过session.commit()以后,消息会自动确认。
在非事务型会话中
消息何时被确认取决于创建会话时的应答模式(acknowledgement mode). 有三个可选项。
1.Session.AUTO_ACKNOWLEDGE
当客户成功从 receive 方法返回或者从MessageListenner.onMessage 方法成功返回的时候,会话自动确认客户收到消息。
2.Session.CLIENT_ACKNOWLEDGE
客户通过调用消息的 acknowledge 方法确认消息。
textMessage.acknowledge();
在这种模式中,确认是在会话层上进行,确认一个被消费的消息将自动确认所有已被会话消费的消息。列如,如果一个消息消费者消费了 10 个消息,然后确认了第 5 个消息,那么 0~5 的消息都会被确认。
演示如下:发送端发送 10 个消息,接收端接收 10 个消息,但是在 i==5 的时候,调用 message.acknowledge()进行确认,会发现 0~4 的消息都会被确认
发送端
package com.adviser.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import javax.jms.*;
public class JMSQueueProducer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory
("tcp://192.168.0.128:61616");
Connection connection = null;
try {
connection = connectionFactory.createConnection();
connection.start();
// Session session = connection.createSession
// (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
Session session = connection.createSession
(Boolean.FALSE,Session.AUTO_ACKNOWLEDGE);
//创建队列
Destination destination = session.createQueue("myQueue");
//创建发送者
MessageProducer producer = session.createProducer(destination);
//创建需要发送的消息
for (int i = 0; i < 10;i++) {
TextMessage message = session.createTextMessage("Hello 女友"+i);
producer.send(message);
}
//如果Boolean为true时需要commit
//session.commit();
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if (connection!=null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
接收端
package com.adviser.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSQueueConsumer {
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory
("tcp://192.168.0.128:61616");
Connection connection = null;
try {
connection = connectionFactory.createConnection();
connection.start();
//需要commit提交
// Session session = connection.createSession
// (Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
Session session = connection.createSession
(Boolean.FALSE,Session.CLIENT_ACKNOWLEDGE);
//创建队列
Destination destination = session.createQueue("myQueue");
//创建发送者
MessageConsumer consumer = session.createConsumer(destination);
//接收消息
for (int i = 0; i < 10; i++) {
TextMessage textMessage = (TextMessage) consumer.receive();
System.out.println(textMessage.getText());
if (i==5){
//消息签收
textMessage.acknowledge();
}
}
//session.commit();表示消息被自动确认 Boolean.FALSE
session.close();
} catch (JMSException e) {
e.printStackTrace();
}finally {
if (connection!=null){
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
}
3.Session.DUPS_ACKNOWLEDGE
消息延迟确认。指定消息提供者在消息接收者没有确认发送时重新发送消息,这种模式不在乎接受者收到重复的消息。
消息的持久化存储
消息的持久化存储也是保证可靠性最重要的机制之一,也就是消息发送到 Broker 上以后,如果 broker 出现故障宕机了,那么存储在 broker 上的消息不应该丢失。可以通过下面的代码来设置消息发送端的持久化和非持久化特性
//创建发送者
MessageProducer producer = session.createProducer(destination);
//消息的持久化存储
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
➢ 对于非持久的消息,JMS provider 不会将它存到文件/数据库等稳定的存储介质中。也就是说非持久消息驻留在内存中,如果 jms provider 宕机,那么内存中的非持久消息会丢失
➢ 对于持久消息,消息提供者会使用存储-转发机制,先将消息存储到稳定介质中,等消息发送成功后再删除。如果 jms provider 挂掉了,那么这些未送达的消息不会丢失;jms provider 恢复正常后,会重新读取这些消息,并传送给对应的消费者。