Spring AMQP #RabbitListener convert to origin object - spring

I try to send a message based on a flatten MAP using Spring Boot and AMQP. The message should then be received using #RabbitListener and transfer it back to a MAP.
First I have nested json String and flat it and send it using the following code:
// Flatten the JSON String returned into a map
Map<String,Object> jsonMap = JsonFlattener.flattenAsMap(result);
rabbitTemplate.convertAndSend(ApplicationProperties.rmqExchange, ApplicationProperties.rmqTopic, jsonMap, new MessagePostProcessor() {
#Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setHeader("amqp_Key1", "wert1");
message.getMessageProperties().setHeader("amqp_Key2", "Wert2");
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}
});
So far so good.
On the receiving site I try to use a Listener and convert the message payload back to the Map as it was send before.
The problem ist that I have no idea how to do it.
I receive the message with the following code:
#RabbitListener(queues = "temparea")
public void receiveMessage(Message message) {
log.info("Receiving data from RabbitMQ:");
log.info("Message is of type: " + message.getClass().getName());
log.info("Message: " + message.toString());
}
As I mentioned before I have no idea how I can convert the message to my old MAP. The __ TypeId __ of the Message is: com.github.wnameless.json.flattener.JsonifyLinkedHashMap
I would be more than glad if somebody could assist me how I get this message back to an Java Map.
BR
Update after answer from Artem Bilan:
I added the following code to my configuration file:
#Bean
public SimpleRabbitListenerContainerFactory myRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConnectionFactory(connectionFactory());
factory.setMaxConcurrentConsumers(5);
return factory;
}
But still I have no idea how to get the Map out of my message.
The new code block does not change anything.

You have to configure Jackson2JsonMessageConverter bean and Spring Boot will pick it up for the SimpleRabbitListenerContainerFactory bean definition which is used to build listener containers for the #RabbitListener methods.
UPDATE
Pay attention to the Spring AMQP JSON Sample.
There is a bean like jsonConverter(). According Spring Boot auto-configuration this bean is injected to the default:
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
Which is really used for the #RabbitListener by default, when the containerFactory attribute is empty.
So, you need just configure that bean and don't need any custom SimpleRabbitListenerContainerFactory. Or if you do that you should specify its bean name in that containerFactory attribute of your #RabbitListener definitions.
Another option to consider is like Jackson2JsonMessageConverter.setTypePrecedence():
/**
* Set the precedence for evaluating type information in message properties.
* When using {#code #RabbitListener} at the method level, the framework attempts
* to determine the target type for payload conversion from the method signature.
* If so, this type is provided in the
* {#link MessageProperties#getInferredArgumentType() inferredArgumentType}
* message property.
* <p> By default, if the type is concrete (not abstract, not an interface), this will
* be used ahead of type information provided in the {#code __TypeId__} and
* associated headers provided by the sender.
* <p> If you wish to force the use of the {#code __TypeId__} and associated headers
* (such as when the actual type is a subclass of the method argument type),
* set the precedence to {#link TypePrecedence#TYPE_ID}.
* #param typePrecedence the precedence.
* #since 1.6
* #see DefaultJackson2JavaTypeMapper#setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence)
*/
public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePrecedence) {
So, if you want still to have a Message as a method argument but get a gain of the JSON conversion based on the __TypeId__ header, you should consider to configure Jackson2JsonMessageConverter to be based on the Jackson2JavaTypeMapper.TypePrecedence.TYPE_ID.

Related

Spring Integration w. JavaDSL - Dynamic RabbitMQ Routing Key

I'm converting a Spring Integration application from XML to JavaDSL. The part I'm struggling with is the part that publishes a message to RabbitMQ. In the XML version the routing-key was extracted from the headers dynamically. However, in the JavaDSL version this does not work. To summarise, this works:
<int-amqp:outbound-channel-adapter id="toRabbit" channel="rabbitChannel" amqp-template="rabbitMqTemplate"
routing-key-expression="headers['routing-key']" exchange-name="invoice">
</int-amqp:outbound-channel-adapter>
This does not:
#Bean
IntegrationFlow salesforceEventsFlow(MessageHandler messageHandler, RabbitTemplate rabbitTemplate) {
return IntegrationFlows.from("rabbitChannel")
.transform(messageHandler)
.handle(m -> Amqp.outboundAdapter(rabbitTemplate)
.exchangeName("invoice")
.routingKey(m.getHeaders().get("routing-key").toString()))
.get();
}
Interestingly, having the routing key hardcoded works.
#Bean
IntegrationFlow salesforceEventsFlow(MessageHandler messageHandler, RabbitTemplate rabbitTemplate) {
return IntegrationFlows.from("rabbitChannel")
.transform(messageHandler)
.handle(Amqp.outboundAdapter(rabbitTemplate)
.exchangeName("invoice")
.routingKey("invoice.updated"))
.get();
}
How can I have dynamic routing keys when using JavaDSL?
For your use-case there is this configuration option:
/**
* A function to evaluate routing key at runtime.
* #param routingKeyFunction the {#link Function} to use.
* #return the spec
*/
public S routingKeyFunction(Function<Message<?>, String> routingKeyFunction) {
So, your code should be like this:
.routingKeyFunction(m -> m.getHeaders().get("routing-key", String.class))
There is also a routingKeyExpression() if you want:
.routingKeyExpression("headers.routing-key")
But Function is better by performance: SpEL works with reflection.

Filter messages before executing #RabbitListener

How can I filter a message before is processed by a #RabbitListener annotated method ?
If the message is for. ex. is "duplicated" because contains an header with a determinate value I would like to return "ack" and skip processing. (skip the body of #RabbitListener method)
I tried to do it in a MessagePostProcessor (with addAfterReceivePostProcessors) but cannot skip execution for ex. based on a message property (header).
This is the signature of MessageProcessor :
Message postProcessMessage(Message message) throws AmqpException;
I would like to return an "ack" here so the message processing is skipped.
thank you for your support.
I think an AmqpRejectAndDontRequeueException is what you need to throw from your MessagePostProcessor impl.
See its javadocs:
/**
* Exception for listener implementations used to indicate the
* basic.reject will be sent with requeue=false in order to enable
* features such as DLQ.
* #author Gary Russell
* #since 1.0.1
*
*/
#SuppressWarnings("serial")
public class AmqpRejectAndDontRequeueException extends AmqpException {
And respective docs: https://docs.spring.io/spring-amqp/docs/current/reference/html/#exception-handling

Equivalent to ChannelInterceptor for Spring Cloud Stream Kafka Streams Binder

We develop an internal company framework on top of Spring Boot and we'd like to support Kafka-Streams with Spring Cloud Stream. We need to automagically inject some headers to all outbound messages. We've achieved this with standard Spring Cloud Stream Kafka Binder registering a custom ChannelInterceptor, but this is not working for Kafka Streams, as they seem to follow a different path.
Is there any equivalent to ChannelInterceptor for Spring Cloud Stream Kafka Streams binder?
I found this customizer/configurer:
#Bean
public StreamsBuilderFactoryBeanConfigurer streamsBuilderFactoryBeanConfigurer() {
return factoryBean -> {
factoryBean.setInfrastructureCustomizer(new KafkaStreamsInfrastructureCustomizer() {
#Override
public void configureBuilder(final StreamsBuilder builder) {
}
#Override
public void configureTopology(final Topology topology) {
}
});
factoryBean.setKafkaStreamsCustomizer(new KafkaStreamsCustomizer() {
#Override
public void customize(final KafkaStreams kafkaStreams) {
}
});
};
}
My last idea was to use the configureTopology method to automagically modify the Topology and insert a Transformer just before the last sink node, but in order to add this new node I have to specify the parent node, so I would need to know the name of last sink node and maybe all node names were generated automatically by Kafka Streams... The only way is to use topology.describe() method and maybe parse the String output... This sounds too complicated compared to a simple ChannelInterceptor.
Any ideas?
You could add a ProducerInterceptor to the streams config.
/**
* A plugin interface that allows you to intercept (and possibly mutate) the records received by the producer before
* they are published to the Kafka cluster.
* <p>
* This class will get producer config properties via <code>configure()</code> method, including clientId assigned
* by KafkaProducer if not specified in the producer config. The interceptor implementation needs to be aware that it will be
* sharing producer config namespace with other interceptors and serializers, and ensure that there are no conflicts.
* <p>
* Exceptions thrown by ProducerInterceptor methods will be caught, logged, but not propagated further. As a result, if
* the user configures the interceptor with the wrong key and value type parameters, the producer will not throw an exception,
* just log the errors.
* <p>
* ProducerInterceptor callbacks may be called from multiple threads. Interceptor implementation must ensure thread-safety, if needed.
* <p>
* Implement {#link org.apache.kafka.common.ClusterResourceListener} to receive cluster metadata once it's available. Please see the class documentation for ClusterResourceListener for more information.
*/
public interface ProducerInterceptor<K, V> extends Configurable {
You can modify the record's headers there.
(You can use this technique for the message channel binder too, instead of a ChannelInterceptor).

Type Id property error is get when sending text

I use spring boot 2.2 application who receive mq message via jms.
Data is json
In my code I have this config
#Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
When I send data via IBM MQ Explorer, I get this error
org.springframework.jms.support.converter.MessageConversionException: Could not find type id property [_type] on message
Is there a way to set this with IBM MQ explorer?
If your message doesn't have a property containing type information, you can subclass the MappingJackson2MessageConverter and override getJavaTypeForMessage
/**
* Determine a Jackson JavaType for the given JMS Message,
* typically parsing a type id message property.
* <p>The default implementation parses the configured type id property name
* and consults the configured type id mapping. This can be overridden with
* a different strategy, e.g. doing some heuristics based on message origin.
* #param message the JMS Message to set the type id on
* #throws JMSException if thrown by JMS methods
* #see #setTypeIdOnMessage(Object, javax.jms.Message)
* #see #setTypeIdPropertyName(String)
* #see #setTypeIdMappings(java.util.Map)
*/
protected JavaType getJavaTypeForMessage(Message message) throws JMSException {

#RabbitListener (having id set) not registered with RabbitListenerEndpointRegistry

I am using #RabbitListener to consume a message from RabbitMQ. I want to have the ability to pause/resume the message consume process based on some a threshold.
As per this and this SO posts, I could be able to use RabbitListenerEndpointRegistry and get the list of containers and pause/resume consuming based on need. I also understand that to be able to register to RabbitListenerEndpointRegistry, I need to specify an id to #RabbitListener annotation.
However, I have add an id to #RabbitListener annotation and still RabbitListenerEndpointRegistry.getListenerContainers() returns me a empty collection.
I am not sure, what could be the issue due to which i could not get the ListenerContainers collection.
Here is now I create the SimpleRabbitListenerContainerFactory. ( I use SimpleRabbitListenerContainerFactory because I need to consume from different queues)
public static SimpleRabbitListenerContainerFactory getSimpleRabbitListenerContainerFactory(
final ConnectionFactory connectionFactory,
final Jackson2JsonMessageConverter jsonConverter,
final AmqpAdmin amqpAdmin,
final String queueName, final String bindExchange,
final String routingKey, final int minConsumerCount,
final int maxConsumerCount, final int msgPrefetchCount,
final AcknowledgeMode acknowledgeMode) {
LOGGER.info("Creating SimpleRabbitListenerContainerFactory to consume from Queue '{}', " +
"using {} (min), {} (max) concurrent consumers, " +
"prefetch count set to {} and Ack mode is {}",
queueName, minConsumerCount, maxConsumerCount, msgPrefetchCount, acknowledgeMode);
/**
* Before creating the connector factory, ensure that the model with appropriate
* binding exists.
*/
createQueueAndBind(amqpAdmin, bindExchange, queueName, routingKey);
// Create listener factory
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// set connection factory
factory.setConnectionFactory(connectionFactory);
// set converter
factory.setMessageConverter(jsonConverter);
// set false so that if model is missing the app will not crash
factory.setMissingQueuesFatal(false);
// set min concurrent consumer
factory.setConcurrentConsumers(minConsumerCount);
// set max concurrent consumer
factory.setMaxConcurrentConsumers(maxConsumerCount);
// how many message should be fetched in a go
factory.setPrefetchCount(msgPrefetchCount);
// set acknowledge mode to auto
factory.setAcknowledgeMode(acknowledgeMode);
// captures the error on consumer side if any error
factory.setErrorHandler(errorHandler());
return factory;
}
#RabbitListener annotation declaration on a method
#RabbitListener(queues = "${object.scan.queue-name}",
id = "object-scan",
containerFactory = "object.scan")
/**
* Before creating the connector factory, ensure that the model with appropriate
* binding exists.
*/
createQueueAndBind(amqpAdmin, bindExchange, queueName, routingKey);
You should not be doing this during the bean definition phase; it's too early in the application context's lifecycle.
All you need to do is add the queue/exchange/binding #Beans to the application context and Spring (RabbitAdmin) will automatically do the declarations when the container is first started.

Resources