Search code examples
springspring-integrationspring-amqpspring-rabbit

Spring AMQP RabbitMQ InboundChannelAdapter and OutboundEndpoint Different Serialization/Deserialization Strategy


I have a class:

public class SomeClass implements Serializable {
    private String name;

    public static SomeClass valueOf(String value) {
        // Here validate and return SomeClass
    }
}

Serializers/Deserializers for it:

public class SomeClassSerializer extends JsonSerializer<SomeClass> {

    @Override
    public void serialize(SomeClass someClass, JsonGenerator generator, SerializerProvider provider) throws IOException {
        generator.writeString(someClass.getName());
    }
}

public class SomeClassDeserializer extends JsonDeserializer<SomeClass> {

    @Override
    public SomeClass deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        final String name = ((TextNode) parser.getCodec().readTree(parser)).textValue();
        return SomeClass.valueOf(name);
    }
}

In my Jackson2ObjectMapperBuilder configuration:

builder.serializerByType(SomeClass.class, new SomeClassSerializer());
builder.deserializerByType(SomeClass.class, new SomeClassDeserializer());

And everything works smoothly when SomeClass is in the body of an POST/GET requests. However, with Spring integration I have:

return Amqp
           .inboundAdapter(container)
           .outputChannel(someClassChannel)
           .messageConverter(new Jackson2JsonMessageConverter(objectMapperBuilder.build()))
           .get();

and for the the outbound:

@ServiceActivator(inputChannel = "someChannel")
public AmqpOutboundEndpoint someOutboundEndpoint(RabbitTemplate rabbitTemplate,
                                                               FanoutExchange exchange) {
    return Amqp
               .outboundAdapter(rabbitTemplate)
               .exchangeName(exchange.getName())
               .get();
}

With my RabbitTemplate config:

@Bean
public RabbitTemplate rabbitTemplate(Jackson2ObjectMapperBuilder objectMapperBuilder){
    final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter(objectMapperBuilder
            .serializerByType(SomeClass.class, new SomeClassSerializer())
            .build()));
    return rabbitTemplate;
}

And my Gateway:

@Gateway(requestChannel = "someChannel")
void publishEvent(@Header(value = "someClass") SomeClass someClass, @Payload Object payload);

When I send the message, I get an exception in my SomeClassDeserializer calling valueOf method of SomeClass (I throw the InvalidArgumentException):

Full stack trace:

