Control Azure Service Bus Message Listener to start or stop listening from the Topic or queue in spring boot - spring-boot

What I want to Achieve - Azure Service Bus Message Listener to start / stop receiving messages from queue/topic.
Below is a detailed explanation.
Currently I have integrated Azure Service Bus in my application and we listen message as soon as spring boot application starts. Now I want to modify this logic. By default Azure Service Bus Message Listener will be disable. On ApplicationReadyEvent I want to perform some task and after that again I want to enable Azure Service Bus Message Listener to start listening from topic or queue.
So how can I achieve that ?
application.yml
spring:
cloud:
azure:
servicebus:
namespace: **********
xxx:
azure:
servicebus:
connection: ***********
queue: **********
AzureConfiguration.java
import com.azure.spring.integration.servicebus.inbound.ServiceBusInboundChannelAdapter;
import com.azure.spring.messaging.servicebus.core.ServiceBusProcessorFactory;
import com.azure.spring.messaging.servicebus.core.listener.ServiceBusMessageListenerContainer;
import com.azure.spring.messaging.servicebus.core.properties.ServiceBusContainerProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.messaging.MessageChannel;
#Configuration
public class AzureConfiguration{
#Value("${xxx.azure.servicebus.connection}")
private String serviceBusConnection;
#Value("${xxx.azure.servicebus.queue}")
private String serviceBusQueue;
private static final String SERVICE_BUS_INPUT_CHANNEL = "yyyyy";
private static final String SENSOR_DATA_CHANNEL = "zzzzz";
private static final String SERVICE_BUS_LISTENER_CONTAINER = "aaaaa";
#Bean(name = SERVICE_BUS_LISTENER_CONTAINER)
public ServiceBusMessageListenerContainer serviceBusMessageListenerContainer(ServiceBusProcessorFactory processorFactory) {
ServiceBusContainerProperties containerProperties = new ServiceBusContainerProperties();
containerProperties.setConnectionString(serviceBusConnection);
containerProperties.setEntityName(serviceBusQueue);
containerProperties.setAutoComplete(true);
return new ServiceBusMessageListenerContainer(processorFactory, containerProperties);
}
#Bean
public ServiceBusInboundChannelAdapter serviceBusInboundChannelAdapter(
#Qualifier(SERVICE_BUS_INPUT_CHANNEL) MessageChannel inputChannel,
#Qualifier(SERVICE_BUS_LISTENER_CONTAINER) ServiceBusMessageListenerContainer listenerContainer) {
ServiceBusInboundChannelAdapter adapter = new ServiceBusInboundChannelAdapter(listenerContainer);
adapter.setOutputChannel(inputChannel);
return adapter;
}
#Bean(name = SERVICE_BUS_INPUT_CHANNEL)
public MessageChannel serviceBusInputChannel() {
return new DirectChannel();
}
#Bean(name = SENSOR_DATA_CHANNEL)
public MessageChannel sensorDataChannel() {
return new DirectChannel();
}
#Bean
public IntegrationFlow serviceBusMessageFlow() {
return IntegrationFlows.from(SERVICE_BUS_INPUT_CHANNEL)
.<byte[], String>transform(String::new)
.channel(SENSOR_DATA_CHANNEL)
.get();
}
}
AppEventListenerService.java
import com.azure.spring.integration.servicebus.inbound.ServiceBusInboundChannelAdapter;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import java.util.List;
#Slf4j
#Service
#AllArgsConstructor
public class AppEventListenerService{
#EventListener(ApplicationReadyEvent.class)
public void OnApplicationStarted() {
log.debug("Enter OnApplicationStarted");
// By Default Azure Service Bus Message Listener will be disable
// do some task
// Enable Azure Bus Message Listener
log.debug("Exit OnApplicationStarted");
}
}
In above code in AppEventListenerService.java ,
// Enable Azure Bus Message Listener - Here I want to start ServiceBusConsumer to receive message from topic/queue.

Literally, if you just want to stop the listener and then start it on ApplicationReadyEvent, then you can autowire the ServiceBusInboundChannelAdapter(or ServiceBusMessageListenerContainer) in your AppEventListenerService.java and then simply call the its stop() and start() API in the AppEventListenerService#OnApplicationStarted method.
However, both the ServiceBusMessageListenerContainer and ServiceBusInboundChannelAdapter implements SmartLifecycle interface and is enabled auto-start-up by default. So if you use the above solution, the listener (as well as adapter) has been triggered to start before ApplicationReadyEvent, which means there will still be a period that the listener is consuming messages.
So I assume you may want to turn off the listener till your own business logic has been done. If so, then currently ServiceBusMessageListenerContainer doesn't provide the function to disable auto-start-up, and we will put your feature request to our backlog.
But you could still use the below workarounds to meet your request.
Workaround-1
You can extend the ServiceBusMessageListenerContainer to override the auto-start-up behavior,
public class CustomServiceBusMessageListenerContainer extends ServiceBusMessageListenerContainer {
private boolean autoStartUp = true;
/**
* Create an instance using the supplied processor factory and container properties.
* #param processorFactory the processor factory.
* #param containerProperties the container properties.
*/
public CustomServiceBusMessageListenerContainer(ServiceBusProcessorFactory processorFactory, ServiceBusContainerProperties containerProperties) {
super(processorFactory, containerProperties);
}
public void setAutoStartUp(boolean autoStartUp) {
this.autoStartUp = autoStartUp;
}
#Override
public final boolean isAutoStartup() {
return this.autoStartUp;
}
}
When declaring the ServiceBusMessageListenerContainer and ServiceBusInboundChannelAdapter bean, disable their auto-start-up function.
#Bean(SERVICE_BUS_LISTENER_CONTAINER)
public ServiceBusMessageListenerContainer messageListenerContainer(ServiceBusProcessorFactory processorFactory) {
ServiceBusContainerProperties containerProperties = new ServiceBusContainerProperties();
containerProperties.setEntityName(QUEUE_NAME);
...
CustomServiceBusMessageListenerContainer listenerContainer = new CustomServiceBusMessageListenerContainer(processorFactory, containerProperties);
listenerContainer.setAutoStartUp(false);
return listenerContainer;
}
#Bean
public ServiceBusInboundChannelAdapter queueMessageChannelAdapter(
#Qualifier(SERVICE_BUS_INPUT_CHANNEL) MessageChannel inputChannel,
#Qualifier(SERVICE_BUS_LISTENER_CONTAINER) ServiceBusMessageListenerContainer listenerContainer) {
ServiceBusInboundChannelAdapter adapter = new ServiceBusInboundChannelAdapter(listenerContainer);
adapter.setOutputChannel(inputChannel);
adapter.setAutoStartup(false);
return adapter;
}
Start the ServiceBusInboundChannelAdapter after your business logic in AppEventListenerService#OnApplicationStarted.
Workaround-2
This might be a bit hack, since we don't expose the api to disable auto-start-up in ServiceBusMessageListenerContainer, but it can be done in ServiceBusInboundChannelAdapter. So you can choose to not declare a bean of ServiceBusMessageListenerContainer but change it as a local variable for the adapter,
#Bean
public ServiceBusInboundChannelAdapter queueMessageChannelAdapter(
#Qualifier(SERVICE_BUS_INPUT_CHANNEL) MessageChannel inputChannel, ServiceBusProcessorFactory processorFactory) {
ServiceBusContainerProperties containerProperties = new ServiceBusContainerProperties();
containerProperties.setEntityName(QUEUE_NAME);
...
ServiceBusMessageListenerContainer listenerContainer = new ServiceBusMessageListenerContainer(processorFactory, containerProperties);
ServiceBusInboundChannelAdapter adapter = new ServiceBusInboundChannelAdapter(listenerContainer);
adapter.setOutputChannel(inputChannel);
adapter.setAutoStartup(false);
return adapter;
}
then start the ServiceBusInboundChannelAdapter after your business logic in AppEventListenerService#OnApplicationStarted.

Here I have a work around where we use the JMS to consume the service bus message
The reason to use JMS is that when we use the #JMSListener we can start stop it.
Now to Implement JMS with ServiceBus refer this MSDOC
Now you have to Autowired this JmsListenerEndpointRegistry object and stop the listener.
#Autowired
JmsListenerEndpointRegistry registry;
To stop the JMS you will have to use the stop function:
registry.stop();
Here I have create two Api which will start/Stop the JMS and receiver of messages:
#Component
#RestController
public class Reciever {
#Autowired
JmsListenerEndpointRegistry registry;
#GetMapping("/stop")
public String readBlobFile ()
{
registry.stop();
return "Stopped" ;
}
#GetMapping("/start")
public String readBlobFile1 ()
{
registry.start();
return "StARTED" ;
}
private static final String QUEUE_NAME = "test";
private final Logger logger = LoggerFactory.getLogger(Reciever.class);
#JmsListener(destination = QUEUE_NAME, containerFactory = "jmsListenerContainerFactory")
public void receiveMessage(String s) {
logger.info("Received message: {}", s);
}
}
Now First I call the /stop Api which will stop the JMS and the message will only start coming once the /startapi is called .
output:

As you're using an integration flow registered as a bean the simplest way to start/stop it is to autowire it as StandardIntegrationFlow and call the corresponding method like so:
#Slf4j
#Service
#DependsOn({"serviceBusMessageFlow"})
#RequiredArgsConstructor
public class AppEventListenerService {
private final StandardIntegrationFlow serviceBusMessageFlow;
#EventListener(ApplicationReadyEvent.class)
public void OnApplicationStarted() {
log.debug("Enter OnApplicationStarted");
// Disable Azure Bus Message Listener
serviceBusMessageFlow.stop();
// do some task
// Enable Azure Bus Message Listener
serviceBusMessageFlow.start();
log.debug("Exit OnApplicationStarted");
}
}
Note the #DependsOn annotation might be needed to force the flow bean to be initialized before the event listener bean.
Also, it should be noted that some messages might happen to go through after the flow is initialized and before the listener is triggered.

Related

Read messages from different AWS account using #SqsListener

I have an SQS standard queue that is provided by a third party vendor who has given access to our IAM user to read messages from there. So the AWS account ID for the queue is different than the one of my user.
I'm trying to use spring's #SqsListener annotation to consume these messages but I'm having trouble specifying the accountId that should be consumed from.
My bean configuration for the client looks like this:
#Bean
fun amazonSQSAsyncClient(): AmazonSQSAsync = AmazonSQSAsyncClientBuilder.standard()
.withCredentials(AWSStaticCredentialsProvider(BasicAWSCredentials(awsProperties.accessKey, awsProperties.secretKey)))
.withEndpointConfiguration(AwsClientBuilder.EndpointConfiguration(awsProperties.url, awsProperties.region))
.build()
I see no way of specifying the account Id in the credentials, and I also could not find any properties that can be used to define an accountId.
I tried setting the awsProperties.url shown above to something like https://sqs.us-east-1.amazonaws.com/<accountId> but this does not seem to be working. It is still trying to look for the queue in my own account Id and throwing a queue not found error.
Any ideas how to fix this and force the Spring AWS bean to consume from a specific AwsAccount?
You have a user that can access the queu in another account. That means you can run code with that user in your account and that can access the queue on another account.
Initializing a sqsclient will always use the account it is running on
You don't have to adjust this.
#Bean
fun amazonSQSAsyncClient(): AmazonSQSAsync = AmazonSQSAsyncClientBuilder.standard()
.withCredentials(AWSStaticCredentialsProvider(BasicAWSCredentials(awsProperties.accessKey, awsProperties.secretKey)))
.build()
You need to make sure the code can access the queue.
In the code you should set your queue URL like this:
https://sqs.<region>.amazonaws.com/<account>/<queuename>
, I quickly tried to access a queue from another account. If the permissions on the queue are correctly set, you have two possibilities. The first one is using the queue URL instead of the name (I checked, it works). The second one is creating you own DestinationResolver and providing it to the SimpleMessageListenerContainer. I created a small app with Spring Boot and it worked well. I pasted you the code below.
In a next feature release I'll figure out a better way to support this use case.
package demo;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.GetQueueUrlRequest;
import com.amazonaws.services.sqs.model.GetQueueUrlResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.aws.core.env.ResourceIdResolver;
import org.springframework.cloud.aws.messaging.config.SimpleMessageListenerContainerFactory;
import org.springframework.cloud.aws.messaging.support.destination.DynamicQueueUrlDestinationResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.util.Assert;
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public MessageListener messageListener() {
return new MessageListener();
}
#Bean
public SimpleMessageListenerContainerFactory simpleMessageListenerFactory(AmazonSQS amazonSqs, ResourceIdResolver resourceIdResolver) {
SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
factory.setDestinationResolver(new DynamicAccountAwareQueueUrlDestinationResolver(amazonSqs, resourceIdResolver));
return factory;
}
public static class DynamicAccountAwareQueueUrlDestinationResolver implements DestinationResolver<String> {
public static final String ACCOUNT_QUEUE_SEPARATOR = ":";
private final AmazonSQS amazonSqs;
private final DynamicQueueUrlDestinationResolver dynamicQueueUrlDestinationResolverDelegate;
public DynamicAccountAwareQueueUrlDestinationResolver(AmazonSQS amazonSqs, ResourceIdResolver resourceIdResolver) {
Assert.notNull(amazonSqs, "amazonSqs must not be null");
this.amazonSqs = amazonSqs;
this.dynamicQueueUrlDestinationResolverDelegate = new DynamicQueueUrlDestinationResolver(amazonSqs, resourceIdResolver);
}
#Override
public String resolveDestination(String queue) throws DestinationResolutionException {
if (queue.contains(ACCOUNT_QUEUE_SEPARATOR)) {
String account = queue.substring(0, queue.indexOf(ACCOUNT_QUEUE_SEPARATOR));
String queueName = queue.substring(queue.indexOf(ACCOUNT_QUEUE_SEPARATOR) + 1);
GetQueueUrlResult queueUrlResult = this.amazonSqs.getQueueUrl(new GetQueueUrlRequest()
.withQueueName(queueName)
.withQueueOwnerAWSAccountId(account));
return queueUrlResult.getQueueUrl();
} else {
return this.dynamicQueueUrlDestinationResolverDelegate.resolveDestination(queue);
}
}
}
public static class MessageListener {
private static Logger LOG = LoggerFactory.getLogger(MessageListener.class);
#MessageMapping("633332177961:queue-name")
public void listen(String message) {
LOG.info("Received message: {}", message);
}
}
}

