SpringBoot整合消息中间件 RabbitMQ 第 5 篇 —— Publish/Subscribe 发布订阅模式(重点掌握,可模拟发邮件)

这个可能是消息队列中最重要的队列了,其他的都是在它的基础上进行了扩展。

RabbitMQ消息传递模型的核心思想是生产者从不将任何消息直接发送到队列。实际上,生产者通常甚至根本不知道是否将消息传递到任何队列。

相反,生产者只能将消息发送到交换机。交换机一方面它接收来自生产者的消息,另一方面,将消息推入队列。交换机必须确切知道如何处理收到的消息。是否应将其附加到特定队列?是否应该将其附加到许多队列中?还是应该将其丢弃。规则由交换类型定义 。

有几种交换类型可用:

Direct exchange(直连交换机):是根据消息携带的路由键(routing key)将消息投递给对应队列的。

Fanout exchange(扇型交换机):将消息路由给绑定到它身上的所有队列。

Topic exchange(主题交换机):队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。

Headers exchange(头交换机):类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。

这里讲解 Fanout exchange(扇型交换机)

功能实现:

一个生产者发送消息,多个消费者获取消息(同样的消息),包括一个生产者,一个交换机,多个队列,多个消费者。

思路解读(重点理解):

(1)一个生产者,多个消费者

(2)每一个消费者都有自己的一个队列

(3)生产者没有直接发消息到队列中,而是发送到交换机

(4)每个消费者的队列都绑定到交换机上

(5)消息通过交换机到达每个消费者的队列,该模式就是Fanout Exchange(扇型交换机)将消息路由给绑定到它身上的所有队列。

注意:交换机没有存储消息功能,如果消息发送到没有绑定消费队列的交换机,消息则丢失。

 

本篇博客的示意图:

我们的代码结构如图:

需要在 pom.xml 文件增加Java发送邮件功能的依赖,pom.xml 完整配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.study</groupId>
    <artifactId>RabbitMQStudy</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>Rabbit_P2P</module>
        <module>Rabbit_Work</module>
        <module>Rabbit_Publish</module>
        <module>Rabbit_Routing</module>
        <module>Rabbit_Topic</module>
    </modules>


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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--Spring boot 集成包-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 阿里巴巴 fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>
        <!--增加邮件的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

bootstrap.yml 配置文件如下:

spring:
  rabbitmq:
    #主机名
    host: 127.0.0.1
    #端口号
    port: 5672
    #账号
    username: guest
    #密码
    password: guest
    #虚拟主机,这里配置的是我们的测试主机
    virtual-host: /test_host

  mail:
    # host是163邮箱的服务器地址
    host: smtp.163.com
    # 邮箱账号
    username: 请替换成你的邮箱地址
    # 授权码,不是登录密码
    password: 你的邮箱授权码,可以查看往期 ActiveMQ 的博客
    enable: true
    smtp:
      auth: true
    starttls:
      enable: true
      required: true

# 自定义配置信息
queueConfig:
  # 邮件队列名
  emailQueue: emailQueue
  # 短信队列名
  SMSQueue: SMSQueue
  # 交换机名称
  fanoutExchangeName: fanoutExchangeName



server:
  port: 8080

# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
  client:
    fetch-registry: false
    register-with-eureka: false

配置类 QueueConfig,这里动态生成2个队列和1个交换机:

package com.study.config;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sun.net.www.protocol.http.HttpURLConnection;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-04-05 上午 12:39
 */
@Configuration
public class QueueConfig {

    @Value("${spring.rabbitmq.host}")
    private String host;

    @Value("${spring.rabbitmq.port}")
    private Integer port;

    @Value("${spring.rabbitmq.username}")
    private String username;

    @Value("${spring.rabbitmq.password}")
    private String password;

    @Value("${spring.rabbitmq.virtual-host}")
    private String virtualHost;


    //邮件队列名
    @Value("${queueConfig.emailQueue}")
    private String emailQueue;

    //短信队列名
    @Value("${queueConfig.SMSQueue}")
    private String SMSQueue;

    //扇形交换机名称
    @Value("${queueConfig.fanoutExchangeName}")
    private String fanoutExchangeName;

    /**
     * 封装连接类
     *
     * @return
     */
    @Bean
    public CachingConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
        connectionFactory.setHost(host);
        connectionFactory.setPort(port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost(virtualHost);
        return connectionFactory;
    }


