RabbitTemplate单元测试以及使用Jackson2JsonMessageConverter作为messageConverter问题笔记

记录排查问题过程和解决方案也算是总结排查问题方法。

  • 需求:
    用户基本信息修改成功之后,需要同步修改的用户信息给别的服务,同步其冗余的用户信息。
  • 设计思路
    修改完成用户信息数据库已经结束操作确保已经写入数据库之后发送更新后的用户信息。
    发送消息操作单独启动一个线程发送,统一使用一个线程池管理。
  • MQ
    选用RabbitMQ,服务内使用CachingConnectionFactory创建和维护MQ
    连接,统一使用RabbitTemplate发送消息。
  • 踩坑:
  1. 单元测试发送MQ出错
    单元测试,修改完成信息,正常写入数据库,正常进去MQ发送逻辑,到达发送逻辑时抛出异常,截取关键错误信息
Caused by: java.io.IOException: null
	at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:126)
	at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:122)
	at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:144)
	at com.rabbitmq.client.impl.ChannelN.open(ChannelN.java:133)
	at com.rabbitmq.client.impl.ChannelManager.createChannel(ChannelManager.java:176)
	at com.rabbitmq.client.impl.AMQConnection.createChannel(AMQConnection.java:553)
	at org.springframework.amqp.rabbit.connection.SimpleConnection.createChannel(SimpleConnection.java:57)
	... 20 common frames omitted
Caused by: com.rabbitmq.client.ShutdownSignalException: clean connection shutdown; protocol method: #method<connection.close>(reply-code=200, reply-text=OK, class-id=0, method-id=0)
	at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66)
	at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
	at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:494)
	at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:288)
	at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:138)

看到有一个最关键的错误提示:

clean connection shutdown; protocol method: #method<connection.close>(reply-code=200, reply-text=OK, class-id=0, method-id=0)

直接看就是链接已经关闭,通过调用链直接定位到异常的抛出方法:

    /**
     * Placeholder until we address bug 15786 (implementing a proper exception hierarchy).
     */
    public AMQCommand exnWrappingRpc(Method m)
        throws IOException
    {
        try {
            return privateRpc(m);
        } catch (AlreadyClosedException ace) {
            // Do not wrap it since it means that connection/channel
            // was closed in some action in the past
            throw ace;
        } catch (ShutdownSignalException ex) {
            throw wrap(ex);
        }
    }

搭上断点直接调试,发现是ShutdownSignalException异常,抛出方法是privateRpc内部的异常被wrap方法包装成了一个IOException,就有了另外一部分错误日志,这里继续深入看原始的异常信息,追踪进入方法

    private AMQCommand privateRpc(Method m)
        throws IOException, ShutdownSignalException
    {
        SimpleBlockingRpcContinuation k = new SimpleBlockingRpcContinuation(m);
        rpc(m, k);
        // At this point, the request method has been sent, and we
        // should wait for the reply to arrive.
        //
        // Calling getReply() on the continuation puts us to sleep
        // until the connection's reader-thread throws the reply over
        // the fence or the RPC times out (if enabled)
        if(_rpcTimeout == NO_RPC_TIMEOUT) {
            return k.getReply();
        } else {
            try {
                return k.getReply(_rpcTimeout);
            } catch (TimeoutException e) {
                throw wrapTimeoutException(m, e);
            }
        }
    }

前后各打断点进入之后直接到了下面的代码块代码块

try {
    return k.getReply(_rpcTimeout);
} catch (TimeoutException e) {
    throw wrapTimeoutException(m, e);
}

这个时候基本定位出来是获取返回确认值的时候除了错误,结合上面的错误异常提示猜测是连接已经在获取确认的返回值之前断开,导致获取不到返回确认。从而有异常抛出。
因为是单元测试,并且使用过线程来法送的消息,则可能是单元测试主线程已经结束并且关闭了连接但是MQ的返回值还没有正常获取到,从而有错误抛出,尝试在单元测试中增加等待时间。

try {
    TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
}

直接粗暴的sleep之后发现单元测试用例正常结束,不再有异常抛出。

小结:其实在debug中花了比较长时间,但是正常来说问题可以提前定位到。
这里自己查了一些资料也跟了一些源码,可能后续会配合源码写一个整个发送过程的剖析

  1. 发送消息将对象格式化JSON之后发现空字段也被格式化为null值。
    本意是null数据在转化为JSON是忽略,可是转换之后没有达到预期,查看配置:
    @Bean(name = "rabbitTemplate")
    public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }

使用的是Jackson2JsonMessageConverter来格式化发送的对象,内部使用的应该是Jackson,查询文档得知Jackson有注解@JsonInclude(Include.NON_NULL)可以忽略null数据,考虑到MQ发送方需要每个对象都加注解略麻烦,必须统一全部做规范,则考虑全局处理。
自定义添加ObjectMapperJackson2JsonMessageConverter中,配置代码修改如下:

    @Bean(name = "rabbitTemplate")
    public RabbitTemplate rabbitTemplate(@Qualifier("connectionFactory") ConnectionFactory bpConnectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(bpConnectionFactory);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter(objectMapper));
        return rabbitTemplate;
    }

至此JSON格式化问题解决。

小结:一般这种配置用的通用对象都可以设置比较多属性,可以尝试看源码属性,查文档。
解决问题时时直接看了源码里面对象有ObjectMapper属性,刚好网上查询到自定义ObjectMapper,也角色根据类名大概是一个类型转换匹配的类,则尝试查询自定义,然后修改。

你可能感兴趣的:(RabbitTemplate,Jackson,Spring,JAVA)