Azure Service Bus - Spring Boot disable autostart (com.microsoft.azure : azure-servicebus-spring-boot-starter)

I have the following implementation to consume the message from Azure Service Bus using Spring Boot application however I want to be able to control the ServiceBusConsumer from automatically start listening to the Topic using Spring boot profile property
something like this in the application.yaml
servicebus.consumer.enable=false
it should disable the ServiceBusConsumer from listening to the Topic(s) as well as I should be able to start the ServiceBusConsumer using a REST API - eg: ./api/servicebus/consumer/start?
import com.microsoft.azure.servicebus.ExceptionPhase;
import com.microsoft.azure.servicebus.IMessage;
import com.microsoft.azure.servicebus.IMessageHandler;
import com.microsoft.azure.servicebus.ISubscriptionClient;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
#Log4j2
#Component
class ServiceBusConsumer implements Ordered {
private final ISubscriptionClient iSubscriptionClient;
ServiceBusConsumer(ISubscriptionClient isc) {
this.iSubscriptionClient = isc;
}
#EventListener(ApplicationReadyEvent.class)
public void consume() throws Exception {
this.iSubscriptionClient.registerMessageHandler(new IMessageHandler() {
#Override
public CompletableFuture<Void> onMessageAsync(IMessage message) {
log.info("received message " + new String(message.getBody()) + " with body ID " + message.getMessageId());
return CompletableFuture.completedFuture(null);
}
#Override
public void notifyException(Throwable exception, ExceptionPhase phase) {
log.error("eeks!", exception);
}
});
}
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
You can create the ServiceBusConsumer bean conditionally by adding the #ConditionalOnProperty annotation like so, to make sure the bean is created only when servicebus.consumer.enabled=true:
#Log4j2
#Component
#ConditionalOnProperty(prefix = "servicebus.consumer", name = "enabled")
class ServiceBusConsumer implements Ordered {
...
}

Retry mechanism for producer's client of ActiveMQ with JMS and spring

Is there a mechanism or example implementation of a retry mechanism/solution for a producer using ActiveMQ with JMS (more precisely, with JmsTemplate) and spring framework ?
My use case, which I want to handle is, when the broker is not available, for example, I want to make some number of retries, maximum 6 (if possible with exponential delays between each). So, I need also to track the number of retries for a message between each attempt.
I am aware the the redelivery policy for the consumer, but also I want to implement a reliable producer's client side as well
Thanks,
Simeon
i think that the easiest way is to use what exists for this by using an embedded broker with persistence enabled which must be used by the producer to send the messages to and by creating a Camel route to read from local Queue and forward to the remote one or by using a JmsBridgeConnector or NetworkConnector nut i think the JmsBridgeConnector is easier.
here is an Spring code example :
producer have to use jmsConnectionFactory() to create a ConnectionFactory
package com.example.amq;
import java.io.File;
import javax.jms.ConnectionFactory;
import javax.jms.QueueConnectionFactory;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.network.jms.JmsConnector;
import org.apache.activemq.network.jms.OutboundQueueBridge;
import org.apache.activemq.network.jms.ReconnectionPolicy;
import org.apache.activemq.network.jms.SimpleJmsQueueConnector;
import org.apache.activemq.store.PersistenceAdapter;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class ActiveMQConfiguration {
public static final String DESTINATION_NAME = "localQ";
#Bean // (initMethod = "start", destroyMethod = "stop")
public BrokerService broker() throws Exception {
final BrokerService broker = new BrokerService();
broker.addConnector("vm://localhost");
SimpleJmsQueueConnector simpleJmsQueueConnector = new SimpleJmsQueueConnector();
OutboundQueueBridge bridge = new OutboundQueueBridge();
bridge.setLocalQueueName(DESTINATION_NAME);
bridge.setOutboundQueueName("remoteQ");
OutboundQueueBridge[] outboundQueueBridges = new OutboundQueueBridge[] { bridge };
simpleJmsQueueConnector.getReconnectionPolicy().setMaxSendRetries(ReconnectionPolicy.INFINITE);
simpleJmsQueueConnector.setOutboundQueueBridges(outboundQueueBridges);
simpleJmsQueueConnector.setLocalQueueConnectionFactory((QueueConnectionFactory) jmsConnectionFactory());
simpleJmsQueueConnector.setOutboundQueueConnectionFactory(outboundQueueConnectionFactory());
JmsConnector[] jmsConnectors = new JmsConnector[] { simpleJmsQueueConnector };
broker.setJmsBridgeConnectors(jmsConnectors);
PersistenceAdapter persistenceAdapter = new KahaDBPersistenceAdapter();
File dir = new File(System.getProperty("user.home") + File.separator + "kaha");
if (!dir.exists()) {
dir.mkdirs();
}
persistenceAdapter.setDirectory(dir);
broker.setPersistenceAdapter(persistenceAdapter);
broker.setPersistent(true);
broker.setUseShutdownHook(false);
broker.setUseJmx(true);
return broker;
}
#Bean
public QueueConnectionFactory outboundQueueConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
"auto://localhost:5671");
connectionFactory.setUserName("admin");
connectionFactory.setPassword("admin");
return connectionFactory;
}
#Bean
public ConnectionFactory jmsConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost");
connectionFactory.setObjectMessageSerializationDefered(true);
connectionFactory.setCopyMessageOnSend(false);
return connectionFactory;
}
}
By using Camel :
import org.apache.activemq.camel.component.ActiveMQComponent;
import org.apache.activemq.camel.component.ActiveMQConfiguration;
import org.apache.camel.CamelContext;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
public class ActiveMQCamelBridge {
public static void main(String args[]) throws Exception {
CamelContext context = new DefaultCamelContext();
context.addComponent("inboundQueue", ActiveMQComponent.activeMQComponent("tcp://localhost:61616"));
ActiveMQComponent answer = ActiveMQComponent.activeMQComponent("tcp://localhost:5671");
if (answer.getConfiguration() instanceof ActiveMQConfiguration) {
((ActiveMQConfiguration) answer.getConfiguration()).setUserName("admin");
((ActiveMQConfiguration) answer.getConfiguration()).setPassword("admin");
}
context.addComponent("outboundQueue", answer);
context.addRoutes(new RouteBuilder() {
public void configure() {
from("inboundQueue:queue:localQ").to("outboundQueue:queue:remoteQ");
}
});
context.start();
Thread.sleep(60 * 5 * 1000);
context.stop();
}
}
Producer does not provide any kind of retry mechanism like consumer. You need to make sure in your code that message sent by producer acknowledge by broker.

