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")
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.