java客户端操作RabbitMQ

·  阅读 91

上一篇:RabbitMQ安装与原理详解

官方文档:next.rabbitmq.com/api-guide.h… API文档:rabbitmq.github.io/rabbitmq-ja…

一、Java操作RabbitMQ(未使用SpringBoot)

1. 添加依赖

<!--rabbitMQ java客户端依赖-->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.1.1</version>
</dependency>
复制代码

2. 与RabbitMQ建立连接

  • 先创建连接工厂ConnectionFactory ,并指定连接RabbitMQ的四要素(ip,端口,账号,密码)
  • 通过连接工厂创建连接对象Connection,再通过连接对象获取到信道对象Channel
  • 通过信道对象实现对 RabbitMQ 的操作
  • 关闭接连和信道

注意:有些类名与其它包中的重名了(比如java.sql.Connection,java.nio.channels.Channel等),导包的时候,一定是com.rabbitmq.client包下的

private static void send() {
    //创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    //设置连接信息
    factory.setHost("192.168.245.128");//设置RabbitMQ所在机器的IP地址
    factory.setPort(5672);//指定端口
    factory.setUsername("root");//指定连接账号
    factory.setPassword("123");//指定连接密码
    Connection connection = null;
    final Channel channel;  //后面可能会在匿名内部类中使用,故设为常量
    try {
        //创建连接对象,用于连接到RabbitMQ
        connection=factory.newConnection();
        //创建通道对象
        channel=connection.createChannel();

        /**
         * 在这里实现对RabbitMQ的操作,之后的代码,如果没有特殊说明,默认都是写在这里的
         */

    } catch (IOException e) {
        e.printStackTrace();
    } catch (TimeoutException e) {
        e.printStackTrace();
    }finally {
        if(channel != null){
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码

3. Channel操作RabbitMQ

在获取到 Channel 对象后(channel),通过该对象的方法来操作 RabbitMQ,常用的方法有:

(1)创建队列

队列是单例的,如果多次创建同一个名字的队列,仍是原来的那个队列

创建一个指定名字的队列:

channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                     Map<String, Object> arguments) 
复制代码
  • queue:队列名
  • durable:是否持久化,true表示开启持久化
  • exclusive:是否排外,true表示排外,一个队列只允许一个消费者连接
  • autoDelete:如果没有消费者连接是否自动删除队列
  • arguments:指定参数,通常为null

创建一个随机的队列:

//创建一个随机的默认的队列,队列名是随机的,当然也可以自己指定队列名
String queueName=channel.queueDeclare().getQueue();
复制代码
  • 返回 String:队列的名字

(2)创建交换机

交换机是单例的,如果多次创建同一个名字的交换机,并不会改变原来的那个交换机

channel.exchangeDeclare(String exchange, String type, boolean durable)
复制代码
  • exchange:交换机的名称
  • type:交换机类型
  • durable:是否是持久化的消息

(3)将队列和交换机绑定到到某个RoutingKey中

无论是接收消息还是发送消息,必须保证交换机已经创建和队列已经创建并实现绑定

因此这个3个步骤一般是在项目启动时直接创建好,例如交给Spring在启动容器时就可以创建

注意:

  1. 无论是交换机还是队列都不会因为重复的创建而给覆盖(单例)
  2. 如果不能在项目启动时就创建好交换机和队列,以及绑定,那么建议在消息消费者中完成这些操作,如果这么做了就必须要先启动消费者(一般也是先启动消费者)
channel.queueBind(String queue, String exchange, String routingKey)
复制代码
  • queue:队列名,必须已经存在
  • exchange:需要绑定的交换机名称,必须已经存在
  • routionKey:RoutingKey 这个值取值任意但必须要与发送时完全一致

(4)发送消息

channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
复制代码
  • exchange:将消息发送的指定的队列中,必须已经存在
  • routingKey:routingKey的名称,如果不指定exchange,则routingKey表示队列名字,直接往队列里发送
  • props:消息属性
  • body:具体的消息数据

注意:

  1. 使用direct消息模式时,必须要指定routingKey(路由键),将指定的消息绑定到指定的路由键上
  2. fanout模式的消息需要将一个消息同时绑定到多个队列中,因此这里不能创建并指定某个队列,即不绑定队列和交换机,方法中的routingKey为""
  3. 在topic模式中必须要指定routingkey,并且可以同时指定多层的routingKey,每个层次之间使用点(".")分隔即可 例如:aa.bb.cc

(5)接收消息

channel.basicConsume(String queue, boolean autoAck, String consumerTag, Consumer callback)
复制代码
  • queue:队列名称,必须已经存在
  • autoAck:是否自动确认消息 true表示自动确认 false表示手动确认
  • consumerTag:消费的标签,用于区分不同的消费者
  • callback:消息接收后的回调方法,新建一个DefaultConsumer(channel)对象,构造方法的参数为信道对象,并重写handleDelivery方法,在该方法中对消息进行处理
    • handleDelivery方法的参数:
      • consumerTag:标识信道中投递的消息,每个信道中,每条消息的 consumerTag 从 1 开始递增
      • body:表示取到的消息的字节数组

注意:

  1. 消息消费者消费完成消息以后可以不关闭通道和链接,如果不关闭通道和链接那么消费者会不间断的接收消息,因为我们的消息接收底层会启动一个子线程,异步实现接收
  2. 使用Exchange的direct模式时接收者的RoutingKey必须要与发送时的RoutingKey完全一致否则无法获取消息,接收消息时队列名也必须要发送消息时的完全一致
  3. 使用fanout模式获取消息时不需要绑定特定的队列名称,只需使用channel.queueDeclare().getQueue();获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。这种模式中,可以同时启动多个接收者,只要都绑定到同一个Exchang上,即可让所有接收者同时接收同一个消息,是一种广播的消息机制
  4. Topic模式的消息接收时必须要指定RoutingKey并且可以使用#*来做统配符号,#表示通配任意一个单词,*表示通配任意多个单词,例如aa.*.*或者aa.#都可以接收到 routingKey 为 aa.bb.cc 的发送者发送的消息

(6)举例

接收消息(先启动接收消息进行监听,再启动发送消息):

  1. 不经过交换机,直接接收名字为 myQueue 的队列中的消息:

    //不经过交换机,直接接收名字为 myQueue 的队列中的消息
    channel.basicConsume("myQueue",true,"",new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
    	}
    });
    复制代码
  2. 接收交换机类型为 direct 的交换机绑定的队列中的数据

    //接收与类型为 fanout 的交换机绑定的队列中的数据
    channel.queueDeclare("myDirectQueue", true, false, false, null);
    channel.exchangeDeclare("directExchange", "fanout", true);
    channel.queueBind("myDirectQueue", "directExchange", "");
    
    channel.basicConsume("myDirectQueue", true, "", new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
        }
    });
    复制代码
  3. 接收交换机类型为 fanout 的交换机绑定的队列中的数据

    //接收与类型为 fanout 的交换机绑定的队列中的数据
    String queueName=channel.queueDeclare().getQueue();
    channel.exchangeDeclare("fanoutExchange", "fanout", true);
    channel.queueBind(queueName, "fanoutExchange", "");
    
    channel.basicConsume(queueName, true, "", new DefaultConsumer(channel){
        @Override
        public void handleDelivery(String consumerTag, 
                                   Envelope envelope, 
                                   AMQP.BasicProperties properties, 
                                   byte[] body) throws IOException {
            
            //获取到队列中的消息
            String message=new String(body,"UTF-8");
        }
    });
    复制代码
  4. 接收交换机类型为 topic 的交换机绑定的队列中的数据

    String queueName = channel.queueDeclare().getQueue();
    
    //创建一个交换机
    channel.exchangeDeclare("topicExchange", "topic", true);
    
    //将队列和交换机绑定到到某个RoutingKey中
    channel.queueBind(queueName, "topicExchange", "aa.*");
    
    //接收消息
    channel.basicConsume(queueName, true, "", new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag,
                                   Envelope envelope,
                                   AMQP.BasicProperties properties,
                                   byte[] body) throws IOException {
            //获取消息
            String message = new String(body, "UTF-8");
            /*
                这里对消息进行处理
            */
        }
    });
    复制代码
  5. 给上面的消息消费者发送消息:

    //定义消息数据
    String message="这是发送的消息数据";
    
    //不经过交换机,直接发送到名字为 myQueue 的队列中
    channel.basicPublish("", "myQueue", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 direct 的交换机中 
    channel.basicPublish("directExchange", "directRoutingKey", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 fanout 的交换机中 
    channel.basicPublish("fanoutExchange", "", null, message.getBytes("UTF-8"));
    
    //将消息发送到类型为 topic 的交换机中 
    channel.basicPublish("topicExchange", "aa.bb", null, message.getBytes("UTF-8"));
    复制代码

4. 事务与消息确认模式

事务消息与数据库的事务类似,只是MQ中的消息是要保证消息是否会全部发送成功,防止丢失消息的一种策略。

RabbitMQ有两种方式来解决这个问题:

  1. 通过AMQP提供的事务机制实现;
  2. 使用Confirm发送方和接收方确认模式实现;

由于事务机制的性能很差,故使用较多的是Confirm发送方确认模式

(1)事务机制

事务的实现主要是对信道(Channel)的设置,主要的方法有三个:

  1. channel.txSelect():声明启动事务模式;
  2. channel.txComment():提交事务;
  3. channel.txRollback():回滚事务;

注意:要在消息发送之前启动信道的事务模式,发送完毕后要提交事务,否则不会发送成功

(2)发送者确认模式

Confirm发送方确认模式使用和事务类似,也是通过设置Channel进行发送方确认的,最终达到确保所有的消息全部发送成功

Confirm的三种实现方式: 开启发送方确认模式:channel.confirmSelect(); 方式一:channel.waitForConfirms():普通发送方确认模式; 方式二:channel.waitForConfirmsOrDie():批量确认模式; 方式三:channel.addConfirmListener():异步监听发送方确认模式

使用方式:在发送消息前,开启发送方确认模式,在发送完毕后,进行消息的确认

方式一: 在推送消息之前,channel.confirmSelect()声明开启发送方确认模式,再使用channel.waitForConfirms()等待消息被服务器确认即可。

//开启消息确认模式
channel.confirmSelect();

//发送消息到指定队列
channel.basicPublish("", "directRoutingKey", null, message.getBytes("UTF-8"));

if (channel.waitForConfirms()) {
    System.out.println("消息发送成功");
}
复制代码

方式二: channel.waitForConfirmsOrDie()使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未被确认就会抛出IOException异常。

//开启消息确认模式
channel.confirmSelect();

for (int i = 0; i < 10000; i++) {
    channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}
channel.waitForConfirmsOrDie(); //直到所有信息都发布,只要有一个未确认就会IOException
System.out.println("全部执行完成");
复制代码

方式三: 异步模式的优点,就是执行效率高,不需要等待消息执行完,只需要监听消息即可

//开启消息确认模式
channel.confirmSelect();


//发送消息到指定队列
for (int i = 0; i < 10000; i++) {
    channel.basicPublish("", "directRoutingKey", null, String.valueOf(i).getBytes("UTF-8"));
}

//异步监听确认和未确认的消息
channel.addConfirmListener(new ConfirmListener() {
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
    	//这里是确认的消息
        System.out.println("成功确认的消息" + deliveryTag + "==> " + multiple);
    }

    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
    	//这里是未确认的消息
        System.out.println("未确认的消息");
    }
});
复制代码

