Spring AMQP handling 2 ConnectionFactory - spring-boot

My app has 2 rabbit instances it needs to connect to.
Rabbit1 is an entry point to my app, where my listener waits for message and
#RabbitListener(queues = "#{myAmqpProperties.getRequest().getQueue()}")
it is configured through regular Springboot2 properties
spring:
rabbitmq:
host: localhost
username: myUser
password: myPass
port: 5672
virtual-host: myVhost
This works fine.
Now I need to send rabbit message on another rabbitMQ instance, Rabbit2.
So I created a configuration class building the rabbitTemplete and its associated connectionFactory.
package com.mycompany.socle.amqp.ocr;
import com.mycompany.doccontrol.messaging.app.AppMessageConverter;
import com.mycompany.socle.amqp.common.service.CpyAmqpRequestWithReplyToProp;
import com.mycompany.socle.amqp.common.service.CpyAmqpServerProperties;
import com.mycompany.socle.common.bean.SslProperties;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Valid;
#Configuration
#EnableRabbit
public class OcrAmqpConfiguration {
#Autowired
private OcrAmqpProperties ocrAmqpProperties;
/**
* Template used to send analysis request.
*
* #return the template to use
* #throws Exception if there are issues while initialising the connection factory.
*/
#Bean
public RabbitTemplate appRequestTemplate() throws Exception {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(ocrConnectionFactory());
rabbitTemplate.setMessageConverter(new AppMessageConverter());
#Valid final CpyAmqpRequestWithReplyToProp appProperties = ocrAmqpProperties.getApp().getRequest();
// Settings to send the request
rabbitTemplate.setExchange(appProperties.getExchange());
if (appProperties.getRoutingKey() != null) {
rabbitTemplate.setRoutingKey(appProperties.getRoutingKey());
}
return rabbitTemplate;
}
/**
* Define the Connection factory used to contact the broker.
*
* #return the connection factory
* #throws Exception if there are issues while defining the factory context.
*/
#Bean
public CachingConnectionFactory ocrConnectionFactory() throws Exception {
final CachingConnectionFactory result = new CachingConnectionFactory(builConnectionFactory(ocrAmqpProperties.getRabbitmq()).getObject());
result.afterPropertiesSet();
return result;
}
private static RabbitConnectionFactoryBean builConnectionFactory(MyAmqpServerProperties pAmqpProperties) {
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
// Generic connection properties
factory.setHost(pAmqpProperties.getHost());
factory.setPort(pAmqpProperties.getPort());
factory.setUsername(pAmqpProperties.getUsername());
factory.setPassword(pAmqpProperties.getPassword());
factory.setVirtualHost(pAmqpProperties.getVirtualHost());
factory.afterPropertiesSet();
return factory;
}
}
The class MyAmqpServerProperties is just a property class matching extra property i added in my property file to define Rabbit2 info.
But then the default SpringBoot ConnectionFactory is NOT generated, and my listener which listened originaly to Rabbit1, now also listen to Rabbit2.
I see RabbitAutoConfiguration java doc says:
Registers the following beans: * * {#link
org.springframework.amqp.rabbit.core.RabbitTemplate RabbitTemplate} if
there * is no other bean of the same type in the context. *
{#link
org.springframework.amqp.rabbit.connection.CachingConnectionFactory *
CachingConnectionFactory} instance if there is no other bean of the
same type in the * context.
And in the code there is the annotation
#ConditionalOnMissingBean(ConnectionFactory.class)
=> If I remove the #Bean of the ocrConnectionFactory method, it works fine, however I need to have the ocrConnectionFactory registered in spring context for monitoring ...
So is there a way to either register it AFTER the default one has been generated, or some other properties to force the default one to be generated as normal ?

No. Boot will only configure one if you haven't any. If you configure one you must configure both.

Related

Control Azure Service Bus Message Listener to start or stop listening from the Topic or queue in 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.

Problem with GCP Secret Manager and Spring Boot app

