Spring 3 and Rabbit MQ integration (not Spring Boot) - spring

I'm having difficulty getting a Spring 3 application to integrate with RabbitMQ, in order to receive messages from a queue (I do not need to send messages).
Part of the challenge is much of the documentation now relates to Spring Boot. The related Spring guide is helpful, but following the steps does not seem to work in my case. For instance, the guide includes the text:
The message listener container and receiver beans are all you need to listen for messages.
So I have setup the listener container and receiver beans with the following code.
Setting up message handler
#Component
public class CustomMessageHandler {
public void handleMessage(String text) {
System.out.println("Received: " + text);
}
}
Setting up configuration
#Configuration
public class RabbitConfig {
#Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory){
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setRoutingKey("queue-name");
return rabbitTemplate;
}
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost("...host...");
connectionFactory.setPort(5671);
connectionFactory.setVirtualHost("...virtual host..");
connectionFactory.setUsername("...username...");
connectionFactory.setPassword("...password...");
return connectionFactory;
}
#Bean
public MessageListenerAdapter messageListenerAdapter(CustomMessageHandler messageHandler) {
return new MessageListenerAdapter(messageHandler, "handleMessage");
}
#Bean
public SimpleMessageListenerContainer listenerContainer(ConnectionFactory connectionFactory,
MessageListenerAdapter messageListenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setQueueNames("queue-name");
container.setConnectionFactory(connectionFactory);
container.setMessageListener(messageListenerAdapter);
return container;
}
}
Unfortunately with this setup, the application will start up, but it never triggers the message handler. The queue it is trying to read from also has one message sitting in it, waiting to be consumed.
Any ideas on something that is missing, or appears misconfigured?

Thanks to some dependency management assistance from #GaryRussell, I was able to see that the version of spring-rabbit and spring-amqp were too recent. Using the older 1.3.9.RELEASE unfortunately proved to add additional challenges.
Some other assistance came in the form of using an actual RabbitMQ Java client. This option was much simpler to implement, and avoided the dependency problems. Ultimately I needed to include the following dependency:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.3</version>
</dependency>
And then I simply followed their documentation on creating a connection, and consuming messages.
Voila, it works!

Related

spring-kafka: DefaultErrorHandler with DeadLetterPublishingRecoverer(BiFunction) not considered. No DL topic created

In my Spring Boot application using spring-kafka, I am trying to configure an error handler with 2 things:-
Retry message consumption failures a certain times (FixedBackOff) before publishing to a dead letter topic
Create a dead letter topic with a name of my choice
Using
// Version highlights
id 'org.springframework.boot' version '2.7.2'
...
implementation 'org.springframework.kafka:spring-kafka' // 2.8.8
Here is the code I am using based on what I read in Spring docs and reiterated in several articles online:
#Bean
public DefaultErrorHandler byteArrayDefaultErrorHandler(KafkaTemplate<String, byte[]> template) {
var recoverer =
new DeadLetterPublishingRecoverer(
template,
(record, e) -> new TopicPartition("%s.deadLetter".formatted(record.topic()), 0);
);
return new DefaultErrorHandler(recoverer, new FixedBackOff(3000, 3));
}
But the above bean is not considered/used. So, when consumption encounters a failure (currently simulating failure by throwing an exception),
the FixedBackOff is not considered but the default one with 10 attempts back to back is used.
No DL topic is created.
Currently, the consumer config class has minimal stuff:
#Bean public ConsumerFactory<String, byte[]> byteArrayConsumerFactory() { ... }
#Bean public ConcurrentKafkaListenerContainerFactory<String, byte[]> byteArrayListenerContainerFactory() { .. }
#Bean public DefaultErrorHandler byteArrayDefaultErrorHandler(KafkaTemplate<String, byte[]> template) { ...code pasted above... }
And the listener is as follows:
#KafkaListener(
topics = "${app.config.kafka.topic}",
containerFactory = "byteArrayListenerContainerFactory"
)
public void consumeMessage(ConsumerRecord<String, byte[]> record) { ... }
Am at a loss figuring out what I have missed or added something conflicting the wiring. Help figuring out is highly appreciated.
The error handler bean will only be wired in by boot if you are using Boot's auto configured container factory.
Since you are creating your own container factory bean...
#Bean public ConcurrentKafkaListenerContainerFactory<String, byte[]> byteArrayListenerContainerFactory() { .. }
...you must add the error handler yourself - see setCommonErrorHandler().
The framework does not automatically provision the dead letter topic; add a #Bean NewTopic dlt() { ... }.
https://docs.spring.io/spring-kafka/docs/current/reference/html/#configuring-topics

