Search code examples
jsonspring-bootrabbitmqspring-rabbit

spring-rabbit JSON deserialization for ArrayList contents


I am using Spring-boot with rabbitmq with JSON message serialization. Replies using the Direct-Reply-to feature cannot deserialize my classes inside the java.util.List container.

Using my debugger in Jackson2JsonMessageConverter.fromMessage(), the MessageProperties states the __TypeID__ is correctly set to java.util.ArrayList. However the __ContentTypeId__ is java.lang.Object is incorrect as I would be expecting FooDto (I assume...). The exception message is: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to FooDto

Please note, I am using spring-rabbit 1.7.3 and not v2.0 so cannot use the ParameterizedTypeReference for rabbitTemplate.convertSendAndReceiveAsType() method.

I have attempted to use the DefaultClassMapper and the DefaultJackson2JavaTypeMapper (with TypePrecedence tested under both TYPE_ID and INFERRED) without success:

private DefaultJackson2JavaTypeMapper classMapper() {
    final DefaultJackson2JavaTypeMapper classMapper = new DefaultJackson2JavaTypeMapper();
    final Map<String, Class<?>> idClassMapping = new HashMap<>();
    idClassMapping.put(FooDto.class.getSimpleName(), FooDto.class);
    classMapper.setIdClassMapping(idClassMapping);
return classMapper;
}

The exception is now: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to FooDto

My workaround so far has been to use arrays of the raw types i.e. FooDto[].

Library versions: - spring-boot 1.5.6 - RabbitMQ: 3.7.4 - spring-rabbit 1.7.3

Maven pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.6.RELEASE</version>
    <relativePath />
</parent>

<dependencies>
    <dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    </dependency>    
</dependencies>

Effective Pom:

<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-amqp</artifactId>
    <version>1.7.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>1.7.3.RELEASE</version>
</dependency>

RabbitMQ configuration:

@Configuration
@EnableRabbit
public class MessagingConfiguration implements ShutdownListener {

    // FIXME: List<FooDto> in the direct-reply response contains  ArrayList<Object> due to __ContentTypeID__ == SimpleObject/Object .  __TypeID__ is correctly ArrayList

    @Bean
    public List<Declarable> bindings() {
    final List<Declarable> declarations = new ArrayList<>();
        final FanoutExchange exchange = new FanoutExchange("fx", true, false);
        final Queue queue = QueueBuilder.durable("orders").build();
        declarations.add(exchange);
        declarations.add(queue);
        declarations.add(BindingBuilder.bind(queue).to(exchange));
        return declarations;
    }

    // @Bean
    // public DefaultClassMapper classMapper() {
    // DefaultClassMapper classMapper = new DefaultClassMapper();
    // Map<String, Class<?>> idClassMapping = new HashMap<>();
    // idClassMapping.put("FooDto", FooDto.class);
    // java.util.List<FooDto>
    // classMapper.setIdClassMapping(idClassMapping);
    // return classMapper;
    // }
    //
    // @Bean
    // public DefaultClassMapper classMapper() {
    // final DefaultClassMapper typeMapper = new DefaultClassMapper();
    // // typeMapper.setDefaultType(new ArrayList<FooDto>().getClass());
    // typeMapper.setDefaultType(FooDto[].class);
    // return typeMapper;
    // }

    @Bean
    public Jackson2JsonMessageConverter jsonConverter() {
        // https://stackoverflow.com/questions/40491628/jackson-configuration-to-consume-list-of-records-in-rabbitmq
        // https://github.com/FasterXML/jackson-core/issues/295
        final Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        converter.setTypePrecedence(TypePrecedence.TYPE_ID);
        // converter.setClassMapper(classMapper());
        return converter;
    }

    @ConditionalOnProperty(name = "consumer", havingValue = "true")
    @Bean
    public ConsumerListener listenerConsumer() {
        return new ConsumerListener();
    }

