Spring Integration Connecting a Gateway to a Service Activator - spring

I've created a Gateway and a polling notificationChannel which the Gateway uses to route messages. I want a service activator to poll from the channel and do its thing. But I can't seem to grasp a few things about Spring Integration.
In this case would we need an IntegrationFlow Bean? Wouldn't calling the gateway method just send the message trough the channel and the service activator can just poll automatically when there is a new message?
ConfigurationClass:
#EnableIntegration
#Configuration
#IntegrationComponentScan
class IntegrationConfiguration {
#Bean
fun notificationChannel(): MessageChannel {
return MessageChannels.queue().get()
}
#Bean
fun integrationFlow(): IntegrationFlow {
TODO()
}
}
Gateway:
#MessagingGateway(defaultRequestChannel = "notificationChannel")
#Component
interface NotificationGateway {
fun sendNotification(bytes: ByteArray)
}
Service:
#Service
class NotificationService {
#ServiceActivator(inputChannel = "notificationChannel")
fun sendNotification(bytes: ByteArray) {
TODO()
}
}
I am new to Spring Integration and having a rough time since I can't find understandable documentation for my level of knowledge especially on Spring Integration DSL.
My main problem might be that I do now understand the use of the IntegrationFlow Bean

For a simple use-case like yours you indeed don't need an IntegrationFlow. The simple #ServiceActivator as you have now is fully enough to process messages from the notificationChannel. Only what you need is a #Poller in that #ServiceActivator configuration since your notificationChannel is a PollableChannel one and it is not subscribable one.
See Reference Manual for more info: https://docs.spring.io/spring-integration/docs/current/reference/html/#configuration-using-poller-annotation
Also pay attention to the paragraph in the beginning of the doc: https://docs.spring.io/spring-integration/docs/current/reference/html/#programming-considerations

Related

What is the recommended way to communicate between IntergrationFlow and services?

