JMS是什么?
JMS:java消息服务
什么是Java消息服务?
- Java消息服务指的是
两个应用程序
之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口
,包括创建
、发送
、读取
消息等,用于支持Java应用程序开发。 - 在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的
消息收发服务组件
关联起来以达到解耦/异步削峰
的效果。 - JavaEE
消息头
JMS的消息头有哪些属性:
- JMSDestination:消息目的地
- JMSDeliveryMode:消息持久化模式
- JMSExpiration:消息过期时间
- JMSPriority:消息的优先级
- JMSMessageID:消息的唯一标识符。后面我们会介绍如何解决幂等性。
注意:以上只是部分常用的消息头属性
说明:
- 消息的生产者可以
设置
这些属性,在生产消息之后,使用属性对应的setJMSxxx()
方法 - 消息的消费者可以
获得
这些属性,在消费消息的时候,使用属性对应的getJMSxxx()
方法 - 这些属性在send方法里面也可以设置。
消息生产者代码演示:
public class JmsProduce_topic {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "topic01";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
for (int i = 1; i < 4 ; i++) {
TextMessage textMessage = session.createTextMessage("topic_name--" + i);
// 这里可以指定每个消息的目的地
textMessage.setJMSDestination(topic);
/*
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。
*/
textMessage.setJMSDeliveryMode(0);
/*
可以设置消息在一定时间后过期,默认是永不过期。
消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
*/
textMessage.setJMSExpiration(1000);
/* 消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。
JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
*/
textMessage.setJMSPriority(10);
// 唯一标识每个消息的标识。MQ会给我们默认生成一个,我们也可以自己指定。
textMessage.setJMSMessageID("ABCD");
// 上面有些属性在send方法里也能设置
messageProducer.send(textMessage);
}
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** TOPIC_NAME消息发送到MQ完成 ****");
}
}
消息消费者代码演示:
public class JmsConsummer_topic {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "topic01";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageConsumer messageConsumer = session.createConsumer(topic);
messageConsumer.setMessageListener( (message) -> {
// 判断消息是哪种类型之后,再强转。
if (null != message && message instanceof TextMessage){
TextMessage textMessage = (TextMessage)message;
try {
System.out.println("****消费者text的消息:"+textMessage.getText());
}catch (JMSException e) {
}
}
if (null != message && message instanceof MapMessage){
MapMessage mapMessage = (MapMessage)message;
try {
System.out.println("****消费者的map消息:"+mapMessage.getString("name"));
System.out.println("****消费者的map消息:"+mapMessage.getInt("age"));
}catch (JMSException e) {
}
}
});
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
消息体
消息体:封装具体的消息数据,因为消息有很多种类型,发送和接收的消息体类型必须一致
5中消息体格式:
- TextMessage:普通字符串消息,包含一个String
- MapMessage:一个Map类型的消息,key为String类型,value为java的基本类型
- BytesMessage:二进制数组消息,包含一个byte[]
- StreamMessage:java数据流消息,用标准流操作来顺序的填充和读取
- ObjectMessage:对象消息,包含一个可序列化的对象
下面演示TextMessage
和MapMessage
的用法:
public class JmsProduce_topic {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "topic01";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
for (int i = 1; i < 4 ; i++) {
// 发送TextMessage消息体
TextMessage textMessage = session.createTextMessage("topic_name--" + i);
messageProducer.send(textMessage);
// 发送MapMessage 消息体
//set方法: 添加,get方式:获取
MapMessage mapMessage = session.createMapMessage();
mapMessage.setString("name", "张三"+i);
mapMessage.setInt("age", 18+i);
messageProducer.send(mapMessage);
}
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** TOPIC_NAME消息发送到MQ完成 ****");
}
}
消息属性
如果需要除消息头字段之外的值,那么可以使用消息属性,它是识别/去重/重点标注
等操作,非常有用的方法。
它们是以属性名和属性值对的形式制定的,可以将属性视为消息头的扩展
,属性指定一些消息头没有包括的附加信息
,比如可以在属性里指定消息选择器
。
消息的属性就像可以分配给一条消息的附加消息头一样,它们允许开发者添加有关消息的不透明附加信息,它们还用于暴露消息选择器在消息过滤时使用的数据。
下图是设置消息属性的API:
下图是获得消息属性的API:
注意:使用方式和设置消息头属性以及获取消息头属性类似,这里就不做代码演示了
JMS的可靠性
消息的持久化
非持久化,如果服务器宕机,则消息不存在,设置方式如下:
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
持久化,当服务器宕机,消息依然存在,设置方式如下:
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
queue消息持久化
queue非持久:
- 当服务器宕机,消息不存在(消息丢失了)。
- 如果是消费者不在线的话,即便是非持久,消息也不会丢失,等待消费者上线消费消息。
queue持久化:
- 当服务器宕机,消息依然存在,queue消息
默认
是持久化的。
持久化消息,保证这些消息只被传送一次和成功使用一次,对于这些消息,可靠性是优先考虑的因素。
可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息。
持久化queue生产者代码:
//持久化queue的消息生产者
public class JmsProduce_persistence {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String QUEUE_NAME= "queue_persistence";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue=session.createQueue(QUEUE_NAME);
MessageProducer messageProducer = session.createProducer(queue);
// 设置持久化queue
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 设置持久化queue之后再,启动连接
connection.start();
for (int i = 1; i < 4 ; i++) {
TextMessage textMessage = session.createTextMessage("queue_name--" + i);
messageProducer.send(textMessage);
MapMessage mapMessage = session.createMapMessage();
}
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** QUEUE_NAME消息发送到MQ完成 ****");
}
}
持久化的queue消费者代码:
// 持久化queue的消息消费者
public class JmsConsummer_persistence {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String QUEUE_NAME = "queue_persistence";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
// 设置客户端ID。向MQ服务器注册自己的名称
connection.setClientID("AISMALL_QUEUE01");
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue=session.createQueue(QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
connection.start();
while(true){
TextMessage message = (TextMessage)messageConsumer.receive();
if (null != message){
System.out.println("****消费者的消息:"+message.getText());
}else {
break;
}
}
messageConsumer.close();
session.close();
connection.close();
}
}
topic消息持久化
topic默认
就是非持久化的,因为生产者生产消息时,消费者也要在线,这样消费者才能消费到消息。
topic消息持久化,只要消费者向MQ服务器注册过,所有生产者发布成功的消息,该消费者都能收到,不管是MQ服务器宕机还是消费者不在线。
注意:
- 1、一定要先运行一次消费者,等于向MQ注册,类似我订阅了这个主题。
- 2、然后再运行生产者发送消息。
- 3、之后无论消费者是否在线,都会收到消息。如果不在线的话,下次连接的时候,会把没有收过的消息都接收过来。
持久化topic生产者代码:
//持久化topic 的消息生产者
public class JmsProduce_persistence {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "topic_persistence";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
// 设置持久化topic
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 设置持久化topic之后再,启动连接
connection.start();
for (int i = 1; i < 4 ; i++) {
TextMessage textMessage = session.createTextMessage("topic_name--" + i);
messageProducer.send(textMessage);
MapMessage mapMessage = session.createMapMessage();
}
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** TOPIC_NAME消息发送到MQ完成 ****");
}
}
持久化topic消费者代码:
// 持久化topic 的消息消费者
public class JmsConsummer_persistence {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "topic_persistence";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
// 设置客户端ID。向MQ服务器注册自己的名称
connection.setClientID("AISMALL_TOPIC01");
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
// 创建一个topic订阅者对象。一参是topic,二参是订阅者名称
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"remark...");
// 之后再开启连接
connection.start();
Message message = topicSubscriber.receive();
while (null != message){
TextMessage textMessage = (TextMessage)message;
System.out.println(" 收到的持久化 topic :"+textMessage.getText());
message = topicSubscriber.receive();
}
session.close();
connection.close();
}
}
控制台介绍: topic页面还是和之前的一样,另外在subscribers页面也会显示。
消息的事务性
1、生产者开启事务后,执行commit方法,这批消息才真正的被提交,不执行commit方法,这批消息不会提交,执行rollback方法,之前的消息会回滚掉,生产者的事务机制,要高于签收机制
,当生产者开启事务,签收机制不再重要。
2、消费者开启事务后,执行commit方法,这批消息才算真正的被消费,不执行commit方法,这些消息不会标记已消费,下次还会被消费,执行rollback方法,是不能回滚之前执行过的业务逻辑,但是能够回滚之前的消息,回滚后的消息,下次还会被消费,消费者利用commit和rollback方法,甚至能够违反一个消费者只能消费一次消息的原理。
3、问:消费者和生产者需要同时操作事务才行吗?
- 答:消费者和生产者的事务,完全没有关联,各自是各自的事务。
带事务的生产者代码:
public class Jms_TX_Producer {
private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-TX";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//1.创建会话session,两个参数transacted=事务,acknowledgeMode=确认模式(签收)
//设置为开启事务
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageProducer producer = session.createProducer(queue);
try {
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("tx msg--" + i);
producer.send(textMessage);
if(i == 2){
throw new RuntimeException("GG.....");
}
}
// 2. 开启事务后,使用commit提交事务,这样这批消息才能真正的被提交。
session.commit();
System.out.println("消息发送完成");
} catch (Exception e) {
System.out.println("出现异常,消息回滚");
// 3. 工作中一般,当代码出错,我们在catch代码块中回滚。这样这批发送的消息就能回滚。
session.rollback();
} finally {
//4. 关闭资源
producer.close();
session.close();
connection.close();
}
}
}
带事务的消费者代码:
public class Jms_TX_Consumer {
private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-TX";
public static void main(String[] args) throws JMSException, IOException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 创建会话session,两个参数transacted=事务,acknowledgeMode=确认模式(签收)
// 消费者开启了事务就必须手动提交,不然会重复消费消息
final Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(new MessageListener() {
int a = 0;
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println("***消费者接收到的消息: " + textMessage.getText());
if(a == 0){
System.out.println("commit");
session.commit();
}
if (a == 2) {
System.out.println("rollback");
session.rollback();
}
a++;
} catch (Exception e) {
System.out.println("出现异常,消费失败,放弃消费");
try {
session.rollback();
} catch (JMSException ex) {
ex.printStackTrace();
}
}
}
}
});
//关闭资源
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
消息的签收机制
签收的几种方式
-
1、自动签收(
Session.AUTO_ACKNOWLEDGE
):该方式是默认的,该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。 -
2、手动签收(
Session.CLIENT_ACKNOWLEDGE
):该种方式,需要我们手动调用Message.acknowledge(),来签收消息,如果不签收消息,该消息会被我们反复消费,直到被签收。 -
3、允许重复消息(
Session.DUPS_OK_ACKNOWLEDGE
):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费,该种方式很少使用到。 -
4、事务下的签收(
Session.SESSION_TRANSACTED
):开始事务的情况下,可以使用该方式,该种方式很少使用到。
事务和签收的关系
- 1、在事务性会话中,当一个事务被成功提交则消息被自动签收,如果事务回滚,则消息会被再次传送,事务优先于签收,开始事务后,签收机制不再起任何作用。
- 2、非事务性会话中,消息何时被确认取决于创建会话时的应答模式。
- 3、生产者事务开启,只有commit后才能将全部消息变为可以被消费。
- 4、
事务偏向生产者,签收偏向消费者,也就是说,生产者使用事务更好点,消费者使用签收机制更好点
非事务下的生产者:
public class Jms_TX_Producer {
private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-ACK";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageProducer producer = session.createProducer(queue);
try {
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("tx msg--" + i);
producer.send(textMessage);
}
System.out.println("消息发送完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
producer.close();
session.close();
connection.close();
}
}
}
非事务下的消费者手动签收:
public class Jms_TX_Consumer {
private static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-ACK";
public static void main(String[] args) throws JMSException, IOException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
try {
TextMessage textMessage = (TextMessage) message;
System.out.println("***消费者接收到的消息: " + textMessage.getText());
/* 设置为Session.CLIENT_ACKNOWLEDGE后,要调用该方法,标志着该消息已被签收(消费)。
如果不调用该方法,该消息的标志还是未消费,下次启动消费者或其他消费者还会收到改消息。
*/
textMessage.acknowledge();
} catch (Exception e) {
System.out.println("出现异常,消费失败,放弃消费");
}
}
}
});
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
JMS的点对点总结
点对点模型是基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能,和我们平时给朋友发送短信类似。
如果在Session关闭时有部分消息己被收到但还没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会被再次接收
队列可以长久地保存消息直到消费者收到消息,消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势。
JMS的发布订阅总结
(1) JMS的发布订阅总结
- JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。
- 主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
- 主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送
(2) 非持久订阅
- 非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
- 如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
- 一句话:先订阅注册才能接受到发布,只给订阅者发布消息。
(3) 持久订阅
- 客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
- 非持久订阅状态下,不能恢复或重新派送一个未签收的消息。
- 持久订阅才能恢复或重新派送一个未签收的消息。
(4) 非持久和持久化订阅如何选择
- 当所有的消息必须被接收,则用持久化订阅。
- 当消息丢失能够被容忍,则用非持久订阅