handleAck()方法与handleNack()方法的参数:

  • deliveryTag:表示第几条消息
  • multiple:boolean 类型,表示是否批量处理了消息,true表示批量执行了deliveryTag这个值的消息和它之前的所有消息,false的话表示单条确认

(3)消费者确认模式

为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(message acknowledgment)。接收者消息确认指的是否要将数据从队列中进行移除,如果确认消息则是将这条消息从队列中彻底移除掉。如果这条消息被成功处理(例如完成数据库的插入等等),这条消息才能被确认删除,如果没有被成功处理(例如服务崩溃),我们在队列中的消息不应该被确认移除

在声明接收消息时(channel.basicConsume),可以指定 autoAck 参数,当 autoAckfalse时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则(autoAck=true),消息被消费后会在队列中立即删除它,不管消息是否被接收到。

在Consumer中Confirm模式中分为手动确认和自动确认(autoAck=true)。

手动确认主要并使用以下方法:

  • basicAck:用于肯定确认

    //deliveryTage:消息的编号,由RabbitMQ提供
    //multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
    channel.basicAck(long deliveryTag, boolean multiple);
    复制代码
  • basicRecover:路由不成功的消息可以使用recover重新发送到队列中。

    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicRecover(boolean requeue);
    复制代码
  • basicReject:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。

    //deliveryTage:消息的编号,由RabbitMQ提供
    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicReject(long deliveryTag, boolean requeue);
    复制代码
  • basicNack:可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true。

    //deliveryTage:消息的编号,由RabbitMQ提供
    //multiple:true表示开启批量处理
    //requeue:true时将确认不成功的消息重新发送到队列中,false 直接丢弃
    channel.basicNack(long deliveryTag, boolean multiple, boolean requeue);
    复制代码
  • 当程序执行中断或者因为网络原因,RabbitMQ 没有收到 ack,则也会将消息重新入队。

