/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jadira.jms.template;
import java.util.ArrayList;
import java.util.List;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;
import org.jadira.jms.container.BatchedMessageListenerContainer;
import org.jadira.jms.mdp.AbstractMessageDriven;
import org.springframework.jms.JmsException;
import org.springframework.jms.connection.JmsResourceHolder;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.SessionCallback;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.support.JmsUtils;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* BatchedJmsTemplate customises Spring's {@link JmsTemplate} with additional methods that enable multiple items to be processed in a single transaction for the various supported operations. The
* additional methods are identified by the suffix 'batch'.
* <p>
* As with {@link BatchedMessageListenerContainer}, Within each transaction, the first read is a blocking read, that blocks for {@link JmsTemplate#setReceiveTimeout(long)}. Subsequent messages up to
* the maximum batch size {@link #setBatchSize(int)} are read as non-blocking reads, with the batch completing as soon as the queue cannot service further messages.
* </p>
* <p>
* Users of this class must handle rollback appropriately. A rollback triggered by failure processing a single message will cause all the messages in the transaction to rollback. It is recommended to
* design you message processing so that rollback only occurs for fatal, unexpected and unrecoverable errors such as a failure in the infrastructure. You should handle other errors by, for example,
* delivering messages directly to an error queue rather than throwing an exception. To assist in constructing this pattern, the {@link AbstractMessageDriven} POJO is also provided which provides the
* basic framework for implementing a {@link MessageListener} that is aligned with this contract.
* </p>
* <p>
* NB. Due to the design and structure of Spring's {@link DefaultMessageListenerContainer} and its superclasses, implementing this class must by necessity duplicate certain parts of
* {@link DefaultMessageListenerContainer}. Consequently, this class has been managed at a source code level as a derivative of {@link DefaultMessageListenerContainer} and copyright messages and
* attributions reflect this.
* </p>
* @author Mark Pollack and Juergen Hoeller were the original authors of the {@link JmsTemplate}. Modifications to this class to enable batching were made by Chris Pheby.
*/
public class BatchedJmsTemplate extends JmsTemplate {
private static final TransactionSynchronization BATCH_SYNCHRONIZATION = new BatchTransactionSynchronization();
/**
* Flag to indicate the start of a batch
*/
private static final ThreadLocal<Boolean> IS_START_OF_BATCH = new ThreadLocal<Boolean>() {
protected Boolean initialValue() {
return Boolean.TRUE;
}
};
/**
* The default maximum size for a transactional batch
*/
public static final int DEFAULT_BATCH_SIZE = 150;
/**
* The configured maximum batch size, must be at least 1
*/
private int batchSize = DEFAULT_BATCH_SIZE;
/**
* Creates a new instance
*/
public BatchedJmsTemplate() {
}
/**
* Configures the maximum number of messages that can be read in a transaction
* @param batchSize The maximum batch size
*/
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
/**
* Get the maximum number of messages that can be read in a transaction
* @return The maximum batch size
*/
public int getBatchSize() {
return batchSize;
}
/**
* Receive a batch of up to default batch size for the default destination. Other than batching this method is the same as {@link JmsTemplate#receive()}
* @return A list of {@link Message}
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch() throws JmsException {
return receiveBatch(batchSize);
}
/**
* Receive a batch of up to batchSize. Other than batching this method is the same as {@link JmsTemplate#receive()}
* @return A list of {@link Message}
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch(int batchSize) throws JmsException {
Destination defaultDestination = getDefaultDestination();
if (defaultDestination != null) {
return receiveBatch(defaultDestination, batchSize);
} else {
return receiveBatch(getRequiredDefaultDestinationName(), batchSize);
}
}
/**
* Receive a batch of up to default batch size for given destination. Other than batching this method is the same as {@link JmsTemplate#receive(Destination)}
* @return A list of {@link Message}
* @param destination The Destination
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch(Destination destination) throws JmsException {
return receiveBatch(destination, batchSize);
}
/**
* Receive a batch of up to batchSize for given destination. Other than batching this method is the same as {@link JmsTemplate#receive(Destination)}
* @return A list of {@link Message}
* @param destination The Destination
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch(final Destination destination, final int batchSize) throws JmsException {
return execute(new SessionCallback<List<Message>>() {
public List<Message> doInJms(Session session) throws JMSException {
return doBatchReceive(session, destination, null, batchSize);
}
}, true);
}
/**
* Receive a batch of up to default batch size for given destinationName. Other than batching this method is the same as {@link JmsTemplate#receive(String)}
* @return A list of {@link Message}
* @param destinationName The Destination name
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch(String destinationName) throws JmsException {
return receiveBatch(destinationName, getBatchSize());
}
/**
* Receive a batch of up to default batch size for given destination. Other than batching this method is the same as {@link JmsTemplate#receive(String)}
* @return A list of {@link Message}
* @param destinationName The Destination
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveBatch(final String destinationName, final int batchSize) throws JmsException {
return execute(new SessionCallback<List<Message>>() {
public List<Message> doInJms(Session session) throws JMSException {
Destination destination = resolveDestinationName(session, destinationName);
return doBatchReceive(session, destination, null, batchSize);
}
}, true);
}
/**
* Receive a batch of up to default batch size for default destination and given message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(String)}
* @return A list of {@link Message}
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(String messageSelector) throws JmsException {
return receiveSelectedBatch(messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for default destination and given message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(String)}
* @return A list of {@link Message}
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(String messageSelector, int batchSize) throws JmsException {
Destination defaultDestination = getDefaultDestination();
if (defaultDestination != null) {
return receiveSelectedBatch(defaultDestination, messageSelector, batchSize);
} else {
return receiveSelectedBatch(getRequiredDefaultDestinationName(), messageSelector, batchSize);
}
}
/**
* Receive a batch of up to default batch size for given destination and message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(Destination, String)}
* @return A list of {@link Message}
* @param destination The Destination
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(Destination destination, String messageSelector) throws JmsException {
return receiveSelectedBatch(destination, messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given destination and message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(Destination, String)}
* @return A list of {@link Message}
* @param destination The Destination
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(final Destination destination, final String messageSelector,
final int batchSize) throws JmsException {
return execute(new SessionCallback<List<Message>>() {
public List<Message> doInJms(Session session) throws JMSException {
return doBatchReceive(session, destination, messageSelector, batchSize);
}
}, true);
}
/**
* Receive a batch of up to default batch size for given destination name and message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(String, String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(String destinationName, String messageSelector) throws JmsException {
return receiveSelectedBatch(destinationName, messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given destination name and message selector. Other than batching this method is the same as {@link JmsTemplate#receiveSelected(String, String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Message> receiveSelectedBatch(final String destinationName, final String messageSelector,
final int batchSize) throws JmsException {
return execute(new SessionCallback<List<Message>>() {
public List<Message> doInJms(Session session) throws JMSException {
Destination destination = resolveDestinationName(session, destinationName);
return doBatchReceive(session, destination, messageSelector, batchSize);
}
}, true);
}
/**
* Receive a batch of up to default batch size for default destination and convert each message in the batch. Other than batching this method is the same as {@link JmsTemplate#receiveAndConvert()}
* @return A list of {@link Message}
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch() throws JmsException {
return receiveAndConvertBatch(getBatchSize());
}
/**
* Receive a batch of up to batchSize for default destination and convert each message in the batch. Other than batching this method is the same as {@link JmsTemplate#receiveAndConvert()}
* @return A list of {@link Message}
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch(int batchSize) throws JmsException {
List<Message> messages = receiveBatch(batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* Receive a batch of up to default batch size for the given Destination and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveAndConvert(Destination)}
* @return A list of {@link Message}
* @param destination The Destination
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch(Destination destination) throws JmsException {
return receiveAndConvertBatch(destination, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given Destination and convert each message in the batch. Other than batching this method is the same as {@link JmsTemplate#receiveAndConvert(Destination)}
* @return A list of {@link Message}
* @param destination The Destination
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch(Destination destination, int batchSize) throws JmsException {
List<Message> messages = receiveBatch(destination, batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* Receive a batch of up to default batch size for given destination name and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveAndConvert(String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch(String destinationName) throws JmsException {
return receiveAndConvertBatch(destinationName, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given destination name and convert each message in the batch. Other than batching this method is the same as {@link JmsTemplate#receiveAndConvert(String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveAndConvertBatch(String destinationName, int batchSize) throws JmsException {
List<Message> messages = receiveBatch(destinationName, batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* Receive a batch of up to default batch size for default destination and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(String)}
* @return A list of {@link Message}
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(String messageSelector) throws JmsException {
return receiveSelectedAndConvertBatch(messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for default destination and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(String)}
* @return A list of {@link Message}
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(String messageSelector, int batchSize) throws JmsException {
List<Message> messages = receiveSelectedBatch(messageSelector, batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* Receive a batch of up to default batch size for given Destination and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(Destination, String)}
* @return A list of {@link Message}
* @param destination The Destination
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(Destination destination, String messageSelector)
throws JmsException {
return receiveSelectedAndConvertBatch(destination, messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given Destination and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(Destination, String)}
* @return A list of {@link Message}
* @param destination The Destination
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(Destination destination, String messageSelector, int batchSize)
throws JmsException {
List<Message> messages = receiveSelectedBatch(destination, messageSelector, batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* Receive a batch of up to default batch size for given destination name and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(String, String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @param messageSelector The Selector
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(String destinationName, String messageSelector)
throws JmsException {
return receiveSelectedAndConvertBatch(destinationName, messageSelector, getBatchSize());
}
/**
* Receive a batch of up to batchSize for given destination name and message selector and convert each message in the batch. Other than batching this method is the same as
* {@link JmsTemplate#receiveSelectedAndConvert(String, String)}
* @return A list of {@link Message}
* @param destinationName The destination name
* @param messageSelector The Selector
* @param batchSize The batch size
* @throws JmsException The {@link JmsException}
*/
public List<Object> receiveSelectedAndConvertBatch(String destinationName, String messageSelector, int batchSize)
throws JmsException {
List<Message> messages = receiveSelectedBatch(destinationName, batchSize);
List<Object> result = new ArrayList<Object>(messages.size());
for (Message next : messages) {
result.add(doConvertFromMessage(next));
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public Message receive() throws JmsException {
Destination defaultDestination = getDefaultDestination();
if (defaultDestination != null) {
return receive(defaultDestination);
} else {
return receive(getRequiredDefaultDestinationName());
}
}
/**
* {@inheritDoc}
*/
@Override
public Message receive(final Destination destination) throws JmsException {
return execute(new SessionCallback<Message>() {
public Message doInJms(Session session) throws JMSException {
return doSingleReceive(session, destination, null);
}
}, true);
}
/**
* {@inheritDoc}
*/
@Override
public Message receive(final String destinationName) throws JmsException {
return execute(new SessionCallback<Message>() {
public Message doInJms(Session session) throws JMSException {
Destination destination = resolveDestinationName(session, destinationName);
return doSingleReceive(session, destination, null);
}
}, true);
}
/**
* {@inheritDoc}
*/
@Override
public Message receiveSelected(String messageSelector) throws JmsException {
Destination defaultDestination = getDefaultDestination();
if (defaultDestination != null) {
return receiveSelected(defaultDestination, messageSelector);
} else {
return receiveSelected(getRequiredDefaultDestinationName(), messageSelector);
}
}
/**
* {@inheritDoc}
*/
@Override
public Message receiveSelected(final Destination destination, final String messageSelector) throws JmsException {
return execute(new SessionCallback<Message>() {
public Message doInJms(Session session) throws JMSException {
return doSingleReceive(session, destination, messageSelector);
}
}, true);
}
/**
* {@inheritDoc}
*/
@Override
public Message receiveSelected(final String destinationName, final String messageSelector) throws JmsException {
return execute(new SessionCallback<Message>() {
public Message doInJms(Session session) throws JMSException {
Destination destination = resolveDestinationName(session, destinationName);
return doSingleReceive(session, destination, messageSelector);
}
}, true);
}
/**
* Method replicates the simple logic from JmsTemplate#getRequiredDefaultDestinationName which is private and therefore cannot be accessed from this class
* @return The default destination name
* @throws IllegalStateException Example if no destination or destination name is specified
*/
private String getRequiredDefaultDestinationName() throws IllegalStateException {
String name = getDefaultDestinationName();
if (name == null) {
throw new IllegalStateException(
"No 'defaultDestination' or 'defaultDestinationName' specified. Check configuration of JmsTemplate.");
}
return name;
}
/**
* Method replicates the simple logic from JmsTemplate#doReceive(MessageConsumer, long) which is private and therefore cannot be accessed from this class
* @param consumer The consumer to use
* @param timeout The timeout to apply
* @return A Message
* @throws JMSException Indicates an error occurred
*/
private Message doReceive(MessageConsumer consumer, long timeout) throws JMSException {
if (timeout == RECEIVE_TIMEOUT_NO_WAIT) {
return consumer.receiveNoWait();
} else if (timeout > 0) {
return consumer.receive(timeout);
} else {
return consumer.receive();
}
}
protected List<Message> doBatchReceive(Session session, Destination destination, String messageSelector,
int batchSize) throws JMSException {
return doBatchReceive(session, createConsumer(session, destination, messageSelector), batchSize);
}
protected List<Message> doBatchReceive(Session session, MessageConsumer consumer, int batchSize)
throws JMSException {
try {
final List<Message> result;
long timeout = determineTimeout();
Message message = doReceive(consumer, timeout);
if (message == null) {
result = new ArrayList<Message>(0);
} else {
result = new ArrayList<Message>(batchSize);
result.add(message);
for (int i = 1; i < batchSize; i++) {
message = doReceive(consumer, RECEIVE_TIMEOUT_NO_WAIT);
if (message == null) {
break;
}
result.add(message);
}
}
if (session.getTransacted()) {
if (isSessionLocallyTransacted(session)) {
JmsUtils.commitIfNecessary(session);
}
} else if (isClientAcknowledge(session)) {
if (message != null) {
message.acknowledge();
}
}
return result;
} finally {
JmsUtils.closeMessageConsumer(consumer);
}
}
protected Message doSingleReceive(Session session, Destination destination, String messageSelector)
throws JMSException {
return doSingleReceive(session, createConsumer(session, destination, messageSelector));
}
protected Message doSingleReceive(Session session, MessageConsumer consumer) throws JMSException {
if (!session.getTransacted() || isSessionLocallyTransacted(session)) {
// If we are not using JTA we should use standard JmsTemplate behaviour
return super.doReceive(session, consumer);
}
// Otherwise batching - the batch can span multiple receive() calls, until you commit the
// batch
try {
final Message message;
if (Boolean.TRUE.equals(IS_START_OF_BATCH.get())) {
// Register Synchronization
TransactionSynchronizationManager.registerSynchronization(BATCH_SYNCHRONIZATION);
// Use transaction timeout (if available).
long timeout = determineTimeout();
message = doReceive(consumer, timeout);
IS_START_OF_BATCH.set(Boolean.FALSE);
} else {
message = doReceive(consumer, RECEIVE_TIMEOUT_NO_WAIT);
}
if (isClientAcknowledge(session)) {
// Manually acknowledge message, if any.
if (message != null) {
message.acknowledge();
}
}
return message;
} finally {
JmsUtils.closeMessageConsumer(consumer);
}
}
/**
* Determines receive timeout, using logic equivalent to that of {@link JmsTemplate#doReceive(Session, MessageConsumer)
* @return The timeout determined
*/
private long determineTimeout() {
long timeout = getReceiveTimeout();
JmsResourceHolder resourceHolder = (JmsResourceHolder) TransactionSynchronizationManager
.getResource(getConnectionFactory());
if (resourceHolder != null && resourceHolder.hasTimeout()) {
timeout = Math.min(timeout, resourceHolder.getTimeToLiveInMillis());
}
return timeout;
}
/**
* A simple TransactionSynchronization implementation that resets the batch indicator so that the next read begins a new batch
*/
private static class BatchTransactionSynchronization extends TransactionSynchronizationAdapter {
@Override
public void afterCompletion(int status) {
IS_START_OF_BATCH.set(Boolean.TRUE);
}
}
}