I am playing with Spring-boot and jms message driven beans.
I installed Apache ActiveMQ.
One queue is being used on which different message types are being send and read.
One simple MessageConverter was written to convert a POJO instance into XML.
A property Class was set in the message to determine how to convert a message to a POJO:
#Component
#Slf4j
public class XMLMessageConverter implements MessageConverter {
private static final String CLASS_NAME = "Class";
private final Map<Class<?>, Marshaller> marshallers = new HashMap<>();
#SneakyThrows
private Marshaller getMarshallerForClass(Class<?> clazz) {
marshallers.putIfAbsent(clazz, JAXBContext.newInstance(clazz).createMarshaller());
Marshaller marshaller = marshallers.get(clazz);
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
return marshaller;
}
#Override
public Message toMessage(#NonNull Object object, Session session) throws JMSException, MessageConversionException {
try {
Marshaller marshaller = getMarshallerForClass(object.getClass());
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
TextMessage message = session.createTextMessage();
log.info("Created message\n{}", stringWriter);
message.setText(stringWriter.toString());
message.setStringProperty(CLASS_NAME, object.getClass().getCanonicalName());
return message;
} catch (JAXBException e) {
throw new MessageConversionException(e.getMessage());
}
}
#Override
public Object fromMessage(#NonNull Message message) throws JMSException, MessageConversionException {
TextMessage textMessage = (TextMessage) message;
String payload = textMessage.getText();
String className = textMessage.getStringProperty(CLASS_NAME);
log.info("Converting message with id {} and {}={}into java object.", message.getJMSMessageID(), CLASS_NAME, className);
try {
Class<?> clazz = Class.forName(className);
JAXBContext context = JAXBContext.newInstance(clazz);
return context.createUnmarshaller().unmarshal(new StringReader(payload));
} catch (JAXBException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
Messages of different type (OrderTransaction or Person) where send every 5 seconds to the queue:
#Scheduled(fixedDelay = 5000)
public void sendMessage() {
if ((int)(Math.random()*2) == 0) {
jmsTemplate.convertAndSend("DummyQueue", new OrderTransaction(new Person("Mark", "Smith"), new Person("Tom", "Smith"), BigDecimal.TEN));
}
else {
jmsTemplate.convertAndSend("DummyQueue", new Person("Mark", "Rutte"));
}
}
Two listeners were defined:
#JmsListener(destination = "DummyQueue", containerFactory = "myFactory")
public void receiveOrderTransactionMessage(OrderTransaction transaction) {
log.info("Received {}", transaction);
}
#JmsListener(destination = "DummyQueue", containerFactory = "myFactory")
public void receivePersonMessage(Person person) {
log.info("Received {}", person);
}
When I place breakpoints in the converter I see everything works fine but sometimes (not always) I get the following exception:
org.springframework.jms.listener.adapter.ListenerExecutionFailedException: Listener method could not be invoked with incoming message
Endpoint handler details:
Method [public void nl.smith.springmdb.configuration.MyListener.**receiveOrderTransactionMessage**(nl.smith.springmdb.domain.**OrderTransaction**)]
Bean [nl.smith.springmdb.configuration.MyListener#790fe82a]
; nested exception is org.springframework.messaging.converter.MessageConversionException: Cannot convert from [nl.smith.springmdb.domain.**Person**] to [nl.smith.springmdb.domain.**OrderTransaction**] for org.springframework.jms
It seems that after the conversion Spring invokes the wrong method.
I am complete in the dark why this happens.
Can somebody clarify what is happening?
I am using JMS to send receive message from IBM MQ message broker. I am currently working on listener service throwing unhandled excepion and message sent
back to queue without acknowledgement.
I want the service to retry a configurable number of time and throw meaning full exception message that listener service is unavailable.
My listener and container factory looks like below.
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(String message) throws JMSException
{ handle(message); }
#Bean(name = "queuejmsfactory") public JmsListenerContainerFactory getQueueTopicFactory(ConnectionFactory con ,
DefaultJmsListenerContainerFactoryConfigurer config)
{ DefaultJmsListenerContainerFactory d = new DefaultJmsListenerContainerFactory();
d.setSessionTransacted(true);
d.setSessionAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
config.configure(d,con);
return d; }
I short, I have an existing code using the SessionawareMessageListener onMessage which i am trying to
replicate to #JmsListener. How do i handle the session commit and rollback automatically and
how do i get the session in JmsListener if have to handle manually similar to onMessage.
#Override
public void onMessage(Mesage mes, Session ses) throws JMSException
{ try
{ TestMessage txtMessage = (TextMessage)message;
handle(txtMessage); ses.commit();
} catch (Exception exp)
{ if (shouldRollback(message))
{ ses.rollback();}
else{logger,warn("moved to dlq");
ses.commit();
}
} }
private boolean shouldRollback(Message mes) throws JMSException
{ int rollbackcount = mes.getIntProperty("JMSXDeliveryCount");
return (rollbackcount <= maxRollBackCountFromApplication.properties)
}
Updated code:
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(Message message) throws JMSException
{
try {TestMessage txtMessage = (TextMessage)message;
handle(txtMessage);}
catch(Excepton ex) {
if shouldRollback(message)
{throw ex;}
else {logger.warn("moved to dlq")}
}}
private boolean shouldRollback(Message mes) throws JMSException
{ int rollbackcount = mes.getIntProperty("JMSXDeliveryCount");
return (rollbackcount <= maxRollBackCountFromApplication.properties)
}
#Bean(name = "queuejmsfactory") public JmsListenerContainerFactory getQueueTopicFactory(ConnectionFactory con ,
DefaultJmsListenerContainerFactoryConfigurer config)
{ DefaultJmsListenerContainerFactory d = new DefaultJmsListenerContainerFactory();
d.setSessionTransacted(true);
d.setSessionAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);
config.configure(d,con);
return d; }
I have also tried to access the JMSXDeliveryCount from Headers, but couldnt get the exact object to access delivery count. Can you clarify plz.
#JmsListener(destination = "testqueue", containerFactory = "queuejmsfactory")
public void consumer(Message message,
#Header(JmsHeaders.CORRELATION_ID) String correlationId,
#Header(name = "jms-header-not-exists") String nonExistingHeader,
#Headers Map<String, Object> headers,
MessageHeaders messageHeaders,
JmsMessageHeaderAccessor jmsMessageHeaderAccessor) {}
You can add the Session as another parameter to the JmsListener method.
Let see one scenario is there when jms send message to consumer and we have to save that to db,but when db is down we cant able to save that to db due to db server is down.So how to acknowledge jms to send message again?
This issue raised by many people.I resolved this using below code snippet.
#Configuration
public class AppConfiguration {
#Bean
public JmsListenerContainerFactory<?> jmsContainerFactory(DefaultJmsListenerContainerFactoryConfigurer configurer) {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setClientID(clientId);
connectionFactory.setBrokerURL(brokerUrl);
CachingConnectionFactory cf = new CachingConnectionFactory(connectionFactory);
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(cf);
factory.setSubscriptionDurable(true);
configurer.configure(factory, cf);
return factory;
}
}
#Component("ApprovalSubsCriber")
public class ApprovalSubscriber implements SessionAwareMessageListener<TextMessage> {
#Override
#JmsListener(destination = "topic/jmsReplyTest1", containerFactory = "jmsContainerFactory", subscription = "ApprovalSubsCriber")
public void onMessage(TextMessage message, Session session) throws JMSException {
// This is the received message
System.out.println("Receive: " + message.getText());
// Let's prepare a reply message - a "ACK" String
ActiveMQTextMessage textMessage = new ActiveMQTextMessage();
textMessage.setText("ACK");
System.out.println(session.getAcknowledgeMode());
if ("notexception".equals(message.getText())) {
session.commit();
} else {
session.rollback();//If exception comes
}
}
}
I am setting PubSub using RabbitMQ, so whenever there is some failure in message processing a negative acknowledgement is sent in Listener, ideally retry should be done after some preconfigured time interval but retries are done continuously for each second till time of message-ttl value. For retry added created Advice chain in SimpleMessageListenerContainer. Queues and consumers are created dynamically depending on subscription. Below is code for rabbitmq service
#Service
public class RabbitMQQueueServiceImpl {
private final String JAVA_OBJECT_SERIALIZER_HEADER_NAME = "Content-Type";
private final String JAVA_OBJECT_SERIALIZER_HEADER_VALUE = "application/x-java-serialized-object";
private final String TIME_TO_LIVE_HEADER_NAME = "x-message-ttl";
#Value("${rabbitmq.message.timetolive}")
private Long messageTimeToLive;
#Value("${rabbitmq.host}")
private String host;
#Value("${rabbitmq.username}")
private String userName;
#Value("${rabbitmq.password}")
private String password;
#Value("${rabbitmq.port}")
private Integer port;
private RabbitTemplate rabbitTemplate;
#PostConstruct
public void init() {
createRabitTemplate();
}
public void subscribe(WebHookSubscription webHookSubscription,String queueName) throws Exception {
createProducer(webHookSubscription,queueName);
createConsumer(webHookSubscription,queueName);
}
public void publish(PublishEvent publishEvent,String queueName) {
//Append eventId for queueName and exchange name as accountId_accountTypeId
String exchange = queueName;
queueName = queueName+"_"+publishEvent.getEventId();
BroadcastMessageBean broadcastMessageBean = new BroadcastMessageBean();
/*
* Set properties for message - message persistence, JAVA object serializer
*/
MessageProperties messageProperties = new MessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
messageProperties.setHeader(JAVA_OBJECT_SERIALIZER_HEADER_NAME, JAVA_OBJECT_SERIALIZER_HEADER_VALUE);
BeanUtils.copyProperties(publishEvent, broadcastMessageBean);
Gson gson = new Gson();
Message messageBean = new Message(gson.toJson(broadcastMessageBean).getBytes(), messageProperties);
rabbitTemplate.send(exchange, publishEvent.getEventId().toString(), messageBean);
}
public void createQueue(String queueName) {
RabbitTemplate rabbitTemplate = getRabbitTemplate(queueName);
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
Queue queue = new Queue(queueName, true,false,false);
rabbitAdmin.declareQueue(queue);
}
private void createProducer(WebHookSubscription webHookSubscription, String queueName) {
queueName = queueName+"_"+webHookSubscription.getEventId();
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
String exchangeName = webHookSubscription.getAccountId()+"_"+webHookSubscription.getAccountTypeId();
String routingKey = webHookSubscription.getEventId().toString();
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put(TIME_TO_LIVE_HEADER_NAME, messageTimeToLive);
Queue queue = new Queue(queueName, true,false,false, arguments );
rabbitAdmin.declareQueue(queue);
Exchange exchange = new TopicExchange(exchangeName, true, false) ;
rabbitAdmin.declareExchange(exchange );
Binding binding = new Binding(queueName, DestinationType.QUEUE, exchangeName, routingKey, arguments);
rabbitAdmin.declareBinding(binding );
}
private void createConsumer(WebHookSubscription webHookSubscription,String queueName) throws Exception {
Map<String, String> headers = new HashMap<String, String>();
//Create exchange name same as queue name and append eventId to queueName for each event subscription for account
queueName = queueName+"_"+webHookSubscription.getEventId();
for(WebHookSubscriptionHeaders header : webHookSubscription.getWebHookHeaders()) {
headers.put(header.getName(), header.getValue());
}
/*
* Register consumer through SimpleMessageListenerContainer
* add consumer handler with eventId
*/
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setPrefetchCount(10);
container.setRecoveryInterval(1000);
ConsumerHandler consumerHandler = new ConsumerHandler(webHookSubscription.getEventId(),
webHookSubscription.getWebHookURL(),headers, webHookSubscription.getNotificationEmail());
container.setConnectionFactory(connectionFactory());
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setAdviceChain(new Advice[]{retryAdvice()});
/*
* Register consumer to queue and all messages will be broadcasted to subscriber specific queue
*/
container.setQueues(new Queue(queueName,true,false,false));
container.setMessageListener(consumerHandler);
container.start();
}
private MethodInterceptor retryAdvice() {
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(15000);
backOffPolicy.setMultiplier(100);
backOffPolicy.setMaxInterval(604800);
return RetryInterceptorBuilder.stateless().backOffPolicy(backOffPolicy).maxAttempts(5).recoverer(new RejectAndDontRequeueRecoverer()).build();
}
/**
* Create Connection factory for RabbitMQ template
* #return
*/
private CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(getHost());
connectionFactory.setUsername(getUserName());
connectionFactory.setPassword(getPassword());
connectionFactory.setPort(getPort());
return connectionFactory;
}
private RabbitTemplate createRabitTemplate() {
rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setMessageConverter(new JsonMessageConverter());
//Create retries configuration for message delivery
/*RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(messageRetriesInitialInterval);
backOffPolicy.setMaxInterval(messageRetriesMaxInterval);
backOffPolicy.setMultiplier(messageRetriesMultiplier);
retryTemplate.setBackOffPolicy(backOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(retries);
retryTemplate.setRetryPolicy(retryPolicy);
//Retries configuration ends here
rabbitTemplate.setRetryTemplate(retryTemplate);*/
return rabbitTemplate;
}
}
This is for consumer handler
public class ConsumerHandler implements ChannelAwareMessageListener {
public ConsumerHandler(Long eventId, String url, Map<String, String> headers, String email) {
this.eventId = eventId;
this.url = url;
this.headers = headers;
this.email = email;
}
#Override
public void onMessage(Message message, Channel channel) throws IOException, NoSuchAlgorithmException {
//process message body
if(error){
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
return;
}
//for success
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
What is missing in above code to make retry work?
logs:
2016-02-23 16:34:09 DEBUG [org.springframework.amqp.rabbit.listener.BlockingQueueConsumer] - <Storing delivery for Consumer: tags=[{amq.ctag-a1IonIIN7mf8uhIGXE4eIw=14947_0_4}], channel=Cached Rabbit Channel: AMQChannel(amqp://guest#127.0.0.1:5672/,1), acknowledgeMode=MANUAL local queue size=0>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor] - <Exiting proxied method in stateful retry with result: (null)>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor] - <Exiting proxied method in stateful retry with result: (null)>
2016-02-23 16:34:09 DEBUG [org.springframework.amqp.rabbit.listener.BlockingQueueConsumer] - <Retrieving delivery for Consumer: tags=[{amq.ctag-a1IonIIN7mf8uhIGXE4eIw=14947_0_4}], channel=Cached Rabbit Channel: AMQChannel(amqp://guest#127.0.0.1:5672/,1), acknowledgeMode=MANUAL local queue size=1>
2016-02-23 16:34:09 DEBUG [org.springframework.amqp.rabbit.listener.BlockingQueueConsumer] - <Received message: (Body:'[B#4109b410(byte[199])'MessageProperties [headers={eventId=4, retry=1, Content-Type=application/x-java-serialized-object}, timestamp=null, messageId=31b93494-d6ef-4d63-b90b-c7a7e73acb70, userId=null, appId=null, clusterId=null, type=null, correlationId=null, replyTo=null, contentType=application/octet-stream, contentEncoding=null, contentLength=0, deliveryMode=PERSISTENT, expiration=null, priority=0, redelivered=true, receivedExchange=14947_0, receivedRoutingKey=4, deliveryTag=10234, messageCount=0])>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor] - <Executing proxied method in stateful retry: public abstract void org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$ContainerDelegate.invokeListener(com.rabbitmq.client.Channel,org.springframework.amqp.core.Message) throws java.lang.Exception(27f49de4)>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.support.RetryTemplate] - <Retry: count=0>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor] - <Executing proxied method in stateful retry: public abstract void org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$ContainerDelegate.invokeListener(com.rabbitmq.client.Channel,org.springframework.amqp.core.Message) throws java.lang.Exception(27f49de4)>
2016-02-23 16:34:09 DEBUG [org.springframework.retry.support.RetryTemplate] - <Retry: count=0>
There is no direct way to ensure delayed retry.
You may have to create a Dead letter exchange and create a retryQueue on that exchange with the message ttl set to the the delay interval you want.
Your main queue should look something like this
#Bean
public Queue mainQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "retryExchangeName");
return new Queue("mainQueue", true, false, false, args);
}
and retry Queue
#Bean
public Queue retryQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "mainExchangeName");
args.put("x-message-ttl", RETRY_MESSAGE_TTL);
return new Queue("retryQueue", true, false, false, args);
}
now for the messages you want a delayed retry Nack them with requeue=false
channel().basicNack(10, false, false);
now this message will be sent to retryQueue and after the specified ttl it will be put back to the mainQueue for reprocessing.
Hi i've added this WebListener class to my webproject
#WebListener
public class SelfSend implements ServletContextListener {
private MessageProducer producer;
private Connection sendconnection;
private Connection receiveconnection;
private Session sendsession;
private Session receivesession;
private MessageConsumer receiver;
#Override
public void contextInitialized(ServletContextEvent arg0) {
try {
InitialContext initCtx = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory) initCtx.lookup("java:comp/env/jms/ConnectionFactory");
sendconnection = connectionFactory.createConnection();
receiveconnection = connectionFactory.createConnection();
sendsession = sendconnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
receivesession = receiveconnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
producer = sendsession.createProducer((Destination) initCtx.lookup("java:comp/env/jms/queue/MyQueue"));
receiver = receivesession.createConsumer((Destination) initCtx.lookup("java:comp/env/jms/queue/MyQueue"));
receiver.setMessageListener(new MessageListener() {
#Override
public void onMessage(Message message) {
System.out.println("MESSAGE RECEIVED");
}
});
TextMessage testMessage = sendsession.createTextMessage();
testMessage.setStringProperty("from", "ki");
producer.send(testMessage);
System.out.println("MESSAGE SENT");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public void contextDestroyed(ServletContextEvent arg0) {
}
}
But the message is never received.
When i put the reciver in a #WebServlet like this
#Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
try {
InitialContext initCtx = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory) initCtx.lookup("java:comp/env/jms/ConnectionFactory");
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
receiver = session.createConsumer((Destination) initCtx.lookup("java:comp/env/jms/queue/MyQueue"));
receiver.setMessageListener(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
mud = new MongoUserdata();
}
i recive the message, when i put it in both i receive only every second message with the Servlet-Receiver, the other messasge seems to be lost.
Can anyone explain theis odd behaviour to me?
In your first example class you don't appear to be starting the receiver connection which would mean it will not dispatch any messages that are received. It will however hold onto incoming messages in the consumer prefetch buffer leading to the every other message receive that you are experiencing.