    /**
     * 动态的创建队列(这里仅创建配置文件里的一个)
     *
     * @return
     * @throws Exception
     */
    @Bean
    public String getQueueName() throws Exception {
        //获取连接
        Connection connection = connectionFactory().createConnection();

        //创建通道。true表示有事务功能
        Channel channel = connection.createChannel(true);

         /*
        创建队列声明,参数说明:
        1.队列名queue

        2.是否持久化durable。是否持久化, 队列的声明默认是存放到内存中的,
        如果rabbitmq重启会丢失,如果想重启之后还存在就要使队列持久化,
        保存到Erlang自带的Mnesia数据库中,当rabbitmq重启之后会读取该数据库

        3.是否排外exclusive。有两个作用,一:当连接关闭时connection.close()该队列是否会自动删除;二:该队列是否是私有的private,
        如果不是排外的,可以使用两个消费者都访问同一个队列,没有任何问题,
        如果是排外的,会对当前队列加锁,其他通道channel是不能访问的,如果强制访问会报异常。
        一般等于true的话用于一个队列只能有一个消费者来消费的场景

        4.是否自动删除autoDelete。当最后一个消费者断开连接之后队列是否自动被删除,
        可以通过RabbitMQ Management,查看某个队列的消费者数量,当consumers = 0时队列就会自动删除

        5.其它参数 Map<String, Object> arguments
        */
        channel.queueDeclare(emailQueue, true, false, false, null);//创建邮件队列
        channel.queueDeclare(SMSQueue, true, false, false, null);//创建短信队列

        //创建扇形的交换机
        channel.exchangeDeclare(fanoutExchangeName, BuiltinExchangeType.FANOUT, true, false, null);

        //可以在这里直接绑定队列到交换机上,也可以使用代码绑定
        //channel.queueBind(emailQueue, fanoutExchangeName, "");
        //channel.queueBind(SMSQueue, fanoutExchangeName, "");

        //关闭通道
        channel.close();
        //关闭连接
        connection.close();

        return "";
    }
}

 如果遇到启动报错如下,则需要去 RabbitMQ 管理后台把旧的交换机删除。

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'auto_delete' for exchange 'fanoutExchangeName' in vhost '/test_host': received 'false' but current is 'true', class-id=40, method-id=10)
	at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:494) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:288) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:138) ~[amqp-client-5.4.3.jar:5.4.3]
	... 39 common frames omitted
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'auto_delete' for exchange 'fanoutExchangeName' in vhost '/test_host': received 'false' but current is 'true', class-id=40, method-id=10)
	at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:516) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:346) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:178) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:111) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:670) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:48) ~[amqp-client-5.4.3.jar:5.4.3]
	at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:597) ~[amqp-client-5.4.3.jar:5.4.3]
	at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_91]

删除的方式如下:

 

队列绑定交换机配置如下:FanoutConfig。需要注意的是绑定队列到交换机,入参是获取队列的函数名。

package com.study.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.swing.text.rtf.RTFEditorKit;

/**
 * @author biandan
 * @description 扇形交换机绑定队列的配置
 * @signature 让天下没有难写的代码
 * @create 2021-04-04 下午 6:11
 */
@Configuration
public class FanoutConfig {
    //邮件队列名称
    @Value("${queueConfig.emailQueue}")
    private String emailQueue;

    //短信队列名称
    @Value("${queueConfig.SMSQueue}")
    private String SMSQueue;

    //扇形交换机名称
    @Value("${queueConfig.fanoutExchangeName}")
    private String fanoutExchangeName;


    /**
     * 定义邮件队列
     *
     * @return
     */
    @Bean
    public Queue getEmailQueue() {
        return new Queue(emailQueue);
    }

    /**
     * 定义短信队列
     *
     * @return
     */
    @Bean
    public Queue getSMSQueue() {
        return new Queue(SMSQueue);
    }

    /**
     * 定义扇形交换机
     *
     * @return
     */
    @Bean
    public FanoutExchange getFanoutExchange() {
        return new FanoutExchange(fanoutExchangeName);
    }


