Reputation: 5399
I'm doing an event based system which consists of multiple services that communicate via kind of domain events. All services are on spring boot 3.1.3 and send messages via activemq artemis.
I've got some interfaces and common code to unify behaviour across all the services:
interface DomainEvent
data class DomainEventContainer<D : DomainEvent>(
val event: D,
val type: DomainEventType,
val createdBy: DomainEventOrigin,
val createdAt: Instant
)
fun interface BaseDomainEventProducer<T : DomainEvent> {
fun produce(event: T): DomainEventContainer<T>?
}
fun interface BaseDomainEventConsumer<T : DomainEvent> {
fun consume(container: DomainEventContainer<T>)
}
On producer side I do:
fun produce(event: DomainEvent) {
// some logic here
val container = DomainEventContainer(event, type, DomainEventOrigin.MY_SERVICE, Instant.now())
return try {
log.info("Sending event $container")
jmsTemplate.convertAndSend(destination, container)
container
} catch (e: Exception) {
log.info("Error during sending event $container ", e)
null
}
}
On receiver side I'm trying to do this:
@JmsListener(destination = "\${random.artemis.queue}")
override fun consume(message: DomainEventContainer<MyCustomDomainEvent>) {
eventHandler.handle(container.event)
}
The thing is that there is not enough information for jms internals to guess the MyCustomDomainEvent
type and it unable to deserealize such a message for my jms listener failing with error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `DomainEvent` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
I can deserealize message myself with use of jakarta.jms.Message
like this:
@JmsListener(destination = "\${random.artemis.queue}")
override fun consume(message: Message) {
val container = objectMapper.readValue(
message.getBody(String::class.java),
object : TypeReference<DomainEventContainer<EmploymentRegisteredDomainEvent>>(){})
)
eventHandler.handle(container.event)
}
or with use of org.springframework.messaging.Message
like this:
@JmsListener(destination = "\${random.artemis.queue}")
override fun consume(message: Message<DomainEventContainer<MyCustomDomain>>) {
val container = message.payload
eventHandler.handle(container.event)
}
But all these options looks weird to me since I have to add conversion logic to every listener which should be a concern of platform code. Am I missing something? Can I configure spring to deserialize messages the way I need?
p.s. I have remembered that rabbit/amqp part of spring boot works exactly the way I need, so we can send my message as DomainEventContainer<MyCustomDomain>
with RabbitTemplate
and expect to listen to it like this:
@RabbitListener(queues = ["random.rabbit.queue"])
fun consume(event: DomainEvent<MyCustomDomain>)
And it works fine. Not sure why it works this way in jms internals of spring.
Upvotes: 0
Views: 141
Reputation: 121552
You need to configure a MappingJackson2MessageConverter
on a JmsTemplate
and AbstractJmsListenerContainerFactory
. And set its typeIdPropertyName
to you type to serialize properly and then deserialize, respectively.
Upvotes: 0