    @ConditionalOnProperty(name = "producer", havingValue = "true")
    @Bean
    public ProducerListener listenerProducer() {
        return new ProducerListener();
    }

    @Bean
    public RabbitAdmin rabbitAdmin(final CachingConnectionFactory connectionFactory) {
        return new RabbitAdmin(connectionFactory);
    }

    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            final ConnectionFactory connectionFactory) {
        // Setting the annotation @RabbitListener to use Jackson2JsonMessageConverter
        final SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(jsonConverter());
        factory.setConcurrentConsumers(5);
        factory.setMaxConcurrentConsumers(5);
        return factory;
    }

    @Bean
    public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
        final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(jsonConverter()); // convert all sent messages to JSON
        rabbitTemplate.setReplyTimeout(TimeUnit.SECONDS.toMillis(3));
        rabbitTemplate.setReceiveTimeout(TimeUnit.SECONDS.toMillis(3));
        return rabbitTemplate;
    }

    @Override
    public void shutdownCompleted(final ShutdownSignalException arg0) {
    }
}

The Listener consuming messages containing MyQuery objects from queue "orders" on exchange "fx":

public class ConsumerListener {
    private static final Logger log = LoggerFactory.getLogger(ConsumerListener.class);

    @RabbitListener(queues = { "orders" })
    public FooDto[] receiveMessage(final MyQuery query) {
        log.info(query);
        List<FooDto> response = new ArrayList<>();
        response.add(new FooDto());
        response.add(new FooDto());
        response.add(new FooDto());
        return response;
    }
}

POJO used when sending a message to the Exchange: class MyQuery { private String content = "test";

    public MyQuery();

    public String toString() {
        return content;
    }
}

POJO used for the response: class FooDto { private String content = "foo";

    public FooDto();

    public String toString() {
        return content;
    }
}

Solution

  • There's something weird about your listener; it has a return type of void but you return a list.

    That said, I think the problem is due to type erasure.

    A custom ClassMapper won't help because that's just for a top-level class.

    You should, however, be able to construct a custom Jackson2JavaTypeMapper to create a more complex type. The type mapper is consulted if there is not a class mapper. See here.

    I am not at a computer right now, but I can take a look tomorrow if you can't figure it out.

    EDIT

    Here's an example of how to customize the converter...

    @SpringBootApplication
    public class So49566278Application {
    
        public static void main(String[] args) {
            SpringApplication.run(So49566278Application.class, args);
        }
    
        @Bean
        public ApplicationRunner runner(RabbitTemplate template) {
            template.setReplyTimeout(60_000);
            return args -> {
                @SuppressWarnings("unchecked")
                List<Foo> reply = (List<Foo>) template.convertSendAndReceive("so49566278", "baz");
                System.out.println(reply);
                Foo foo = reply.get(0);
                System.out.println(foo);
            };
        }
    
        @RabbitListener(queues = "so49566278")
        public List<Foo> handle(String in) {
            return Collections.singletonList(new Foo(in));
        }
    
        @Bean
        public Queue queue() {
            return new Queue("so49566278", false, false, true);
        }
    
        @Bean
        public MessageConverter converter() {
            Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
            converter.setJavaTypeMapper(new DefaultJackson2JavaTypeMapper() {
    
                @Override
                public JavaType toJavaType(MessageProperties properties) {
                    JavaType javaType = super.toJavaType(properties);
                    if (javaType instanceof CollectionLikeType) {
                        return TypeFactory.defaultInstance()
                                .constructCollectionLikeType(List.class, Foo.class);
                    }
                    else {
                        return javaType;
                    }
                }
    
            });
            return converter;
        }
    
        public static class Foo {
    
            private String bar;
    
            public Foo() {
                super();
            }
    
            public Foo(String bar) {
                this.bar = bar;
            }
    
            public String getBar() {
                return this.bar;
            }
    
            public void setBar(String bar) {
                this.bar = bar;
            }
    
            @Override
            public String toString() {
                return "Foo [bar=" + this.bar + "]";
            }
    
        }
    
    }