Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Cloud @SqsListener MessageConversionException: Cannot convert from [java.lang.String] for GenericMessage

I'm seeing the following exception when I try to consume an SQS message:

org.springframework.messaging.converter.MessageConversionException: 

Cannot convert from [java.lang.String] to [com.example.demo.Foo] for GenericMessage [payload={}, headers={LogicalResourceId=my-queue, ApproximateReceiveCount=1, SentTimestamp=1529021258825, ReceiptHandle=xxxx, Visibility=org.springframework.cloud.aws.messaging.listener.QueueMessageVisibility@47ce6922, SenderId=xxxx, lookupDestination=my-queue, ApproximateFirstReceiveTimestamp=1529021264456, MessageId=xxxx}]
    at org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver.resolveArgument(PayloadArgumentResolver.java:144)
    at org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:116)
    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:137)
    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:109)
    at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMatch(AbstractMethodMessageHandler.java:515)
    at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMessageInternal(AbstractMethodMessageHandler.java:473)
    at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMessage(AbstractMethodMessageHandler.java:409)
    at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.executeMessage(SimpleMessageListenerContainer.java:205)
    at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer$MessageExecutor.run(SimpleMessageListenerContainer.java:342)
    at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer$SignalExecutingRunnable.run(SimpleMessageListenerContainer.java:397)
    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)

Spring Boot code is as follows:

@Configuration
@EnableSqs
public class AmazonSqsConfiguration {

    @Bean
    public AmazonSQS amazonSQSAsync() {
        return AmazonSQSAsyncClientBuilder.standard()
            .withRegion(Regions.US_WEST_2)
            .build();
    }
}

@Service
public class MyService {

    // Throws MessageConversionException
    @SqsListener("my-queue")
    public void listen(Foo payload) {

    }

    // Works fine
    @SqsListener("my-queue")
    public void listen(String payload) {

    }
}

I'm using org.springframework.cloud:spring-cloud-aws-messaging:2.0.0.RC2

I do have Jackson 2 libraries on my classpath, so PayloadArgumentResolver is trying to use MappingJackson2MessageConverter to deserialize my message payload. However, because the SQS message is missing a contentType header and strictContentTypeMatch is set to true, canConvertFrom returns false.

https://github.com/spring-projects/spring-framework/blob/f5e8f4983f7653169f3da8a3287499fce93cadd4/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java#L237

I don't see how it is possible to set contentType header for SQS messages - am I missing something?

Should Spring Cloud QueueMessageHandler be setting strictContentTypeMatch to true?

https://github.com/spring-cloud/spring-cloud-aws/blob/6a7c3c31709d4239131b27936de29385df414d41/spring-cloud-aws-messaging/src/main/java/org/springframework/cloud/aws/messaging/listener/QueueMessageHandler.java#L217

like image 757
Joe Stepowski Avatar asked Jun 15 '18 00:06

Joe Stepowski


2 Answers

Ran into the same issue, and I would answer the question one of two ways depending on who generates the message

  • Yes it is possible to set the contentType on the message, and if you control the messages being generated, this is preferred. In the AWS Console, when you send a message by hand, there is a tab for "message attributes". You would add an attribute with name contentType and value application/json. AWS SDK calls should allow you to do the same from application code.

  • For messages that AWS generates without content specified content types, like S3 events, you do in fact need to set strictContentMatch to false. This is documented here: http://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.0.0.RELEASE/multi/multi__messaging.html#_consuming_aws_event_messages_with_amazon_sqs

The document is confusing because it says "without the mime-type header" but the actual name of the header is contentType, as you found yourself.

like image 108
wrschneider Avatar answered Nov 08 '22 06:11

wrschneider


Consistent converter in both "send" and "listen" methods:

/** Provides a deserialization template for incoming SQS messages */
@Bean
public QueueMessageHandlerFactory queueMessageHandlerFactory(MessageConverter messageConverter) {

    var factory = new QueueMessageHandlerFactory();
    factory.setArgumentResolvers(singletonList(new PayloadArgumentResolver(messageConverter)));
    return factory;
}

/** Provides a serialization template for outgoing SQS messages */
@Bean
public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync, MessageConverter messageConverter) {
    return new QueueMessagingTemplate(amazonSQSAsync, (ResourceIdResolver) null, messageConverter);
}

/** Provides JSON converter for SQS messages */
@Bean
protected MessageConverter messageConverter(ObjectMapper objectMapper) {

  var converter = new MappingJackson2MessageConverter();
  converter.setObjectMapper(objectMapper);
  // Serialization support:
  converter.setSerializedPayloadClass(String.class);
  // Deserialization support: (suppress "contentType=application/json" header requirement)
  converter.setStrictContentTypeMatch(false);
  return converter;
}

See details in the original accepted answer. Credits to @wrschneider.

NOTE: the above example injects and sets ObjectMapper. That is for HTTP REST controllers consistency (if any controllers).


Imports:

import static java.util.Collections.singletonList;

import com.amazonaws.services.sqs.AmazonSQSAsync;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.aws.core.env.ResourceIdResolver;
import org.springframework.cloud.aws.messaging.config.QueueMessageHandlerFactory;
import org.springframework.cloud.aws.messaging.core.QueueMessagingTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver;

Usage:

  @SqsListener("my-queue")
  public void listen(Foo payload) {

  }

  public void send(Foo dto) {
    queueMessagingTemplate.convertAndSend(url, dto);
  }
like image 3
epox Avatar answered Nov 08 '22 08:11

epox