I'm looking into spring integration, more specifically the Java DSL.
The DSL is presented by the IntegrationFlows factory for the IntegrationFlowBuilder. This produces the IntegrationFlow component, which should be registered as a Spring bean (by using the #Bean annotation). The builder pattern is used to express arbitrarily complex structures as a hierarchy of methods that can accept lambdas as arguments.
In my mind then, I define a configuration class with a #Bean.
#Bean
fun ftpInboundIntegrationFlow(ftpSf: DefaultFtpSessionFactory?): IntegrationFlow {
val ftpSpecification = Ftp
.inboundStreamingAdapter(template())
.patternFilter("*.csv")
.remoteDirectory(fromFolder)
return IntegrationFlows
.from(ftpSpecification) { pc: SourcePollingChannelAdapterSpec ->
pc.poller { pm: PollerFactory ->
pm.cron(cronSyncExpression)
}
}
.transform(StreamTransformer())
.channel(doSomeBusinessLogic)
.get()
}
But I am rather confused how to, in my more specific service layer, handle this message.
I could, and it seems to work, create another #Bean in the service class.
#Bean
fun handleDoSomeBusinessLogicFlow(): IntegrationFlow {
return IntegrationFlows.from("doSomeBusinessLogic")
.handle { customers: List<SomeDomainModel>, headers: MessageHeaders ->
//Some repository and service logic
}
.get()
I tried looking at examples (https://github.com/spring-projects/spring-integration-samples/tree/833f55d662fb17edda6bcb000d952a3d00aa1dd8). But almost all of them are just creating all the logic in the same place. It feels strange to define a #Bean in the #Service class.
You are right: you can just have a business method in your service and use it from the .handle() instead of channel. You also can just use a #ServiceActivator on that method if you’d prefer loosely coupled distribution via channels. There is also an IntegrationFlowAdapter to have a service incapsulation, but still gain from flow definition.

Can Spring Boot be configured to act as both client and server (mesh topology) with RabbitMQ RPC?

I have 2 apps developed with spring boot:
App1:
exposes an API for app2, for example /api/members
makes a call to app2 in order to retrieve weather details, for example /api/weather
App2:
exposes an API for app1, /api/weather
makes a call to app1 in order to retrieve member details, /api/members
The communication between them is made using HTTP at the moment. Is there a way to configure rabbitMQ inside spring boot to act as a consumer on an exchange, and a producer on another one? Would this be ok from the architectural POV? I could not find any articles related to this. Any links/ examples would be greatly appreciated.
I figured this one out:
This is the RabbitMQ config file:
#Configuration
class RabbitCfg {
#Bean
fun queue() = Queue("queueName")
#Bean
fun exchange() = DirectExchange("exc")
#Bean
fun binding(exchange: DirectExchange, queue: Queue) = BindingBuilder.bind(queue).to(exchange).with("routeKey")
#Bean
fun jsonMessageConverter() = Jackson2JsonMessageConverter(ObjectMapper())
}
Afterwards I am able to call from app1:
val response = rabbitTemplate.convertSendAndReceive(exchange.name, "otherRouteKey", req) as MyObj
and handle requests from app2 in the same spring boot project:
#Service
#RabbitListener(queues = ["queueName"])
class Receiver {
#RabbitHandler
fun handleMenuMessage(obj: MyObj) = OtherObj()
...
}
The only condition required is that both apps are configured on the same exchange, with different "routeKey" values.

Proper ultimate way to migrate JMS event listening to Spring Integration with Spring Boot

I got a JmsConfig configuration class that handles JMS events from a topic in the following way:
It defines a #Bean ConnectionFactory, containing an ActiveMQ implementation
It defines a #Bean JmsListenerContainerFactory instantiating a DefaultJmsListenerContainerFactory and passing it through Boot's DefaultJmsListenerContainerFactoryConfigurer
It defines a #Bean MessageConverter containing a MappingJackson2MessageConverter and setting a custom ObjectMapper
I use #JmsListener annotation pointing to myfactory on a method of my service. This is the only use I have for the topic, subscription alone.
Now I want to move to Spring Integration. After reading a lot, and provided I don't need a bidirectional use (discarding Gateways) neither a polling mechanism (discarding #InboundChannelAdapter), I am going for a message-driven-channel-adapter, in traditional XML configuration wording. I found that Java idiom should be accomplished by means of the new Spring Integration DSL library, and thus, I look for the proper snippet.
It seems JmsMessageDrivenChannelAdapter is the proper equivalent, and I found a way:
IntegrationFlows.from(Jms.messageDriverChannelAdapter(...))
But the problem is that this only accepts the ActiveMQ ConnectionFactory or an AbstractMessageListenerContainer, but no my boot pre-configured JmsListenerContainerFactory !
How should this be implemented in an ultimate way?
JmsListenerContainerFactory is specific for the #JmsListener, it's a higher level abstraction used to configure a DefaultMessageListenerContainer. Boot does not provide an auto configuration option for a raw DefaultMessageListenerContainer; you have to wire it up yourself. But you can still use the Boot properties...
#Bean
public IntegrationFlow flow(ConnectionFactory connectionFactory,
JmsProperties properties) {
return IntegrationFlows.from(Jms.messageDrivenChannelAdapter(container(connectionFactory, properties)))
...
.get();
}
private DefaultMessageListenerContainer container(ConnectionFactory connectionFactory,
JmsProperties properties) {
DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
container.setConcurrentConsumers(properties.getListener().getConcurrency());
container.setMaxConcurrentConsumers(properties.getListener().getMaxConcurrency());
...
return container;
}
There is even a better approach. I am surprised Gary did not comment it.
There's an out-of-the-box builder called Jms.container(...).
#Bean
public IntegrationFlow jmsMyServiceMsgInboundFlow(ConnectionFactory connectionFactory, MessageConverter jmsMessageConverter, MyService myService, JmsProperties jmsProperties, #Value("${mycompany.jms.destination.my-topic}") String topicDestination){
JmsProperties.Listener jmsInProps = jmsProperties.getListener();
return IntegrationFlows.from(
Jms.messageDrivenChannelAdapter( Jms.container(connectionFactory, topicDestination)
.pubSubDomain(false)
.sessionAcknowledgeMode(jmsInProps .getAcknowledgeMode().getMode())
.maxMessagesPerTask(1)
.errorHandler(e -> e.printStackTrace())
.cacheLevel(0)
.concurrency(jmsInProps.formatConcurrency())
.taskExecutor(Executors.newCachedThreadPool())
.get()))
)
.extractPayload(true)
.jmsMessageConverter(jmsMessageConverter)
.destination(topicDestination)
.autoStartup(true)
//.errorChannel("NOPE")
)
.log(LoggingHandler.Level.DEBUG)
.log()
.handle(myService, "myMethod", e -> e.async(true).advice(retryAdvice()))
.get();

Spring 4.1 #JmsListener configuration

I would like to use the new annotations and features provided in Spring 4.1 for an application that needs a JMS listener.
I've carefully read the notes in the Spring 4.1 JMS improvements post but I continue to miss the relationship between #JmsListener and maybe the DestinationResolver and how I would setup the application to indicate the proper Destination or Endpoint.
Here is the suggested use of #JmsListener
#Component
public class MyService {
#JmsListener(containerFactory = "myContainerFactory", destination = "myQueue")
public void processOrder(String data) { ... }
}
Now, I can't use this in my actual code because the "myQueue" needs to be read from a configuration file using Environment.getProperty().
I can setup an appropriate myContainerFactory with a DestinationResolver but mostly, it seems you would just use DynamicDestinationResolver if you don't need JNDI to lookup a queue in an app server and didn't need to do some custom reply logic. I'm simply trying to understand how Spring wants me to indicate the name of the queue in a parameterized fashion using the #JmsListener annotation.
Further down the blog post, I find a reference to this Configurer:
#Configuration
#EnableJms
public class AppConfig implements JmsListenerConfigurer {
#Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
registrar.setDefaultContainerFactory(defaultContainerFactory());
SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
endpoint.setDestination("anotherQueue");
endpoint.setMessageListener(message -> {
// processing
});
registrar.registerEndpoint(endpoint);
}
Now, this makes some amount of sense and I could see where this would allow me to set a Destination at runtime from some external string, but this seems to be in conflict with using #JmsListener as it appears to be overriding the annotation in favor of endpoint.setMessageListener in the code above.
Any tips on how to specify the appropriate queue name using #JmsListener?
Also note that depending on use case you can already parameterize using properties file per environment and PropertySourcesPlaceholderConfigurer
#JmsListener(destinations = "${some.key}")
As per https://jira.spring.io/browse/SPR-12289
In case people are using #JmsListener with spring boot, you do not have to configure PropertySourcesPlaceholderConfigurer. It work's out the box
Sample:
class
#JmsListener(destination = "${spring.activemq.queue.name}")
public void receiveEntityMessage(final TextMessage message) {
// process stuff
}
}
application.properties
spring.activemq.queue.name=some.weird.queue.name.that.does.not.exist
Spring boot output
[26-Aug;15:07:53.475]-[INFO ]-[,]-[DefaultMes]-[o.s.j.l.DefaultMessageListenerContainer ]-[931 ]-Successfully refreshed JMS Connection
[26-Aug;15:07:58.589]-[WARN ]-[,]-[DefaultMes]-[o.s.j.l.DefaultMessageListenerContainer ]-[880 ]-Setup of JMS message listener invoker failed for destination 'some.weird.queue.name.that.does.not.exist' - trying to recover. Cause: User user is not authorized to read from some.weird.queue.name.that.does.not.exist
[26-Aug;15:07:59.787]-[INFO ]-[,]-[DefaultMes]-[o.s.j.l.DefaultMessageListenerContainer ]-[931 ]-Successfully refreshed JMS Connection
[26-Aug;15:08:04.881]-[WARN ]-[,]-[DefaultMes]-[o.s.j.l.DefaultMessageListenerContainer ]-[880 ]-Setup of JMS message listener invoker failed for destination 'some.weird.queue.name.that.does.not.exist' - trying to recover. Cause: User user is not authorized to read from some.weird.queue.name.that.does.not.exist
This proves that #JmsListener is able to pickup property values from application.properties without actually setting up any explicit PropertySourcesPlaceholderConfigurer
I hope this helps!
You could eventually do that right now but it's a bit convoluted. You can set a custom JmsListenerEndpointRegistry using JmsListenerConfigurer
#Configuration
#EnableJms
public class AppConfig implements JmsListenerConfigurer {
#Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
registrar.setEndpointRegistry(customRegistry());
}
}
and then override the registerListenerContainer method, something like
public void registerListenerContainer(JmsListenerEndpoint endpoint, JmsListenerContainerFactory<?> factory) {
// resolve destination according to whatever -> resolvedDestination
((AbstractJmsListenerEndpoint)endpoint).setDestination(resolvedDestination);
super.registerListenerContainer(endpoint, factory);
}
But we could do better. Please watch/vote for SPR-12280

