RabbitMq 中消息的生产和消费

439 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

1. Java 中 RabbitMq 的使用

  1. 安装 erlang

  2. 安装 rabbitmq

  3. 启动 rabbitmq 服务

  4. 安装 rabbitmq 管理页面插件

  5. rabbitmq 新增账户允许远程访问,默认 guest 只运行本地访问,可以新增账户并设置角色和访问权限

    • rabbitmqctl add_user [username] [password],新增用户

    • rabbitmqctl set_user_tags [username] administrator,设置为管理员权限

    • rabbitmqctl set_permissions -p "/" [username] ".*" ".*" ".*",设置用户可以远程访问

    • rabbitmqctl list_users,查看 rabbitmq 所有用户列表

更多的 SpringBoot 中操作 RabbitMq 的内容可以参考:SpringBoot 中使用 RabbitMq

2. @RabbitListener 注解

@RabbitListener 注解是用于消费者监听队列内容的注解,可以标注在方法或者类上。

2.1 注解标注在方法上

  1. 使用 @RabbitListener 注解标记方法时,指定队列名称 [queueName],当监听到队列中有消息时则会进行接收并处理。
@Component
public class Consumer{
    @RabbitListener(queues = "queueName")
    public void consumer(String message){
        System.out.println("consumer 获取到队列中消息:" + message);
    }
}
  1. 其次,还可以在方法的参数中使用 @Payload 和 @Header 注解来获取指定队列的 Payload 和 Header 内容值。
@Component
public class Consumer{
    @RabbitListener(queues = "queueName")
    public void consumer(@Payload String body, @Header String header){
        System.out.println("consumer 获取到队列中消息体:" + body);
        System.out.println("consumer 获取到队列中消息头:" + header);
    }
}
  1. 另外,还可以通过 @RabbitListener 注解的 bindings 属性来声明 Binding 内容,并在其中指定 exchange、key、queue 等信息,如果连接的 RabbitMQ 服务中不存在该绑定所需要的 Queue、Exchange、RouteKey ,则会自动创建。
@Component
public class Consumer{
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "queueName",durable = "true"),
        exchange = @Exchange(value = "directExchangeName",durable = "true",type = "topic"),
        key = "routingKey"
    ))
    public void consumer(String message){
        System.out.println("consumer 获取到队列中消息:" + message);
    }
}

2.2 注解标注在类上

  1. @RabbitListener 注解不仅可以作用在方法上,还可以在类上进行标注,此时需要与 @RabbitHandler 注解配合使用。
@Component
@RabbitListener(queues = "queueName")
public class Consumer {
    @RabbitHandler
    public void consumer(String message) {
        System.out.println("consumer 消费消息:" + message);
    }
}
  1. @RabbitListener 标注在类上面时,有收到消息就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换的参数类型。
@Component
@RabbitListener(queues = "queueName")
public class Consumer {
    @RabbitHandler
    public void consumer1(String message) {
        System.out.println(message);
    }

    @RabbitHandler
    public void consumer2(byte[] byte) {
        System.out.println(new String(byte));
    }
}
  • 标注在类上时,类中可以定义多个不同的方法,但是要保证方法接受的参数类型是不同的,否则启动时会抛出异常

3. 消费者监听队列进行实时消费

3.1 单个消费者监听

在单个消费者监听时,可以选择 @RabbitListener 注解在类或方法上,根据实际队列中消息的类型和需要选择即可。

// 参数是 String 类型时,两种均适用
@Component
@RabbitListener(queues = "queueName")
public class Consumer {
    @RabbitHandler
    public void consumer(String message) {
        System.out.println("consumer 消费消息:" + message);
    }
}

3.2 多个消费者监听

设置多个消费者监听队列进行消费时,生产者会根据 Exchange 和 RoutingKey 进行投递。

  1. 如果对应多个 queueName ,则都会推送。因此在 Exchange 和 RoutingKey 相同、queueName 不同时,每个队列的消费者都能消费到信息。
@Component
public class RabbitMQConsumers {
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "queueName1"),
        exchange = @Exchange(value = "directExchange"),
        key = "routingKey"
    ))
    public void consumer1(String message){
        System.out.println("consumer1 获取到队列中消息:" + message);
    }

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "queueName2"),
        exchange = @Exchange(value = "directExchange"),
        key = "routingKey"
    ))
    public void consumer2(String message){
        System.out.println("consumer2 获取到队列中消息:" + message);
    }

}
  • 使用 @RabbitListener 注解的多个方法可以并列在同一个类中
  1. 如果 Exchange 和 RoutingKey 以及 queueName 都相同时,每次消息传递到消费者时,只有一个能消费信息,其他消费者都不能消费该信息,即一个 queueName 中的同一个消息不会被多个消费者同时消费。