How can I use ActiveMQ and Spring Integration JMS to receive messages one time in multiple groups?

I'm trying to use a messaging service to send messages to other services when a user in our system is created. The services receiving the messages are load balanced Spring Boot applications (in our case, pods in a Kubernetes environment).
I would like:
Every pod in each service is configured to receive messages.
One (and only one) pod in each service actually receives each message.
The message can be received by multiple services in this way.
So in the picture, the "New User" message is received by one pod in Notification Service and one pod in Logging Service.
I've set up ActiveMQ and Spring Integration to send/receive the messages and have got it working both as a) a Queue (one recipient) and b) a Topic (whoever's subscribed receives it). Problem is:
a) With a queue, one recipient means Notifications would receive it but Logging wouldn't (or vice versa).
b) With a topic, whoever's subscribed means all six pods would receive it.
I feel like what I want is a grouping, like "one recipient of type NOTIFICATION and one recipient of type LOGGING", but I'm not sure how to implement that. It seems like that's possible with a Message Routing pattern, but I was wondering if it could be achieved entirely using the Spring Integration.
Some code. The configuration for the sender:
import org.apache.activemq.spring.ActiveMQConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;
#Configuration
public class MessagingConfig {
#Bean
public ActiveMQConnectionFactory connectionFactory(){
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setBrokerURL("tcp://myactivemq:61616");
return connectionFactory;
}
#Bean
public JmsTemplate jmsTemplate(){
JmsTemplate template = new JmsTemplate();
template.setConnectionFactory(connectionFactory());
template.setDefaultDestinationName("user-dest");
return template;
}
}
The service that sends the message (simplified):
#Service
public class MessageSender {
private final JmsTemplate jmsTemplate;
#Autowired
public MessageSender(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void sendMessage(String userId) {
jmsTemplate.send(new MessageCreator() {
#Override
public Message createMessage(Session session) throws JMSException{
return session.createTextMessage("NEW USER:" + userId);
}
});
}
}
Configuration of receiver:
import org.apache.activemq.spring.ActiveMQConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
#Configuration
#EnableJms
public class ReceiverConfig {
private ActiveMQConnectionFactory getConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setBrokerURL("tcp://myactivemq:61616");
return connectionFactory;
}
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(getConnectionFactory());
factory.setConcurrency("1-1");
return factory;
}
}
And the service receiving the messages.
import javax.jms.JMSException;
import javax.jms.TextMessage;
import org.springframework.jms.annotation.JmsListener;
#Service
public class MessageReceiver {
#JmsListener(destination = "user-dest")
public void receiveMessage(final TextMessage message) throws JMSException {
// Do something with message.getText()
}
}
This is working, but acts like a Queue, only one recipient. Any idea how this could be received by one Notification Service pod and one Logging Service pod?
FYI. I ended up using RabbitMQ, possibly because I found the documentation much easier. With RabbitMQ, the solution was pretty straightforward:
Send the message to an exchange
Create a queue and bind the queue to the exchange
I used the Management UI, but the binding can be in code too.
From my example above, I created two queues, one for Notification and one for Logging.
The Notification and Logging services listen to their respective queues.
So the User Service sends a message to its Exchange, both queues pick it up, and only one pod of each listening service gets the message. Exactly what I was looking for.

