Message conversion after retrieving it from activemq topic - jms

I wanted to implement messaging between microservices using ActiveMQ as a broker, but after setting everyting as should be, one thing got me stuck. Before I describe the problem, here is I am approaching messaging:
Producer config:
#Slf4j
#Configuration
#EnableJms
public class JmsConfig {
public static final String EBAY_TOPIC = "ebay.topic";
#Bean
public ActiveMQTopic destinationTopic() {
return new ActiveMQTopic(EBAY_TOPIC);
}
#Bean
public JmsListenerContainerFactory<?> connectionFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setPubSubDomain(true);
return factory;
}
#Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
#Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
jmsTemplate.setDefaultDestinationName(EBAY_TOPIC);
jmsTemplate.setConnectionFactory(connectionFactory);
jmsTemplate.setMessageConverter(messageConverter);
jmsTemplate.setPubSubDomain(true);
return jmsTemplate;
}
}
Producer sending message:
#RequiredArgsConstructor
#Slf4j
#RestController
#RequestMapping("/message")
public class IntercommunicationController {
private final JmsTemplate jmsTemplate;
#PostMapping("/send")
public void sendMessageToOtherService(#RequestBody Message message) {
jmsTemplate.convertAndSend(JmsConfig.EBAY_TOPIC, message);
}
}
Receiver config:
#Configuration
#EnableJms
public class JmsConfig {
public static final String EBAY_TOPIC = "ebay.topic";
#Bean
public JmsListenerContainerFactory<?> connectionFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer,
ErrorHandler errorHandler) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setErrorHandler(errorHandler);
factory.setPubSubDomain(true);
return factory;
}
#Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
}
Receiver implementation:
#Slf4j
#Component
public class MessageListener {
#JmsListener(destination = JmsConfig.EBAY_TOPIC, containerFactory = "connectionFactory")
public void receiveMessage(#Payload Message receivedMessage) {
log.info("Got message saying {}", receivedMessage);
}
}
POJO which is being sent:
#NoArgsConstructor
#Data
public class Message {
String content;
String from;
}
Problem:
I'm getting error once I try to POST object through json and send it on topic. Once message get to the topic, the consumer can't handle deserialization of the message as in TypeIdPropertyName is different package path of the object which I'm sending to the one I'm trying to receive, they are 1:1 in both applications, but I'm getting:
2018-03-20 16:32:08.601 ERROR 1568 --- [enerContainer-1] c.g.g.s.c.config.MessageErrorHandler : Listener method 'public void com.gft.graduate2018.sabb.client.listener.MessageListener.receiveMessage(client.domain.Message)' threw exception; nested exception is org.springframework.jms.support.converter.MessageConversionException: Failed to resolve type id [backend.model.dtos.Message]; nested exception is java.lang.ClassNotFoundException: backend.model.dtos.Message
What's the proper way of addressing that problem ? I could end up writing custom parser from string to object but probably that would not be the best solution. Hopefully there is someone who dealt with it before and can help resolve that problem :)

See...
/**
* Specify mappings from type ids to Java classes, if desired.
* This allows for synthetic ids in the type id message property,
* instead of transferring Java class names.
* <p>Default is no custom mappings, i.e. transferring raw Java class names.
* #param typeIdMappings a Map with type id values as keys and Java classes as values
*/
public void setTypeIdMappings(Map<String, Class<?>> typeIdMappings) {
...
}
...on the converter.
Set the mappings on both sides, so the __type header just contains a token that's mapped from the class name on the sending side and to the class name on the receiving side.

Related

Cannot publish Topic-message to two subscriber simlutaneously using ActiveMQ

