什麼是JMS?
JMS即Java消息服務(Java Message Service)應用程序接口,指的是兩個應用程序之間進行異步通信的API,它爲標準協議和消息服務提供了一組通用接口,包括創建、發送、讀取消息等,用於支持Java應用程序開發。在JavaEE中,當兩個應用程序使用JMS進行通信時,它們之間不是直接相連的,而是通過一個共同的消息收發服務組件關聯起來以達到解耦/異步削峯的效果。
JMS組成結構
- JMS Provider:實現JSM接口和規範的消息中間件,也就是我們所說的MQ服務端
- JMS Producer:消息生產者,創建和發送JSM消息的客戶端應用
- JMS Consumer:消息消費者,接收和處理JSM消息的客戶端應用
- JSM Message:通過JSM傳遞的消息
- 消息頭
- 消息體
- 消息屬性
消息頭
JMS消息頭常用屬性:
- JMSDestination:消息發送的目的地,主要是之Queue和Topic
- JMSDeliveryMode:消息持久化模式
- JMSExpiration:消息過期時間
- JMSPriority:消息優先級
- JMSMessageID:消息id,消息的唯一標識符
生產者在發送消息的時候可以設置這些屬性,消費者在消費消息的時候可以獲取到這些屬性;
代碼示例:
TextMessage textMessage = session.createTextMessage("hello_activeMQ_Server" + i);
// 單獨設置消息的目的地,可以是queue或topic
textMessage.setJMSDestination(queue);
/**
* 設置消息持久化模式
* 持久模式:DeliveryMode.PERSISTENT
* 非持久化模式:DeliveryMode.NON_PERSISTENT
* 消息默認是持久化的
*
* 一條持久化的消息:應該被傳送“一次僅僅一次”,這就意味着如果JMS提供者出現故障,該消息並不會丟失,它會在服務器恢復之後再次傳遞。
* 一條非持久的消息:最多會傳遞一次,這意味着服務器出現故障,該消息將會永遠丟失。
*/
textMessage.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT);
/**
* 設置消息在一定時間後過期,默認是永不過期。
* 消息過期時間,等於Destination的send方法中的timeToLive值加上發送時刻的GMT時間值。
* 如果timeToLive值等於0,則JMSExpiration被設爲0,表示該消息永不過期。
* 如果發送後,在消息過期時間之後還沒有被髮送到目的地,則該消息會被清除。
*/
textMessage.setJMSExpiration(1000);
/**
* 設置消息優先級
* 從0-9十個級別,0-4是普通消息,5-9是加急消息
* JMS不要求MQ嚴格按照這十個優先級發送消息但必須保證加急消息要先於普通消息到達。默認是4級。
*/
textMessage.setJMSPriority(9);
/**
* 消息唯一標識符,MQ會給我們默認生成一個,也可以自己指定,需全局唯一
*/
textMessage.setJMSMessageID("123");
messageProducer.send(textMessage);
消息體
JMS可以創建五種類型的消息,接收時的消息類型必須與發送時的消息一一對應;
- TextMessage:字符串消息,包含一個string;
- MapMessage:Map類型的消息,鍵值對,key爲String類型,值爲Java的基礎類型;
- BytesMessage:二進制數組消息,包含一個byte[];
- StreamMessage:Java數據流消息,用標準的數據流來順序的填充和讀取
- ObjectMessage:對象型消息,包含一個可序列化的對象
常用的也就TextMessage和MapMessage,下面我們來演示這兩種類型的消息用法:
// 發送
TextMessage textMessage = session.createTextMessage("hello_activeMQ_Server" + i);
messageProducer.send(textMessage);
MapMessage mapMessage = session.createMapMessage();
mapMessage.setString("k1","mapMessage");
mapMessage.setInt("k2",1);
messageProducer.send(mapMessage);
// 接收
messageConsumer.setMessageListener((message) -> {
if (message != null) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消費掉的消息 -> " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
} else if (message instanceof MapMessage) {
MapMessage mapMessage = (MapMessage) message;
try {
System.out.println("消費掉的Map消息 -> " + mapMessage.getString("k1"));
System.out.println("消費掉的Map消息 -> " + mapMessage.getInt("k2"));
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
消息屬性
如果消息頭攜帶的信息還不夠滿足你的需要的話,那麼還可以通過消息屬性來攜帶更多的信息,常用於識別/去重/重點標註等操作;
他們是以屬性名和屬性值對的形式制定的。可以將屬性是爲消息頭得擴展,屬性指定一些消息頭沒有包括的附加信息,比如可以在屬性裏指定消息選擇器。消息的屬性就像可以分配給一條消息的附加消息頭一樣。它們允許開發者添加有關消息的不透明附加信息。它們還用於暴露消息選擇器在消息過濾時使用的數據。
根據自己攜帶消息屬性數據類型,選擇相應的設置方法
// 發送
TextMessage textMessage = session.createTextMessage("hello_activeMQ_Server" + i);
textMessage.setStringProperty("user","zhangsan");
textMessage.setBooleanProperty("isLogin", true);
messageProducer.send(textMessage);
// 獲取
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消費掉的消息 -> " + textMessage.getText());
System.out.println("消費掉的消息屬性 -> " + textMessage.getStringProperty("user"));
System.out.println("消費掉的消息屬性 -> " + textMessage.getBooleanProperty("isLogin"));
} catch (JMSException e) {
e.printStackTrace();
}
消息的持久化
消息持久化就是在消息發送至目的地時,消息服務器會將其持久化存儲,假如消費者還沒來得及消費的時候,服務器出現故障、宏機了,當服務再次恢復時,此消息不會丟失,會依舊存在,並能夠保證(topic要之前註冊過,queue不用)消息消費者能成功消費之前未消費的消息,通過這種方式就保證了消息的可靠性;
Queue:queue非持久,當服務器宕機,消息不存在(消息丟失了)。即便是非持久,消費者在不在線的話,消息也不會丟失,等待消費者在線,還是能夠收到消息的。
queue持久化,當服務器宕機,消息依然存在。queue消息默認是持久化的。
持久化消息,保證這些消息只被傳送一次和成功使用一次。對於這些消息,可靠性是優先考慮的因素。
可靠性的另一個重要方面是確保持久性消息傳送至目標後,消息服務在向消費者傳送它們之前不會丟失這些消息。
Queue消息持久化只需要設置生產者,代碼如下:
// 根據session創建隊列
Queue queue = session.createQueue(QUEUE_NAME);
// 創建消息的生產者並指定隊列
messageProducer = session.createProducer(queue);
// 持久化:當服務器出現故障,服務恢復後消息依然存在
// messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 非持久化:當服務器出現故障,服務恢復後消息不存在
// messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
Topic:topic默認就是非持久化的,因爲生產者生產消息時,消費者也要在線,這樣消費者才能消費到消息。topic消息持久化,只要消費者向MQ服務器註冊過,所有生產者發佈成功的消息,該消費者都能收到,不管是MQ服務器宕機還是消費者不在線。
Topic消息持久化,,生產者和消費者都需要設置,代碼如下:
生產者:
// 根據session創建主題
Topic topic = session.createTopic(TOPIC_NAME);
// 創建消息的生產者並指定隊列
messageProducer = session.createProducer(topic);
// 設置持久化topic
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 設置持久化topic之後再啓動連接
connection.start();
消費者:
// 通過連接工廠,獲得connection,持久化的topic必須在消費者創建並創建好訂閱者完成後調用start
connection = activeMQConnection.createConnection();
// 設置客戶端id,向MQ服務器註冊自己
connection.setClientID("Consumer_Persistence");
/**
* 創建session,得到會話
* 兩個參數transacted=事務,acknowledgeMode=確認模式(簽收)
*/
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 根據session創建主題
Topic topic = session.createTopic(TOPIC_NAME);
// 創建一個topic訂閱者對象,參數以:topic;參數二:訂閱者名稱
session.createDurableSubscriber(topic,"訂閱者");
// 打開連接
connection.start();
當有消費者訂閱了一個持久化主題,可以在activeMQ控制檯中的Subscribers
頁面看到:
此時我們停掉消費者程序,啓動生產者發送消息:
從上圖可以看到有一個消費者,入隊消息數量爲3,出對消息數量爲0,也就是沒有消費者消費消息,消息訂閱者也是離線狀態;
此時我們來啓動消息訂閱者,看能否收到消息:
可以看到消費者能夠收到了生產者發送的消息,出對消息數量由0變爲了3
消息的事物
說到事物,大家可能都不陌生,我們在日常項目開發中,數據的插入,修改,都會涉及到事物,同理,activeMQ也有事物的支持;
生產者開啓事務後,執行commit方法,這批消息才真正的被提交。不執行commit方法,這批消息不會提交。執行rollback方法,之前的消息會回滾掉。生產者的事務機制,要高於簽收機制,當生產者開啓事務,簽收機制不再重要。
消費者開啓事務後,執行commit方法,這批消息纔算真正的被消費。不執行commit方法,這些消息不會標記已消費,下次還會被消費。執行rollback方法,是不能回滾之前執行過的業務邏輯,但是能夠回滾之前的消息,回滾後的消息,下次還會被消費。消費者利用commit和rollback方法,甚至能夠違反一個消費者只能消費一次消息的原理。
生產者和消費者事物各自獨立,可獨立開啓,沒有關聯
生產者代碼示例:
package com.chaytech.activemq.transaction;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 消息生產者(開啓事物)
*
* @author Chency
* @email [email protected]
* @Date 2020/03/21 22:20
*/
public class JmsProducer {
private static final String BROKER_URL = "tcp://192.168.0.166:61616"; // activeMQ服務地址
private static final String QUEUE_NAME = "test_queue_1"; // 消息隊列名稱
public static void main(String[] args) {
/**
* 根據我們指定activeMQ服務地址來創建activeMQ工廠
* 如果我們自己不指定activeMQ服務地址則默認爲本機
*/
ActiveMQConnectionFactory activeMQConnection = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = null;
Session session = null;
MessageProducer messageProducer = null;
try {
// 通過連接工廠,獲得connection
connection = activeMQConnection.createConnection();
// 打開連接
connection.start();
/**
* 創建session,得到會話
* 兩個參數transacted=事務,true開啓,acknowledgeMode=確認模式(簽收)
*/
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
// 根據session創建隊列
Queue queue = session.createQueue(QUEUE_NAME);
// 創建消息的生產者並指定隊列
messageProducer = session.createProducer(queue);
// 通過使用消息生產者,生產消息,發送到MQ的隊列裏面
for (int i = 1; i <= 3; i++) {
TextMessage textMessage = session.createTextMessage("hello_activeMQ_Server" + i);
messageProducer.send(textMessage);
}
// 開啓事務後,使用commit提交事務,這樣這批消息才能真正的被提交。
session.commit();
System.out.println("**********msg send sucess**********");
} catch (JMSException e) {
e.printStackTrace();
// 當出現異常了,我們可以在catch代碼塊中回滾。這樣這批發送的消息就能回滾。
try {
session.rollback();
} catch (JMSException e1) {
}
} finally {
try {
messageProducer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
消費者代碼示例:
package com.chaytech.activemq.transaction;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 消息消費者(開啓事物)
*
* @author Chency
* @email [email protected]
* @Date 2020/03/22 10:45
*/
public class JmsConsumer {
private static final String BROKER_URL = "tcp://192.168.0.166:61616"; // activeMQ服務地址
private static final String QUEUE_NAME = "test_queue_1"; // 消息隊列名稱
public static void main(String[] args) {
/**
* 根據我們指定activeMQ服務地址來創建activeMQ工廠
* 如果我們自己不指定activeMQ服務地址則默認爲本機
*/
ActiveMQConnectionFactory activeMQConnection = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = null;
Session session = null;
MessageConsumer messageConsumer = null;
try {
// 通過連接工廠,獲得connection
connection = activeMQConnection.createConnection();
// 打開連接
connection.start();
/**
* 創建session,得到會話
* 兩個參數transacted=事務,true 開啓,acknowledgeMode=確認模式(簽收)
* 消費者開啓了事務就必須手動提交,不然會出現消息重複消費
*/
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
// 根據session創建隊列
Queue queue = session.createQueue(QUEUE_NAME);
// 創建消息消費者並指定消費的隊列
messageConsumer = session.createConsumer(queue);
/**
* 通過監聽的方式來消費消息,是以異步非阻塞的方式來消費消息
* 通過messageConsumer 的setMessageListener 註冊一個監聽器,當有消息發送來時,系統自動調用MessageListener 的 onMessage 方法處理消息
*/
messageConsumer.setMessageListener((message) -> {
if (message != null) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消費掉的消息 -> " + textMessage.getText());
System.out.println("消費掉的消息屬性 -> " + textMessage.getStringProperty("user"));
System.out.println("消費掉的消息屬性 -> " + textMessage.getBooleanProperty("isLogin"));
} catch (JMSException e) {
e.printStackTrace();
}
} else if (message instanceof MapMessage) {
MapMessage mapMessage = (MapMessage) message;
try {
System.out.println("消費掉的Map消息 -> " + mapMessage.getString("k1"));
System.out.println("消費掉的Map消息 -> " + mapMessage.getInt("k2"));
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
// 開啓事務後,使用commit提交事務,這樣這批消息纔會真正的被消費掉。
session.commit();
/**
* 此處是爲了不讓主線程結束,因爲一旦主線程結束了,其他的線程(如此處的監聽消息的線程)也都會被迫結束。
* 實際開發中不需要
*/
System.in.read();
} catch (Exception e) {
e.printStackTrace();
try {
// // 當出現異常了,我們可以在catch代碼塊中回滾。這樣就可以再次消費此消息
session.rollback();
} catch (JMSException e1) {
}
} finally {
try {
messageConsumer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
消息的簽收機制
前面我們講生產者和消費者在創建session的時候,需要傳兩個參數,第一個是是否開啓事物,第二個是消息簽收方式,本次我們將來聊一聊activeMQ的消息簽收機制;
消息簽收方式,一共分以下幾種:
- Session.AUTO_ACKNOWLEDGE(自動簽收):此方式是默認的。無需我們程序做任何操作,會幫我們自動簽收收到的消息。
- Session.CLIENT_ACKNOWLEDGE(手動簽收):此方式需要我們手動調用
Message.acknowledge()
方法來簽收消息。如果不簽收消息,該消息會被我們反覆消費,直到被簽收。 - Session.DUPS_OK_ACKNOWLEDGE(允許重複消息):多線程或多個消費者同時消費到一個消息,因爲線程不安全,可能會重複消費。該種方式很少使用到。
- Session.SESSION_TRANSACTED(事物下的簽收):開始事務的情況下,可以使用該方式。該種方式很少使用到。
事物和消息簽收的關係:
- 在事務性會話中,當一個事務被成功提交則消息被自動簽收。如果事務回滾,則消息會被再次傳送。事務優先於簽收,開始事務後,簽收機制不再起任何作用。
- 非事務性會話中,消息何時被簽收取決於創建會話時的應答模式。
- 生產者事務開啓,只有commit後才能將全部消息變爲已消費
- 事務偏向生產者,簽收偏向消費者。也就是說,生產者使用事務更好點,消費者使用簽收機制更好點。
下面我們來演示一下,非事物下的消費者手動簽收消息:
package com.chaytech.activemq.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 消息消費者
*
* @author Chency
* @email [email protected]
* @Date 2020/03/22 10:45
*/
public class JmsConsumer {
private static final String BROKER_URL = "tcp://192.168.0.166:61616"; // activeMQ服務地址
private static final String QUEUE_NAME = "test_queue_1"; // 消息隊列名稱
public static void main(String[] args) {
/**
* 根據我們指定activeMQ服務地址來創建activeMQ工廠
* 如果我們自己不指定activeMQ服務地址則默認爲本機
*/
ActiveMQConnectionFactory activeMQConnection = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = null;
Session session = null;
MessageConsumer messageConsumer = null;
try {
// 通過連接工廠,獲得connection
connection = activeMQConnection.createConnection();
// 打開連接
connection.start();
/**
* 創建session,得到會話
* 兩個參數transacted=事務,acknowledgeMode=確認模式(簽收)
*/
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
// 根據session創建隊列
Queue queue = session.createQueue(QUEUE_NAME);
// 創建消息消費者並指定消費的隊列
messageConsumer = session.createConsumer(queue);
/**
* 通過監聽的方式來消費消息,是以異步非阻塞的方式來消費消息
* 通過messageConsumer 的setMessageListener 註冊一個監聽器,當有消息發送來時,系統自動調用MessageListener 的 onMessage 方法處理消息
*/
messageConsumer.setMessageListener((message) -> {
if (message != null) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消費掉的消息 -> " + textMessage.getText());
System.out.println("消費掉的消息屬性 -> " + textMessage.getStringProperty("user"));
System.out.println("消費掉的消息屬性 -> " + textMessage.getBooleanProperty("isLogin"));
/**
* 設置爲Session.CLIENT_ACKNOWLEDGE後,要調用該方法,標誌着該消息已被簽收(消費)。
* 如果不調用該方法,該消息的標誌還是未消費,下次啓動消費者或其他消費者還會收到此消息。
*/
textMessage.acknowledge();
} catch (JMSException e) {
e.printStackTrace();
}
} else if (message instanceof MapMessage) {
MapMessage mapMessage = (MapMessage) message;
try {
System.out.println("消費掉的Map消息 -> " + mapMessage.getString("k1"));
System.out.println("消費掉的Map消息 -> " + mapMessage.getInt("k2"));
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
/**
* 此處是爲了不讓主線程結束,因爲一旦主線程結束了,其他的線程(如此處的監聽消息的線程)也都會被迫結束。
* 實際開發中不需要
*/
System.in.read();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
messageConsumer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
總結
點對點(queue):
- 點對點模型是基於隊列的,生產者發消息到隊列,消費者從隊列消費消息,隊列的存在使得消息的異步傳輸成爲可能。和我們平時給朋友發送短信類似;
- 如果在Session關閉時有部分消息己被收到但還沒有被簽收(acknowledged),那當消費者下次連接到相同的隊列時,這些消息還會被再次接收;
- 隊列可以長久地保存消息直到消費者收到消息。消費者不需要因爲擔心消息會丟失而時刻和隊列保持激活的連接狀態,充分體現了異步傳輸模式的優勢;
發佈訂閱(topic):
- JMS Pub/Sub 模型定義瞭如何向一個內容節點發布和訂閱消息,這些節點被稱作topic;
- 主題可以被認爲是消息的傳輸中介,發佈者(publisher)發佈消息到主題,訂閱者(subscribe)從主題訂閱消息;
- 主題使得消息訂閱者和消息發佈者保持互相獨立不需要解除即可保證消息的傳送
- 訂閱方式:
- 持久化訂閱:
- 客戶端首先向MQ註冊一個自己的身份ID識別號,當這個客戶端處於離線時,生產者會爲這個ID保存所有發送到主題的消息,當客戶再次連接到MQ的時候,會根據消費者的ID得到所有當自己處於離線時發送到主題的消息
- 當持久訂閱狀態下,不能恢復或重新派送一個未簽收的消息;
- 持久訂閱才能恢復或重新派送一個未簽收的消息;
- 非持久化訂閱:
- 非持久訂閱只有當客戶端處於激活狀態,也就是和MQ保持連接狀態才能收發到某個主題的消息;
- 如果消費者處於離線狀態,生產者發送的主題消息將會丟失作廢,消費者永遠不會收到;
- 先訂閱註冊才能接收的到,只給訂閱者發佈消息
- 持久化訂閱:
- 如何選用:當所有的消息必須被接收,則用持久化訂閱。當消息丟失能夠被容忍,則用非持久訂閱