How to set up a Spring Integration 4.3 Email Sending flow with a java class instead of XML?

I am trying to add Spring Integration to a REST MVC Spring app I have been writing. I am using the latest Spring 4.2.x for core, integration and mvc. The idea is to create separate application contexts as on the Dynamic FTP example. The reason why is because I can send emails from 2 separated accounts as well as listen from 2 separated accounts hence having separate application contexts as well as environment variables to aid on bean creation for each context helps a bunch.
I apologize for the newbie questions, but I am having a hard time with the manual as well as trying to figure out how to setup SMTP email configuration class without XML.
I want to have both receive and send integration channels. All email settings will be configured from enviroment variables so I have injected the enviroment: #Autowired Environment env;
I can define:
A MailSender bean
A MailSendingMessageHandler bean
A MessageChannel for the SMTP (outbound)
Now, on XML configurations you have an outbound-channel-adapter where you wire the mail-sender bean as well as the MessageChannel
My goal is to have configurations for:
Send emails.
Listen to IMAP emails and process them.
For sending emails, the idea is to get from a rest endpoint, calling a service and that service is what will put a message to Integration SMTP outbound channel to send an email. Looks like, by using the MailSendingMessageHandler it will get the Integration Message and convert to a Mail Message for the MailSender. I have no idea on how to wire the MailSendingMessageHandler to the outbound channel so that an email can be send. Also I do not know how to, from my #Service class that is called by the rest endpoint how to create the messages and send them through the outbound SMTP channel so emails can be send. On one rest call I send all email recipients I want to reach. Before, each email message body is properly formatted so that I can create each Integration Message (as an email) that will be handled and converted by MailSendingMessageHandler. I have tried to find examples online without success on how to accomplish this.
Any examples you could redirect me? Thanks in advance!
So far I have for the configuration:
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.mail.MailReceiver;
import org.springframework.integration.mail.MailReceivingMessageSource;
import org.springframework.integration.mail.MailSendingMessageHandler;
import org.springframework.mail.MailMessage;
import org.springframework.mail.MailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.core.env.Environment;
#Configuration
#EnableIntegration
public class IntegrationEmailConfig {
#Autowired
Environment env;
#Bean
public static PropertySourcesPlaceholderConfigurer pspc() {
return new PropertySourcesPlaceholderConfigurer();
}
#Bean
#InboundChannelAdapter(value = "emailInboundChannel", poller = #Poller(fixedDelay = "5000") )
public MailReceivingMessageSource mailMessageSource(MailReceiver imapMailReceiver) {
return new MailReceivingMessageSource(imapMailReceiver);
}
private Properties additionalMailProperties() {
Properties properties = new Properties();
if (env.containsProperty("mail.smtp.auth")) {
properties.setProperty("mail.smtp.auth",env.getProperty("mail.smtp.auth"));
}
if (env.containsProperty("mail.smtp.starttls.enable")) {
properties.setProperty("mail.smtp.starttls.enable",env.getProperty("mail.smtp.starttls.enable"));
}
return properties;
}
#Bean
public MailSender mailSender() throws Exception {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
if (env.containsProperty("mail.server.host")) {
mailSender.setHost(env.getProperty("mail.server.host"));
} else {
throw new Exception("Missing mail.server.host property");
}
if (env.containsProperty("mail.server.port")) {
mailSender.setPort(Integer.parseInt(env.getProperty("mail.server.port")));
} else {
throw new Exception("Missing mail.server.port property");
}
if (env.containsProperty("mail.server.username")) {
mailSender.setUsername(env.getProperty("mail.server.username"));
} else {
throw new Exception("Missing mail.server.username property");
}
if (env.containsProperty("mail.server.password")) {
mailSender.setPassword(env.getProperty("mail.server.password"));
} else {
throw new Exception("Missing mail.server.password property");
}
mailSender.setJavaMailProperties(additionalMailProperties());
return mailSender;
}
#Bean
public MailSendingMessageHandler mailSendingMessageHandler() throws Exception {
MailSendingMessageHandler mailSendingMessageHandler = new MailSendingMessageHandler(mailSender());
//mailSendingMessageHandler.setChannelResolver(channelResolver);
return mailSendingMessageHandler;
}
/* #Bean
public DirectChannel outboundMail() {
DirectChannel outboundChannel = new DirectChannel();
return outboundChannel;
}
*/
#Bean
public MessageChannel smtpChannel() {
return new DirectChannel();
}
/* #Bean
#Value("${imap.url}")
public MailReceiver imapMailReceiver(String imapUrl) {
// ImapMailReceiver imapMailReceiver = new ImapMailReceiver(imapUrl);
// imapMailReceiver.setShouldMarkMessagesAsRead(true);
// imapMailReceiver.setShouldDeleteMessages(false);
// // other setters here
// return imapMailReceiver;
MailReceiver receiver = mock(MailReceiver.class);
MailMessage message = mock(Message.class);
when(message.toString()).thenReturn("Message from " + imapUrl);
Message[] messages = new Message[] {message};
try {
when(receiver.receive()).thenReturn(messages);
}
catch (MessagingException e) {
e.printStackTrace();
}
return receiver;
}*/
}
Simply annotate the MailSendingMessageHandler bean with #ServiceActivator, the framework will register a ConsumerEndpointFactoryBean to wrap the handler. See the documentation about "Annotations on #Beans".

Resources