I have referred to the SpringBoot application publish and read from ActiveMQ topic to, publish a Topic using ActiveMQ. I have created two receiver micro-services which reads message from the topic.I have also created rest endpoint to publish the Topic.However, I have to execute this rest end point two times to publish the message for two receivers 1). The first execution of rest endpoint will send message to Receiver1
2). The second execution of rest endpoint will send message to Receiver2
Hence 2 receivers could not read from the Topic simultaneously.
Here is my code.
PublisherApplication.java
package com.springboot;
//import statements
#SpringBootApplication
#EnableDiscoveryClient
#EnableJms
public class PublisherApplication {
#Bean
public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// This provides all boot's default to this factory, including the message converter
configurer.configure(factory, connectionFactory);
//setPubSubDomain identifies Topic in ActiveMQ
factory.setPubSubDomain(true);
return factory;
}
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class, args);
}
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
}
PublishMessage.java
[Rest-end point to publish Topic]
package com.springboot.controller;
//import statements
#RestController
#RequestMapping(path = "/schoolDashboard/topic")
class PublishMessage {
public static final String MAILBOX_TOPIC = "mailbox.topic";
#Autowired
private JmsTemplate jmsTemplate;
#GetMapping(path = "/sendEmail")
public void sendStudentById() throws Exception{
System.out.println("Anindya-TopicSendMessage.java :: Publishing Email sent....");
jmsTemplate.convertAndSend(MAILBOX_TOPIC, "Topic - Email Sent");
}
}
ReceiverApplication01
[Note - Receiver01 is first microservice]
package com.springboot;
//import statements
#SpringBootApplication
#EnableJms
public class ReceiverApplication01 {
#Bean
public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// This provides all boot's default to this factory, including the message converter
configurer.configure(factory, connectionFactory);
//setPubSubDomain identifies Topic in ActiveMQ
factory.setPubSubDomain(true);
return factory;
}
public static void main(String[] args) {
SpringApplication.run(ReceiverApplication01.class, args);
}
}
TopicMesssgeReceiver01.java
[Receiver01 Read message from Topic]
package com.springboot.message;
//import statement
#Component
public class TopicMesssgeReceiver01 {
private final SimpleMessageConverter converter = new SimpleMessageConverter();
public static final String MAILBOX_TOPIC = "mailbox.topic";
#JmsListener(destination = MAILBOX_TOPIC, containerFactory = "topicListenerFactory")
public void receiveMessage(final Message message) throws JMSException{
System.out.println("Receiver01 <" + String.valueOf(this.converter.fromMessage(message)) + ">");
}
}
ReceiverApplication02
[Note:-Receiver02 is second microservice]
package com.springboot;
//import statement
#SpringBootApplication
#EnableJms
public class ReaderApplication02 {
#Bean
public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(true);
configurer.configure(factory, connectionFactory);
return factory;
}
public static void main(String[] args) {
SpringApplication.run(ReaderApplication02.class, args);
}
}
TopicMesssgeReceiver02
[Receiver02 Read message from Topic]
package com.springboot.message;
//import statement
#Component
public class TopicMesssgeReceiver02 {
private final SimpleMessageConverter converter = new SimpleMessageConverter();
public static final String MAILBOX_TOPIC = "mailbox.topic";
#JmsListener(destination = MAILBOX_TOPIC, containerFactory = "topicListenerFactory")
public void receiveMessage(final Message message) throws Exception{
System.out.println("Receiver02 <" + String.valueOf(this.converter.fromMessage(message)) + ">");
}
}
Thanks Naveen!! Finally, I am able to do it.
We have to set only setPubSubDomain(true); and spring-boot will take care all boiler-plate code. Now, two receiver Microservices can read message from Topic simultaneously Following are the code changes
PublishMessage.java
[Rest-end point to publish Topic]
package com.springboot.controller;
//import statements
#RestController
#RequestMapping(path = "/schoolDashboard/topic")
class PublishMessage {
public static final String MAILBOX_TOPIC = "mailbox.topic";
#Autowired
private JmsTemplate jmsTemplate;
#GetMapping(path = "/sendEmail")
public void sendStudentById() throws Exception{
System.out.println("Publisher :: Message sent...");
/* Added this statement. setPubSubDomain(true) identifies Topic in ActiveMQ */
jmsTemplate.setPubSubDomain(true);
jmsTemplate.convertAndSend(MAILBOX_TOPIC, "Topic - Email Sent");
}
}
ReceiverApplication02
[Note:-Receiver02 is second microservice]
package com.springboot;
//import statement
#SpringBootApplication
#EnableJms
public class ReaderApplication02 {
#Bean
public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
/* setPubSubDomain(true) should be placed after
* configuration of the specified jms listener container factory*/
factory.setPubSubDomain(true);
return factory;
}
public static void main(String[] args) {
SpringApplication.run(ReaderApplication02.class, args);
}
}

