在之前的博客中,我们介绍了如何搭建RabbitMQ环境,在这篇博客中,会介绍SpringBoot和RabbitMQ的简单的综合使用。
目录
RabbitMQ对比ActiveMQ结合SpringBoot的优点
RabbitMQ对比ActiveMQ结合SpringBoot的优点
这里所说的优点,其实是我个人的理解,主要目的仅仅是为了帮助我更好的理解与使用RabbitMQ。
在之前博客中,我们提到了SpringBoot结合ActiveMQ,消费者默认只接收queue的消息,而如果将spring.jms.pub-sub-domain=true之后,我们将只能接收topic消息,如果要让其既消费queue消息又消费topic消息是要进行不少的改动的。
另外一点就是,RabbitMQ使用Erlang语言编写,所以并发能力很强,性能极其好,延时很低。
结合使用的简单介绍
POM.xml添加依赖
只需要在pom.xml文件添加如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
修改application.properties
spring.applicaiton.name=spring-boot-rabbitmq-sender
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=lei
根据自己的配置设置RabbitMQ的配置信息,这里设置了virtual-host,如果使用默认的virtual-host(即 /)则可以不设置此项。
创建RabbitMQ相关类
我们知道RabbitMQ 有queue和exchange,消息经由exchange被发送到需要发送到的与此exchange绑定queue(或者其他绑定的exchange),因而我们需要在使用之前定义 queue和exchange,并绑定在一起。
@Configuration
public class RabbitConf {
@Bean
public Queue queue() {
return new Queue("queue");
}
@Bean(name="message")
public Queue queueMessage() {
return new Queue("topic.message");
}
@Bean(name="messages")
public Queue queueMessages() {
return new Queue("topic.messages");
}
@Bean(name="Amessage")
public Queue AMessage() {
return new Queue("fanout.A");
}
@Bean(name="Bmessage")
public Queue BMessage() {
return new Queue("fanout.B");
}
@Bean(name="Cmessage")
public Queue CMessage() {
return new Queue("fanout.C");
}
@Bean
public TopicExchange exchange() {
return new TopicExchange("exchange");
}
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");//配置广播路由器
}
@Bean
Binding bindingExchangeMessage(@Qualifier("message") Queue queueMessage, TopicExchange exchange) {
return BindingBuilder.bind(queueMessage).to(exchange).with("topic.message");
}
@Bean
Binding bindingExchangeMessages(@Qualifier("messages") Queue queueMessages, TopicExchange exchange) {
return BindingBuilder.bind(queueMessages).to(exchange).with("topic.#");
}
@Bean
Binding bindingExchangeA(@Qualifier("Amessage") Queue AMessage,FanoutExchange fanoutExchange) {
return BindingBuilder.bind(AMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeB(@Qualifier("Bmessage") Queue BMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(BMessage).to(fanoutExchange);
}
@Bean
Binding bindingExchangeC(@Qualifier("Cmessage") Queue CMessage, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(CMessage).to(fanoutExchange);
}
}
从上面代码我们 可以看到 我们定义了一个名为exchange的Topic Exchange(也就是会通过队列与其绑定时候的binding key与发送到此exchange的消息的routing key进行模糊匹配,如果binding key能匹配routing key则发送消息到此队列)和一个名为fanoutExchange的Fanout Exchange(发送到此exchange的消息 将直接发送到与这个exchange绑定的队列中)。并且分别定义了名为queue,topic.message,topic.messages,fanout.A,fanout.B,fanout.C 等6个queue,而且 queue与默认队列exchange绑定(这里没有显示调用绑定,所以就是使用默认的绑定),topic.message 以topic.message 为binding key与exchange绑定,topic.messages 以topic.# 为binding key与exchange绑定,其余3个 fanout.A,fanout.B,fanout.C与fanoutExchange绑定(由于是Fanout Exchang 所以bindingKey无效,这里没设置)。
创建消息提供者
@Component
public class HelloSender {
@Autowired
private AmqpTemplate template;
public void send() {
User user = new User();
user.setUsername("username_lei");
user.setPassword("password_lei");
template.convertAndSend("queue",user);
}
public void sendTopicMessage() {
template.convertAndSend("exchange","topic.message","hello,rabbit topic.message!");
}
public void sendTopicMessages() {
template.convertAndSend("exchange","topic.messages","hello,rabbit topic.messages!");
}
public void sendFanoutMessage() {
template.convertAndSend("fanoutExchange","","hello,rabbit fanoutmessage");
}
}
上面代码我看可以看出,我们主要使用注入的AmqpTemplate来进行消息的发送。更多有关的操作 可以通过注入RabbitTemplate来使用。RabbitTemplate继承于AmqpTemplate,可以实现更强大的功能。
我们写了4个消息发送相关的方法。
send方法 里面调用了 template.convertAndSend(routingKey,message) 只指定了消息的routingKey,也就是这个消息发送到默认exchange,然后将消息发送到 指定的队列中(根据queue与exchange绑定的bindingKey 与routingKey匹配)。
sendTopicMessage 则 以routingKey为topic.message,发送一条消息体为("hello,rabbit topic.message!")的消息 到名为exchange的交换器。
sendTopicMessages 是以routingKey为topic.messages,发送一条消息体为("hello,rabbit topic.messages!")的消息 到名为exchange的交换器。
sendFanoutMessage 以routingKey为 “”,发送一条消息体为"hello,rabbit fanoutmessage"的消息到名为fanoutExchange的消息。
创建消息消费者
@Component
public class HelloReceive {
@RabbitListener(queues="queue") //监听器监听指定的Queue
public void processC(User user) {
System.out.println("Receive:"+user);
}
@RabbitListener(queues="fanout.A")
public void processA(String str1) {
System.out.println("ReceiveA:"+str1);
}
@RabbitListener(queues="fanout.B")
public void processB(String str) {
System.out.println("ReceiveB:"+str);
}
@RabbitListener(queues="fanout.C")
public void processC(String str) {
System.out.println("ReceiveC:"+str);
}
@RabbitListener(queues="topic.message") //监听器监听指定的Queue
public void process1(String str) {
System.out.println("message:"+str);
}
@RabbitListener(queues="topic.messages") //监听器监听指定的Queue
public void process2(String str) {
System.out.println("messages:"+str);
}
}
这里看到我们创建了不同方法监听了 不同消息队列的消息(这里queues可以是个数组类型的,也就是方法消费多个消息队列的消息),跟ActiveMQ类似 我们使用@RabbitListener(queues)就能把一个方法定义为可以消费 消息队列中消息的方法。
创建Controller调用模拟发送消息
@RestController
public class HelloController {
@Autowired
private HelloSender helloSender;
@RequestMapping("/sendMessage")
public void sendMessage() {
helloSender.send();
helloSender.sendTopicMessage();
helloSender.sendTopicMessages();
helloSender.sendFanoutMessage();
}
}
这里我们仅仅是注入HelloSender,然后在访问localhost:8080/sendMessage 的时候发送消息。当然也可以使用单元测试类来测试结果。
测试结果
最后我们运行程序,然后调用localhost:8080/sendMessage观察后台打印:
从结果中我们可以看到,消息打印符合 FanoutExchange 将消息发送到所有与之绑定的队列中,TopicExchange根据消息发送设置的RoutingKey 与 所有与之绑定的队列的BindingKey相匹配,发送消息到匹配成功的队列。(进行通配符匹配) DirectExchange 是RoutingKey和BindingKey完全匹配。
补充
AmqpTemplate与RabbitTemplate
Spring AMQP提供了一个发送和接收消息的操作模板类AmqpTemplate。 AmqpTemplate它定义包含了发送和接收消息等的一些基本的操作功能。RabbitTemplate是AmqpTemplate的一个实现。
RabbitTemplate支持消息的确认与返回,为了返回消息,RabbitTemplate 需要设置mandatory 属性为true,并且CachingConnectionFactory 的publisherReturns属性也需要设置为true。返回的消息会根据它注册的RabbitTemplate.ReturnCallback setReturnCallback 回调发送到给客户端,
一个RabbitTemplate仅能支持一个ReturnCallback 。
为了确认Confirms消息, CachingConnectionFactory 的publisherConfirms 属性也需要设置为true,确认的消息会根据它注册的RabbitTemplate.ConfirmCallback setConfirmCallback回调发送到给客户端。一个RabbitTemplate也仅能支持一个ConfirmCallback。
SpringBoot与RabbitMQ使用手动Ack
首先需要在application.properties设置如下,取消自动Ack
#开启发送确认 默认为false
spring.rabbitmq.publisher-confirms=true
#开启发送失败退回 默认为false
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode= manual
然后要给注入的RabbitTemplate 添加一个确认回调函数如下,注意这里只能对一个rabbitTemplate 设置一个confirmCallback设置第二个会报错:
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
System.out.println("HelloSender消息处理失败" + cause + correlationData.toString());
} else {
System.out.println("HelloSender 消息处理成功 ");
}
});
这里仅仅判断了ack,并没有进行多的操作,其实发送ack过来可以得到其他多余的信息,然后做别的操作,这里只是简单示例。
在消息消费端 也要在处理完消息之后发送ack回来如下方法:
@Component
public class HelloReceive {
@RabbitListener(queues = "queue")
public void process(String msg, Channel channel, Message message) throws IOException{
System.out.println("helloReceive:"+msg);
try{
//根据消息进行操作
//告诉服务器收到这条消息 已经被消费 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 后续还会继续发
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("receiver success");
}catch(IOException e){
e.printStackTrace();
//丢弃这条消息 第三个参数为是否重新入栈,如果为false则这条消息被丢弃,如果为true则直接重新入栈(慎用,如果处理一直错误可能会发生死循环)
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true);
System.out.println("receiver fail");
}
}
}
可以看到我们在处理try catch中 如果成功执行就发送ack 如果抛出异常就不发送ack,而是调用channel.basicNack() 发送nack,注意第三个参数 为true的时候重新放回队列(慎用设置为true,因为如果消费者一直处理错误,会发生死循环),如果为false,直接丢弃。这样就实现了如果客户端处理出错,消息被回退。
当然我们也可以直接获取ConnectionFactory,然后自己创建Connection、Channel等来操作RabbitMQ,具体操作可以参照我RabbitMQ系列博客。