@Component
public class RabbitMQConsumers {
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "queueName"),
        exchange = @Exchange(value = "directExchange"),
        key = "routingKey"
    ))
    public void consumer1(String message){
        System.out.println("consumer1 获取到队列中消息:" + message);
    }

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "queueName1"),
        exchange = @Exchange(value = "directExchange"),
        key = "routingKey"
    ))
    public void consumer2(String message){
        System.out.println("consumer2 获取到队列中消息:" + message);
    }

    @RabbitListener(queues = "queueName")
    public void consumer3(String message){
        System.out.println("consumer3 获取到队列中消息:" + message);
    }
}
  • 多个消费者消费同一个 queueName 时,默认适用轮询的方式依次将消息交给每个消费者进行消费

  • 使用 @RabbitListener 注解加到方法上,可以直接指明当前方法消费的 queueName 名称信息,也可以指定完整的 Exchange、RoutingKey、queueName 信息。

4. RabbitMq 消息序列化

4.1 MessageConvert

MessageConvert 是 RabbitMq 中用来进行消息序列化处理的接口,序列化主要针对消息的 body(Payload) 内容。

MessageConvert 接口有 SimpleMessageConverter 和 Jackson2JsonMessageConverter 两个实现,其中 SimpleMessageConverter 是默认选择的序列化类。

在 RabbitMq 环境准备好之后,生产者调用 convertAndSend() 方法时就会使用 MessageConvert 接口的方法对消息体进行序列化处理,不同的实现类处理逻辑不大一样。

  1. 对于默认的 SimpleMessageConverter 序列化方式:

    • 如果消息体是 String 类型,则会将内容转成字节数组;
    • 如果参数是 byte[] 类型,则不做序列化处理;
    • 如果是 Java Bean 对象,则使用 JDK 默认序列化方式转为字节数组,类比较复杂时可能性能比较差。
  2. Jackson2JsonMessageConverter 实现类是将消息内容转为 JSON 串形式来传递的,如果 Java 对象比较复杂,可以考虑使用 Jackson2JsonMessageConverter 序列化方式提升性能

消费者消费消息时,参数类型可以使用 Message 类型作为通用类型,Message 中包含消息属性等信息,也可以指定消息内容的具体接收类型,此时如果类型不匹配则会抛出异常。

@Component
@RabbitListener(queues = "queueName")
public class Consumer {
    @RabbitHandler
    public void consumer(Message message) {
        System.out.println("consumer 消费消息:" + message);
    }
}

4.2 序列化与反序列化

对于复杂的消息内容,往往需要进行序列化来保证消息内容的正确性和传输的高效性,而消息的消费过程中又需要根据序列化的方法来反序列化为源对象格式。

SimpleMessageConverter实现

如果使用 SimpleMessageConverter 作为序列化方式,并且 convertAndSend() 方法参数为 Java Bean 类型,SimpleMessageConverter 默认在发送消息时将 Java 对象序列化为字节数组,若想在消费者接收参数时也进行反序列化得到 Java Bean,需要自定义 MessageConvert 的反序列化方法。

@Configuration
public class RabbitMQConfig {
    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(new MessageConverter() {
        @Override
        public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
            return null;
        }

        @Override
        public Object fromMessage(Message message) throws MessageConversionException {
            try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(message.getBody()))){
                return (User)ois.readObject();
            }catch (Exception e){
                e.printStackTrace();
                return null;
            }
            }
        });
        return factory;
    }
}
  • 之后的消费者的方法参数中可以使用 Java Bean 来接收消息并消费

Jackson2JsonMessageConverter 实现

RabbitMQ 提供Jackson2JsonMessageConverter 来支持消息内容 JSON 序列化与反序列化,消息发送者在发送消息时应设置 MessageConverter 为 Jackson2JsonMessageConverter 。

@Configuration
public class RabbitMQConfig {

    @Bean
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }
}
  • 对于生产者,还可以在创建 rabbitTemplate 对象后单独设置序列化方式,消费者只能使用配置设置
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());