Spring JMS - Access to raw message before message conversion - spring

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).

Related

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<?>.

converting activemq msg in spring boot

I am using spring boot to decode the jms message
configuration
#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);
factory.setMessageConverter(this.jacksonJmsMessageConverter());
// You could still override some of Boot's default if necessary.
return factory;
}
#Bean // Serialize message content to json using TextMessage
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setObjectMapper(new ObjectMapper());
return converter;
}
and I am able to receive the message, and decode the message
#JmsListener(destination = "myQueue", containerFactory = "myFactory")
public void receiveMessage(ActiveMQTextMessage msg) throws JMSException, IOException {
String text = msg.getText();
ObjectMapper mapper = new ObjectMapper();
MyObject obj = mapper.readValue(text, MyObject.class);
my problem is I don't want to call mapper.readValue(xxx) everytime, I prefer
to configure the mapper and conversion in the messageconverter bean, I the key
is to call the msg.getText() from the converter, but how do I get a reference of
of the msg in the converter, or there are smarter way to do it.
Spring do that for you since you have configured the MessageConverter of the factory by factory.setMessageConverter(this.jacksonJmsMessageConverter());
so use the argument like this :
#JmsListener(destination = "myQueue", containerFactory = "myFactory")
public void receiveMessage(MyObject obj) throws JMSException, IOException {
//do stuff with obj
}

Message conversion after retrieving it from activemq topic

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.

Spring-Rabbitmq MessageConverter - not invoking custom object handleMessage

I am implementing a consumer class that binds to fanout exchange in RabbitMQ and receives the message published as json. For some reason, the handleMessage within the Consumer class is not being invoked when its argument is a custom object. Same code works when the handleMessage is changed to take Object. Would appreciate your help in identity the missing piece.
Here is the configuration and consumer classes. This is not a SpringBoot application. My Configuration class has #Configuration annotation and not #SpringBootApplication.
#Bean
public SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(rabbitConnectionFactory());
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(listenerAdapter());
container.setMessageConverter(new Jackson2JsonMessageConverter());
container.setMissingQueuesFatal(false);
return container;
}
#Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(rabbitConnectionFactory());
}
#Bean
public Queue queue() {
return new Queue(QUEUE_NAME, false, false, false);
}
#Bean
public FanoutExchange exchange() {
return new FanoutExchange(EXCHANGE_NAME, false, false);
}
#Bean
public Binding inboundEmailExchangeBinding() {
return BindingBuilder.bind(queue()).to(exchange());
}
#Bean
public ConnectionFactory rabbitConnectionFactory() {
return new CachingConnectionFactory("localhost");
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory());
rabbitTemplate.setExchange(EXCHANGE_NAME);
return rabbitTemplate;
}
#Bean
MessageListenerAdapter listenerAdapter() {
return new MessageListenerAdapter(new Consumer(), "receiveMessage");
}
Here is the consumer ...
public class Consumer {
// This works
/*
public void receiveMessage(Object message) {
System.out.println("Received <" + message + ">");
}
*/
// This does not works, whereas I expect this to work.
public void receiveMessage(CustomObject message) {
System.out.println("Received <" + message + ">");
}
}
where CustomObject class is a plain POJO.
Here is an example of what is being published in RabbitMQ.
{
"state": "stable",
"ip": "1.2.3.4"
}
Its being published as json content-type
exchange.publish(message_json, :content_type => "application/json")
Appreciate all your help in making me understand the problem. Thanks.
The Jackson2JsonMessageConverter needs to be told what object to map the json to.
This can be provided via information in a __TypeId__ header (which would be the case if Spring was on the sending side); the header can either contain the full class name, or a token that is configured to map to the class name.
Or, you need to configure the converter with a class mapper.
For convenience there is a DefaultClassMapper that be configured with your target class:
ClassMapper classMapper = new DefaultClassMapper();
classMapper.setDefaultType(CustomObject.class);
converter.setClassMapper(classMapper);

How do I get my converted object using #JmsListener

I am using Spring and Jaxb to listen to a JMSQueue and then unmarshall the JMS message into a java object. I am then expecting to get that Java Object on my #JmsListener endpoint. But instead I'm getting a TextMessage object. Using a debugger I can step through the code and see that the conversion to the java object is happening but it never makes it to my end point.
Here is my config:
#Bean
public DefaultJmsListenerContainerFactory myContainerFactory() {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactoryProxy());
factory.setDestinationResolver(destinationResolver());
factory.setMessageConverter(messageConverter());
factory.setConcurrency("1-1");
return factory;
}
#Bean
public MessageConverter messageConverter(){
MarshallingMessageConverter converter = new MarshallingMessageConverter();
Jaxb2Marhsaller jaxbMarshaller = new Jaxb2Marhsaller ();
jaxbMarshaller.setPackagesToScan("mypackage.jms.model");
converter.setUnmarshaller(jaxbMarshaller);
converter.setMarshaller(jaxbMarshaller);
return converter;
}
And my endpoint:
#Component
public class QueueMessageReceiver {
#JmsListener(containerFactory = "myContainerFactory", destination = "jms/Queue")
public void process(Message message) {
try {
System.out.println(message);
}catch(Exception e){
e.printStackTrace();
}
}
}
The problem is that the QueueMessageReceiver.process method has a TextMessage and not the converted object. Help would be greatly appreciated.
Try changing the process method to use the object you are expecting and not Message
#JmsListener(containerFactory = "myContainerFactory", destination = "jms/Queue")
public void process(YourAwesomeObject theObject) {
....
}

Resources