Springcloud微服務項目——人力資源管理(HRM)Day07_2 消息隊列RabbitMQ操作

RabbitMQ java操作

RabbitMQ提供了6種消息模型,但是第6種其實是RPC,並不是MQ,因此不予學習。那麼也就剩下5種。
但是其實3、4、5這三種都屬於訂閱模型,只不過進行路由的方式不同

在這裏插入圖片描述

Helloworld-基本消息模型

在這裏插入圖片描述
一對一

創建Maven項目 test-rabbitmq
導入依賴


連接工具類

package cn.itsource.util;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * 獲取鏈接的工具類
 */
public class ConnectionUtil {
    /**
     * 建立與RabbitMQ的連接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定義連接工廠
        ConnectionFactory factory = new ConnectionFactory();
        //設置服務地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672); //15672management管理界面的端口,5672是rabbitmq的端口
        //設置賬號信息,用戶名、密碼、vhost
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 通過工程獲取連接
        Connection connection = factory.newConnection();
        return connection;
    }
}

測試一下連接上沒有

System.out.println(ConnectionUtil.getConnection());

在這裏插入圖片描述
由此可見我們java端訪問的端口是5672

然後我們創建一個生產者和一個消費者

生產者

package org.leryoo._01base;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import org.leryoo.util.ConnectionUtil;


/**
 * 生產者
 */
public class Producer {
    //隊列名稱
    private static final String QUEUE = "helloworld";

    public static void main(String[] args) throws Exception {
        Connection connection = null;
        Channel channel = null;
        try {
            //獲取連接
            connection = ConnectionUtil.getConnection();
            //創建與Exchange的通道,每個連接可以創建多個通道,每個通道代表一個會話任務
            channel = connection.createChannel();
            /**
             * 聲明隊列,如果Rabbit中沒有此隊列將自動創建
             * param1:隊列名稱
             * param2:是否持久化
             * param3:隊列是否獨佔此連接
             * param4:隊列不再使用時是否自動刪除此隊列
             * param5:隊列參數
             */
            channel.queueDeclare(QUEUE, true, false, false, null);
            String message = "helloworld小明" + System.currentTimeMillis();
            /**
             * 消息發佈方法
             * param1:Exchange的名稱,如果沒有指定,則使用Default Exchange
             * param2:routingKey(路由的key),消息的路由Key,是用於Exchange(交換機)將消息轉發到指定的消息隊列
             * param3:消息包含的屬性
             * param4:消息體
             */
            /**
             * 這裏沒有指定交換機,消息將發送給默認交換機,每個隊列也會綁定那個默認的交換機,但是不能顯
             示綁定或解除綁定
             * 默認的交換機,routingKey等於隊列名稱
             */
            channel.basicPublish("", QUEUE, null, message.getBytes());
            System.out.println("Send Message is:'" + message + "'");
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        }
    }
} 

消費者

package org.leryoo._01base;

import com.rabbitmq.client.*;
import org.leryoo.util.ConnectionUtil;

import java.io.IOException;

public class Consumer {
    private static final String QUEUE = "helloworld";

