Reply Channel for Messaging Gateway using Java DSL - spring-integration

I have a REST API which receives a POST request from a client application.
#Autowired
private EdiTranslationEvents.TransformToString transformToString;
#PostMapping("/testPost")
#ResponseStatus(HttpStatus.OK)
public String testPostLogging(#RequestBody Student student) {
log.info("In controller....");
System.out.println(this.transformToString.objectToInputGateway(student));
return "testPost logging";
}
As you can see, in the controller, I have an autowired messaging gateway and I am using it to send data to a channel.
#Configuration
#EnableIntegration
public class EdiTranslationEvents {
#Component
#MessagingGateway
public interface TransformToString {
#Gateway(requestChannel = "inputObjectChannel")
String objectToInputGateway(Student student);
}
#Bean
public IntegrationFlow inputObjectString() {
return IntegrationFlows.from(inputObjectChannel())
.transform(Transformers.objectToString())
.log(LoggingHandler.Level.DEBUG, "com.dash.Logger")
.get();
}
}
When I send data to the REST API, the API just hangs. The gateway does not return anything. I have the return type specified, and I was assuming that the gateway creates a temporary reply channel and sends the response to that channel.
However, I am not doing anything in the DSL configuration for creating or managing a reply. So, how do I send a reply back to the reply channel from the DSL flow?

Your current flow does not return a value, you are simply logging the message.
A terminating .log() ends the flow.
Delete the .log() element so the result of the transform will automatically be routed back to the gateway.
Or add a .bridge() (a bridge to nowhere) after the log and it will bridge the output to the reply channel.

Related

Abstracting Spring Cloud Stream Producer and Consumer code

