mr.nothing
mr.nothing

Reputation: 5399

JmsListener listen generic container object

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

Answers (1)

Artem Bilan
Artem Bilan

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

Related Questions