    public static void main(String[] args) throws Exception {
        Connection connection = null;
        Channel channel = null;  //上上層,所以訪問不了
        try
        {
            connection = ConnectionUtil.getConnection();
            channel = connection.createChannel();
            //聲明隊列
            channel.queueDeclare(QUEUE, true, false, false, null);
            //定義消費方法
            Channel finalChannel = channel; //匿名內部類只能訪問上一層
            DefaultConsumer consumer = new DefaultConsumer(finalChannel) {
                /**
                 * 消費者接收消息調用此方法
                 * @param consumerTag 消費者的標籤,在channel.basicConsume()去指定
                 * @param envelope 消息包的內容,可從中獲取消息id,消息routingkey,交換機,消息和重傳標誌
                (收到消息失敗後是否需要重新發送)
                 * @param properties
                 * @param body
                 */
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException {
                    //交換機
                    String exchange = envelope.getExchange();
                    //路由key
                    String routingKey = envelope.getRoutingKey();
                    //消息id
                    long deliveryTag = envelope.getDeliveryTag();
                    //消息內容
                    String msg = new String(body, "utf8");
                    System.out.println("receive message.." + msg);

                    //如果正常處理後需要做回覆
                    finalChannel.basicAck(deliveryTag,false);
                }
            };
            /**
             * 監聽隊列:QUEUE 如果有消息來了,通過consumer來處理
             * 參數明細
             * 1、隊列名稱
             * 2、是否自動回覆,設置爲true爲表示消息接收到自動向mq回覆接收到了,mq接收到回覆會刪除消息,設置
             爲false則需要手動回覆
             * 3、消費消息的方法,消費者接收到消息後調用此方法
             */
            channel.basicConsume(QUEUE, true, consumer);

            //阻塞住,讓他一直監聽
           System.in.read();
        }catch (Exception e){

           e.printStackTrace();
        }finally {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        }

    }
} 

然後我們運行消費者

然後訪問http://localhost:15672 就能看到 消息對隊列裏已經有消息在準備了

在這裏插入圖片描述
然後我們啓動消費者

就能看到消息已經被消費者消費了

在這裏插入圖片描述
需要值得注意的是
我們需要在方法西面阻塞住進程 如果不進行一個阻塞 那麼程序就會直接運行完畢 要讓消費者一直監聽

這也控制檯也才能打印
在這裏插入圖片描述
說明 :
1 啓動的順序都可以 不論先後
2
在這裏插入圖片描述
關於自動回覆 如果設置成true 就是自動回覆 當一有消息進來 消費者就會進行一個自動回覆 這樣會出現一個問題就是 如果消費者在程序執行的過程中 運行出錯了 那麼就會刪除掉這消息 導致消息的一個丟失

這就涉及到了消息確認機制(ACK)

消息確認機制(ACK)

通過剛纔的案例可以看出,消息一旦被消費者接收,隊列中的消息就會被刪除。

那麼問題來了:RabbitMQ怎麼知道消息被接收了呢?

如果消費者領取消息後,還沒執行操作就掛掉了呢?或者拋出了異常?消息消費失敗,但是RabbitMQ無從得知,這樣消息就丟失了

因此,RabbitMQ有一個ACK機制。當消費者獲取消息後,會向RabbitMQ發送回執ACK,告知消息已經被接收。不過這種回執ACK分兩種情況:

自動ACK:消息一旦被接收,消費者自動發送ACK 收到還沒有正確處理。。
手動ACK:消息接收後,不會發送ACK,需要手動調用,等到正確處理後再來手動確認

那麼怎麼選擇呢

  • 如果消息不太重要,丟失也沒有影響,那麼自動ACK會比較方便
  • 如果消息非常重要,不容丟失。那麼最好在消費完成後手動ACK,否則接收消息後就自動ACK,RabbitMQ就會把消息從隊列中刪除。如果此時消費者宕機,那麼消息就丟失了

我們來測試一下 我們來製造一個異常
在這裏插入圖片描述
然後啓動

可以發現消費者的控制檯報錯了
在這裏插入圖片描述
但是我們的消息並沒有消失
在這裏插入圖片描述
這樣消息就會還原回去

然後我們在異常處理完成之後 我們需要 手動確認實現
在這裏插入圖片描述
需要值得注意的是:
因爲這個方法是在匿名內部類裏面 然後內部類又在try/catch裏面 所以不能直接訪問channel 這個變量 我們需要將channel在try/catch裏面再進行定義
在這裏插入圖片描述

如果要確保rabbitmq中怎麼確保一個消息被正常處理 ————> 手動回覆機制
總體的流程就是

  1. 發送端操作流程
    1)創建連接
    2)創建通道
    3)聲明隊列
    4)發送消息
  2. 接收端
    1)創建連接
    2)創建通道
    3)聲明隊列
    4)監聽隊列
    5)接收消息
    6)ack回覆-自動回覆 手動回覆

Work queues

在這裏插入圖片描述

work queues與入門程序相比,多了一個消費端,兩個消費端共同消費同一個隊列中的消息。
應用場景:對於任務過重或任務較多情況使用工作隊列可以提高任務處理的速度

工作隊列,又稱任務隊列。主要思想就是避免執行資源密集型任務時,必須等待它執行完成。相反我們稍後完成任務,我們將任務封裝爲消息並將其發送到隊列。 在後臺運行的工作進程將獲取任務並最終執行作業。當你運行許多工人時,任務將在他們之間共享,但是一個消息只能被一個消費者獲取。