org.springframework.messaging.MessageHandlingException: error occurred during processing message in 'MethodInvokingMessageProcessor' [org.springframework.integration.handler.MethodInvokingMessageProcessor@43a251c4]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.messaging.handler.annotation.Header com.company.project.domain.package.SomeClass] for value 'com.company.project.domain.package.SomeClass@ec7887e4'; nested exception is java.lang.IllegalArgumentException: Couldn't convert value : com.company.project.domain.package.SomeClass@ec7887e4 into a valid com.company.project.domain.package.SomeClass object, failedMessage=GenericMessage [payload=com.company.project.security.domain.client.Client@55095ce3, headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedExchange=com.company, amqp_deliveryTag=1, amqp_consumerQueue=com.company.web.192.168.1.34, amqp_redelivered=false, amqp_contentEncoding=UTF-8, json__TypeId__=com.company.project.security.domain.client.Client, amqp_timestamp=Fri Oct 30 12:13:00 EET 2020, amqp_messageId=1dffe404-2235-4f79-4a14-315c896c3c4e, id=bcaa71d5-b454-e65c-6939-b9f07b7cc47b, event=com.company.project.domain.package.SomeClass@ec7887e4, amqp_consumerTag=amq.ctag-ImFjO9LBGoks9Lm3E0EkbQ, contentType=application/json, __TypeId__=com.company.project.security.domain.client.Client, timestamp=1604049180277}]
    at org.springframework.integration.support.utils.IntegrationUtils.wrapInHandlingExceptionIfNecessary(IntegrationUtils.java:191)
    at org.springframework.integration.handler.MethodInvokingMessageProcessor.processMessage(MethodInvokingMessageProcessor.java:111)
    at org.springframework.integration.handler.ServiceActivatingHandler.handleRequestMessage(ServiceActivatingHandler.java:95)
    at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:127)
    at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:170)
    at org.springframework.integration.dispatcher.BroadcastingDispatcher.invokeHandler(BroadcastingDispatcher.java:224)
    at org.springframework.integration.dispatcher.BroadcastingDispatcher.access$000(BroadcastingDispatcher.java:56)
    at org.springframework.integration.dispatcher.BroadcastingDispatcher$1.run(BroadcastingDispatcher.java:204)
    at org.springframework.integration.util.ErrorHandlingTaskExecutor.lambda$execute$0(ErrorHandlingTaskExecutor.java:57)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.messaging.handler.annotation.Header com.company.project.domain.package.SomeClass] for value 'com.company.project.domain.package.SomeClass@ec7887e4'; nested exception is java.lang.IllegalArgumentException: Couldn't convert value : com.company.project.domain.package.SomeClass@ec7887e4 into a valid com.company.project.domain.package.SomeClass object
    at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:112)
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:191)
    at org.springframework.messaging.handler.annotation.support.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:111)
    at org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:117)
    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:148)
    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:116)
    at org.springframework.integration.handler.support.MessagingMethodInvokerHelper$HandlerMethod.invoke(MessagingMethodInvokerHelper.java:1096)
    at org.springframework.integration.handler.support.MessagingMethodInvokerHelper.invokeHandlerMethod(MessagingMethodInvokerHelper.java:580)
    at org.springframework.integration.handler.support.MessagingMethodInvokerHelper.processInternal(MessagingMethodInvokerHelper.java:476)
    at org.springframework.integration.handler.support.MessagingMethodInvokerHelper.process(MessagingMethodInvokerHelper.java:355)
    at org.springframework.integration.handler.MethodInvokingMessageProcessor.processMessage(MethodInvokingMessageProcessor.java:108)
    ... 10 more
Caused by: java.lang.IllegalArgumentException: Couldn't convert value : com.company.project.domain.package.SomeClass@ec7887e4 into a valid com.company.project.domain.package.SomeClass object
    at com.company.project.domain.package.SomeClass.valueOf(SomeClass.java:148)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.core.convert.support.ObjectToObjectConverter.convert(ObjectToObjectConverter.java:102)

From trying to deserialize SomeClass with it's valueOf method, I see that the deserializer is using the correct deserializer. However, the value it's trying to deserialize, com.company.project.SomeClass@ec7887e4 is not the outcome of the serializer I provide (though I provide the needed serializer to Jackson2ObjectMapperBuilder in RabbitTemplate configuration). Could it be a bug, or what am I doing wrong?

Update:

Here's another unexpected behavior:

I have temporarily switched SomeClassDeserializer to:

public class SomeClassDeserializer extends JsonDeserializer<SomeClass> {

    @Override
    public SomeClass deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        final String name = ((TextNode) parser.getCodec().readTree(parser)).textValue();
        return new SomeClass(name);
    }
}

and the valueOf method on SomeClass kept calling instead of new SomeClass(name) on the new SomeClassDeserializer. It's the moment I switched the name of the static method from valueOf to valueOfName my actual new implementation started getting called.

Since there's no validation on the constructor of SomeClass, the exception goes, although this time it started to deserialize as

new SomeClass("com.company.project.domain.package.SomeClass@ec7887e4")

instead of

new SomeClass("myActualValue")

Solution

  • Couldn't convert value : com.company.project.domain.package.SomeClass@ec7887e4 into a valid com.company.project.domain.package.SomeClass

    It looks like are just sending the class name as a String.

    com.company.project.domain.package.SomeClass@ec7887e4

    This is the default toString() implementation on Object.

    i.e. SomeClass.getName() instead of someClass.getName() (assuming there is a getName() method on the class.

    EDIT

    I didn't notice it was a header, sorry.

    The message converter only performs conversion on the payload.

    For headers, you need to implement a custom HeaderMapper - Spring just passes headers to the amqp-client, which does a toString() on types it doesn't know.

    Subclass the DefaultAmqpHeaderMapper and override populateUserDefinedHeader on the outbound side and extractUserDefinedHeaders on the inbound side.