    /**
     * 绑定邮件队列到交换机
     *
     * @param getEmailQueue     获取邮件队列的函数
     * @param getFanoutExchange 获取扇形交换机的函数
     * @return
     */
    @Bean
    public Binding bindEmailQueueToExchange(Queue getEmailQueue, FanoutExchange getFanoutExchange) {
        return BindingBuilder.bind(getEmailQueue).to(getFanoutExchange);
    }

    /**
     * 绑定短信队列到交换机
     *
     * @param getSMSQueue       获取短信队列的函数
     * @param getFanoutExchange 获取扇形交换机的函数
     * @return
     */
    @Bean
    public Binding bindSMSQueueToExchange(Queue getSMSQueue, FanoutExchange getFanoutExchange) {
        return BindingBuilder.bind(getSMSQueue).to(getFanoutExchange);
    }

}

 

消息生产者 FanoutProducer 代码如下:

package com.study.producer;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author biandan
 * @description 消息生产者(扇形交换机)
 * @signature 让天下没有难写的代码
 * @create 2021-04-04 下午 10:49
 */
@Component
public class FanoutProducer {

    private SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //邮件队列名称
    @Value("${queueConfig.emailQueue}")
    private String emailQueue;

    //短信队列名称
    @Value("${queueConfig.SMSQueue}")
    private String SMSQueue;

    //扇形交换机名称
    @Value("${queueConfig.fanoutExchangeName}")
    private String fanoutExchangeName;

    /**
     * 注入 AMQP 消息模板
     */
    @Autowired
    private AmqpTemplate template;

    /**
     * 每隔5秒产生一条消息
     */
    @Scheduled(fixedRate = 1000 * 5)
    public void sendMsg() {
        String msg = "扇形交换机消息生产者:" + SDF.format(new Date());
        System.out.println(msg);
        //发送消息
        template.convertAndSend(fanoutExchangeName, emailQueue, msg);
        template.convertAndSend(fanoutExchangeName, SMSQueue, msg);
    }

}

启动类 RabbitMQPublishApplication:

package com.study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling //启用任务调度
@EnableEurekaClient
public class RabbitMQPublishApplication {
    public static void main(String[] args) {
        SpringApplication.run(RabbitMQPublishApplication.class, args);
    }
}

OK,我们启动项目。看下控制台输出,每隔5秒生产一条消息,并推送到2个队列:

我们到 RabbitMQ 后台查看:

①队列里已经有了2个队列名称,并且有待消费的消息。

②交换机里也有扇形交换机的名称了

 

点进进入交换机管理页面,看到2个队列已经绑定到了交换机上(可手动解绑):

 

然后我们编写消费者代码。

短信消费者代码如下:

package com.study.consumer;

import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @author biandan
 * @description 短信消费者
 * @signature 让天下没有难写的代码
 * @create 2021-04-04 下午 11:39
 */
@Component
@RabbitListener(queues = "${queueConfig.SMSQueue}")
public class Consumer_SMS {

    @RabbitHandler
    public void receiveMsg(String msg) {
        System.out.println("短信消费者_消费掉的消息:" + msg);
    }

}

邮件消费者代码如下(可以真实发送邮件,需要配置对应的邮箱号码和授权码):

package com.study.consumer;

import com.alibaba.fastjson.JSONObject;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

/**
 * @author biandan
 * @description 邮件消费者
 * @signature 让天下没有难写的代码
 * @create 2021-04-04 下午 11:39
 */
@Component
@RabbitListener(queues = "${queueConfig.emailQueue}")
public class Consumer_Email {

    @Value("${spring.mail.username}")
    private String emailName;

    @Autowired
    JavaMailSender javaMailSender;

    @RabbitHandler
    public void receiveMsg(String msg) {
        System.out.println("邮件消费者_消费掉的消息:" + msg);
        SimpleMailMessage message = new SimpleMailMessage();
        //发送者邮箱号
        message.setFrom(emailName);
        //接收者邮箱账号
        message.setTo(emailName);
        //主题
        message.setSubject("RabbitMQ发送邮件学习");
        //文本内容
        message.setText(msg);
        javaMailSender.send(message);
        System.out.println("邮件发送完成," + JSONObject.toJSONString(message));
    }

}

 

OK,我们启动项目,看到控制台输出。

然后登陆我们 163 邮箱,看到邮件过来了。

 

代码在百度网盘连接:https://pan.baidu.com/s/1Izb0WbsuLCJJXlm445xjrw   提取码:xnjd

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值