消息队列一般的正常业务开发中基本上不怎么涉猎,都是系统整合或特殊业务场景下才有用武之地。所以大部分程序员在工作中基本没有怎么接触。本章主要内容,就是带领大家入门体验一下消息队列的使用过程,扩展一下知识体系,方便今后有需要时根据实际的业务场景进行定制开发。为了方便,本文中使用的RabbitMQ直接使用了阿里的云服务,这个是按照发送消息的次数计费,比较便宜。Kafka服务的话,阿里云的服务比较贵,就没有再购买服务,自己在原来买的ECS服务器上安装了一下,方便进行程序的开发调试。
消息队列(Message Queue)简称为MQ,是应用程序对应用程序的通信方法,也是分布式系统中必不可少的组件,主要用来解决应用解耦、异步消息、流量削峰等问题,实现高性能、高可用、可伸缩和最终一致性的架构。如今比较常用的开源消息队列的组件包括:RabbitMQ和Kafka等。
消息队列基本作用:
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,用于在分布式系统中存储转发消息。
RabbitMQ本身是一个消息代理,他的主要工作就是接收和转发消息。
Producer:消息的投递方即生产者。生产者创建消息,然后发布到RabbitMQ中。消息一般可以包含2个部分:(消息体和标签label)。在实际应用中,消息体一般是一个带有业务逻辑结构的数据,可以使数据对象,也可以是JSON字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签来表述这条消息,比如一个交换机的名称和一个路由键。生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签把消息发送给感兴趣的消费者(Consumer)。
Consumer :消息接收方即消费者。消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。
Queue :队列,是RabbitMQ的内部对象,用于存储消息。 RabbitMQ消息都只能存储在队列中,RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin即轮询)多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
Exchange:交换机。生产者将消息发送到Exchange(交换机,通常也可以用大写的"X"来表示),由交换机将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。这里可以将RabbitMQ中的交换机看作一个简单的实体。RabbitMQ常用的交换机类型有fanout、direct、topic、headers这四种。
RoutingKey:路由键。生产者将消息发给交换机的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个RoutingKey需要与交换机类型和绑定键( BindingKey )联合使用才能最终生效。在交换机类型和绑定键( BindingKey )固定的情况下,生产者可以在发送消息给交换机时,通过指定RoutingKey来决定消息流向哪里。
Binding:绑定。RabbitMQ中通过绑定将交换机与队列关联起来,在绑定的时会指定绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了。生产者将消息发送给交换机时,需要一个RoutingKey,当BindingKey 和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey ,而是将消息路由到所有绑定到该交换器的队列中。
本小节主要来介绍使用阿里云的AMQP服务开进行演示Demo的开发。注册阿里云账户并开通AMQP服务(这里不做过多介绍),本人选用的是默认的按量付费。
创建阿里云AMQP服务
创建SpringBoot工程
编写配置文件
编写生产者和消费者
1、AMQP开启准备
开通阿里云的AMQP服务后,需要进行初始化的设定,才能进行相应的开发,主要有如下几个步骤:
详细内容参考官方网址
2、Exchange说明
Producer将消息发送到Exchange ,由 Exchange 将消息路由到一个或多个Queue中(或者丢弃)。Exchange 根据Routing Key 和Binding Key将消息路由到Queue。不同类型的Exchange的路由规则不同。消息队列AMQP版目前支持三种类型的Exchange。
Fanout:该类型路由规则非常简单,会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,相当于广播功能。阿里云控制台绑定时,必须Binding Key,在该模式下随意输入。
Direct:该类型路由规则会将消息路由到Binding Key与Routing Key完全匹配的 Queue 中。
Topic:该类型与 Direct 类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配,即该类型Exchange使用 Routing Key 模式匹配和字符串比较的方式将消息路由至绑定的 Queue。
示例:Routing Key为 use.name
的消息会转发给绑定匹配模式为 *.name
、use.name
、 *.*
的Queue。
参考《三》基于Maven多模块项目搭建 进行工程项目的搭建。创建一个子工程:gavinbj-mqprocess
POM文件引入相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>com.alibaba.mq-amqpgroupId>
<artifactId>mq-amqp-clientartifactId>
<version>1.0.5version>
dependency>
application.properties
中阿里云AMQP的相关配置如下:
#接入点根据控制台获取接入点
spring.rabbitmq.host=183692890418XX.mq-amqp.cn-shenzhen-429403-a.aliyuncs.com
#端口 默认5672
spring.rabbitmq.port=5672
#accessKey ram控制台获取用户子账户ak
spring.rabbitmq.username=LTAI4FtHr6bE5UqptHnJX2XX
#secretKey ram控制台获取用户子账户ak
spring.rabbitmq.password=Pv9g0Si17hl5iu4AUauPoYo3uPZUXX
#virtual-host 你要使用的virtaulhost
spring.rabbitmq.virtual-host=gavinbj
具体内容取得可以参考阿里云的官方说明文档。
本章使用的Demo的消息队列发送消息,使用数据实体进项消息的承载,具体人员信息结构可以参考《八》SpringBoot2数据库访问之整合MyBatis 中的UserInfo。
package com.gavinbj.mqprocess.entity;
import java.io.Serializable;
import java.util.Date;
/**
* user_info
* @author
*/
public class UserInfo implements Serializable {
private String userId;
/**
* 用户姓名
*/
private String userName;
/**
* 个人介绍
*/
private String introduce;
/**
* 移动电话
*/
private String mobilephone;
/**
* 邮箱
*/
private String email;
/**
* 生日
*/
private Date birthday;
/**
* 性别
*/
private String gender;
private static final long serialVersionUID = 1L;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getIntroduce() {
return introduce;
}
public void setIntroduce(String introduce) {
this.introduce = introduce;
}
public String getMobilephone() {
return mobilephone;
}
public void setMobilephone(String mobilephone) {
this.mobilephone = mobilephone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
UserInfo other = (UserInfo) that;
return (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
&& (this.getUserName() == null ? other.getUserName() == null : this.getUserName().equals(other.getUserName()))
&& (this.getIntroduce() == null ? other.getIntroduce() == null : this.getIntroduce().equals(other.getIntroduce()))
&& (this.getMobilephone() == null ? other.getMobilephone() == null : this.getMobilephone().equals(other.getMobilephone()))
&& (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail()))
&& (this.getBirthday() == null ? other.getBirthday() == null : this.getBirthday().equals(other.getBirthday()))
&& (this.getGender() == null ? other.getGender() == null : this.getGender().equals(other.getGender()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
result = prime * result + ((getUserName() == null) ? 0 : getUserName().hashCode());
result = prime * result + ((getIntroduce() == null) ? 0 : getIntroduce().hashCode());
result = prime * result + ((getMobilephone() == null) ? 0 : getMobilephone().hashCode());
result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode());
result = prime * result + ((getBirthday() == null) ? 0 : getBirthday().hashCode());
result = prime * result + ((getGender() == null) ? 0 : getGender().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", userId=").append(userId);
sb.append(", userName=").append(userName);
sb.append(", introduce=").append(introduce);
sb.append(", mobilephone=").append(mobilephone);
sb.append(", email=").append(email);
sb.append(", birthday=").append(birthday);
sb.append(", gender=").append(gender);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
接下来我们将介绍Direct
模式、Topic
模式、Fanout Exchange
模式三种来进行消息的发送和接收
Direct模式:简单消息发送模式,发送者将消息发送到指定的消息队列,然后由消息队列转发到消费者。该类型路由规则会将消息路由到 Binding Key与Routing Key完全匹配的 Queue 中。
阿里云AMQP绑定设定实例如下:
源Exchange | 绑定目标类型 | 目标Queue | Binding key |
---|---|---|---|
gavinbj.direct | Queue | direct.queue | direct |
1、创建AMQP的配置类
使用注解@Configuration
用来标记配置类。
AmqpConfig.java
package com.gavinbj.mqprocess.config;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AmqpConfig {
// 阿里云控制台
private static final String INSTANCE_ID="1836928904186087";
@Autowired
private RabbitProperties rabbitProperties;
@Bean
public ConnectionFactory getConnectionFactory() {
com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory =
new com.rabbitmq.client.ConnectionFactory();
rabbitConnectionFactory.setHost(rabbitProperties.getHost());
rabbitConnectionFactory.setPort(rabbitProperties.getPort());
rabbitConnectionFactory.setVirtualHost(rabbitProperties.getVirtualHost());
AliAmqpCredentialsProvider credentialsProvider = new AliAmqpCredentialsProvider(
rabbitProperties.getUsername(), rabbitProperties.getPassword(), INSTANCE_ID);
rabbitConnectionFactory.setCredentialsProvider(credentialsProvider);
rabbitConnectionFactory.setAutomaticRecoveryEnabled(true);
rabbitConnectionFactory.setNetworkRecoveryInterval(5000);
ConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);
((CachingConnectionFactory)connectionFactory).setPublisherReturns(rabbitProperties.isPublisherReturns());
return connectionFactory;
}
}
2、实现阿里云AMQP的认证代理
AliAmqpCredentialsProvider.java
package com.gavinbj.mqprocess.config;
import org.springframework.util.StringUtils;
import com.alibaba.mq.amqp.utils.UserUtils;
import com.rabbitmq.client.impl.CredentialsProvider;
public class AliAmqpCredentialsProvider implements CredentialsProvider {
/**
* Access Key ID.
*/
private final String accessKeyId;
/**
* Access Key Secret.
*/
private final String accessKeySecret;
/**
* security temp token. (optional)
*/
private final String securityToken;
/**
* instanceId(实例Id,可从AMQP控制台首页获取)
*/
private final String instanceId;
public AliAmqpCredentialsProvider(final String accessKeyId, final String accessKeySecret, final String instanceId) {
this(accessKeyId, accessKeySecret, null, instanceId);
}
public AliAmqpCredentialsProvider(final String accessKeyId, final String accessKeySecret, final String securityToken,
final String instanceId) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.securityToken = securityToken;
this.instanceId = instanceId;
}
@Override
public String getUsername() {
if (!StringUtils.isEmpty(securityToken)) {
return UserUtils.getUserName(accessKeyId, instanceId, securityToken);
} else {
return UserUtils.getUserName(accessKeyId, instanceId);
}
}
@Override
public String getPassword() {
try {
return UserUtils.getPassord(accessKeySecret);
} catch (Exception e) {
}
return null;
}
}
3、创建一个消息发送者
一般使用AmqpTemplate
来进行消息发送的操作,在该类中我们创建一个发送消息的方法,并调用convertAndSend
方法进行消息发送,代码如下:
AmqpSender.java
package com.gavinbj.mqprocess.sender;
import java.util.Date;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message发送者
*
* @author gavinbj
*
*/
@Component
public class AmqpSender {
@Autowired
private AmqpTemplate amqpTemplate;
/**
* Direct模式发送消息
*
*/
public void sendAmqpDirect() {
String exchange = "gavinbj.direct";
String routingKey = "direct";
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
}
}
4、消息接收者
使用@RabbitListener
来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:
AmqpReceiver.java
package com.gavinbj.mqprocess.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message消费者
*
* @author gavinbj
*
*/
@Component
public class AmqpReceiver {
/**
* Direct队列消费者
* @param userInfo
*/
@RabbitListener(queues = "direct.queue")
public void processDirect(UserInfo userInfo) {
System.out.println("Receiver Direct: " + userInfo.toString());
}
}
5、创建Controller进行测试
调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。
AmqpController.java
package com.gavinbj.mqprocess.controller;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;
@RestController
@RequestMapping("/api")
public class AmqpController {
@Resource
private AmqpSender sender;
@RequestMapping(value="/messages/direct")
public BaseResult<String> sendMessageDirect(){
sender.sendAmqpDirect();
System.out.println("简单消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
}
6、启动工程进行测试:
访问地址:http://localhost:9004/gavin/api/messages/direct
返回结果:
{
"status": 0,
"code": 1003,
"msg": "处理成功!",
"data": "OK"
}
控制台输出结果:
2020-03-11 14:59:24.306 INFO 17292 --- [ main] o.a.coyote.http11.Http11NioProtocol : Starting ProtocolHandler ["http-nio-9004"]
2020-03-11 14:59:24.329 INFO 17292 --- [ main] com.gavinbj.mqprocess.AmqpApplication : Started AmqpApplication in 2.641 seconds (JVM running for 3.213)
2020-03-11 14:59:25.805 INFO 17292 --- [nio-9004-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/gavin] : Initializing Spring DispatcherServlet 'dispatcherServlet'
简单消息发送成功
Receiver Direct: UserInfo [Hash = -202819125, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 14:59:25 CST 2020, gender=男, serialVersionUID=1]
从上可以看出,使用Direct方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。
Topic转发模式主要是通过设置主题的方式进行消息的发送和接收。该类型与 Direct 类型相似,只是规则没有那么严格,可以模糊匹配和多条件匹配,即该类型Exchange使用Routing Key模式匹配和字符串比较的方式将消息路由至绑定的Queue。
阿里云AMQP绑定设定实例如下:
源Exchange | 绑定目标类型 | 目标Queue | Binding key |
---|---|---|---|
gavinbj.topic | Queue | topic.queue2 | topic.* |
gavinbj.topic | Queue | topic.queue1 | topic.zhangsan |
说明:当生产者发送消息Routing Key=F.C.E的时候,这时候只满足Queue1,所以会被路由到Queue中,如果Routing Key=A.C.E这时候会被同是路由到Queue1和Queue2中,如果Routing Key=A.F.B时,这里只会发送一条消息到Queue2中。
1、创建消息发送者
直接利用上一节中的发送者,添加一个发送Topic消息的方法,并调用convertAndSend
方法进行消息发送,代码如下:
AmqpSender.java
package com.gavinbj.mqprocess.sender;
import java.util.Date;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message发送者
*
* @author gavinbj
*
*/
@Component
public class AmqpSender {
@Autowired
private AmqpTemplate amqpTemplate;
/**
* Topic模式发送消息
*
*/
public void sendAmqpTopic() {
String exchange = "gavinbj.topic";
String routingKey = "topic.zhangsan";
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
}
}
2、消息接收者
使用@RabbitListener
来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:
AmqpReceiver.java
package com.gavinbj.mqprocess.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message消费者
*
* @author gavinbj
*
*/
@Component
public class AmqpReceiver {
/**
* Topic队列消费者
* @param userInfo
*/
@RabbitListener(queues = "topic.queue1")
public void processTopicQueue1(UserInfo userInfo) {
System.out.println("Receiver Topic Queue1 : " + userInfo.toString());
}
/**
* Topic队列消费者
* @param userInfo
*/
@RabbitListener(queues = "topic.queue2")
public void processTopicQueue2(UserInfo userInfo) {
System.out.println("Receiver Topic Queue2 : " + userInfo.toString());
}
}
3、创建Controller进行测试
调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。
AmqpController.java
package com.gavinbj.mqprocess.controller;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;
@RestController
@RequestMapping("/api")
public class AmqpController {
@Resource
private AmqpSender sender;
@RequestMapping(value="/messages/topic")
public BaseResult<String> sendMessageTopic(){
sender.sendAmqpTopic();
System.out.println("Topic消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
}
4、启动工程进行测试:
访问地址:http://localhost:9004/gavin/api/messages/topic
返回结果:
{
"status": 0,
"code": 1003,
"msg": "处理成功!",
"data": "OK"
}
控制台输出结果:
Topic消息发送成功
Receiver Topic Queue2 : UserInfo [Hash = -48188738, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 16:22:34 CST 2020, gender=男, serialVersionUID=1]
Receiver Topic Queue1 : UserInfo [Hash = -48188738, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 16:22:34 CST 2020, gender=男, serialVersionUID=1]
从上可以看出,使用Direct方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。
Fanout路由规则是把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
阿里云AMQP绑定设定实例如下:
源Exchange | 绑定目标类型 | 目标Queue | Binding key |
---|---|---|---|
gavinbj.fanout | Queue | fanout.queue1 | user.zhangsan |
gavinbj.fanout | Queue | fanout.queue2 | user. |
说明:生产者P生产消息推送到Exchange,由于Exchange Type=fanout这时候会遵循fanout的规则将消息推送到所有与他绑定的Queue。所以在阿里云的AMQP进行绑定时指定的Key没有意义,但是不指定不让创建绑定。您就在使用时随便写就可以了。
1、创建消息发送者
直接利用上一节中的发送者,添加一个发送Fanout消息的方法,并调用convertAndSend
方法进行消息发送,代码如下:
AmqpSender.java
package com.gavinbj.mqprocess.sender;
import java.util.Date;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message发送者
*
* @author gavinbj
*
*/
@Component
public class AmqpSender {
@Autowired
private AmqpTemplate amqpTemplate;
/**
* Direct模式发送消息
*
*/
public void sendAmqpDirect() {
String exchange = "gavinbj.direct";
String routingKey = "direct";
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
}
/**
* Topic模式发送消息
*
*/
public void sendAmqpTopic() {
String exchange = "gavinbj.topic";
String routingKey = "topic.zhangsan";
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
}
/**
* Fanout模式发送消息
*
*/
public void sendAmqpFanout() {
String exchange = "gavinbj.fanout";
String routingKey = "nouse";
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
amqpTemplate.convertAndSend(exchange, routingKey, userInfo);
}
}
2、消息接收者
使用@RabbitListener
来设定监听队列的方法,方法的参数是接收到实体对象,代码如下:
AmqpReceiver.java
package com.gavinbj.mqprocess.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Message消费者
*
* @author gavinbj
*
*/
@Component
public class AmqpReceiver {
/**
* Direct队列消费者
* @param userInfo
*/
@RabbitListener(queues = "direct.queue")
public void processDirect(UserInfo userInfo) {
System.out.println("Receiver Direct: " + userInfo.toString());
}
/**
* Topic队列消费者
* @param userInfo
*/
@RabbitListener(queues = "topic.queue1")
public void processTopicQueue1(UserInfo userInfo) {
System.out.println("Receiver Topic Queue1 : " + userInfo.toString());
}
/**
* Topic队列消费者
* @param userInfo
*/
@RabbitListener(queues = "topic.queue2")
public void processTopicQueue2(UserInfo userInfo) {
System.out.println("Receiver Topic Queue2 : " + userInfo.toString());
}
/**
* Fanout队列消费者
* @param userInfo
*/
@RabbitListener(queues = "fanout.queue1")
public void processFanoutQueue1(UserInfo userInfo) {
System.out.println("Receiver Fanout Queue1: " + userInfo.toString());
}
/**
* Fanout队列消费者
* @param userInfo
*/
@RabbitListener(queues = "fanout.queue2")
public void processFanoutQueue2(UserInfo userInfo) {
System.out.println("Receiver Fanout Queue2: " + userInfo.toString());
}
}
3、创建Controller进行测试
调用生产者进行消息的发送,消息的接收者收到消息后打印消息内容。
AmqpController.java
package com.gavinbj.mqprocess.controller;
import javax.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.AmqpSender;
@RestController
@RequestMapping("/api")
public class AmqpController {
@Resource
private AmqpSender sender;
@RequestMapping(value="/messages/direct")
public BaseResult<String> sendMessageDirect(){
sender.sendAmqpDirect();
System.out.println("简单消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
@RequestMapping(value="/messages/topic")
public BaseResult<String> sendMessageTopic(){
sender.sendAmqpTopic();
System.out.println("Topic消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
@RequestMapping(value="/messages/fanout")
public BaseResult<String> sendMessageFanout(){
sender.sendAmqpFanout();
System.out.println("Fanout消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
}
4、启动工程进行测试:
访问地址:http://localhost:9004/gavin/api/messages/fanout
返回结果:
{
"status": 0,
"code": 1003,
"msg": "处理成功!",
"data": "OK"
}
控制台输出结果:
Fanout消息发送成功
Receiver Fanout Queue1: UserInfo [Hash = 546595490, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 21:42:20 CST 2020, gender=男, serialVersionUID=1]
Receiver Fanout Queue2: UserInfo [Hash = 546595490, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Wed Mar 11 21:42:20 CST 2020, gender=男, serialVersionUID=1]
从上可以看出,使用Fanout方式消息发送成功,消费者也成功接收到了信息。另外,通过阿里云的控制台也可以看到消息被消费的记录。
首先需要一个Kafka的消息队列,因为阿里云的Kafka消息队列服务比较贵,所以在自己原来的ECS服务器上搭建了一个简单的单机的Kafka消息队列服务,以便我们进行接下来的编码。安装过程参考:【环境部署】华为云ECS安装Kafka消息队列
在SpringBoot2工程中整合Kafka的过程和使用AMQP的过程基本一致。主要分为如下几部分:
加入Kafka依赖
配置Kafka服务信息
编写Kafka的生产者和消费者
前提:需要有可以访问的Kafka消息服务。您可以购买华为云等服务厂商提供的相应服务,或者自行安装Kafka服务器。
POM文件添加依赖
<dependency>
<groupId>org.springframework.kafkagroupId>
<artifactId>spring-kafkaartifactId>
dependency>
在配置文件中配置Kafka生产者和消费者的信息,配置文件内容如下:
application.properties
# Kafka Producer配置
spring.kafka.producer.bootstrap-servers=39.99.177.211:9092
# Kafka Consumer配置
spring.kafka.consumer.bootstrap-servers=39.99.177.211:9092
spring.kafka.consumer.group-id=gavinbj
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-offset-reset=latest
spring.kafka.template.default-topic=users
创建一个Kafka消息发送者,使用KafkaTemplate进行消息队列消息的发送。代码清单如下:
KafkaSender.java
package com.gavinbj.mqprocess.sender;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import com.gavinbj.mqprocess.entity.UserInfo;
/**
* Kafka消息生产者
*
* @author gavinbjAdministrator
*
*/
@Component
public class KafkaSender {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void sendKafkaMsg() {
UserInfo userInfo = new UserInfo();
userInfo.setUserId("gavinbj");
userInfo.setUserName("黑白猿");
userInfo.setMobilephone("13940989820");
userInfo.setEmail("[email protected]");
userInfo.setIntroduce("欢迎光临");
userInfo.setGender("男");
userInfo.setBirthday(new Date());
kafkaTemplate.send("test", userInfo.toString());
}
}
创建一个Kafka消息消费者,通过@KafkaListerner注解来监听对应的主题。当有消息的时候,进行消费。
package com.gavinbj.mqprocess.listener;
import java.util.Optional;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* Kafka消息消费者
*
* @author gavinbj
*
*/
@Component
public class KafkaReceiver {
@KafkaListener(topics = "test", groupId = "test-consumer-group")
public void processKafkaMsg(ConsumerRecord<String, String> record) {
Optional<?> kafkaMsg = Optional.ofNullable(record.value());
if(kafkaMsg.isPresent()) {
System.out.println("Kafka Receiver Data:" + record);
System.out.println("Kafka Receiver Data:" + kafkaMsg.get());
}
}
}
创建一个KafkaController进行调用消息发送测试。
package com.gavinbj.mqprocess.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.mqprocess.sender.KafkaSender;
@RestController
@RequestMapping("/api")
public class KafkaController {
@Autowired
private KafkaSender kafkaSender;
@GetMapping("/messages/kafka")
public BaseResult<String> sendKafkaMessage(){
kafkaSender.sendKafkaMsg();
System.out.println("Kafka消息发送成功");
return ResponseUtils.makeOKRsp("OK");
}
}
启动运行后出现如下错误:
Exception thrown when sending a message with key='' and payload='1' to topic test
解决办法:
尝试了网上的各种方法,最后参考这篇文章解决了问题。
修改kafka配置文件server.properties中的如下两项:listeners 和 advertised.listeners。
listeners=PLAINTEXT://{内网ip}:9092
advertised.listeners=PLAINTEXT://{外网ip}:9092
重新启动本地项目,发送消息成功,我这里将内外网地址直接修改成了ECS服务器的对应IP。修改内容如下:
# The address the socket server listens on. It will get the value returned from
# java.net.InetAddress.getCanonicalHostName() if not configured.
# FORMAT:
# listeners = listener_name://host_name:port
# EXAMPLE:
# listeners = PLAINTEXT://your.host.name:9092
listeners=PLAINTEXT://172.26.46.219:9092
# Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured. Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
advertised.listeners=PLAINTEXT://39.99.177.211:9092
[root@iZ8vbf0eo4dqgjfxtrpinfZ config]# vi server.properties
[root@iZ8vbf0eo4dqgjfxtrpinfZ config]#
[root@iZ8vbf0eo4dqgjfxtrpinfZ config]# cd ..
[root@iZ8vbf0eo4dqgjfxtrpinfZ kafka_2.13-2.4.0]# bin/kafka-console-producer.sh --broker-list 172.26.46.213:9092 --topic test
>helloe
>
[root@iZ8vbf0eo4dqgjfxtrpinfZ kafka_2.13-2.4.0]# bin/kafka-console-consumer.sh --bootstrap-server 172.26.46.213:9092 --topic test --from-beginning
hello world
helloe
UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1]
2020-03-13 17:52:24.922 INFO 103732 --- [ main] o.a.coyote.http11.Http11NioProtocol : Starting ProtocolHandler ["http-nio-9004"]
2020-03-13 17:52:24.940 INFO 103732 --- [ main] com.gavinbj.mqprocess.AmqpApplication : Started AmqpApplication in 1.834 seconds (JVM running for 2.41)
2020-03-13 17:52:39.587 INFO 103732 --- [nio-9004-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/gavin] : Initializing Spring DispatcherServlet 'dispatcherServlet'
Kafka消息发送成功
Kafka Receiver Data:ConsumerRecord(topic = test, partition = 0, leaderEpoch = 0, offset = 8, CreateTime = 1584093159876, serialized key size = -1, serialized value size = 207, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1])
Kafka Receiver Data:UserInfo [Hash = 1181217845, userId=gavinbj, userName=黑白猿, introduce=欢迎光临, mobilephone=13940989820, email=[email protected], birthday=Fri Mar 13 17:52:39 CST 2020, gender=男, serialVersionUID=1]
从上可以看出,设定后通过命令行发送消息和启动SpringBoot工程发送消息,都能正常进行被接收。