Spring Boot RabbitMQ Null Pointer Exception Error

I am using RabbitMQ with Spring Boot to broker messages between two services. I am able to receive the message and format it but when I call a service class in the onMessage method, I get a null pointer exception error. Here is my message listener class which receives the message
public class QueueListener implements MessageListener{
#Autowired
private QueueProcessor queueProcessor;
#Override
public void onMessage(Message message) {
String msg = new String(message.getBody());
String output = msg.replaceAll("\\\\", "");
String jsonified = output.substring(1, output.length()-1);
JSONArray obj = new JSONArray(jsonified);
queueProcessor.processMessage(obj);
}
}
Calling the method processMessage throws null pointer exception
Can someone point to me what I ma doing wrong?
I found out the issue was in the RabbitMqConfig class. Here is the code which was causing the error:
#Configuration
public class RabbitMqConfig {
private static final String QUEUE_NAME = "my.queue.name";
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("<url.to.rabbit>");
connectionFactory.setUsername("<username>");
connectionFactory.setPassword("<password>");
return connectionFactory;;
}
#Bean
public Queue simpleQueue() {
return new Queue(QUEUE_NAME);
}
#Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setRoutingKey(QUEUE_NAME);
template.setMessageConverter(jsonMessageConverter());
return template;
}
#Bean
public SimpleMessageListenerContainer userListenerContainer() {
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory());
listenerContainer.setQueues(simpleQueue());
listenerContainer.setMessageConverter(jsonMessageConverter());
listenerContainer.setMessageListener(new QueueListener());
listenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
return listenerContainer;
}
}
The line listenerContainer.setMessageListener(new QueueListener()); was the source of the error. I solved it by Autowiring the class instead of using new. Here is the working code
#Configuration
public class RabbitMqConfig {
private static final String QUEUE_NAME = "my.queue.name";
#Autowired
private QueueListener queueListener;
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("<url.to.rabbit>");
connectionFactory.setUsername("<username>");
connectionFactory.setPassword("<password>");
return connectionFactory;
}
#Bean
public Queue simpleQueue() {
return new Queue(QUEUE_NAME);
}
#Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setRoutingKey(QUEUE_NAME);
template.setMessageConverter(jsonMessageConverter());
return template;
}
/*#Bean
public SimpleMessageListenerContainer userListenerContainer() {
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory());
listenerContainer.setQueues(simpleQueue());
listenerContainer.setMessageConverter(jsonMessageConverter());
listenerContainer.setMessageListener(queueListener);
listenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
return listenerContainer;
}
}
Hope this helps someone else
Make sure the QueueListener is a component class or service class that can be managed by the Spring IoC. Otherwise, the config class cannot make this a bean out of the box, since this is just a normal Java class that need to be in the container #runtime.
So when u write new QueueListener() in yr config class, then the Java class is not in the SpringContext at the time when the config class is instantiated and is therefore null.
Hope this helps clear out some of this issue!

Custom MessageConverter with Spring JmsMessagingTemplate is not working as I expected

I'm trying to attach a custom message converter that implements org.springframework.jms.support.converter.MessageConverter, to a JmsMessagingTemplate.
I've read somewhere that we can attach the message converter to a MessagingMessageConverter by calling setPayloadConverter, and then attach that messaging message converter to the JmsMessagingTemplate via setJmsMessageConverter. After that, I call convertAndSend, but I notice that it doesn't convert the payload.
When I debugged the code, I notice that setting Jms Message Converter doesn't set the converter instance variable in the JmsMessagingTemplate. So when the convertAndSend method calls doConvert and tries to getConverter, it is getting the default simple message converter and not my custom one.
My question is, can I use an implementation of org.springframework.jms.support.converter.MessageConverter with a JmsMessagingTemplate? Or do I need to use an implementation of org.springframework.messaging.converter.MessageConverter?
I'm using Spring Boot 1.4.1.RELEASE, and Spring 4.3.3.RELEASE. The code is below.
Configuration
#Configuration
#EnableJms
public class MessagingEncryptionPocConfig {
/**
* Listener ActiveMQ Connection Factory
*/
#Bean(name="listenerActiveMqConnectionFactory")
public ActiveMQConnectionFactory listenerActiveMqConnectionFactory() {
return new ActiveMQConnectionFactory("admin","admin","tcp://localhost:61616");
}
/**
* Producer ActiveMQ Connection Factory
*/
#Bean(name="producerActiveMqConnectionFactory")
public ActiveMQConnectionFactory producerActiveMqConnectionFactory() {
return new ActiveMQConnectionFactory("admin","admin","tcp://localhost:61616");
}
/**
* Caching Connection Factory
*/
#Bean
public CachingConnectionFactory cachingConnectionFactory(#Qualifier("producerActiveMqConnectionFactory") ActiveMQConnectionFactory activeMqConnectionFactory) {
return new CachingConnectionFactory(activeMqConnectionFactory);
}
/**
* JMS Listener Container Factory
*/
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(#Qualifier("listenerActiveMqConnectionFactory") ActiveMQConnectionFactory connectionFactory, MessagingMessageConverter messageConverter) {
DefaultJmsListenerContainerFactory defaultJmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
defaultJmsListenerContainerFactory.setConnectionFactory(connectionFactory);
defaultJmsListenerContainerFactory.setMessageConverter(messageConverter);
return defaultJmsListenerContainerFactory;
}
/**
* Jms Queue Template
*/
#Bean(name="queueTemplate")
public JmsMessagingTemplate queueTemplate(CachingConnectionFactory cachingConnectionFactory, MessageConverter messagingMessageConverter) {
JmsMessagingTemplate queueTemplate = new JmsMessagingTemplate(cachingConnectionFactory);
queueTemplate.setJmsMessageConverter(messagingMessageConverter);
return queueTemplate;
}
#Bean
public MessageConverter encryptionDecryptionMessagingConverter(Jaxb2Marshaller jaxb2Marshaller) {
MessageConverter encryptionDecryptionMessagingConverter = new EncryptionDecryptionMessagingConverter(jaxb2Marshaller);
MessagingMessageConverter messageConverter = new MessagingMessageConverter();
messageConverter.setPayloadConverter(encryptionDecryptionMessagingConverter);
return messageConverter;
}
/**
* Jaxb marshaller
*/
#Bean(name="producerJaxb2Marshaller")
public Jaxb2Marshaller jaxb2Marshaller() {
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setPackagesToScan("com.schema");
return jaxb2Marshaller;
}
}
MessageProducer Class
#Component
public class MessageProducer {
private static final Logger LOG = LoggerFactory.getLogger(MessageProducer.class);
#Autowired
#Qualifier("queueTemplate")
private JmsMessagingTemplate queueTemplate;
public void publishMsg(Transaction trx, Map<String,Object> jmsHeaders, MessagePostProcessor postProcessor) {
LOG.info("Sending Message. Payload={} Headers={}",trx,jmsHeaders);
queueTemplate.convertAndSend("queue.source", trx, jmsHeaders, postProcessor);
}
}
Unit Test
#RunWith(SpringRunner.class)
#SpringBootTest
#ActiveProfiles("test")
public class WebsMessagingEncryptionPocApplicationTests {
#Autowired
private MessageProducer producer;
#Autowired
private MessageListener messageListener;
/**
* Ensure that a message is sent, and received.
*/
#Test
public void testProducer() throws Exception{
//ARRANGE
CountDownLatch latch = new CountDownLatch(1);
messageListener.setCountDownLatch(latch);
Transaction trx = new Transaction();
trx.setCustomerAccountID(new BigInteger("111111"));
Map<String,Object> jmsHeaders = new HashMap<String,Object>();
jmsHeaders.put("tid", "1234563423");
MessagePostProcessor encryptPostProcessor = new EncryptMessagePostProcessor();
//ACT
producer.publishMsg(trx, jmsHeaders, encryptPostProcessor);
latch.await();
//ASSERT - assertion done in the consumer
}
}
The converter field is used to convert your input params to a spring-messaging Message<?>.
The JMS converter is used later (in MessagingMessageCreator) to then create a JMS Message from the messaging Message<?>.

