We are currently introducing ActiveMQ into our existing application which was running on a different Queueing system. Spring JMS is used to make use of the existing integration within the Spring framework.
Most of our applications use point-to-point (queue) communication, with the exception of one. It needs to be able to listen to the topic created by another producing application while publishing to multiple queues at the same time.
This means that application needs to support both Topics and Queues. However, when setting the global property
jms:
pub-sub-domain: true
the setting is global and all queue subscribers are immediately subscribing to topics, which we can see in the ActiveMQ web interface.
Is there a way to configure the application to support both topics and queues at the same time?
The boot property is used to configure the default container factory used by #JmsListener methods, as well as to configure the JmsTemplate.
Simply override Boot's default container factory...
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
DefaultJmsListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
return factory;
}
and then add a second one
#Bean
public DefaultJmsListenerContainerFactory jmsTopicListenerContainerFactory(
DefaultJmsListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setPubSubDomain(true); << override the boot property
return factory;
}
Then refer to the alternate factory in the #JmsListener for the topic.
Alternatively, if you don't have listeners for both types, set the property to true, but override Boot's JmsTemplate configuration.
Related
The spring documentation says:
Destinations, as ConnectionFactory instances, are JMS administered
objects that you can store and retrieved in JNDI. When configuring a
Spring application context, you can use the JNDI JndiObjectFactoryBean
factory class or to perform dependency injection on
your object’s references to JMS destinations.
However, this strategy
is often cumbersome if there are a large number of destinations in the
application or if there are advanced destination management features
unique to the JMS provider.
The question is:
How to proceed when I have a large number of destinations in my application?
Using the strategy mentioned above I have to define:
JndiTemplate
JndiDestinationResolver
JndiObjectFactoryBean
CachingConnectionFactory
JmsTemplate
For EACH destination.
So If I have 20 queues, I'll have to define 100 such beans...
The comment in the Spring documentation makes a note on 'using JNDI for destination endpoints' versus 'not using JNDI for destination endpoints'. So in your case - are your destinations stored in JNDI ? If you don't have to use that, forget about it. Only load your ConnectionFactory (one object) from JNDI or simply create it from scratch.
And then you don't have to assign one Spring bean to each destination. You could have just one Java 'consumer bean' which then uses JmsTemplate. I guess your connection factory is the same, so that's only one new JmsTemplate(connectionFactory). Then do createSession/createConsumer, etc. as needed.
You can just use a single JmsTemplate, CachingConnectionFactory and JndiDestinationResolver...
The whole point of using a DestinationResolver is to lazily resolve the destinations for you. Use the specific send or [convertAndSend][5]. ThedestininationNamewill be passed on to theDestinationResolver` to get the destination.
The only drawback is that you need to use the jndi-name as the destinationName.
#Bean
public JndiDestinationResolver jndiDestinationResolver() {
return new JndiDestinationResolver();
}
#Bean
public JmsTemplate jmsTemplate() {
JmsTemplate jmsTemplate = new JmsTemplate();
jmsTemplate.setDestinationResolver(jndiDestinationResolver());
jmsTemplate.setConnectionFactory(connectionFactory());
return jmsTemplate;
}
With this you can use the following to dynamically resolve the destination from JNDI.
jmsTemplate.send("jms/queue1", "This is a message");
jmsTemplate.send("jms/queue3", "This is another message");
In question How to set the consumer-tag value in spring-amqp it is being asked how to change the consumer tag when using Spring Amqp and the answer suggests to provide an implementation of ConsumerTagStrategy.
I'm using Spring Boot 2.0.5 and I'm trying to figure out if I can do the same customization, though I can't find any configuration property about that nor providing a bean of type ConsumerTagStrategy seems to work.
How should I go about this?
Override boot's container factory bean declaration and add it there.
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
SimpleRabbitListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setConsumerTagStrategy(q -> "myConsumerFor." + q);
return factory;
}
I'm using the new Spring Boot 2.0M7 and I am trying to define some conditional logic to load different beans depending on the active profile.
I have this (working) bean configuration. That defines an sqs based connection factory for all environments except test and activemq for test.
#Configuration
#EnableJms
public class QueueConfig {
private static Logger LOG = LoggerFactory.getLogger(QueueConfig.class);
#Profile({"!test"})
#Bean
public ConnectionFactory sqsConnectionFactory() {
LOG.info("using sqs");
return new SQSConnectionFactory(new ProviderConfiguration(), AmazonSQSClientBuilder.standard()
.withRegion(Regions.EU_WEST_1)
.withCredentials(new DefaultAWSCredentialsProviderChain())
);
}
#Profile({"test"})
#Bean
public ConnectionFactory activeMqConnectionFactory() {
LOG.info("using activemq");
return new ActiveMQConnectionFactory("vm://localhost?broker.persistent=false");
}
#Bean
public JmsTemplate defaultJmsTemplate(ConnectionFactory connectionFactory) {
return new JmsTemplate(connectionFactory);
}
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setDestinationResolver(new DynamicDestinationResolver());
factory.setConcurrency("3-10");
return factory;
}
}
This works with a single profile. I can see in my test (annotated with #ActiveProfiles("test") that the test profile is active and the correct bean loads (log message).
However, changing #Profile({"!test"}) to #Profile({"!test","!dev}) on the sqsConnectionFactory and #Profile({"test"}) to #Profile({"test","dev}) on the activeMqConnectionFactory breaks things.
I get an unresolved bean exception because it now has two instances instead of 1. I can see in my logs that the test profile is still active and despite this it happily loads both the sqs and activemq implementations even though it shouldn't.
Did something change with the logic for #Profile in spring boot 2.x? If so,
how can I define that the activemq implementation is used when dev or test profile is active and sqs otherwise?
If not, what am I doing wrong here?
There are many ways you can approach that problem. Here is one:
Create another profile sqs. Use it to enable or disable beans.
#Profile({"sqs"})
#Bean
public ConnectionFactory sqsConnectionFactory() { ... }
#Profile({"!sqs"})
#Bean
public ConnectionFactory activeMqConnectionFactory() { ... }
Then declare your profiles in configuration files as using this one, or not:
---
spring.profiles: dev
...
---
spring.profiles: test
...
---
spring.profiles: prod
spring.profiles.include:
- sqs
#Profile({"!test","!dev}) - here you are missing one " after !dev, however, if it is just a typo in here post, try following (that works for me)
#Profile(value={"!test", "!dev"})
and btw - I personally prefer to have one configuration #Bean per class, in that case you are basically annotating your whole class with #Profile, for me it is much readable
I am using Spring's #JmsListener (spring-jms-4.3.4.RELEASE.jar) for receiving messages from ActiveMQ using the below code:
#Component
public class TopicSubscriber {
#JmsListener(destination="xyz.topic1", subscription="xyz_topic_durable_subscription")
public void send(Product product) {
System.out.println(" reveived message ***"+product);
}
}
As per the Spring API's documentation (link given below), the above code should create a durable subscription with subscription name as xyz_topic_durable_subscription:
http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jms/annotation/JmsListener.html#subscription--
But, the issue is that the above code creates only Non-Durable subscription which I could find by monitoring the ActiveMQ using admin console (added screenshot below, look for 'xyz.topic1' Destination under 'Active Non-Durable Topic Subscribers' section).
Are there any changes to be made in the code to make the durable subscription ?
You need to configure the ListenerContainerFactory appropriately:
#Bean
public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(true);
factory.setSubscriptionDurable(true);
factory.setClientId("jmsDemo");
// This provides all boot's default to this factory, including the message converter
configurer.configure(factory, connectionFactory);
// You could still override some of Boot's default if necessary.
return factory;
}
There interesting part is here:
factory.setSubscriptionDurable(true);
factory.setClientId("jmsDemo");
Now when you enter the ActiveMQ WebConsole you should see this:
In the answer marked as correct above, the code:
factory.setPubSubDomain(true);
factory.setSubscriptionDurable(true);
factory.setClientId("jmsDemo");
must come after
configurer.configure(factory, connectionFactory);
or you will lose those settings.
You also need to configure the listener container factory to create a container for durable subscriptions.
I am using Spring 4.1+ with a decorated #JmsListener method to process incoming messages. Is there a way to get a reference to the underlying DefaultMessageListenerContainer so that I can stop/start the related listeners in the code?
My #Configuration bean only creates a factory:
#Bean
public DefaultJmsListenerContainerFactory myContainerFactory() throws JMSException {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setDestinationResolver(destinationResolver());
factory.setConcurrency(env.getProperty("RE_MQ_SET_CONCURRENCY"));
return factory;
}
I understand I can use a DefaultMessageListener explicitly and then use setMessageListener() to assign the method, but I liked the elegance of the #JmsListener annotation.