這個概念在Web應用程序中特別有用,因爲在短的HTTP請求窗口中無法處理複雜的任務


接下來我們來模擬這個流程:

P:生產者:任務的發佈者

C1:消費者,領取任務並且完成任務,假設完成速度較快

C2:消費者2:領取任務並完成任務,假設完成速度慢

我們在C2後面
在這裏插入圖片描述
然後讓兩個消費者同時啓動 讓生產者發送50條消息
在這裏插入圖片描述
可以發現

在這裏插入圖片描述
在這裏插入圖片描述
兩個消費者接受消息的條數是一樣的 這樣就會出現問題

  • 消費者1比消費者2的效率要高,一次任務的耗時較短
  • 然而兩人最終消費的消息數量是一樣的
  • 消費者1大量時間處於空閒狀態,消費者2一直忙碌

現在的狀態屬於是把任務平均分配,正確的做法應該是消費越快的人,消費的越多


能者多勞

我們可以使用basicQos方法和prefetchCount = 1設置。 這告訴RabbitMQ一次不要向工作人員發送多於一條消息。 或者換句話說,不要向工作人員發送新消息,直到它處理並確認了前一個消息 。 相反,它會將其分派給不是仍然忙碌的下一個工作人員。 再同一時間一個消費者還沒有處理的任務只能有一個

在這裏插入圖片描述
再次測試
能力弱的:
在這裏插入圖片描述
能力強的
在這裏插入圖片描述

需要注意的是:因爲需要處理完了纔去抓取下一個 所以需要設置手動回覆


訂閱模型分類

在之前的模式中,我們創建了一個工作隊列。 工作隊列背後的假設是:每個任務只被傳遞給一個工作人員。 在這一部分,我們將做一些完全不同的事情 - 我們將會傳遞一個信息給多個消費者。 這種模式被稱爲“發佈/訂閱”。
在這裏插入圖片描述
一對多:1條消息對應多個消費者。 可以給一個隊列綁多個消費者 一個交換機綁多個隊列

解讀:

1、1個生產者,多個消費者

2、每一個消費者都有自己的一個隊列

3、生產者沒有將消息直接發送到隊列,而是發送到了交換機

4、每個隊列都要綁定到交換機

5、生產者發送的消息,經過交換機到達隊列,實現一個消息被多個消費者獲取的目的

X(Exchanges):交換機一方面:接收生產者發送的消息。另一方面:知道如何處理消息,例如遞交給某個特別隊列、遞交給所有隊列、或是將消息丟棄。到底如何操作,取決於Exchange的類型。

分類

Exchange類型有以下幾種:

     Fanout:廣播,將消息交給所有綁定到交換機的隊列 all

     Direct:定向,把消息交給符合指定routing key 的隊列 一堆或一個

     Topic:通配符,把消息交給符合routing pattern(路由模式)的隊列 一堆或者一個

我們這裏先看一下 Fanout:即廣播模式

Exchange(交換機)只負責轉發消息,不具備存儲消息的能力,因此如果沒有任何隊列與Exchange綁定,或者沒有符合路由規則的隊列,那麼消息會丟失!

訂閱模型-FANOUT

在這裏插入圖片描述
發佈訂閱模式:
在廣播模式下,消息發送流程是這樣的:

  • 可以有多個消費者
  • 每個消費者有自己的queue(隊列)
  • 每個隊列都要綁定到Exchange(交換機)
  • 生產者發送的消息,只能發送到交換機,交換機來決定要發給哪個隊列,生產者無法決定。
  • 交換機把消息發送給綁定過的所有隊列
  • 隊列的消費者都能拿到消息。實現一條消息被多個消費者消費

生產者:
兩個變化:

  • 聲明Exchange,不再聲明Queue
  • 發送消息到Exchange,不再發送到Queue
public class Producer {
    //隊列名稱
    private static final String QUEUE = "helloworld";
    private final static String EXCHANGE_NAME = "fanout_exchange_test";
    public static void main(String[] args) throws Exception {
        Connection connection = null;
        Channel channel = null;
        try {
            //獲取連接
            connection = ConnectionUtil.getConnection();
            //創建與Exchange的通道,每個連接可以創建多個通道,每個通道代表一個會話任務
            channel = connection.createChannel();
            String message = "helloworld小明" + System.currentTimeMillis();

            //聲明交換-fanout廣播模式
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
            System.out.println("Send Message is:'" + message + "'");

        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        }
    }
} 

