JMS 在 Spring Boot 中的使用

jasonGeng88發表於2017-06-07

當前環境

  1. Mac OS 10.11.x
  2. docker 1.12.1
  3. JDK 1.8
  4. SpringBoot 1.5

前言

基於之前一篇“一個故事告訴你什麼是訊息佇列”,瞭解了訊息佇列的使用場景以及相關的特性。本文主要講述訊息服務在 JAVA 中的使用。

市面上的有關訊息佇列的技術選型非常多,如果我們的程式碼框架要支援不同的訊息實現,在保證框架具有較高擴充套件性的前提下,我們勢必要進行一定的封裝。

在 JAVA 中,大可不必如此。因為 JAVA 已經制定了一套標準的 JMS 規範。該規範定義了一套通用的介面和相關語義,提供了諸如持久、驗證和事務的訊息服務,其最主要的目的是允許Java應用程式訪問現有的訊息中介軟體。就和 JDBC 一樣。

基本概念

在介紹具體的使用之前,先簡單介紹一下 JMS 的一些基本知識。這裡我打算分為 3 部分來介紹,即 訊息佇列(MQ)的連線、訊息傳送與訊息接收。

這裡我們的技術選型是 SpringBoot、JMS、ActiveMQ

為了更好的理解 JMS,這裡沒有使用 SpringBoot 零配置來搭建專案

MQ 的連線

使用 MQ 的第一步一定是先連線 MQ。因為這裡使用的是 JMS 規範,對於任何遵守 JMS 規範的 MQ 來說,都會實現相應的ConnectionFactory介面,因此我們只需要建立一個ConnectionFactory工廠類,由它來實現 MQ 的連線,以及封裝一系列特性的 MQ 引數。

例子:這裡我們以 ActiveMQ 為例,

maven 依賴:

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>1.5.3.RELEASE</version>
</parent>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-activemq</artifactId>
	</dependency>
</dependencies>

建立 ActiveMQ 連線工廠:

@Bean
public ConnectionFactory connectionFactory(){

    ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
    connectionFactory.setBrokerURL(ActiveMQ_URL);
    connectionFactory.setUserName(ActiveMQ_USER);
    connectionFactory.setPassword(ActiveMQ_PASSWORD);
    return connectionFactory;

}

訊息傳送

關於訊息的傳送,是通過 JMS 核心包中的JmsTemplate類來實現的,它簡化了 JMS 的使用,因為在傳送或同步接收訊息時它幫我們處理了資源的建立和釋放。從它的作用也不難推測出,它需要引用我們上面建立的連線工廠,具體程式碼如下:

@Bean
public JmsTemplate jmsQueueTemplate(){

    return new JmsTemplate(connectionFactory());

}

JmsTemplate建立完成後,我們就可以呼叫它的方法來傳送訊息了。這裡有兩個概念需要注意:

  1. 訊息會傳送到哪裡?-> 即需要指定傳送佇列的目的地(Destination),是可以在 JNDI 中進行儲存和提取的 JMS 管理物件。
  2. 傳送的訊息體具體是什麼?-> 實現了javax.jms.Message的物件,類似於 JAVA RMI 的 Remote 物件。

程式碼示例:

@Autowired
private JmsTemplate jmsQueueTemplate;

/**
 * 傳送原始訊息 Message
 */
public void send(){

    jmsQueueTemplate.send("queue1", new MessageCreator() {
        @Override
        public Message createMessage(Session session) throws JMSException {
            return session.createTextMessage("我是原始訊息");
        }
    });

}

優化:當然,我們不用每次都通過MessageCreator匿名類的方式來建立Message物件,JmsTemplate類中提供了物件實體自動轉換為Message物件的方法,convertAndSend(String destinationName, final Object message)

優化程式碼示例:

/**
 * 傳送訊息自動轉換成原始訊息
 */
public void convertAndSend(){

    jmsQueueTemplate.convertAndSend("queue1", "我是自動轉換的訊息");

}

注:關於訊息轉換,還可以通過實現MessageConverter介面來自定義轉換內容

訊息接收

講完了訊息傳送,我們最後來說說訊息是如何接收的。訊息既然是以Message物件的形式傳送到指定的目的地,那麼訊息的接收勢必會去指定的目的地上去接收訊息。這裡採用的是監聽者的方式來監聽指定地點的訊息,採用註解@JmsListener來設定監聽方法。

程式碼示例:

@Component
public class Listener1 {

