spring-boot-starter-amqp踩坑記

二胡嘈子發表於2016-06-05

踩坑記錄

近日在用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()); 
  
    @RabbitHandler
    public void process(@Payload UpdateMerchant request) {
        UpdateMerchantResponse response = new UpdateMerchantResponse();
        logger.info(request.getMerchantId() + "->" + 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;
    }

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return 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

總結

看起來很簡單,可是掉坑裡面之後怎麼也得折騰個幾個小時才能爬出來,此文獻給掉進同一個坑的童鞋,希望你能滿意。