在這裏插入圖片描述
消費者1

public class Consumer1 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_1";
    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] args) throws Exception {
        Connection connection = null;
        Channel channel = null;  //上上層,所以訪問不了
        try
        {
            connection = ConnectionUtil.getConnection();
            channel = connection.createChannel();
            //聲明隊列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            //隊列綁定交換機
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
            //定義消費方法
            Channel finalChannel = channel; //匿名內部類只能訪問上一層
            DefaultConsumer consumer = new DefaultConsumer(finalChannel) {
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException {
                    //交換機
                    String exchange = envelope.getExchange();
                    //路由key
                    String routingKey = envelope.getRoutingKey();
                    //消息id
                    long deliveryTag = envelope.getDeliveryTag();
                    //消息內容
                    String msg = new String(body, "utf8");
                    System.out.println("Consumer1 receive message.." + msg);

                }
            };
            channel.basicConsume(QUEUE_NAME, true, consumer);

            System.in.read();
        }catch (Exception e){

            e.printStackTrace();
        }finally {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        }

    }
} 

消費者2

public class Consumer2 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_2";
    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] args) throws Exception {
        Connection connection = null;
        Channel channel = null;  //上上層,所以訪問不了
        try
        {
            connection = ConnectionUtil.getConnection();
            channel = connection.createChannel();
            //聲明隊列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            //隊列綁定交換機
            channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
            //定義消費方法
            Channel finalChannel = channel; //匿名內部類只能訪問上一層
            DefaultConsumer consumer = new DefaultConsumer(finalChannel) {
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body)
                        throws IOException {
                    //交換機
                    String exchange = envelope.getExchange();
                    //路由key
                    String routingKey = envelope.getRoutingKey();
                    //消息id
                    long deliveryTag = envelope.getDeliveryTag();
                    //消息內容
                    String msg = new String(body, "utf8");
                    System.out.println("Consumer2 receive message.." + msg);

                }
            };
            channel.basicConsume(QUEUE_NAME, true, consumer);

            //阻塞住,讓他一直監聽
           System.in.read();
        }catch (Exception e){

           e.printStackTrace();
        }finally {
            if (channel != null) {
                channel.close();
            }
            if (connection != null) {
                connection.close();
            }
        }

    }
} 

啓動他們 然後在網頁中查看
在這裏插入圖片描述

在這裏插入圖片描述

說明 : 我們先聲明一個交換機 然後將多個隊列綁定到交換機上 然後消費者在去監聽隊列 當發送了一條消息到交換機後 我們將消息發送到綁定到這個交換機上的隊列 然後再去由消費者調用

注意 :啓動的時候我們需要先啓動交換機 不然我們連交換機都沒有


訂閱模型-Direct

有選擇性的接收消息

在訂閱模式中,生產者發佈消息,所有消費者都可以獲取所有消息。

路由模式中,我們將添加一個功能 - 我們將只能訂閱一部分消息。 例如,我們只能將重要的錯誤消息引導到日誌文件(以節省磁盤空間),同時仍然能夠在控制檯上打印所有日誌消息。

但是,在某些場景下,我們希望不同的消息被不同的隊列消費。這時就要用到Direct類型的Exchange。

在Direct模型下,隊列與交換機的綁定,不能是任意綁定了,而是要指定一個RoutingKey(路由key)

消息的發送方在向Exchange發送消息時,也必須指定消息的routing key。
在這裏插入圖片描述
P:生產者,向Exchange發送消息,發送消息時,會指定一個routing key。 才知道要轉給哪些隊列

X:Exchange(交換機),接收生產者的消息,然後把消息遞交給 與routing key完全匹配的隊列

C1:消費者,其所在隊列指定了需要routing key 爲 error 的消息

C2:消費者,其所在隊列指定了需要routing key 爲 info、error、warning 的消息

通過消息發送時指定的routingkey判斷轉發給那些隊列

生產者
在這裏插入圖片描述
在這裏插入圖片描述