    @JmsListener(destination = "queue1")
    public void receive(String msg){
        System.out.println("監聽到的訊息內容為: " + msg);
    }

}

有了監聽的目標和方法後,監聽器還得和 MQ 關聯起來,這樣才能運作起來。這裡的監聽器可能不止一個,如果每個都要和 MQ 建立連線,肯定不太合適。所以需要一個監聽容器工廠的概念,即介面JmsListenerContainerFactory,它會引用上面建立好的與 MQ 的連線工廠,由它來負責接收訊息以及將訊息分發給指定的監聽器。當然也包括事務管理、資源獲取與釋放和異常轉換等。

程式碼示例:

@Bean
public DefaultJmsListenerContainerFactory jmsQueueListenerContainerFactory() {

    DefaultJmsListenerContainerFactory factory =
            new DefaultJmsListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    //設定連線數
    factory.setConcurrency("3-10");
    //重連間隔時間
    factory.setRecoveryInterval(1000L);
    return factory;

}

場景

程式碼地址:https://github.com/jasonGeng88/springboot-jms

對 JMS 有了基本的理解後,我們就來在具體的場景中使用一下。

首先,我們需要先啟動 ActiveMQ,這裡我們以 Docker 容器化的方式進行啟動。

啟動命令:

docker run -d -p 8161:8161 -p 61616:61616 --name activemq webcenter/activemq