完整的程序:

channel.basicConsume("confirmDirectQueue", false, "", new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, 
                               Envelope envelope, 
                               AMQP.BasicProperties properties, 
                               byte[] body) throws IOException {
        String message = new String(body, "UTF-8");
        System.out.println(message);
        //获取消息在队列中的唯一标识
        long messageNo = envelope.getDeliveryTag();
        //根据消息的编号来确认消息,确认以后则表示这个消息已经全部完成处理
        //进行消息确认,需要将这个消息从队列中移除掉
        channel.basicAck(messageNo, true);
    }
});
复制代码

(4)事务与确认模式混用

强烈建议只使用消息确认模式,因为事务开销太大,假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,那么RabbitMQ会产生什么样的变化?

结果分为两种情况:

  • autoAck=false手动确认的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到,但在确认消息会等到事务提交之后,如果你手动确认现在之后,又回滚了事务,那么会以事务回滚为主,此条消息会重新放回队列;
  • autoAck=true如果自定确认为true的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了;

注意:如果两者都使用了的话,如果确认模式中使用的是异步的方法,则事务提交不能放在主线程中,因为主线程运行完后,确认模式的子线程可能还在运行,如果事务提交放在主线程中的话,则主线程执行完后,子线程中确认模式就无法进行事务提交了。故事务提交应放在模式确认的子线程中