消費者1
在這裏插入圖片描述
在這裏插入圖片描述
消費者2
在這裏插入圖片描述
在這裏插入圖片描述
消費者3
在這裏插入圖片描述
在這裏插入圖片描述
消費者4
在這裏插入圖片描述
在這裏插入圖片描述

說明 這裏的消費者1和消費者2是不同的隊列 而消費者3和消費者4 是相同的隊列 不同的消費者

在這裏插入圖片描述

傳遞對象可以通過json字符串的方式

訂閱模型-Topics

Topic類型的Exchange與Direct相比,都是可以根據RoutingKey把消息路由到不同的隊列。只不過Topic類型Exchange可以讓隊列在綁定Routing key 的時候使用通配符!

Routingkey 一般都是有一個或多個單詞組成,多個單詞之間以”.”分割,例如: goods.insert

通配符規則:
#:匹配一個或多個詞
*:匹配不多不少恰好1個詞

舉例:
audit.#:能夠匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
在這裏插入圖片描述
生產者:
在這裏插入圖片描述
在這裏插入圖片描述
消費者1
在這裏插入圖片描述
在這裏插入圖片描述
消費者2
在這裏插入圖片描述
在這裏插入圖片描述
如果生產者的routing key換成

在這裏插入圖片描述
消費者1將不再接受到
在這裏插入圖片描述
消費者2依然能接受
在這裏插入圖片描述


Header模式和RPC我們基本不用 所以我在這也不介紹了 大家想要了解請自行學習

持久化-解決數據安全

如何避免消息丟失?

1) 消費者的ACK機制。可以防止消費者丟失消息。

2) 但是,如果在消費者消費之前,MQ就宕機了,消息就沒了

是可以將消息進行持久化呢?

要將消息持久化,前提是:隊列、Exchange都持久化

交換機持久化

在這裏插入圖片描述

隊列持久化

在這裏插入圖片描述

消息持久化

在這裏插入圖片描述

Springboot整合rabbitmq

先創建一個Maven項目 用於我們測試springbootrabbitmq

首先我們需要導入依賴

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

    <!--spirngboot集成rabbitmq-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
</dependencies>


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.0.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

創建RabbitMQ的配置類

package org.leryoo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {
    //兩個隊列
    public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
    public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
    //交換機名字
    public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";


    /**
     * 交換機配置
     * ExchangeBuilder提供了fanout、direct、topic、header交換機類型的配置
     *
     * @return the exchange
     */
    @Bean(EXCHANGE_TOPICS_INFORM) //spring中bean
    public Exchange EXCHANGE_TOPICS_INFORM() {
        //durable(true)持久化,消息隊列重啓後交換機仍然存在
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
    }


    //聲明隊列
    @Bean(QUEUE_INFORM_SMS)
    public Queue QUEUE_INFORM_SMS() {
        Queue queue = new Queue(QUEUE_INFORM_SMS);
        return queue;
    }

    //聲明隊列
    @Bean(QUEUE_INFORM_EMAIL)
    public Queue QUEUE_INFORM_EMAIL() {
        Queue queue = new Queue(QUEUE_INFORM_EMAIL);
        return queue;
    }


    /**
     * channel.queueBind(INFORM_QUEUE_SMS,"inform_exchange_topic","inform.#.sms.#");
     * 綁定隊列到交換機 .
     *
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @Bean
    public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue, //通過名字從spring獲取bean
                                            @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("inform.#.sms.#").noargs();
    }

    @Bean
    public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
                                              @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("inform.#.email.#").noargs();
    }
}

測試代碼
生產者

@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class ProducerTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMsg() {

        String message = "sms message!";
        rabbitTemplate.convertAndSend(RabbitMqConfig.EXCHANGE_TOPICS_INFORM,"inform.email",
                message);

    }
}

消費者:

@Component
public class MessageHandler {

    //綁定sms隊列的方法

    @RabbitListener(queues = {RabbitMqConfig.QUEUE_INFORM_SMS})
    public void xxx(String msg, Message message, Channel channel){

        System.out.println("sms message:"+msg);
    }

    //綁定email隊列方法
    @RabbitListener(queues = {RabbitMqConfig.QUEUE_INFORM_EMAIL})
    public void yyy(String msg, Message message, Channel channel)
    {
        System.out.println("email message:"+msg);
    }
}

在這裏插入圖片描述

集成完畢

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章