Spring Integration Java DSL - How to call a ServiceActivator method with retry advice

I have a Component class with a ServiceActivator method:
#Component("payloadService")
public class PayloadService {
#Transactional
#ServiceActivator
#Description("Pre-check service")
public Message<String> preCheck(Message<String> message) {
...
}
}
I have a Spring Integration 4 Java DSL flow which calls the ServiceActivator's preCheck method like so:
IntegrationFlows.from("input.ch")
.handle("payloadService", "preCheck")
...
.get();
I am now trying add a retry advice to the service call (as shown here http://docs.spring.io/spring-integration/reference/htmlsingle/#retry-config) but I would like to do this in Java DSL form as documented in https://github.com/spring-projects/spring-integration-extensions/wiki/Spring-Integration-Java-DSL-Reference#dsl-and-endpoint-configuration.
However I can't quite figure out how to apply this advice in practice to my flow in DSL form. Probably struggling because I am not yet too familiar with lambdas, etc.
Could somebody give me some pointers on how to do this?
Thanks in advance,
PM
Like this:
....
IntegrationFlows.from("input.ch")
.handle("payloadService", "preCheck", e -> e.advice(retryAdvice()))
...
.get();
....
#Bean
public Advice retryAdvice() {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
...
return advice;
}
From other side you can try the new annotation stuff from Spring Retry project:
#Configuration
#EnableIntegration
#EnableRetry
....
#Transactional
#ServiceActivator
#Retryable
#Description("Pre-check service")
public Message<String> preCheck(Message<String> message) {

Resources