二、SpringBoot集成RabbitMQ

1. 添加依赖

<!--spring集成amqp的起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!--这个是测试的-->
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-test</artifactId>
    <scope>test</scope>
</dependency>
复制代码

2. 配置RabbitMQ

(1)通用配置

# 配置 rabbitmq 的ip
spring.rabbitmq.host=192.168.29.128
# 配置 rabbitmq 的端口
spring.rabbitmq.port=5672
# 配置 rabbitmq 的用户名
spring.rabbitmq.username=root
# 配置 rabbitmq 的密码
spring.rabbitmq.password=123
# 配置虚拟主机
spring.rabbitmq.virtual-host=/
# 配置连接超时时间
spring.rabbitmq.connection-timeout=15000
复制代码

如果rabbitmq是集群的,则使用 addresses 来替换 host 和 port 配置如下:

#配置RabbitMQ的集群访问地址
spring.rabbitmq.addresses=192.168.222.129:5672,192.168.222.130:5672
复制代码

(2)配置Producer

#开启发布者确认确认机制,snone为不启用,correlated是发布消息成功到交换器后会触发回调方法
spring.rabbitmq.publisher-confirm-type=correlated
#消息从交换机抵达队列确认,当消息没有路由从交换机到队列时,会进行回调
spring.rabbitmq.publisher-returns=true
#设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
spring.rabbitmq.template.mandatory=true
复制代码