啟動成功後,在 ActiveMQ 視覺化介面檢視效果(http://localhost:8161):

點對點模式(單消費者)

下面介紹訊息佇列中最常用的一種場景,即點對點模式。基本概念如下:

  1. 每個訊息只能被一個消費者(Consumer)進行消費。一旦訊息被消費後,就不再在訊息佇列中存在。
  2. 傳送者和接收者之間在時間上沒有依賴性,也就是說當傳送者傳送了訊息之後,不管接收者有沒有正在執行,它不會影響到訊息被髮送到佇列。
  3. 接收者在成功接收訊息之後需向佇列應答成功。

程式碼實現(為簡化程式碼,部分程式碼沿用上面所述):

啟動檔案(Application.java)

@SpringBootApplication
@EnableJms
public class Application {

    ...

    /**
     * JMS 佇列的模板類
     * connectionFactory() 為 ActiveMQ 連線工廠
     */
    @Bean
    public JmsTemplate jmsQueueTemplate(){
        return new JmsTemplate(connectionFactory());
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

註解@EnableJms設定在@Configuration類上,用來宣告對 JMS 註解的支援。

訊息生產者(PtpProducer.java)

@Component
public class PtpProducer {

    @Autowired
    private JmsTemplate jmsQueueTemplate;

    /**
     * 傳送訊息自動轉換成原始訊息
     */
    public void convertAndSend(){
        jmsQueueTemplate.convertAndSend("ptp", "我是自動轉換的訊息");
    }
}

生產者呼叫類(PtpController.java)

@RestController
@RequestMapping(value = "/ptp")
public class PtpController {

    @Autowired
    private PtpProducer ptpProducer;

    @RequestMapping(value = "/convertAndSend")
    public Object convertAndSend(){
        ptpProducer.convertAndSend();
        return "success";
    }

}

訊息監聽容器工廠

@SpringBootApplication
@EnableJms
public class Application {

	...

    /**
     * JMS 佇列的監聽容器工廠
     */
    @Bean(name = "jmsQueueListenerCF")
    public DefaultJmsListenerContainerFactory jmsQueueListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory =
                new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        //設定連線數
        factory.setConcurrency("3-10");
        //重連間隔時間
        factory.setRecoveryInterval(1000L);
        return factory;
    }

   ...

}

訊息監聽器

@Component
public class PtpListener1 {

    /**
     * 訊息佇列監聽器
     * destination 佇列地址
     * containerFactory 監聽器容器工廠, 若存在2個以上的監聽容器工廠,需進行指定
     */
    @JmsListener(destination = "ptp", containerFactory = "jmsQueueListenerCF")
    public void receive(String msg){

        System.out.println("點對點模式1: " + msg);

    }
}

演示

啟動專案啟動後,通過 REST 介面的方式來呼叫訊息生產者傳送訊息,請求如下:

curl -XGET 127.0.0.1:8080/ptp/convertAndSend

消費者控制檯資訊:

ActiveMQ 控制檯資訊:

列表說明:

  • Name:佇列名稱。
  • Number Of Pending Messages:等待消費的訊息個數。
  • Number Of Consumers:當前連線的消費者數目,因為我們採用的是連線池的方式連線,初始連線數為 3,所以顯示數字為 3。
  • Messages Enqueued:進入佇列的訊息總個數,包括出佇列的和待消費的,這個數量只增不減。
  • Messages Dequeued:出了佇列的訊息,可以理解為是已經消費的訊息數量。

點對點模式(多消費者)

基於上面一個消費者消費的模式,因為生產者可能會有很多,同時像某個佇列傳送訊息,這時一個消費者可能會成為瓶頸。所以需要多個消費者來分攤消費壓力(消費執行緒池能解決一定壓力,但畢竟在單機上,做不到分散式分佈,所以多消費者是有必要的),也就產生了下面的場景。

程式碼實現

新增新的監聽器

@Component
public class PtpListener2 {

    @JmsListener(destination = Constant.QUEUE_NAME, containerFactory = "jmsQueueListenerCF")
    public void receive(String msg){

        System.out.println("點對點模式2: " + msg);

    }
}

演示

這裡我們發起 10 次請求,來觀察消費者的消費情況:

這裡因為監聽容器設定了執行緒池的緣故,在實際消費過程中,監聽器消費的順序會有所差異。

釋出訂閱模式

除了點對點模式,釋出訂閱模式也是訊息佇列中常見的一種使用。試想一下,有一個即時聊天群,你在群裡傳送一條訊息。所有在這個群裡的人(即訂閱了該群的人),都會收到你傳送的資訊。

基本概念:

  1. 每個訊息可以有多個消費者。
  2. 釋出者和訂閱者之間有時間上的依賴性。針對某個主題(Topic)的訂閱者,它必須建立一個訂閱者之後,才能消費釋出者的訊息。
  3. 為了消費訊息,訂閱者必須保持執行的狀態。

程式碼實現

修改 JmsTemplate 模板類,使其支援釋出訂閱功能

@SpringBootApplication
@EnableJms
public class Application {

    ...

    @Bean
    public JmsTemplate jmsTopicTemplate(){
        JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory());
        jmsTemplate.setPubSubDomain(true);
        return jmsTemplate;
    }

    ...

}

訊息生產者(PubSubProducer.java)

@Component
public class PtpProducer {

    @Autowired
    private JmsTemplate jmsTopicTemplate;

    public void convertAndSend(){
		jmsTopicTemplate.convertAndSend("topic", "我是自動轉換的訊息");
    }
}

生產者呼叫類(PubSubController.java)

@RestController
@RequestMapping(value = "/pubsub")
public class PtpController {

    @Autowired
    private PubSubProducer pubSubProducer;

    @RequestMapping(value = "/convertAndSend")
    public String convertAndSend(){
        pubSubProducer.convertAndSend();
        return "success";
    }

}

修改 DefaultJmsListenerContainerFactory 類,使其支援釋出訂閱功能

@SpringBootApplication
@EnableJms
public class Application {

	...

    /**
     * JMS 佇列的監聽容器工廠
     */
    @Bean(name = "jmsTopicListenerCF")
    public DefaultJmsListenerContainerFactory jmsTopicListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory =
                new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        factory.setConcurrency("1");
        factory.setPubSubDomain(true);
        return factory;
    }

   ...

}

訊息監聽器(這裡設定2個訂閱者)

@Component
public class PubSubListener1 {

    @JmsListener(destination = "topic", containerFactory = "jmsTopicListenerCF")
    public void receive(String msg){
        System.out.println("訂閱者1 - " + msg);
    }
}

@Component
public class PubSubListener2 {

    @JmsListener(destination = "topic", containerFactory = "jmsTopicListenerCF")
    public void receive(String msg){
        System.out.println("訂閱者2 - " + msg);
    }
}

演示

curl -XGET 127.0.0.1:8080/pubSub/convertAndSend

消費者控制檯資訊:

ActiveMQ 控制檯資訊:

總結

這裡只是對 SpringBoot 與 JMS 整合的簡單說明與使用,詳細的介紹可以檢視 Spring 的官方文件,我這裡也有幸參與 併發程式設計網 發起的 Spring 5 的翻譯工作,我主要翻譯了 Spring 5 的 JMS 章節,其內容對於上述 JMS 的基本概念,都有詳細的展開說明,有興趣的可以看一下,當然翻譯水平有限,英文好的建議看原文。

相關文章