spring-boot-starter-amqp踩坑记
踩坑记录
近日在用 spring boot 架构一个微服务框架,服务发现与治理、发布 REST 接口各种轻松惬意。但是服务当设计 MQ 入口时,就发现遇到无数地雷,现在整理成下文,供各路大侠围观与嘲笑。
版本
当前使用的 spring-boot-starter-amqp 版本为 2016.5 发布的1.3.5.RELEASE
也许若干年后,你们版本都不会有这些问题了。:(
RabbitMQ
当需要用到 MQ 的时候,我的第一反映就是使用 RabbitMQ,猫了一眼 spring boot 的官方说明,上面说 spring boot 为 rabbit 准备了 spring-boot-starter-amqp,并且为 RabbitTemplate 和 RabbitMQ 提供了自动配置选项。暗自窃喜 ~~
瞅瞅 [官方文档]http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-rabbitmq 和例子,SO EASY,再看一眼 GITHUB 上的官方例了,也有例子。
心情愉悦的照着例子,开干 ~~。
踩坑
十五分钟后的代码类似这样:
@Service
@RabbitListener(queues = "merchant")
public class MQReceiver {
protected Logger logger = Logger.getLogger(MQReceiver.class
.getName());
<span class="hljs-meta">@RabbitHandler</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">process</span><span class="hljs-params">(<span class="hljs-meta">@Payload</span> UpdateMerchant request)</span> {
<span class="hljs-type">UpdateMerchantResponse</span> <span class="hljs-variable">response</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">UpdateMerchantResponse</span>();
logger.info(request.getMerchantId() + <span class="hljs-string">"->"</span> + response.getReturnCode());
}
}
消费信息后,应该记录一条日志。
结果得到只有 org.springframework.amqp.AmqpException: No method found for class [B 这个异常,并且还无限循环抛出这个异常。。。
记得刚才官方文档好像说了异常什么的,转身去猫一眼,果然有:
If retries are not enabled and the listener throws an exception, by default the delivery will be retried indefinitely. You can modify this behavior in two ways; set the defaultRequeueRejected
property to false
and zero re-deliveries will be attempted; or, throw an AmqpRejectAndDontRequeueException
to signal the message should be rejected. This is the mechanism used when retries are enabled and the maximum delivery attempts are reached.
知道了为啥会无限重试了,下面来看看为啥会抛出这个异常,google 搜一下,貌似还有一个倒霉鬼遇到了这个问题。
进去看完问题和大神的解答,豁然开朗。
There are two conversions in the @RabbitListener pipeline.
The first converts from a Spring AMQP Message to a spring-messaging Message.
There is currently no way to change the first converter from SimpleMessageConverter which handles String, Serializable and passes everything else as byte[].
The second converter converts the message payload to the method parameter type (if necessary).
With method-level @RabbitListeners there is a tight binding between the handler and the method.
With class-level @RabbitListener s, the message payload from the first conversion is used to select which method to invoke. Only then, is the argument conversion attempted.
This mechanism works fine with Java Serializable objects since the payload has already been converted before the method is selected.
However, with JSON, the first conversion returns a byte[] and hence we find no matching @RabbitHandler.
We need a mechanism such that the first converter is settable so that the payload is converted early enough in the pipeline to select the appropriate handler method.
A ContentTypeDelegatingMessageConverter is probably most appropriate.
And, as stated in AMQP-574, we need to clearly document the conversion needs for a @RabbitListener, especially when using JSON or a custom conversion.
得嘞,官方示例果然是坑,试试大神的解决方案,手动新增下转换。
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> SimpleRabbitListenerContainerFactory <span class="hljs-title function_">rabbitListenerContainerFactory</span><span class="hljs-params">(ConnectionFactory connectionFactory)</span> {
<span class="hljs-type">SimpleRabbitListenerContainerFactory</span> <span class="hljs-variable">factory</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">SimpleRabbitListenerContainerFactory</span>();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Jackson2JsonMessageConverter</span>());
<span class="hljs-keyword">return</span> factory;
}
然后在生产和消费信息的地方使用他们:
@RabbitListener(queues = "merchant", containerFactory="rabbitListenerContainerFactory")
public void process(@Payload UpdateMerchant request) {
UpdateMerchantResponse response = new UpdateMerchantResponse();
logger.info(request.getMerchantId() + "->" + response.getReturnCode());
}
再来一次,果然可以了
c.l.s.m.service.MQReceiver : 00000001->null
总结
看起来很简单,可是掉坑里面之后怎么也得折腾个几个小时才能爬出来,此文献给掉进同一个坑的童鞋,希望你能满意。