RabbitMQ,消费者获取correlationId或correlationIdString均为null的解决方案 + 源码层面解析

springboot2.x请移步此处


找这个问题的,估计很多人都是找的资料都是一样的,多个抄袭的网站都是下图的答案。

RabbitMQ,消费者获取correlationId或correlationIdString均为null的解决方案 + 源码层面解析_第1张图片

但实际操作就发现,这个代码是不全的,比如:this.buildMessage(content,correlationId.getId()),这个方法就是没附上的。直接黑人问号。

先下个结论:按照该网站的做法,还是无法解决问题


请注意:后续版本已修复部分bug,使用起来方便了很多
本文因为建立在springboot 1.x的版本上,很久没更新了。(笔者都没发现,还认真了debug了好久)

本文不作删除,就作为自己不搞清楚版本,还一头debug的教训吧。
当然了,springboot从1.x升级到2.x也是会有很多问题的。
如果在不升级springboot的情况下,本文还是有参考价值的

再次提醒,springboot 2.x 版本以上,请移步另一篇文章:点击跳转


本文环境:springboot 1.5.8.RELEASE + amqp-client 4.0.3.jar

本文分两部分,第一部分先直接给代码实现,第二部分进行原理解析。

文章目录

      • 实现代码
      • 原理解析
        • 核心做法
        • 反推原方法

实现代码

交换机、路由那些就自己改吧,附上全部代码很累赘,只说关键点

  1. 配置RabbitTemplate (这步同网站的操作)
	@Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) //这个不知道有没有用,测试时是有无都可以,多线程无测试,保留
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessagePropertiesConverter(defaultMessagePropertiesConverter());
        return rabbitTemplate;
    }

    @Bean
    public MessagePropertiesConverter defaultMessagePropertiesConverter() {
        DefaultMessagePropertiesConverter messagePropertiesConverter = 
            							new DefaultMessagePropertiesConverter();
        messagePropertiesConverter
            .setCorrelationIdPolicy(DefaultMessagePropertiesConverter.CorrelationIdPolicy.STRING);
        return messagePropertiesConverter;
    }
  1. 生产者发送消息前,通过CorrelationAwareMessagePostProcessor设置MessageProperties,绑定correlationId

    (这步的Processor除了在发送时设置,还可以全局设置,原理会提及)

    public void sendMiaoshaMessage(String msg) {
            CorrelationData correlationData = new CorrelationData();
           String uuid = UUID.randomUUID().toString();
           correlationData.setId(uuid);
            System.out.println("分配ID:" + correlationData.getId());
    
            MessagePostProcessor messagePostProcessor = new CorrelationAwareMessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message, Correlation correlation) {
                    MessageProperties messageProperties = message.getMessageProperties();
                    
                    if(correlation instanceof CorrelationData){
                        String correlationId = ((CorrelationData) correlation).getId();
                        messageProperties.setCorrelationIdString(correlationId);
                    }
                    // 持久化的设置也是在此处,但与本文无关,因此没有附上
                    return message;
                }
    
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    return message;
                }
            };
    
            System.out.println("发送消息");
            rabbitTemplate.convertAndSend(MQConfig.MIAOSHA_EXCHANGE, MQConfig.MIAOSHA_ROUTING_KEY,
                    msg, messagePostProcessor, correlationData);
    }
    
  2. 消费者接收消息:

    @RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
    @RabbitHandler
        public void receive(String msg, Channel channel, Message messages){
            System.out.println("收到消息");
            MessageProperties messageProperties = messages.getMessageProperties();
            byte[] bytes = messageProperties.getCorrelationId();
            // 这里按上述实现的话,是不会为null的,不过此处还是加上
            if (bytes != null) {
                String id = new String(bytes);
                System.out.println("收到ID:" + id);
            }
        }
    

效果图

RabbitMQ,消费者获取correlationId或correlationIdString均为null的解决方案 + 源码层面解析_第2张图片

原理解析

下述所讲代码全都是RabbitTemplate类里面的

前置知识:

MessageProperties类中,与correlationId相关的,有两个成员变量:

private volatile byte[] correlationId;

private volatile String correlationIdString;

其中correlationId已经过时(@Deprecated),见下图

RabbitMQ,消费者获取correlationId或correlationIdString均为null的解决方案 + 源码层面解析_第3张图片
因此下文用的都是messageProperties.setCorrelationIdString()方法


核心自然是convertAndSend方法。本文不会附上全部代码,还是只解析关键代码。