(3)配置Consumer

首先配置手工确认模式(默认是自动的),用于 ACK 的手工处理,这样我们可以保证消息的可靠性送达,或者在消费端消费失败的时候可以做到重回队列、根据业务记录日志等处理。我们也可以设置消费端的监听个数和最大个数,用于控制消费端的并发情况。我们要开启限流,指定每次处理消息最多只能处理两条消息。

#设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#消费者最小数量
spring.rabbitmq.listener.simple.concurrency=1
#消费之最大数量
spring.rabbitmq.listener.simple.max-concurrency=10

#在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
spring.rabbitmq.listener.simple.prefetch=2

复制代码

3. 创建队列与交换机

  • 创建交换机(Exchange):
    • 直接new对应类型的交换机:DirectExchange,FanoutExchange,TopicExchange,并指定交换机的名字
    • 通过交换机构造器对象创建各种类型的交换机:ExchangeBuilder.directExchange().build() 等
    • 通过 AmqpAdmin(在 RabbitAutoConfiguration 中已经自动注入) 来创建
  • 创建队列(Queue):
    • 直接new一个Queue对象(注意包名是:org.springframework.amqp.core.Queue),并指定队列的名字
    • 通过 AmqpAdmin(在 RabbitAutoConfiguration` 中已经自动注入) 来创建
  • 创建队列与交换机的绑定对象(Binding)
    • 通过BindingBuilder对象来进行绑定并指定RoutingKey

(1)直接创建

//@Configuration 标记当前类是Spring的一个配置类,用于模拟Spring的xml配置文件
@Configuration
public class AmqpConfig {
    //标记当前方法是一个Spring的Bean标签配置,方法名相当于bean标签的id 返回值相当于bean标签的class
    //作用是用于创建一个对象到Spring的容器中
    @Bean
    public DirectExchange directExchange(){
        //创建一个交换机 参数为交换机名
        return new DirectExchange("BootDirectExchange");
    }
    @Bean
    public Queue directQueue(){
        //创建一个队列参数 为队列名称,其他参数设置参考普通Java代码创建队列的参数列表
        return new Queue("BootDirectQueue");
    }
    //将队列与交换机进行绑定,并指定RoutingKey
    //参数 1 为需要绑定的队列对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
    //参数 2 为需要绑定的交换机对象,参数名必须要与某个标记了@Bean的方法名完全一致,Spring就会将这个方法的返回值注入到当前方法参数中
    @Bean
    public Binding directBinding(Queue directQueue, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue).to(directExchange).with("BootDirectRoutingKey");
    }

	/**
     * 创建死信交换机,跟普通交换机一样,只是死信交换机只用来接收过期的消息
     */
    @Bean
    public DirectExchange deadExchange() {
        return new DirectExchange("deadExchange", true, false);
    }

    /**
     * 创建死信队列,该队列没有消费者,消息会设置过期时间,消息过期后会发送到死信交换机,在由死信交换机转发至处理该消息的队列中
     */
    @Bean
    public Queue BeadQueue() {
        Map<String, Object> arguments = new HashMap<>(2);
        // 死信路由到死信交换器DLX
        arguments.put("x-dead-letter-exchange", "deadExchange");
        arguments.put("x-dead-letter-routing-key", "deadRoutingKey");
        return new Queue("deadQueue", true, false, false, arguments);

    }
}
复制代码

(2)使用 AmqpAdmin 创建

@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    private AmqpAdmin amqpAdmin;

    @Test
    void contextLoads() {
        //创建交换机
        amqpAdmin.declareExchange(new DirectExchange("hello-java-exchange", //名字
                true, //是否持久化存储
                false)); //是否自动删除
        //创建队列,注意这里的 Queue 是org.springframework.amqp.core包下的
        amqpAdmin.declareQueue(new Queue("hello-java-queue", //队列名
                true, //是否持久化存储
                false, //是否独占
                false)); //是否自动删除
        //创建绑定关系
        amqpAdmin.declareBinding(new Binding("hello-java-queue", //目的地
                Binding.DestinationType.QUEUE, //目的地类型(队列或者交换机)
                "hello-java-exchange", //交换机
                "hello.java", //路由键
                null)); //自定义参数
    }
}
复制代码

4. AmqpTemplate发送消息

AmqpTemplate 它提供了通用的操作基于Amqp开发的消息队列的方法。同样我们需要进行注入到 Spring 容器中,然后直接使用。AmqpTemplate 在 Spring 整合时需要实例化,但是在 Springboot 整合时,在配置文件里添加配置即可。

  • 获取AmqpTemplate对象,在Springboot中,在需要使用的类中直接获取:

    @Autowired
    private AmqpTemplate amqpTemplate;
    复制代码
  • 将java对象转换为Message对象,并发送到RabbitMQ

    amqpTemplate.convertAndSend(String exchange, String routingKey, Object message)
    复制代码
    • exchange:交换机名称
    • routingKey:路由键
    • message:消息
  • 将Message消息转换为java对象

    amqpTemplate.receiveAndConvert(String queueName)
    复制代码
    • queueName:队列名字
    • 返回值为Object,需要类型强转

5. RabbitTemplate发送消息

RabbitTemplate 即消息模板,RabbitTemplate 是 AmqpTemplate 接口的一个实现类,它除了提供了 AmqpTemplate 通用的方法外,还提供了针对RabbitMQ操作的方法,比如回调监听消息接口 ConfirmCallback、返回值确认接口 ReturnCallback 等等。

(1)发送消息

@SpringBootTest
class GulimallOrderApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    void sendMessage() {
        Map<String, Object> map = new HashMap<>();
        map.put("name", "zhangsan");
        map.put("age", 18);
        rabbitTemplate.convertAndSend("hello-java-exchange", //交换机
                "hello.java", //路由键
                map, //消息
                new CorrelationData(UUID.randomUUID().toString())); //唯一Id
    }

}
复制代码

(2)设置发送回调

发送者确认模式,需要在发送消息之前进行设置:

@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

	//使用JSON作为消息的序列化方式
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 定制rabbitTemplate
     * 配置文件中需配置 spring.rabbitmq.publisher-confirm-type=correlated
     */
    @PostConstruct //MyRabbitConfig 对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {
        //设置消息抵达交换机确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            /**
             *
             * @param correlationData 当前消息的唯一关联数据(消息的唯一id)
             * @param ack 消息是否成功收到,只要消息抵达broker,就是true
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("ConfirmCallback ==> correlationData[" + correlationData + "]==>ack[" + ack + "]==>cause[" + cause);
                if (ack) {
                    System.out.println("消息发送确认成功");
                } else {
                    System.out.println("消息发送失败:" + cause);
                }
            }
        });

        /**
         * 设置消息抵达队列确认回调,配置文件中需要配:
         * spring.rabbitmq.publisher-returns=true
         * spring.rabbitmq.template.mandatory=true
         */
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {

            /**
             * 当消息没有投递给指定的队列,就会触发这个失败回调
             * @param message   投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange  消息发送给哪个交换机
             * @param routingKey 消息发送的路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback ==> message[" + message + "]==>replyCode[" + replyCode + "]==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey);
                //在这里可以对消息进行重发
            }
        });
    }
}
复制代码

关于确认与回调:

  • 如果消息没有到 exchange,则 ConfirmCallback 回调,ack=false
  • 如果消息到达 exchange,则 ConfirmCallback 回调,ack=true
  • exchangequeue 成功,则不回调 return
  • exchangequeue 失败,则回调 return

6. Consumer接收消息(@RabbitListener)

(1)使用@RabbitListener监听消息

@RabbitListener:可以标在类上(要和 @RabbitHandler 配合使用)或者方法上,参数 queues 可以指定一个 String[],来监听多个队列。(类必须注入到 Spring 容器中)

  • 方法的参数列表可以写的类型:
    1. Message message:原生消息详细信息,头 + 体
    2. T:发送的消息的类型,比如发送消息时发了个 Map,则接收该消息时也可以使用 Map,
    3. Channel channel:当前传输数据的管道
@Service
public class TestServiceImpl{
	@RabbitListener(queues = "hello-java-queue")
	public void directReceive(Message message, Map<String, Object> body, Channel channel) {
	    System.out.println("收到消息:" + body);
	}
	
	//也可以直接在注解中创建队列,交换机,然后指定routingKey进行绑定,监听
	@RabbitListener(
	       	bindings = @QueueBinding(
	           	value = @Queue(value = "queue2", durable = "true"),
	           	exchange = @Exchange(value = "exchange2", 
		            	type = "direct", 
		            	durable = "true", i
		            	gnoreDeclarationExceptions = "true"
		        ),
	           	key = "routingKey2"
	       	)
	)
	public void directReceive2(Message message, Channel channel) {
	    //这里是对取出来的message进行处理
	}
}
复制代码

(2)使用 @RabbitHandler 重载消息类型

@RabbitHandler:当 @RabbitListener 标在类上时,要使用 @RabbitHandler 标在方法上。这样的好处是,当一个队列中传入了不同类型的对象时,可以通过重载的方式在方法形参中直接接收不同类型的消息

@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {

    //消息类型为 Map
    @RabbitHandler
    public void directReceive(Map<String, Object> body) {
        System.out.println("收到消息Map:" + body);
    }

    //消息类型为 String
    @RabbitHandler
    public void directReceive(String body) {
        System.out.println("收到消息String:" + body);
    }
}
复制代码

(3)消息确认机制

要在配置文件中配置手动 ack:spring.rabbitmq.listener.simple.acknowledge-mode=manual

@Component
@RabbitListener(queues = "hello-java-queue")
public class ConsumerListener {

    //消息类型为 Map
    @RabbitHandler
    public void directReceive(Message message, Map<String, Object> body, Channel channel) {
        System.out.println("收到消息Map:" + body);
        //channel中按顺序自增,所以是唯一的
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            if (deliveryTag % 2 == 0) {
                /*
                 * 肯定确认,表明已经成功消费消息
                 * deliveryTage:消息的编号,由RabbitMQ提供
                 * multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
                 */
                channel.basicAck(deliveryTag, false);
            } else {
                /**
                 * 拒绝消息,表明消息消费失败
                 *deliveryTage:消息的编号,由RabbitMQ提供
                 *multiple:true时用于多个消息确认,确认deliveryTage对应的消息和之前的消息,false为单条消息确认。
                 * requeuq:true 表示重新入队;false 表示丢弃
                 */
                channel.basicNack(deliveryTag, false, true);
            }

        } catch (IOException e) {
            //网络中断
            e.printStackTrace();
        }
    }
}
复制代码

7. 使用JSON的序列化方式

所有的序列化方式都实现了 MessageConverter 接口,默认使用的是 SimpleMessageConverter,其使用 ObjectOutputStream 进行序列化,需要对象实现 Serializable 接口。

若将消息序列化为 json,则只需将自带的 Jackson2JsonMessageConverter 注入容器即可

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
复制代码

讲解比较深,讲的也非常棒:Java SpringBoot集成RabbitMq实战和总结 参考了这一篇博客:www.cnblogs.com/haixiang/p/…

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改