Spring Boot (2.5.9) has problem to access password from the GCP Secret Manager using spring-cloud-gcp-starter-secretmanager ver 2.0.8 throwing error
AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultFeignClientConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.google.protobuf.ByteString$LiteralByteString] to type [java.lang.String]
for a passsword declared in the application.properties as
webservices.security.basic.user.password=${sm://my-password}
when I will replace it with regular string or even env variable it will work fine.
Failing part of the code looks like:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;
/**
* Default feign client configuration. Includes retry policies, basic auth user name and password, and HTTP status decoder.
* #author Greg Meyer
* #since 6.0
*/
public class DefaultFeignClientConfiguration
{
#Value("${webservices.retry.backoff.multiplier:3}")
protected double backoffMultiplier;
#Value("${webservices.retry.backoff.initialBackoffInterval:100}")
protected long initialBackoffInterval;
#Value("${webservices.retry.backoff.maxInterval:20000}")
protected long maxInterval;
#Value("${webservices.security.basic.user.name:}")
protected String user;
#Value("${webservices.security.basic.user.password:}")
protected String pass;
/**
* Creates an instance of the a the default HTTP status translator.
* #return An instance of the a the default HTTP status translator
*/
#Bean
public ErrorDecoder feignClientErrorDecoder()
{
return new DefaultErrorDecoder();
}
/**
* Creates an instance of BasicAuth interceptor configured with a username and password. This bean is only created if the
* "webservices.security.basic.user.name" property is set.
* #return An instance of BasicAuth interceptor configured with a username and password
*/
#Bean
#ConditionalOnProperty(name="webservices.security.basic.user.name", matchIfMissing=false)
public BasicAuthRequestInterceptor basicAuthRequestInterceptor()
{
return new BasicAuthRequestInterceptor(user, pass);
}
/**
* Creates an instance of a back off policy used in conjuntion with the retry policy.
* #return An instance of a back off policy
*/
#Bean
public LoadBalancedRetryFactory backOffPolciyFactory()
{
return new LoadBalancedRetryFactory()
{
#Override
public BackOffPolicy createBackOffPolicy(String service)
{
final ExponentialBackOffPolicy backoffPolicy = new ExponentialBackOffPolicy();
backoffPolicy.setMultiplier(backoffMultiplier);
backoffPolicy.setInitialInterval(initialBackoffInterval);
backoffPolicy.setMaxInterval(maxInterval);
return backoffPolicy;
}
};
}
/**
* Creates a default http retry policy.
* #return A default http retry policy.
*/
#Bean
public Retryer retryer()
{
/*
* Default retryer config
*/
return new Retryer.Default(200, 1000, 5);
}
}
Any thoughts?
The problem is that most likely, Feign autoconfiguration happens early on, before GcpSecretManagerEnvironmentPostProcessor had a chance to run and introduce ByteString converters.
Basically, the solution that works is to implement a Custom Converter which implements the Generic Conversion Service and register the Converter in the main method before calling SpringApplication.run.
public static void main(String[] args)
{
((DefaultConversionService)DefaultConversionService.getSharedInstance()).addConverter(new CustomConverter());
SpringApplication.run(STAApplication.class, args);
}
and custom converter:
#Component
public class CustomConverter implements GenericConverter {
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(ByteString.class, String.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == String.class) {
return source;
}
try {
source = ((ByteString) source).toString("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return source;
}
}

Message grouping with Artemis and Spring JMS not working when using transacted sessions

Message grouping doesn't appear to be working
My Producer application sends the message to the queue via JMS MessageProducer after setting the string property JMSXGroupID to 'product=paper'
My producer application sends another message the same way also with 'product=paper'.
I can see both messages in the queue when I browse that message's headers in the Artemis UI. _AMQ_GROUP_ID has a value of 'product=paper' in both. JMSXGroupID is absent.
When I debug my listener application which uses Spring JMS with a concurrency of 15-15 (15 min 15 max) I can see both messages come through logged under different listener containers. When I look at the map of headers for each, _AMQ_GROUP_ID is absent and JMSXGroupID has a value of null instead of 'product=paper'.
Why isn't message grouping with group id working? Does it have to do with the fact that Artemis didn't translate _AMQ_GROUP_ID back to JMSXGroupID? Or is Spring JMS not registering its multiple consumer threads as different consumers for the broker to see multiple consumers?
Edit:
I was able to get message grouping to work in my application by commenting out lines having to do with using transacted sessions from my container factory bean method. It seems to have to do with using transacted sessions.
Edit2:
Here's a self contained application running against a local standalone Artemis broker (version 2.10.1) and using Spring Boot 2.2.0:
GroupidApplication (spring boot application and beans):
package com.reproduce.groupid;
import java.util.HashMap;
import java.util.Map;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.api.jms.ActiveMQJMSClient;
import org.apache.activemq.artemis.api.jms.JMSFactoryType;
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.connection.JmsTransactionManager;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;
#SpringBootApplication
#EnableJms
public class GroupidApplication implements CommandLineRunner {
private static Logger LOG = LoggerFactory
.getLogger(GroupidApplication.class);
#Autowired
private JmsTemplate jmsTemplate;
#Autowired MessageConverter messageConverter;
public static void main(String[] args) {
LOG.info("STARTING THE APPLICATION");
SpringApplication.run(GroupidApplication.class, args);
LOG.info("APPLICATION FINISHED");
}
#Override
public void run(String... args) throws JMSException {
LOG.info("EXECUTING : command line runner");
jmsTemplate.setPubSubDomain(true);
createAndSendTextMessage("Message1");
createAndSendTextMessage("Message2");
createAndSendTextMessage("Message3");
createAndSendTextMessage("Message4");
createAndSendTextMessage("Message5");
createAndSendTextMessage("Message6");
}
private void createAndSendTextMessage(String messageBody) {
jmsTemplate.send("local-queue", session -> {
Message message = session.createTextMessage(messageBody);
message.setStringProperty("JMSXGroupID", "product=paper");
return message;
});
}
// BEANS
#Bean
public JmsListenerContainerFactory<?> containerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer, JmsTransactionManager jmsTransactionManager) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setSubscriptionDurable(true);
factory.setSubscriptionShared(true);
factory.setSessionAcknowledgeMode(Session.SESSION_TRANSACTED);
factory.setSessionTransacted(Boolean.TRUE);
factory.setTransactionManager(jmsTransactionManager);
return factory;
}
#Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
#Bean
#Primary
public JmsTransactionManager jmsTransactionManager(ConnectionFactory connectionFactory) {
JmsTransactionManager jmsTransactionManager = new JmsTransactionManager(connectionFactory);
// Lazily retrieve existing JMS Connection from given ConnectionFactory
jmsTransactionManager.setLazyResourceRetrieval(true);
return jmsTransactionManager;
}
#Bean
#Primary
public ConnectionFactory connectionFactory() throws JMSException {
// Create ConnectionFactory which enables failover between primary and backup brokers
ActiveMQConnectionFactory activeMqConnectionFactory = ActiveMQJMSClient.createConnectionFactoryWithHA(
JMSFactoryType.CF, transportConfigurations());
activeMqConnectionFactory.setBrokerURL("tcp://localhost:61616?jms.redeliveryPolicy.maximumRedeliveries=1");
activeMqConnectionFactory.setUser("admin");
activeMqConnectionFactory.setPassword("admin");
activeMqConnectionFactory.setInitialConnectAttempts(1);
activeMqConnectionFactory.setReconnectAttempts(5);
activeMqConnectionFactory.setConsumerWindowSize(0);
activeMqConnectionFactory.setBlockOnAcknowledge(true);
activeMqConnectionFactory.setCacheDestinations(true);
activeMqConnectionFactory.setRetryInterval(1000);
return activeMqConnectionFactory;
}
private static TransportConfiguration[] transportConfigurations() {
String connectorFactoryFqcn = NettyConnectorFactory.class.getName();
Map<String, Object> primaryTransportParameters = new HashMap<>(2);
primaryTransportParameters.put("host", "localhost");
primaryTransportParameters.put("port", "61616");
TransportConfiguration primaryTransportConfiguration = new TransportConfiguration(connectorFactoryFqcn,
primaryTransportParameters);
return new TransportConfiguration[] { primaryTransportConfiguration,
new TransportConfiguration(connectorFactoryFqcn) };
}
}
CustomSpringJmsListener:
package com.reproduce.groupid;
import javax.jms.JMSException;
import javax.jms.TextMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
#Component
public class CustomSpringJmsListener {
protected final Logger LOG = LoggerFactory.getLogger(getClass());
#JmsListener(destination = "local-queue", subscription = "groupid-example", containerFactory = "containerFactory", concurrency = "15-15")
public void receive(TextMessage message) throws JMSException {
LOG.info("Received message: " + message);
}
}
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.reproduce</groupId>
<artifactId>groupid</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>groupid</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
You can see that even though all of these messages have the same group id, they get logged by different listener container threads. If you comment out the transaction manager from the bean definition it starts working again.
It's all about consumer caching. By default, when using an external TXM, caching is disabled so each message is received on a new consumer.
For this app, you really don't need a transaction manager, sessionTransacted is enough - the container will use local transactions.
If you must use an external transaction manager for some reason, consider changing the cache level.
factory.setCacheLevel(DefaultMessageListenerContainer.CACHE_CONSUMER);
See the DMLC javadocs...
/**
* Specify the level of caching that this listener container is allowed to apply.
* <p>Default is {#link #CACHE_NONE} if an external transaction manager has been specified
* (to reobtain all resources freshly within the scope of the external transaction),
* and {#link #CACHE_CONSUMER} otherwise (operating with local JMS resources).
* <p>Some Java EE servers only register their JMS resources with an ongoing XA
* transaction in case of a freshly obtained JMS {#code Connection} and {#code Session},
* which is why this listener container by default does not cache any of those.
* However, depending on the rules of your server with respect to the caching
* of transactional resources, consider switching this setting to at least
* {#link #CACHE_CONNECTION} or {#link #CACHE_SESSION} even in conjunction with an
* external transaction manager.
* #see #CACHE_NONE
* #see #CACHE_CONNECTION
* #see #CACHE_SESSION
* #see #CACHE_CONSUMER
* #see #setCacheLevelName
* #see #setTransactionManager
*/
public void setCacheLevel(int cacheLevel) {

How to Give manual Acknowledge using JmsTemplate and delete message from Rabbitmq queue

I am using RabbitMq(with JMS) with jmsTemplate I am able to Consume Message from RabbitMq Queue But it is taking acknowledgment AUTO.
I have Search API for it but not able to find it out.
How can I set manual acknowledgment.
In Below code when Message is consumed from queue I want to call web service with that message and depends on that response from from I want to delete that message from queue.
I have created one project in which I am using Listener and other project with call to read message from queue
first Project:
package com.es.jms.listener;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.MessageListenerContainer;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import com.rabbitmq.jms.admin.RMQConnectionFactory;
#Configuration
public class RabbitMqMessageListener {
#Bean
public ConnectionFactory jmsConnectionFactory() {
RMQConnectionFactory connectionFactory = new RMQConnectionFactory();
connectionFactory.setUsername("Username");
connectionFactory.setPassword("Password");
connectionFactory.setVirtualHost("vhostname");
connectionFactory.setHost("hostname");
return connectionFactory;
}
#Bean
public MessageListener msgListener() {
return new MessageListener() {
public void onMessage(Message message) {
System.out.println(message.toString());
if (message instanceof TextMessage) {
try {
String msg = ((TextMessage) message).getText();
System.out.println("Received message: " + msg);
// call web service here and depends on web service
// response
// if 200 then delete msg from queue else keep msg in
// queue
} catch (JMSException ex) {
throw new RuntimeException(ex);
}
}
}
};
}
#Bean
public MessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(jmsConnectionFactory());
container.setDestinationName("test");
container.setMessageListener(msgListener());
return container;
}
}
2nd Project:
package com.rabbitmq.jms.consumer.controller;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.jms.ConnectionFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jms.JmsException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.rabbitmq.jms.admin.RMQConnectionFactory;
import redis.clients.jedis.Jedis;
#Controller
public class ReceiverController {
#Autowired
JmsTemplate jmsTemplate;
#Bean
public ConnectionFactory jmsConnectionFactory() {
RMQConnectionFactory connectionFactory = new RMQConnectionFactory();
connectionFactory.setUsername("Username");
connectionFactory.setPassword("Password");
connectionFactory.setVirtualHost("vhostname");
connectionFactory.setHost("hostname");
return connectionFactory;
}
#CrossOrigin
#SuppressWarnings({ "unchecked", "rawtypes" })
#RequestMapping(method = RequestMethod.GET, value = "/getdata")
#ResponseBody
public ResponseEntity<String> fecthDataFromRedis()
throws JSONException, InterruptedException, JmsException, ExecutionException, TimeoutException {
System.out.println("in controller");
jmsTemplate.setReceiveTimeout(500L);
// jmsTemplate.
String message = (String) jmsTemplate.receiveAndConvert("test");
// call web service here and depends on web service
// response
// if 200 then delete msg from queue else keep msg in
// queue
System.out.println(message);
}
return new ResponseEntity(message , HttpStatus.OK);
}
}
How Can I do That?
Thanks In Advance.
You are not using a JmsTemplate, you are using a SimpleMessageListenerContainer to receive the message.
If you were using the template, you would have to use the execute method with a SessionCallback since the acknowledgement must occur within the scope of the session within which the message was received.
However, with the SimpleMessageListenerContainer, you simply set the sessionAcknowledgeMode to Session.CLIENT_ACKNOWLEDGE. See the container javadocs...
/**
* Message listener container that uses the plain JMS client API's
* {#code MessageConsumer.setMessageListener()} method to
* create concurrent MessageConsumers for the specified listeners.
*
* <p>This is the simplest form of a message listener container.
* It creates a fixed number of JMS Sessions to invoke the listener,
* not allowing for dynamic adaptation to runtime demands. Its main
* advantage is its low level of complexity and the minimum requirements
* on the JMS provider: Not even the ServerSessionPool facility is required.
*
* <p>See the {#link AbstractMessageListenerContainer} javadoc for details
* on acknowledge modes and transaction options. Note that this container
* exposes standard JMS behavior for the default "AUTO_ACKNOWLEDGE" mode:
* that is, automatic message acknowledgment after listener execution,
* with no redelivery in case of a user exception thrown but potential
* redelivery in case of the JVM dying during listener execution.
*
* <p>For a different style of MessageListener handling, through looped
* {#code MessageConsumer.receive()} calls that also allow for
* transactional reception of messages (registering them with XA transactions),
* see {#link DefaultMessageListenerContainer}.
...
EDIT
When using the JmsTemplate, you must do your work within the scope of the session - here's how...
First, you have to enable client acknowledge in your template...
this.jmsTemplate.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
Then, use the execute method with a SessionCallback ...
Boolean result = this.jmsTemplate.execute(session -> {
MessageConsumer consumer = session.createConsumer(
this.jmsTemplate.getDestinationResolver().resolveDestinationName(session, "bar", false));
String result = null;
try {
Message received = consumer.receive(5000);
if (received != null) {
result = (String) this.jmsTemplate.getMessageConverter().fromMessage(received);
// Do some stuff here.
received.acknowledge();
return true;
}
}
catch (Exception e) {
return false;
}
finally {
consumer.close();
}
}, true);

ClassCastException: javax.naming.Reference cannot be cast to javax.jms.ConnectionFactory

I wrote a Java program to connect to Websphere MQ to publish messages. I created a JNDI namespace, connection factory, destinations, and queue manager in Websphere MQ Explore. When I am running my program it is showing ClassCastException for type casting from string to ConnectionFactory.
Here is my code. Can anyone help resolve this problem.
JNDIUtil.java
package com.tradefinance.jms.util;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
//JNDI classes
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
//Standard Java classes
import java.util.Hashtable;
import java.util.Properties;
/**
*
* A wrapper class for JNDI calls
*
*/
public class JNDIUtil
{
private Context context;
public JNDIUtil(String icf, String url) throws JMSException, NamingException
{
Hashtable environment = new Hashtable();
environment.put(Context.INITIAL_CONTEXT_FACTORY, icf );
environment.put(Context.PROVIDER_URL, url);
context= new InitialContext( environment );
}
/**
* #param ObjName Object Name to be retrieved
* #return Retrieved Object
* #throws NamingException
*/
private Object getObjectByName(String ObjName) throws NamingException
{
return context.lookup(ObjName);
}
/**
* #param factoryName Factory Name
* #return ConnectionFactory object
* #throws NamingException
*/
public ConnectionFactory getConnectionFactory(String factoryName) throws NamingException
{
return (ConnectionFactory) getObjectByName(factoryName);
}
/**
* #param destinationName Destination Name
* #return ConnectionFactory object
* #throws NamingException
*/
public Destination getDestination(String destinationName) throws NamingException
{
return (Destination) getObjectByName(destinationName);
}
}
NewPublisher.java
package com.tradefinance.jms.topics;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.TextMessage;
//JNDI classes
import javax.naming.NamingException;
import com.tradefinance.jms.util.JNDIUtil;
/**
* A class to demonstrate how to a publish to a topic.
*/
public class NewsPublisher
{
public static String icf = "com.sun.jndi.fscontext.RefFSContextFactory";
public static String url = "file:/C:/JNDI-Directory/";
public static void main(String[] vars) throws JMSException, NamingException
{
ConnectionFactory factory = null;
Connection connection = null;
Session session = null;
Destination destination= null; // a destination can be a topic or a queue
MessageProducer producer= null;
try
{
JNDIUtil jndiUtil= new JNDIUtil(icf,url);
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
connection = factory.createConnection();
connection.start();
// Indicate a non-transactional session
boolean transacted = false;
session = connection.createSession( transacted, Session.AUTO_ACKNOWLEDGE);
destination = jndiUtil.getDestination("NewsTopic");
producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("No News is Good News!");
producer.send(message);
System.out.println("NewsPublisher: Message Publication Completed");
}
finally
{
// Always release resources
if ( producer!= null )
producer.close();
if ( session!= null )
session.close();
if ( connection!= null )
connection.close();
}
}
}
Getting the error on these lines:
return (ConnectionFactory) getObjectByName(factoryName);
in JNDIUtil.java
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
in NewPublisher.java
You are missing some JARs of MQ Client to get this working.
I had the same error, and after some further investigation, I ended up with this list of Jars in order to get this working:
com.ibm.mq.jmqi.jar
com.ibm.mqjms.jar
dhbcore.jar
fscontext.jar
providerutil.jar
com.ibm.mq.headers.jar
What you received back from the jndi context was a reference. This is a recipe to build the connection factory and I suspect that the class responsible for this cannot be found because the MQ jars required are not in the classpath. The error message is not intuitive.
Failing that, I find a good way to debug jndi lookup issues is to acquire the context and execute a list() on it, printing out the details of each object returned, just so you're clear on what exactly resides in the directory.
Add the Below jars to the classpath :
sibc.jms
sibc.jndi
In my case, we use TIBCO JMS adding the following dependency is resolved casting issue
ClassCastException: javax.naming.Reference cannot be cast to
javax.jms.ConnectionFactory
implementation 'com.tibco.tibjms:tibjms:5.1.4'

Resources