DEV Community

Eduardo Issao Ito
Eduardo Issao Ito

Posted on • Edited on

Using multiple JMS servers with Spring Boot

Spring Boot support for JMS is straightforward to use. But the default configuration is limited to one server.

Let's see how to use more than one server. (In this example I will be using IBM MQ, but the same principle can be applied to other products.)

First we need to add configuration properties for two servers:

qm1.queueManager=QM1
qm1.channel=DEV.ADMIN.SVRCONN
qm1.connName=localhost(1414)
qm1.user=admin
qm1.password=passw0rd
qm1.pool.enabled=true

qm2.queueManager=QM2
qm2.channel=DEV.ADMIN.SVRCONN
qm2.connName=localhost(1415)
qm2.user=admin
qm2.password=passw0rd
qm2.pool.enabled=true

In this example we added the "qm1" and "qm2" prefixes to the standard properties, to configure the two servers.

For each one of the servers we need to read these properties (using @ConfigurationProperties annotation with the prefix) and create an specific ConnectionFactory and JmsListenerContainerFactory.

The Qm1Config and Qm2Config configure these beans.

@Configuration
public class Qm1Config {

    @Bean
    @ConfigurationProperties("qm1")
    public MQConfigurationProperties qm1ConfigProperties() {
        return new MQConfigurationProperties();
    }

    @Bean
    public MQConnectionFactory qm1ConnectionFactory(@Qualifier("qm1ConfigProperties") MQConfigurationProperties properties, ObjectProvider<List<MQConnectionFactoryCustomizer>> factoryCustomizers) {
        return new MQConnectionFactoryFactory(properties, factoryCustomizers.getIfAvailable()).createConnectionFactory(MQConnectionFactory.class);
    }

    @Bean
    public JmsListenerContainerFactory<?> qm1JmsListenerContainerFactory(@Qualifier("qm1ConnectionFactory") ConnectionFactory connectionFactory, DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        return factory;
    }

}
@Configuration
public class Qm2Config {

    @Bean
    @ConfigurationProperties("qm2")
    public MQConfigurationProperties qm2ConfigProperties() {
        return new MQConfigurationProperties();
    }

    @Bean
    public MQConnectionFactory qm2ConnectionFactory(@Qualifier("qm2ConfigProperties") MQConfigurationProperties properties, ObjectProvider<List<MQConnectionFactoryCustomizer>> factoryCustomizers) {
        return new MQConnectionFactoryFactory(properties, factoryCustomizers.getIfAvailable()).createConnectionFactory(MQConnectionFactory.class);
    }

    @Bean
    public JmsListenerContainerFactory<?> qm2JmsListenerContainerFactory(@Qualifier("qm2ConnectionFactory") ConnectionFactory connectionFactory, DefaultJmsListenerContainerFactoryConfigurer configurer) {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        configurer.configure(factory, connectionFactory);
        return factory;
    }

}

Then, the listener must specify which queue manager is to be used:

@Component
public class QueueConsumer {

    @JmsListener(destination = "${example.queue}", containerFactory = "qm1JmsListenerContainerFactory")
    public void receive1(String text) {
        System.out.println("Received from qm1: " + text);
    }

    @JmsListener(destination = "${example.queue}", containerFactory = "qm2JmsListenerContainerFactory")
    public void receive2(String text) {
        System.out.println("Received from qm2: " + text);
    }

}

And that's it!

Example

A complete runnable example is available in GitHub:
https://github.com/adzubla/qmgrs

Dynamic creation of listeners

The above solution hardwires the JmsListeners and ConnectionFactories in source code. To have a configurable number of queue managers we can use the approach that follows.

The new properties file:

qm.list.0.queueManager=QM1
qm.list.0.channel=DEV.ADMIN.SVRCONN
qm.list.0.connName=localhost(1414)
qm.list.0.user=admin
qm.list.0.password=passw0rd
qm.list.0.pool.enabled=true
qm.list.1.queueManager=QM2
qm.list.1.channel=DEV.ADMIN.SVRCONN
qm.list.1.connName=localhost(1415)
qm.list.1.user=admin
qm.list.1.password=passw0rd
qm.list.1.pool.enabled=true