先说句"废话", convertAndSend方法由多个重载方法,不设置对应的参数,均为null

  1. 上述的参数对应的convertAndSend方法源码:

    public void convertAndSend(String exchange, String routingKey, final Object message,
    			final MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException {
        	// 将 Object 类型的 message 包装成 Message类型,可以忽略
    		Message messageToSend = convertMessageIfNecessary(message);
        	// 关键,调用方法,设置messageToSend的属性
    		messageToSend = messagePostProcessor instanceof CorrelationAwareMessagePostProcessor
    				? ((CorrelationAwareMessagePostProcessor) messagePostProcessor)
    						.postProcessMessage(messageToSend, correlationData)
    				: messagePostProcessor.postProcessMessage(messageToSend);
    		send(exchange, routingKey, messageToSend, correlationData);
    	}
    

    这里的核心就是:

    messageToSend = messagePostProcessor instanceof CorrelationAwareMessagePostProcessor
    				? ((CorrelationAwareMessagePostProcessor) messagePostProcessor)
    						.postProcessMessage(messageToSend, correlationData)
    				: messagePostProcessor.postProcessMessage(messageToSend);
    

    如果是CorrelationAwareMessagePostProcessor的实例,则转型,调用其对应的重载方法,否则只调用原方法。

    这里附上两类的源码,就很容易理解了:

    public interface MessagePostProcessor {
    	Message postProcessMessage(Message message) throws AmqpException;
    }
    
    public interface CorrelationAwareMessagePostProcessor extends MessagePostProcessor {
    	Message postProcessMessage(Message message, Correlation correlation);
    }
    

    CorrelationAwareMessagePostProcessor继承了MessagePostProcessor,并新增了重载方法,显然这里必须用到Correlation去设置属性。而CorrelationData实现了Correlation,原接口是个空实现,附源码

    public interface Correlation {
    	// 没有省略代码,就是空的
    }
    
    public class CorrelationData implements Correlation {
        private volatile String id;
        //...省略
    }
    

    所以我们在发送时,需要的是CorrelationAwareMessagePostProcessor,而且也需要向下强转,来获得correlationId,(CorrelationData中的id

  2. 继续,到send()方法,这里主要就是执行一个任务,笔者也不是很理解,就不赘述了,关键就是进入doSend方法:

    public void send(final String exchange, final String routingKey,
    			final Message message, final CorrelationData correlationData)
    			throws AmqpException {
    		...
    		doSend(channel, exchange, routingKey, message, 
                   RabbitTemplate.this.returnCallback != null && 	                  RabbitTemplate.this.mandatoryExpression.getValue(
    								RabbitTemplate.this.evaluationContext, message, Boolean.class),
    						correlationData);
    		...
    	}
    
  3. dosend方法:

    protected void doSend(Channel channel, String exchange, String routingKey, Message message,
    			boolean mandatory, CorrelationData correlationData) throws Exception {
    
    		Message messageToUse = message;
    		MessageProperties messageProperties = messageToUse.getMessageProperties();
         // mandatory与消息无法路由有关。若为true,会在headers中加入字段,进行标记
    		if (mandatory) {
    			messageProperties.getHeaders().
                    put(PublisherCallbackChannel.RETURN_CORRELATION_KEY, this.uuid);
    		}
        // 这里就是rabbitTemplate全局设置的消息前置处理器,逻辑完全同传入的messagePostProcessor
        // 因此除了传入参数外,也可以全局设置
    		if (this.beforePublishPostProcessors != null) {
    			for (MessagePostProcessor processor : this.beforePublishPostProcessors) {
    				messageToUse = processor instanceof CorrelationAwareMessagePostProcessor
    						? ((CorrelationAwareMessagePostProcessor) processor)
    								.postProcessMessage(messageToUse, correlationData)
    						: processor.postProcessMessage(messageToUse);
    			}
    		}
        	// 设置confirmCallBack的内容,这里可以对返回的correlationData进行自定义操作。
        	// 其实这里也可以完成本文的目的,下文也会附上相关代码,但这样的处理使该方法超出了其作用范围,因此没有采用
    		setupConfirm(channel, messageToUse, correlationData);
        	
        	// 这里是第二个核心点,注释说明太长,额外进行说明
    		BasicProperties convertedMessageProperties = this.messagePropertiesConverter
    				.fromMessageProperties(messageProperties, this.encoding);
    		channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, messageToUse.getBody());
    	}
    
    

    先说明注释的做法:

    • 不通过传入参数,直接全局设置的做法:
    // 是rabbitTemplate的成员变量
    private volatile Collection<MessagePostProcessor> beforePublishPostProcessors;
    
    //所以全局设置代码如下,在设置rabbitTemplate时,直接设置
    rabbitTemplate.setBeforePublishPostProcessors(correlationIdProcessor());
    
    	@Bean
        public MessagePostProcessor correlationIdProcessor(){
            // 代码内容完全同上述传入参数
            MessagePostProcessor messagePostProcessor = new CorrelationAwareMessagePostProcessor() {
                @Override
                public Message postProcessMessage(Message message, Correlation correlation) {
                    MessageProperties messageProperties = message.getMessageProperties();
    
                    if(correlation instanceof CorrelationData){
                        String correlationId = ((CorrelationData) correlation).getId();
                        messageProperties.setCorrelationIdString(correlationId);
                    }
    
                    // 可以设置持久化,但与本文无关,因此没有附上
                    return message;
                }
    
                @Override
                public Message postProcessMessage(Message message) throws AmqpException {
                    return message;
                }
            };
            return messagePostProcessor;
        }
    

    附:通过setupConfirm方法内实现本文目的:

    先附上关键源码

    private void setupConfirm(Channel channel, Message message, CorrelationData correlationData) {
    		if (this.confirmCallback != null && channel instanceof PublisherCallbackChannel) {
    			...
    			correlationData = this.correlationDataPostProcessor != null
    					? this.correlationDataPostProcessor.postProcess(message, correlationData)
    					: correlationData;
    			...
    		}
    }
    
    //其中的this.correlationDataPostProcessor,也是一个成员变量
    private volatile CorrelationDataPostProcessor correlationDataPostProcessor;
    
    

    所以道理也差不多,全局设置就完事了。

    这种方法唯一的好处,就是它的参数直接就是CorrelationData,不用转换。但这样处理,很明显不属于它的功能范畴。

    rabbitTemplate.setCorrelationDataPostProcessor(new CorrelationDataPostProcessor() {
                @Override
                public CorrelationData postProcess(Message message, CorrelationData correlationData) {
                    MessageProperties messageProperties = message.getMessageProperties();
                    
                    messageProperties.setCorrelationIdString(correlationData.getId());
                    correlationData.setId("这里可以处理correlationData,该data会在confirm时接收");
                    return correlationData;
                }
    });
    

核心做法

如果跳到这里发现看不懂的,请先至少把上述的第三点dosend方法的注释看一遍

BasicProperties convertedMessageProperties = this.messagePropertiesConverter
				.fromMessageProperties(messageProperties, this.encoding);

同样,this.xxx,也是一个成员变量

private volatile MessagePropertiesConverter messagePropertiesConverter 
    = new DefaultMessagePropertiesConverter();

区别就在于,该成员变量默认有实现,不会空。

那看看默认实现中,fromMessageProperties方法的逻辑:

同样,只留下关键代码。本方法的效果,大致就是根据source来设置target。

这里需要提及一下,

因此,才会出现上面源码中的如下代码

@SuppressWarnings("deprecation")
byte[] correlationId = source.getCorrelationId();

那既然correlationId过时了,我们自然就应该关注剩下的correlationIdString了,提取相关代码:

if (!CorrelationIdPolicy.BYTES.equals(this.correlationIdPolicy)
				&& StringUtils.hasText(correlationIdString)) {
			target.correlationId(correlationIdString);
}

this.correlationIdPolicy = CorrelationIdPolicy.BYTES,则! equals() == false,这样就无法设置correlationId字段了。那消费者肯定就接收到null了。

所以这里就回到我们最初的设置了:

rabbitTemplate.setMessagePropertiesConverter(defaultMessagePropertiesConverter());

    @Bean
    public MessagePropertiesConverter defaultMessagePropertiesConverter() {
        DefaultMessagePropertiesConverter messagePropertiesConverter = 
            							new DefaultMessagePropertiesConverter();
        messagePropertiesConverter
            .setCorrelationIdPolicy(DefaultMessagePropertiesConverter.CorrelationIdPolicy.STRING);
        return messagePropertiesConverter;
    }

即:主动设置新的DefaultMessagePropertiesConverter,更新其策略CorrelationIdPolicy == STRING。这样就能满足上述的if条件,进而设置correlationId字段。

注意:该处理是无论哪种实现都需要的

其他的各种实现归根到底,就是把correlationId放到messageProperties里。

而这步的处理,是让messageProperties中的correlationId能生效,设置到发送的消息中去。


小剧场:(放松一下,源码脑子疼)

至此,生产者的处理已经完成了,本来以为大功告成的我,开开心心地在消费者端,调用messageProperties.getCorrelationIdString(),结果一看还是null,不禁脑阔疼。

再进一步打印,messageProperties.toString()。居然发现correlationId = [xxx,xxx],不为null了。

既迷惑又惊喜,把它转成String一看,果然是发送的correlationId

至此,任务是完成了,但correlationId不是过时了吗?为什么还用它?

百思不得其解的笔者,硬着头皮又debug找问题。然后"惊喜"的发现,不是自己的问题。

因为最后发送出去的,已经没有IdStringId之分了,只剩下一个String correlationId

BasicProperties convertedMessageProperties = this.messagePropertiesConverter
				.fromMessageProperties(messageProperties, this.encoding);

channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, messageToUse.getBody());

public static class BasicProperties extends com.rabbitmq.client.impl.AMQBasicProperties {
        private String correlationId;
    	....
}

推测是RabbitMQ broker的处理中,没有对应更新,就导致了这个问题。

不过目的也达到了,问题不大。


反推原方法

最后,根据源码,笔者反推出了那网页缺少的buildMessage方法。

这方法也能达成目标,但是就有点小聪明的感觉。

不过原文消费者的:getCorrelationIdString是错误的,需要按照上述做法才能正确获取。

private Message buildMessage(Object object, String correlationId) {
        MessageConverter messageConverter = new SimpleMessageConverter();

        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setCorrelationIdString(correlationId);

        return messageConverter.toMessage(object, messageProperties);
    }

吐槽:看源码的确能解决大多数问题,就是有点累

本文完,有误欢迎指出。

你可能感兴趣的:(RabbitMQ,java,rabbitmq)