Spring JMS - Access to raw message before message conversion

I have used a message converter to convert the XML message from queue to a Java Object and it works fine.
Since my JMSMessageListener get the POJO directly, I would like to know is there any way I can have access to the raw XML which was originally placed in queue.
As part of message tracking, I need to maintain a copy of the raw xml message.
Is there any call back available in spring jms so that I can persits the xml message before it is converted into POJO ?
My application is spring boot and I am configuring the message convertor in the below code
#Configuration
#EnableJms
public class JMSConfig {
#Bean
public JmsListenerContainerFactory<?> myFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// 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;
}
#Bean
public MarshallingMessageConverter createMarshallingMessageConverter(final Jaxb2Marshaller jaxb2Marshaller) {
return new MarshallingMessageConverter(jaxb2Marshaller);
}
#Bean
public Jaxb2Marshaller createJaxb2Marshaller() {
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setPackagesToScan("com.mypackage.messageconsumer.dto");
Map<String, Object> properties = new HashMap<>();
properties.put(Marshaller.JAXB_FORMATTED_OUTPUT, true);
jaxb2Marshaller.setMarshallerProperties(properties);
return jaxb2Marshaller;
}
}
This is the listener code
#Component
public class NotificationReader {
#JmsListener(destination = "myAppQ")
public void receiveMessage(NotificationMessage notificationMessage) {
System.out.println("Received <" + notificationMessage.getStaffNumber() + ">");
// how to get access to the raw xml recieved by sender ?
persistNotification(notificationMessage);
}
Something like this should work...
#Bean
public MarshallingMessageConverter createMarshallingMessageConverter(final Jaxb2Marshaller jaxb2Marshaller) {
return new MarshallingMessageConverter(jaxb2Marshaller) {
#Override
public Object fromMessage(Message message) throws JMSException, MessageConversionException {
Object object = super.fromMessage(message);
((MyObject) object).setSourceXML(((TextMessage) message).getText());
return object;
}
}
}
...but you should add more checks (e.g. verify types before casting).

Access connectionfactory inside messageListener onMessage

I am creating a rabbitmq messageListener and would like to be able to access the connectionfactory configuration while in the onMessage method, is that possible? It would be useful for logging and other details. Being able to log the vhost from which the message was delivered would be helpful and it is not available in the message itself. Here is my consumer and config
public class Consumer implements MessageListener {
#Override
public void onMessage(Message message) {
//how can I get the connection factory configuration when a message is sent?
}
here is the config
{
#Configuration
#EnableAutoConfiguration
public class RabbitConfig {
private static final String SIMPLE_MESSAGE_QUEUE = "qDLX2.dlq";
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("server");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("ad,om");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("vhost1");
return connectionFactory;
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
#Autowired
private Consumer consumer;
#Bean
public SimpleMessageListenerContainer listenerContainer() {
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory());
listenerContainer.setQueueNames(SIMPLE_MESSAGE_QUEUE);
listenerContainer.setMessageListener(consumer);
listenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
return listenerContainer;
}
}
Thanks
Gregg
There's no standard way of doing that.
You could inject the connection factory into the consumer field in your listenerContainer bean definition...
listenerContainer.setMessageListener(this.consumer);
this.consumer.setConnectionFactory(connectionFactory());
You could probably #Autowire it too.
You can then use getRabbitConnectionFactory().getVirtualHost() to access the vhost.

Resources