springboot2.x请移步此处
找这个问题的,估计很多人都是找的资料都是一样的,多个抄袭的网站都是下图的答案。
但实际操作就发现,这个代码是不全的,比如: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
本文分两部分,第一部分先直接给代码实现,第二部分进行原理解析。
交换机、路由那些就自己改吧,附上全部代码很累赘,只说关键点
@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;
}
生产者发送消息前,通过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);
}
消费者接收消息:
@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);
}
}
效果图
下述所讲代码全都是
RabbitTemplate类
里面的
前置知识:
MessageProperties
类中,与correlationId
相关的,有两个成员变量:private volatile byte[] correlationId; private volatile String correlationIdString;
其中
correlationId
已经过时(@Deprecated
),见下图
核心自然是convertAndSend
方法。本文不会附上全部代码,还是只解析关键代码。
先说句"废话", convertAndSend
方法由多个重载方法,不设置对应的参数,均为null
上述的参数对应的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
)
继续,到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);
...
}
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
找问题。然后"惊喜"的发现,不是自己的问题。
因为最后发送出去的,已经没有IdString
和Id
之分了,只剩下一个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);
}
吐槽:看源码的确能解决大多数问题,就是有点累
本文完,有误欢迎指出。