Spring JMS HornetQ user is null

I am trying to connect to a remote HornetQ broker in a spring boot/spring jms application and setup a #JmsListener.
HornetQ ConnectionFactory is being fetched from JNDI registry that HornetQ instance hosts. Everything works fine as long as HornetQ security is turned off but when it is turned on I get this error
WARN o.s.j.l.DefaultMessageListenerContainer : Setup of JMS message listener invoker failed for destination 'jms/MI/Notification/Queue' - trying to recover. Cause: User: null doesn't have permission='CONSUME' on address jms.queue.MI/Notification/Queue
I ran a debug session to figure out that ConnectionFactory instance being returned is HornetQXAConnectionFactory but user and password fields are not set, which I believe is why user is null. I verified that user principal and credentials are set in JNDI properties but somehow it is not being passed on to ConnectionFactory instance. Any help on how I can get this setup working would be greatly appreciated.
This is my jms related config
#Configuration
#EnableJms
public class JmsConfig {
#Bean
public JmsListenerContainerFactory<?> jmsListenerContainerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setDestinationResolver(destinationResolver());
return factory;
}
#Bean // Serialize message content to json using TextMessage
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.BYTES);
converter.setTypeIdPropertyName("_type");
return converter;
}
#Value("${jms.jndi.provider.url}")
private String jndiProviderURL;
#Value("${jms.jndi.principal}")
private String jndiPrincipal;
#Value("${jms.jndi.credentials}")
private String jndiCredential;
#Bean
public JndiTemplate jndiTemplate() {
Properties env = new Properties();
env.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
env.put("java.naming.provider.url", jndiProviderURL);
env.put("java.naming.security.principal", jndiPrincipal);
env.put("java.naming.security.credentials", jndiCredential);
return new JndiTemplate(env);
}
#Bean
public DestinationResolver destinationResolver() {
JndiDestinationResolver destinationResolver = new JndiDestinationResolver();
destinationResolver.setJndiTemplate(jndiTemplate());
return destinationResolver;
}
#Value("${jms.connectionfactory.jndiname}")
private String connectionFactoryJNDIName;
#Bean
public JndiObjectFactoryBean connectionFactoryFactory() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiTemplate(jndiTemplate());
jndiObjectFactoryBean.setJndiName(connectionFactoryJNDIName);
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(ConnectionFactory.class);
return jndiObjectFactoryBean;
}
#Bean
public ConnectionFactory connectionFactory(JndiObjectFactoryBean connectionFactoryFactory) {
return (ConnectionFactory) connectionFactoryFactory.getObject();
}
}
JNDI and JMS are 100% independent as they are completely different specifications implemented in potentially completely different ways. Therefore the credentials you use for your JNDI lookup do not apply to your JMS resources. You need to explicitly set the username and password credentials on your JMS connection. This is easy using the JMS API directly (e.g. via javax.jms.ConnectionFactory#createConnection(String username, String password)). Since you're using Spring you could use something like this:
#Bean
public ConnectionFactory connectionFactory(JndiObjectFactoryBean connectionFactoryFactory) {
UserCredentialsConnectionFactoryAdapter cf = new UserCredentialsConnectionFactoryAdapter();
cf.setTargetConnectionFactory((ConnectionFactory) connectionFactoryFactory.getObject());
cf.setUsername("yourJmsUsername");
cf.setPassword("yourJmsPassword");
return cf;
}
Also, for what it's worth, the HornetQ code-base was donated to the Apache ActiveMQ project three and a half years ago now and it lives on as the Apache ActiveMQ Artemis broker. There's been 22 releases since then with numerous new features and bug fixes. I strongly recommend you migrate if at all possible.
Wrap the connection factory in a UserCredentialsConnectionFactoryAdapter.
/**
* An adapter for a target JMS {#link javax.jms.ConnectionFactory}, applying the
* given user credentials to every standard {#code createConnection()} call,
* that is, implicitly invoking {#code createConnection(username, password)}
* on the target. All other methods simply delegate to the corresponding methods
* of the target ConnectionFactory.
* ...

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

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

Set ConnectionFactory for Camel JMS Producer: camel-jms Vs camel-sjms

Ciao, my basic requirement is to have a route where I can send a message and this is put on a JMS Queue. The camel context run in a JavaEE 6 container namely JBoss AS 7.1.1 so it's HornetQ for JMS which ships with it; I start the context via bootstrap singleton but I don't use the camel-cdi. So far I've been using camel-jms component, but now I'm looking to migrate to the camel-sjms if possible because springless.
My question is: what is the proper way to configure the ConnectionFactory for camel-sjms in this JavaEE scenario, please?
With the camel-jms I could put this in the endpoint URL, as simple as .to("jms:myQueue?connectionFactory=#ConnectionFactory"). With the camel-sjms instead it seems to me that I need to create an instance of the SJMSComponent myself, set the connectionFactory, and set this instance in the camel context before starting it.
I have code below for the camel-jms Vs camel-sjms case, and I would like to know if I "migrated" the setting of the ConnectionFactory correctly. Thanks.
For camel-jms this was done as:
#Singleton
#Startup
public class CamelBootstrap {
private CamelContext camelContext;
private ProducerTemplate producerTemplate;
public CamelContext getCamelContext() {
return camelContext;
}
public ProducerTemplate getProducerTemplate() {
return producerTemplate;
}
#PostConstruct
public void init() throws Exception {
camelContext = new DefaultCamelContext();
camelContext.addRoutes(new MyCamelRoutes());
camelContext.start();
producerTemplate = camelContext.createProducerTemplate();
}
}
Nothing special, and in the MyCamelRoutes I could do route configuration using:
.to("jms:myQueue?connectionFactory=#ConnectionFactory")
For camel-sjms now I have to modify the bootstrap singleton with:
#Singleton
#Startup
public class CamelBootstrap {
#Resource(mappedName="java:/ConnectionFactory")
private ConnectionFactory connectionFactory;
private CamelContext camelContext;
private ProducerTemplate producerTemplate;
public CamelContext getCamelContext() {
return camelContext;
}
public ProducerTemplate getProducerTemplate() {
return producerTemplate;
}
#PostConstruct
public void init() throws Exception {
camelContext = new DefaultCamelContext();
SjmsComponent sjms = new SjmsComponent();
sjms.setConnectionFactory(connectionFactory);
camelContext.addComponent("sjms", sjms);
camelContext.addRoutes(new MyCamelRoutes());
camelContext.start();
producerTemplate = camelContext.createProducerTemplate();
}
}
and please notice #Resource for the connectionFactory this is passed as a reference to the SjmsComponent instance, which is passed to the camelContext. And then in the MyCamelRoutes I could use the sjms while do route configuration using:
.to("sjms:myQueue")
The code seems to work correctly in both scenario, but as I understand the configuration of the ConnectionFactory is quite susceptible of performance issue if not done correctly, therefore I prefer to ask if I migrated to the camel-sjms correctly for my JavaEE scenario. Thanks again
Performance issues are likely to happend if you don't do caching/pooling of JMS resources. Caching is typically configured by wrapping a ConnectionFactory in some Caching ConnectionFactory library - or by handing over the control to the application server.
Camel SJMS includes built-in pooling. However, if you have a container managed resource to handle JMS connections, you should probably consider using it. SJMS has some facilities to deal with that, ConncetionResource instead of ConnectionFactory.

spring-boot configure non exposed properties

I am using spring-boot to configure jms and activemq connectivity. Due to a defect in activemq I need to set the idle timeout on the PooledConnectionFactory. This configuration is not exposed by spring-boot. How do I set it?
I have a #Bean to create a messageListenerContainer which has the connectionFactory as an argument. I can instanceof check the factory and configure it here but this seems not the correct way.
Downcasting to PooledConnectionFactory and calling setIdleTimeout is a perfectly reasonable approach, in my opinion.
If you'd prefer not to do it as part of the creation of the message listener container, you could declare your own ConnectionFactory bean while still making use of ActiveMQProperties. Something like this:
#Configuration
#EnableConfigurationProperties(ActiveMQProperties.class)
class CustomActiveMQConnectionFactoryConfiguration {
#Autowired
private ActiveMQProperties properties;
#Bean
public ConnectionFactory jmsConnectionFactory() {
ConnectionFactory connectionFactory = this.properties.createConnectionFactory();
if (connectionFactory instanceof PooledConnectionFactory) {
((PooledConnectionFactory) connectionFactory).setIdleTimeout(1000);
}
return connectionFactory;
}
}

Resources