To read these properties in a List of properties, we use the configuration below:

@ConfigurationProperties("qm")
@Configuration
public class QmProperties {

    private List<MQConfigurationProperties> list;

    public List<MQConfigurationProperties> getList() {
        return list;
    }

    public void setList(List<MQConfigurationProperties> list) {
        this.list = list;
    }

}

As in the previous example, for each one of the servers we need to create an specific ConnectionFactory and JmsListenerContainerFactory. This is done using JmsListenerConfigurer that allows the creation of listeners programatically.

@Configuration
@EnableJms
public class JmsConfig implements JmsListenerConfigurer {

    @Autowired
    private ObjectProvider<List<MQConnectionFactoryCustomizer>> factoryCustomizers;

    @Autowired
    private DefaultJmsListenerContainerFactoryConfigurer configurer;

    @Autowired
    private QmProperties qmProperties;

    @Autowired
    private QueueConsumer queueConsumer;

    @Value("${example.queue}")
    private String destination;

    @Override
    public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
        for (MQConfigurationProperties properties : qmProperties.getList()) {
            String queueManager = properties.getQueueManager();

            MQConnectionFactory connectionFactory = new MQConnectionFactoryFactory(properties, factoryCustomizers.getIfAvailable()).createConnectionFactory(MQConnectionFactory.class);

            DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
            configurer.configure(factory, connectionFactory);

            SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
            endpoint.setId("jmsEndpoint-" + queueManager);
            endpoint.setDestination(destination);
            endpoint.setMessageListener(message -> {
                try {
                    queueConsumer.receive(queueManager, message.getBody(String.class));
                } catch (JMSException e) {
                    throw new RuntimeException(e);
                }
            });
            registrar.registerEndpoint(endpoint, factory);
        }
    }

}

The QueueConsumer bean will be registered for each of the queue managers the present in the configuration.

Note that we removed the @JmsListener because the QueueConsumer bean will be registered by our JmsConfig.

@Component
public class QueueConsumer {

    public void receive(String queueManeger, String text) {
        System.out.println("Received from " + queueManeger + ": " + text);
    }

}

Top comments (10)

Collapse
 
kundycoos profile image
Eng kue

I am getting an error on this line, MQConnectionFactoryFactory is expecting 0 parameters but found 2

return new MQConnectionFactoryFactory(properties, factoryCustomizers.getIfAvailable()).createConnectionFactory(MQConnectionFactory.class);

Collapse
 
adzubla profile image
Eduardo Issao Ito • Edited

Probably you are importing the wrong class... Look at the complete source code with all imports at github.com/adzubla/qmgrs

Collapse
 
greeger profile image
Deev Greg

but I can not import import com.ibm.mq.spring.boot.MQConnectionFactoryFactory;

Collapse
 
singleta_20db82f05b5a4a23 profile image
singleta

Add an extra property of null in between the existing two. It's a new Spring class SSLBundles. It's optional

Collapse
 
nainiacharya0406 profile image
nainiacharya0406

Can you please help me with Producer and consumer for this..

Collapse
 
akhettar profile image
Ayache Khettar

Thanks for sharing Eduardo. You are referring to only one queue(destination). How do you wire your example to support multiple destinations for given connection manager?

Thanks

Ayache

Collapse
 
ctopcanada profile image
ctopCanada

if the situation is that two channels(One is for GET, the other is for PUT), one queue manager, one queue, can I use the same idea for that?

Collapse
 
adzubla profile image
Eduardo Issao Ito • Edited

Yes, you would have two ConnectionFactories with distinct channels. Note that if you are using the same queue manager name for both ConnectionFactories, you should change how the the endpoint Id is constructed.

Collapse
 
nainiacharya0406 profile image
nainiacharya0406

By using the dynamic approach I am creating the Producer as well and pushing the message as below:
jmsTemplate.convertAndSend("queueName", "msg");
but getting invocationTargetException there

Some comments may only be visible to logged-in visitors. Sign in to view all comments.