Type: Bug
Component:
SQS
Describe the bug
In GraalVM native image, a custom MessagingMessageConverter configured on
SqsMessageListenerContainerFactory is lost before the actual listener container is created.
Setup:
- Spring Cloud AWS SQS 4.0.0
- Spring Boot 4.0.0
- Java 25
- GraalVM 25.0.2
- reproduced locally with LocalStack
What happens:
- Application config customizes the listener container factory using
io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory.Builder#configure(...)
and sets a custom converter via
io.awspring.cloud.sqs.listener.AbstractContainerOptions.Builder#messageConverter(io.awspring.cloud.sqs.support.converter.MessagingMessageConverter<?>).
- On JVM, the listener container uses that custom converter, and message processing works.
- In the native image, the built listener container ends up using the default
SqsMessagingMessageConverter instead.
This causes the custom receive-side conversion to be bypassed in native mode.
In my case:
- JVM used the custom converter and correctly processed an SNS-wrapped FIFO message
- native used the default converter and deserialized the wrapper type incorrectly (
payload == null)
The problem appears to be in the builder copy path. SqsContainerOptions.BuilderImpl.createCopy()
uses reflective field copying, and in the native image, the superclass state (including messageConverter)
does not seem to survive correctly.
A local patch that replaced the reflective copy with explicit field copying made native behave like JVM.
A cleaner upstream fix may be to replace the reflective copy in
SqsContainerOptions.BuilderImpl.createCopy() with explicit builder-state copying
(for example via copy constructors), instead of relying on
ReflectionUtils.shallowCopyFieldState(...). A local patch in that direction
made native image preserve the configured custom messageConverter.
Another possible fix direction would be to make the native runtime hints explicit for the
builder types involved in the reflective copy, but explicit copying seems like the cleaner
and a more robust solution.
Sample
Minimal repro:
- Define a custom
MessagingMessageConverter<Message> bean
- Configure
io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory
using io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory.Builder#configure(...)
- Set the custom converter via
io.awspring.cloud.sqs.listener.AbstractContainerOptions.Builder#messageConverter(io.awspring.cloud.sqs.support.converter.MessagingMessageConverter<?>)
- Add an
@SqsListener
- Run on JVM: custom converter is used
- Build native image: listener container uses default
SqsMessagingMessageConverter instead
Observed behavior:
- JVM:
source.convertMessage converter=<custom converter>
- native:
source.convertMessage converter=io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter
Type: Bug
Component:
SQS
Describe the bug
In GraalVM native image, a custom
MessagingMessageConverterconfigured onSqsMessageListenerContainerFactoryis lost before the actual listener container is created.Setup:
What happens:
io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory.Builder#configure(...)and sets a custom converter via
io.awspring.cloud.sqs.listener.AbstractContainerOptions.Builder#messageConverter(io.awspring.cloud.sqs.support.converter.MessagingMessageConverter<?>).SqsMessagingMessageConverterinstead.This causes the custom receive-side conversion to be bypassed in native mode.
In my case:
payload == null)The problem appears to be in the builder copy path.
SqsContainerOptions.BuilderImpl.createCopy()uses reflective field copying, and in the native image, the superclass state (including
messageConverter)does not seem to survive correctly.
A local patch that replaced the reflective copy with explicit field copying made native behave like JVM.
A cleaner upstream fix may be to replace the reflective copy in
SqsContainerOptions.BuilderImpl.createCopy()with explicit builder-state copying(for example via copy constructors), instead of relying on
ReflectionUtils.shallowCopyFieldState(...). A local patch in that directionmade native image preserve the configured custom
messageConverter.Another possible fix direction would be to make the native runtime hints explicit for the
builder types involved in the reflective copy, but explicit copying seems like the cleaner
and a more robust solution.
Sample
Minimal repro:
MessagingMessageConverter<Message>beanio.awspring.cloud.sqs.config.SqsMessageListenerContainerFactoryusing
io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory.Builder#configure(...)io.awspring.cloud.sqs.listener.AbstractContainerOptions.Builder#messageConverter(io.awspring.cloud.sqs.support.converter.MessagingMessageConverter<?>)@SqsListenerSqsMessagingMessageConverterinsteadObserved behavior:
source.convertMessage converter=<custom converter>source.convertMessage converter=io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter