Spring Integration - Joining messages from 2 channels - spring-integration

I am new to Spring Integration and want to know if there exist a component in Spring Integration which will help me in joining the results of 2 channels. I know the answer would be aggregator, however I don't want messages to be merged into 1 message but they should flow to the downstream component when messages from both the channels have arrived. It is sort of Cyclic Barrier case.
Thank You
Adi

The default behavior of the aggregator is to aggregate the released group of messages into a single message with a collection in the payload.
You can add a splitter after the aggregator to bust them up again.
However, if the aggregator output MessageGroupProcessor produces a Collection<Message<?> then each message is released one after the other.
You can't wire in a custom MessageGroupProcessor using the XML configuration but you can do so when configuring in Java...
AggregatingMessageHandler aggregator = new AggregatingMessageHandler(new FooProcessor());
...
private class FooProcessor implements MessageGroupProcessor {
#Override
public Object processMessageGroup(MessageGroup group) {
return group.getMessages();
}
}

Related

Spring AMQP - How to convert body of message to my custom type?

I have a Spring Boot project (let's call it as project A) which uses JMS template to send and receive messages over rabbitmq and works very well. I am not able to change anything on this project.
On other project (let's call it as project B), I want to use Spring AMQP in this project because this is new project but when the project A sends message, project B takes body part of Message as byte array. I defined a RabbitListener to listen queue which will be populated by project A like below:
#RabbitListener( queues = "theQueueName")
public void listen(org.springframework.amqp.core.Message incomingMessage) {
System.out.println("Message read from myQueue : " + new String(incomingMessage.getBody()));
}
The body part of incoming message is byte but I need to convert it to custom type which is being used in project A.
What should I do to take this body part as I want?
The ability to inject Spring’s message abstraction is particularly useful to benefit from all the information stored in the transport-specific message without relying on the transport-specific API. The following example shows how to do so:
#RabbitListener(queues = "myQueue")
public void processOrder(org.springframework.messaging.Message<Order> order) { ...
}
https://docs.spring.io/spring-amqp/docs/current/reference/html/#async-annotation-driven-enable-signature

SI DSL: Difference between handle() and channel() in flow definition for AMQP queues

I've reviewed the documentation I can find and haven't worked out an answer so was hoping to get some enlightenment here. Is there any difference between these two calls?
.handle(Amqp.outboundAdapter(amqpTemplate).routingKey("my-queue"))
.channel(Amqp.channel(connectionFactory).queueName("my-queue"))
First of all let's come back to the Reference Manual any way:
Prior to version 4.3, AMQP-backed channels only supported messages with Serializable payloads and headers. The entire message was converted (serialized) and sent to RabbitMQ. Now, you can set the extract-payload attribute (or setExtractPayload() when using Java configuration) to true. When this flag is true, the message payload is converted and the headers mapped, in a similar manner to when using channel adapters. This allows AMQP-backed channels to be used with non-serializable payloads (perhaps with another message converter such as the Jackson2JsonMessageConverter). The default mapped headers are discussed in Section 11.12, “AMQP Message Headers”. You can modify the mapping by providing custom mappers using the outbound-header-mapper and inbound-header-mapper attributes. You can now also specify a default-delivery-mode, used to set the delivery mode when there is no amqp_deliveryMode header. By default, Spring AMQP MessageProperties uses PERSISTENT delivery mode.
So, typically (by default) Amqp.channel(connectionFactory) sends the whole Message<?> to the target queue. Meanwhile the Amqp.outboundAdapter(amqpTemplate) does exactly opposite - map the payload to the body and headers:
if (this.amqpTemplate instanceof RabbitTemplate) {
MessageConverter converter = ((RabbitTemplate) this.amqpTemplate).getMessageConverter();
org.springframework.amqp.core.Message amqpMessage = MappingUtils.mapMessage(requestMessage, converter,
getHeaderMapper(), getDefaultDeliveryMode());
addDelayProperty(requestMessage, amqpMessage);
((RabbitTemplate) this.amqpTemplate).send(exchangeName, routingKey, amqpMessage, correlationData);
}
The AMQP-backed channels are aimed for the persistence and should not be used in the logic which relies on the messages content.

Getting an event for aggregator message group expiry with Spring Integration DSL?

I have an aggregator configured via the Java DSL in a Spring Integration flow, and I want to throw an exception that goes to the global error channel when a group timeout occurs.
The discard channel is no good for me because discard messages are at the group member level, rather than for the whole group.
I've tried using the application event publisher as follows, but the publisher object doesn't seem to get invoked:
.aggregate(new Consumer<AggregatorSpec>() {
#Override
public void accept(AggregatorSpec aggregatorSpec) {
try {
aggregatorSpec
.outputProcessor(groupPublishStrategy())
.correlationStrategy(groupPublishStrategy())
.releaseStrategy(groupPublishStrategy())
.groupTimeout(groupTimeout)
.get().getT2().setApplicationEventPublisher(myGroupExpirationPublisher());
}
catch (Exception e) {
e.printStackTrace();
}
}
})
Is there a recommended way to get notification for this use case? Any ideas why the above doesn't seem to work?
I suppose I could extend the AggregatorSpec class to get the message handler configured the way I want, but I wanted to see if I could do this with stock SI classes.
Have just tested and ApplicationEventPublisher is populated properly on the bean initialization phase.
The expireGroup(Object correlationKey, MessageGroup group) is enough big to demonstrate it here, but you can find its code on GitHub. So, MessageGroupExpiredEvent is always published when we reach this method.
Of course if discardMessage(message); doesn't throw exception.
OTOH the exprireGroup() is reachable only in this case:
if (this.releaseStrategy.canRelease(groupNow)) {
completeGroup(correlationKey, groupNow);
}
else {
expireGroup(correlationKey, groupNow);
}
So, please, be sure that your groupPublishStrategy() has proper logic and doesn't return true when your group isn't completed yet.
Well, it really would be better if you debug AbstractCorrelatingMessageHandler for your use-case. If you are sure that your group isn't completed during some groupTimeout, the forceComplete(MessageGroup group) is a good place for you to start debugging.
Otherwise, please, share DEBUG logs for the org.springframework.integration category, when you think that an event has to be emitted.

Spring integration - AMQP backed message channels and message conversion

I am trying to use AMQP-backed message channels in my Spring Integration app, but I think I am fundamentally misunderstanding something, specifically around the Message<?> interface and how instances of GenericMessage<?> are written to and read from, a RabbitMQ queue.
Given I have a Spring Integration app containing the following domain model object:
#Immutable
class Foo {
String name
long quantity
}
and I declare an AMQP backed message channel called fooChannel as follows:
#Bean
public AmqpChannelFactoryBean deliveryPlacementChannel(CachingConnectionFactory connectionFactory) {
AmqpChannelFactoryBean factoryBean = new AmqpChannelFactoryBean(true)
factoryBean.setConnectionFactory(connectionFactory)
factoryBean.setQueueName("foo")
factoryBean.beanName = 'fooChannel'
factoryBean.setPubSub(false)
factoryBean
}
When I initially tried to send a message to my fooChannel I received a java.io.NotSerializableException. I realised this to be caused by the fact that the RabbitTemplate used by my AMQP-backed fooChannel was using a org.springframework.amqp.support.converter.SimpleMessageConverter which can only work with Strings, Serializable instances, or byte arrays, of which my Foo model is none of those things.
Therefore, I thought that I should use a org.springframework.amqp.support.converter.Jackson2JsonMessageConverter to ensure my Foo model is properly converted to/from and AMQP message. However, it appears that the type of the message that is being added to the RabbitMQ queue which is backing my fooChannel is of type org.springframework.messaging.support.GenericMessage. This means that when my AMQP backed fooChannel tries to consume messages from the RabbitMQ queue it receives the following exception:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class org.springframework.messaging.support.GenericMessage]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
From looking at the GenericMessage class I see that is designed to be immutable, which clearly explains why the Jackson2JsonMessageConverter can't convert from JSON to the GenericMessage type. However, I am unsure what I should be doing in order to allow my fooChannel to be backed by AMQP and have the conversion of my Spring Integration messages containing my Foo model work correctly?
In terms of the flow of my application I have the following Transformer component which consumes Bar models from the (non-AMQP backed) barChannel and places Foo models on the fooChannel as follows:
#Transformer(inputChannel = 'barChannel', outputChannel = 'fooChannel')
public Foo transform(Bar bar) {
//transform logic removed for brevity
new Foo(name: 'Foo1', quantity: 1)
}
I then have a ServiceActivator component which I wish to have consume from my fooChannel as follows:
#ServiceActivator(inputChannel = 'fooChannel')
void consumeFoos(Foo foo){
// Do something with foo
}
I am using spring-integration-core:4.2.5.RELEASE and spring-integration-amqp:4.2.5.RELEASE.
Can anyone please advise where I am going wrong with the configuration of my Spring Integration application?
If any further information is needed to in order to better clarify my question or problem, please let me know. Thanks
Yes - amqp-backed channels are currently limited to Java serializable objects.
We should provide an option to map the Message<?> to a Spring AMQP Message (like the channel adapters do) rather than...
this.amqpTemplate.convertAndSend(this.getExchangeName(), this.getRoutingKey(), message);
...which converts the entire message.
You could use a pair of channel adapters (outbound/inbound) instead of a channel.
Since you are using Java config, you could wrap the adapter pair in a new MessageChannel implementation.
I opened a JIRA Issue for this.

Spring Integration: getting the name of the underlying Queue from a Message or MessageChannel

Given a MessageChannel or Message object, how is it possible to get from one of them the name of the underlying JMS Queue which the message was received on ?
Here is the scenario:
Several jms:message-driven-channel-adapter instances are defined in the xml. The destination-name of each adapter uses SEL to receive from different queues. This SEL is dynamic, and is not possible to know these queue names ahead of time. All channel adapters output to the same internal Spring Integration channel.
I want to add the actual underlying queue name which the message was received on to the header of the message.
The idea is to setup a ChannelInterceptor for either the channel-adapters or the internal channel. The postReceive() method has both the Message and MessageChannel as arguments. Using either of these, is it possible to get the name of the underlying Queue name which the message came in on?
Thanks
Looks like you need to extend a bit DefaultJmsHeaderMapper:
class DestinationJmsHeaderMapper extends DefaultJmsHeaderMapper {
public Map<String, Object> toHeaders(javax.jms.Message jmsMessage) {
Map<String, Object> headers = super.toHeaders(jmsMessage);
headers.put("JMS_DESTINATION", ((Queue) jmsMessage.getJMSDestination()).getQueueName());
}
}
And inject it to your <jms:message-driven-channel-adapter>s
This is how we did it:
<int:header-enricher>
<int:header name="JMS_DESTINATION" expression="payload.JMSDestination.queueName"/>
</int:header-enricher>
It requires extract-payload="false" in your <jms:message-driven-channel-adapter>.
P.S. The answer of Artem is missing the return statement (I do not have enough reputations to comment).

Resources