I have a Service that is Producing and Consuming messages from different Spring Cloud Stream Channels (bound to EventHub/Kafka topics). There are several such Services which are setup similarly.
The configuration looks like below
public interface MessageStreams {
String WORKSPACE = "workspace";
String UPLOADNOTIFICATION = "uploadnotification";
String BLOBNOTIFICATION = "blobnotification";
String INGESTIONSTATUS = "ingestionstatusproducer";
#Input(WORKSPACE)
SubscribableChannel workspaceChannel();
#Output(UPLOADNOTIFICATION)
MessageChannel uploadNotificationChannel();
#Input(BLOBNOTIFICATION)
SubscribableChannel blobNotificationChannel();
#Output(INGESTIONSTATUS)
MessageChannel ingestionStatusChannel();
}
#EnableBinding(MessageStreams.class)
public class EventHubStreamsConfiguration {
}
The Producer/Publisher code looks like below
#Service
#Slf4j
public class IngestionStatusEventPublisher {
private final MessageStreams messageStreams;
public IngestionStatusEventPublisher(MessageStreams messageStreams) {
this.messageStreams = messageStreams;
}
public void sendIngestionStatusEvent() {
log.info("Sending ingestion status event");
System.out.println("Sending ingestion status event");
MessageChannel messageChannel = messageStreams.ingestionStatusChannel();
boolean messageSent = messageChannel.send(MessageBuilder
.withPayload(IngestionStatusMessage.builder()
.correlationId("some-correlation-id")
.status("done")
.source("some-source")
.eventTime(OffsetDateTime.now())
.build())
.setHeader("tenant-id", "some-tenant")
.build());
log.info("Ingestion status event sent successfully {}", messageSent);
}
}
Similarly I have multiple other Publishers which publish to different Event Hubs/Topics. Notice that there is a tenant-id header being set for each published message. This is something specific to my multi-tenant application to track the tenant context. Also notice that I am getting the channel to be published to while sending the message.
My Consumer code looks like below
#Component
#Slf4j
public class IngestionStatusEventHandler {
private AtomicInteger eventCount = new AtomicInteger();
#StreamListener(TestMessageStreams.INGESTIONSTATUS)
public void handleEvent(#Payload IngestionStatusMessage message, #Header(name = "tenant-id") String tenantId) throws Exception {
log.info("New ingestion status event received: {} in Consumer: {}", message, Thread.currentThread().getName());
// set the tenant context as thread local from the header.
}
Again I have several such consumers and also there is a tenant context that is set in each consumer based on the incoming tenant-id header that is sent by the Publisher.
My questions is
How do I get rid of the boiler plate code of setting the tenant-id header in Publisher and setting the tenant context in the Consumer by abstracting it into a library which could be included in all the different Services that I have.
Also, is there a way of dynamically identifying the Channel based on the Type of the Message being published. for ex IngestionStatusMessage.class in the given scenario
To set and tenant-id header in the common code and to avoid its copy/pasting in every microservice you can use a ChannelInterceptor and make it as global one with a #GlobalChannelInterceptor and its patterns option.
See more info in Spring Integration: https://docs.spring.io/spring-integration/docs/5.3.0.BUILD-SNAPSHOT/reference/html/core.html#channel-interceptors
https://docs.spring.io/spring-integration/docs/5.3.0.BUILD-SNAPSHOT/reference/html/overview.html#configuration-enable-integration
You can't make a channel selection by the payload type because the payload type is really determined from the #StreamListener method signature.
You can try to have a general #Router with a Message<?> expectation and then return a particular channel name to route according that request message context.
See https://docs.spring.io/spring-integration/docs/5.3.0.BUILD-SNAPSHOT/reference/html/message-routing.html#messaging-routing-chapter

Returning Response from TCP Inbound Gateway

I'm stuck on a seemingly simple task, but am out of ideas. I have a TcpInboundGateway attached to a TcpNetServerConnectionFactory that passes requests to a Service Activator. The Service Activator simply puts the message back on the Gateway's reply channel. I want the Gateway to return that message over the connection.
When I run the test, the message makes it to the Service Activator successfully. I know this because the Service Activator prints the message payload before returning it. I also know the Service Activator is putting the message on the right channel because I have an interceptor on that channel which also prints the message.
The problem seems to be that the Gateway isn't reading off of that channel, even though I set it in setReplyChannel(). I can also see this in the logs:
Adding {bridge:null} as a subscriber to the 'testResponseChannel' channel
which makes me suspect that the message is just getting sent to the null channel instead of being picked up by my Gateway.
Here's the configuration:
#Bean
public TcpNetServerConnectionFactory testServerFactory() {
TcpNetServerConnectionFactory testServerFactory = new TcpNetServerConnectionFactory(0);
testServerFactory.setSerializer(TcpCodecs.lengthHeader2());
testServerFactory.setDeserializer(TcpCodecs.lengthHeader2());
return testServerFactory;
}
#Bean
public DirectChannel testRequestChannel() {
return new DirectChannel();
}
#Bean
public DirectChannel testResponseChannel() {
DirectChannel testResponseChannel = new DirectChannel();
testResponseChannel.addInterceptor(channelInterceptor());
return testResponseChannel;
}
#Bean
public TcpInboundGateway gateway() {
TcpInboundGateway gateway = new TcpInboundGateway();
gateway.setConnectionFactory(testServerFactory());
gateway.setRequestChannel(testRequestChannel());
gateway.setReplyChannel(testResponseChannel());
return gateway;
}
#Bean
#ServiceActivator(inputChannel = "testRequestChannel", outputChannel = "testResponseChannel")
public EchoHandler echoHandler() {
return new EchoHandler();
}
Here's my POJO Service Activator:
public class EchoHandler {
public Message<String> echoMessage(Message<String> request) {
System.out.println(request.getPayload());
return request;
}
}
And here's the error, which happens right after the message passes through the interceptor:
Unexpected message - no endpoint registered with connection interceptor: localhost:6060:59848:3c6c3cff-c697-4fc9-b4e3-9ea14508cec7 - GenericMessage [payload=byte[3], headers={ip_tcp_remotePort=6060, ip_connectionId=localhost:6060:59848:3c6c3cff-c697-4fc9-b4e3-9ea14508cec7, ip_localInetAddress=/127.0.0.1, ip_address=127.0.0.1, id=93c75664-54db-c93e-ab3a-3e06b1e4b626, ip_hostname=localhost, timestamp=1556828832645}]
To react properly for the reply from the server, your client must be a request-response capable. For this purpose Spring Integration IP modules suggests a TcpOutboundGateway. This way a TcpListener is going to be registered on the TcpConnection and ready to parse and handle replies messages on the socket.

Sending messages to different topics using spring integration gateway

I am trying to use spring integration for send mqtt messages to a broker and I am trying to use the gateway interface.
#Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
//set the factory details
return factory:
}
#Bean
#ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler =
new MqttPahoMessageHandler("randomString", mqttClientFactory());
//set handler details
messageHandler.setDefaultTopic(topic);
return messageHandler;
}
#Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
#MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
private interface MyGateway {
void sendToMqtt(String data);
}
My question is: If I want to use the gateway handler to send messages to different topics how would I do that without having to create an adapter for each topic ?
Thanks.
Hope I formulated my question clearly and the code is properly formatted.
You need to set the target topic in a message header.
Here is one way to do that...
void sendToMqtt(String data, #Header(MqttHeaders.TOPIC) String topic);
The gateway proxy will assemble the message with the header, which is then used by the outbound adapter.

create sftp reply-channel to reply with error or messages that was unsuccessfully sent

I am using java dsl to configure sfp outbound flow.
Gateway:
#MessagingGateway
public interface SftpGateway {
#Gateway(requestChannel = "sftp-channel")
void sendFiles(List<Message> messages);
}
Config:
#Bean
public IntegrationFlow sftpFlow(DefaultSftpSessionFactory sftpSessionFactory) {
return IntegrationFlows
.from("sftp-channel")
.split()
.handle(Sftp.outboundAdapter(sftpSessionFactory, FileExistsMode.REPLACE)
.useTemporaryFileName(false)
.remoteDirectory(REMOTE_DIR_TO_CREATE).autoCreateDirectory(true)).get();
}
#Bean
public DefaultSftpSessionFactory sftpSessionFactory() {
...
}
How can i configure flow to make my gateway reply with Messages that were failed?
In other words i want my gateway to be able to return list of messages which were failed, not void.
I marked gateway with
#MessagingGateway(errorChannel = "errorChannel")
and wrote error channel
#Bean
public IntegrationFlow errorFlow() {
return IntegrationFlows.from("errorChannel").handle(new GenericHandler<MessagingException>() {
public Message handle(MessagingException payload, Map headers) {
System.out.println(payload.getFailedMessage().getHeaders());
return payload.getFailedMessage();
}
})
.get();
}
#Bean
public MessageChannel errorChannel() {
return MessageChannels.direct().get();
}
and in case of some errors(i.e. no connection to SFTP) i get only one error (payload of first message in list).
Where should i put Advice to aggregate all messages?
This is not the question to Spring Integration Java DSL.
This is mostly a design and architecture task.
Currently you don't have any choice because you use Sftp.outboundAdapter() which is one-way, therefore without any reply. And your SftpGateway is ready for that behavior with the void return type.
If you have a downstream errorr, you can only throw them or catch and send to some error-channel.
According to your request of:
i want my gateway to be able to return list of messages which were failed, not void.
I'd say it depends. Actually it is just return from your gateway. So, if you return an empty list into gateway that may mean that there is no errors.
Since Java doesn't provide multi-return capabilities we don't have choice unless do something in our stream which builds that single message to return. As we decided list of failed messages.
Since you have there .split(), you should look into .aggregate() to build a single reply.
Aggregator correlates with the Splitter enough easy, via default applySequence = true.
To send to aggregator I'd suggest to take a look into ExpressionEvaluatingRequestHandlerAdvice on the Sftp.outboundAdapter() endpoint (second param of the .handle()). With that you should send both good and bad messages to the same .aggregate() flow. Than you can iterate a result list to clean up it from the good result. The result after that can be send to the SftpGateway using replyChannel header.
I understand that it sounds a bit complicated, but what you want doesn't exist out-of-the-box. Need to think and play yourself to figure out what can be reached.

Spring Integration 4 asynchronous request/response

I am trying to write a simple message flow using Spring Integration v4's DSL APIs which would look like this:
-> in.ch -> Processing -> JmsGatewayOut -> JMS_OUT_QUEUE
Gateway
<- out.ch <- Processing <- JmsGatewayIn <- JMS_IN_QUEUE
With the request/response being asynchronous, when I inject a message via the initial Gateway, the message goes all the way to JMS_OUT_QUEUE. Beyond this message flow, a reply message is put back into JMS_IN_QUEUE which it is then picked up by JmsGatewayIn. At this point, the message is Processed and placed into out.ch (I know the response gets to out.ch because I have a logger interceptor there which logs the message being placed there) but, the Gateway never receives the response.
Instead of a response, the system outside of this message flow which picked up the message from JMS_OUT_QUEUE and placed the response in JMS_IN_QUEUE, receives a javax.jms.MessageFormatException: MQJMS1061: Unable to deserialize object on its own JmsOutboundgateway (I think it is failing to deserialize a jms reply object from looking at the logs).
I have clearly not got something configured correctly but I don't know exactly what. Does anyone know what I am missing?
Working with spring-integration-core-4.0.3.RELEASE, spring-integration-jms-4.0.3.RELEASE, spring-integration-java-dsl-1.0.0.M2, spring-jms-4.0.6.RELEASE.
My Gateway is configured as follows:
#MessagingGateway
public interface WsGateway {
#Gateway(requestChannel = "in.ch", replyChannel = "out.ch",
replyTimeout = 45000)
AResponse process(ARequest request);
}
My Integration flow is configured as follows:
#Configuration
#EnableIntegration
#IntegrationComponentScan
#ComponentScan
public class IntegrationConfig {
#Bean(name = "in.ch")
public DirectChannel inCh() {
return new DirectChannel();
}
#Bean(name = "out.ch")
public DirectChannel outCh() {
return new DirectChannel();
}
#Autowired
private MQQueueConnectionFactory mqConnectionFactory;
#Bean
public IntegrationFlow requestFlow() {
return IntegrationFlows.from("in.ch")
.handle("processor", "processARequest")
.handle(Jms.outboundGateway(mqConnectionFactory)
.requestDestination("JMS_OUT_QUEUE")
.correlationKey("JMSCorrelationID")
.get();
}
#Bean
public IntegrationFlow responseFlow() {
return IntegrationFlows.from(Jms.inboundGateway(mqConnectionFactory)
.destination("JMS_IN_QUEUE"))
.handle("processor", "processAResponse")
.channel("out.ch")
.get();
}
}
Thanks for any help on this,
PM.
First of all your configuration is bad:
Since you start the flow from WsGateway#process you really should wait reply there.
The gateway's request/reply capability is based on TemporaryReplyChannel, which is placed to the headers as non-serializable value.
As long as you wait rely on that gateway, actually there is no reason to provide the replyChannel, if you aren't going to do some publish-subscribe logic on the reply.
As you send message to the JMS queue, you should understand that consumer part might be a separete remote application. And the last one might know nothing about your out.ch.
The JMS request/reply capability is really based on JMSCorrelationID, but it isn't enough. The one more thing here is a ReplyTo JMS header. Hence, if you are going to send reply from the consumer you should really just rely on the JmsGatewayIn stuff.
So I'd change your code to this:
#MessagingGateway
public interface WsGateway {
#Gateway(requestChannel = "in.ch", replyTimeout = 45000)
AResponse process(ARequest request);
}
#Configuration
#EnableIntegration
#IntegrationComponentScan
#ComponentScan
public class IntegrationConfig {
#Bean(name = "in.ch")
public DirectChannel inCh() {
return new DirectChannel();
}
#Autowired
private MQQueueConnectionFactory mqConnectionFactory;
#Bean
public IntegrationFlow requestFlow() {
return IntegrationFlows.from("in.ch")
.handle("processor", "processARequest")
.handle(Jms.outboundGateway(mqConnectionFactory)
.requestDestination("JMS_OUT_QUEUE")
.replyDestination("JMS_IN_QUEUE"))
.handle("processor", "processAResponse")
.get();
}
}
Let me know, if it is appropriate for you or try to explian why you use two-way gateways for one one-way cases. Maybe Jms.outboundAdapter() and Jms.inboundAdapter() are more good for you?
UPDATE
How to use <header-channels-to-string> from Java DSL:
.enrichHeaders(e -> e.headerChannelsToString())

Resources