Java tutorial
/* * Copyright 2016 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.springframework.amqp.rabbit; import java.util.Date; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; import org.springframework.amqp.core.AmqpReplyTimeoutException; import org.springframework.amqp.core.AsyncAmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.core.RabbitTemplate.ConfirmCallback; import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.amqp.rabbit.support.PublisherCallbackChannel; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.BeanNameAware; import org.springframework.context.SmartLifecycle; import org.springframework.expression.Expression; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.SettableListenableFuture; import com.rabbitmq.client.Channel; /** * Provides asynchronous send and receive operations returning a {@link ListenableFuture} * allowing the caller to obtain the reply later, using {@code get()} or a callback. * <p> * When confirms are enabled, the future has a confirm property which is itself a * {@link ListenableFuture}. If the reply is received before the publisher confirm, * the confirm is discarded since the reply implicitly indicates the message was * published. * <p> * Returned (undeliverable) request messages are presented as a * {@link AmqpMessageReturnedException} cause of an * {@link java.util.concurrent.ExecutionException}. * <p> * Internally, the template uses a {@link RabbitTemplate} and an * {@link AbstractMessageListenerContainer} either provided or constructed internally * (a {@link SimpleMessageListenerContainer}). * If an external {@link RabbitTemplate} is provided and confirms/returns are enabled, * it must not previously have had callbacks registered because this object needs to * be the callback. * * @author Gary Russell * @since 1.6 */ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessageListener, ReturnCallback, ConfirmCallback, BeanNameAware, SmartLifecycle { public static final int DEFAULT_RECEIVE_TIMEOUT = 30000; private final Log logger = LogFactory.getLog(this.getClass()); private final RabbitTemplate template; private final AbstractMessageListenerContainer container; private final DirectReplyToMessageListenerContainer directReplyToContainer; private final String replyAddress; @SuppressWarnings("rawtypes") private final ConcurrentMap<String, RabbitFuture> pending = new ConcurrentHashMap<String, RabbitFuture>(); private volatile boolean running; private volatile boolean enableConfirms; private volatile long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT; private int phase; private boolean autoStartup = true; private String beanName; private TaskScheduler taskScheduler; /** * Construct an instance using the provided arguments. Replies will be * routed to the default exchange using the reply queue name as the routing * key. * @param connectionFactory the connection factory. * @param exchange the default exchange to which requests will be sent. * @param routingKey the default routing key. * @param replyQueue the name of the reply queue to listen for replies. */ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, String replyQueue) { this(connectionFactory, exchange, routingKey, replyQueue, null); } /** * Construct an instance using the provided arguments. If 'replyAddress' is null, * replies will be routed to the default exchange using the reply queue name as the * routing key. Otherwise it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param connectionFactory the connection factory. * @param exchange the default exchange to which requests will be sent. * @param routingKey the default routing key. * @param replyQueue the name of the reply queue to listen for replies. * @param replyAddress the reply address (exchange/routingKey). */ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, String replyQueue, String replyAddress) { Assert.notNull(connectionFactory, "'connectionFactory' cannot be null"); Assert.notNull(routingKey, "'routingKey' cannot be null"); Assert.notNull(replyQueue, "'replyQueue' cannot be null"); this.template = new RabbitTemplate(connectionFactory); this.template.setExchange(exchange == null ? "" : exchange); this.template.setRoutingKey(routingKey); this.container = new SimpleMessageListenerContainer(connectionFactory); this.container.setQueueNames(replyQueue); this.container.setMessageListener(this); this.container.afterPropertiesSet(); this.directReplyToContainer = null; if (replyAddress == null) { this.replyAddress = replyQueue; } else { this.replyAddress = replyAddress; } } /** * Construct an instance using the provided arguments. The first queue the container * is configured to listen to will be used as the reply queue. Replies will be * routed using the default exchange with that queue name as the routing key. * @param template a {@link RabbitTemplate} * @param container a {@link AbstractMessageListenerContainer}. */ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerContainer container) { this(template, container, null); } /** * Construct an instance using the provided arguments. The first queue the container * is configured to listen to will be used as the reply queue. If 'replyAddress' is * null, replies will be routed using the default exchange with that queue name as the * routing key. Otherwise it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param template a {@link RabbitTemplate}. * @param container a {@link AbstractMessageListenerContainer}. * @param replyAddress the reply address. */ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerContainer container, String replyAddress) { Assert.notNull(template, "'template' cannot be null"); Assert.notNull(container, "'container' cannot be null"); this.template = template; this.container = container; this.container.setMessageListener(this); this.directReplyToContainer = null; if (replyAddress == null) { this.replyAddress = container.getQueueNames()[0]; } else { this.replyAddress = replyAddress; } } /** * Construct an instance using the provided arguments. "Direct replyTo" is used for * replies. * @param connectionFactory the connection factory. * @param exchange the default exchange to which requests will be sent. * @param routingKey the default routing key. * @since 2.0 */ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) { Assert.notNull(connectionFactory, "'connectionFactory' cannot be null"); Assert.notNull(routingKey, "'routingKey' cannot be null"); this.template = new RabbitTemplate(connectionFactory); this.template.setExchange(exchange == null ? "" : exchange); this.template.setRoutingKey(routingKey); this.container = null; this.replyAddress = null; this.directReplyToContainer = new DirectReplyToMessageListenerContainer( this.template.getConnectionFactory()); this.directReplyToContainer.setChannelAwareMessageListener(this); } /** * Construct an instance using the provided arguments. "Direct replyTo" is used for * replies. * @param template a {@link RabbitTemplate} * @since 2.0 */ public AsyncRabbitTemplate(RabbitTemplate template) { Assert.notNull(template, "'template' cannot be null"); this.template = template; this.container = null; this.replyAddress = null; this.directReplyToContainer = new DirectReplyToMessageListenerContainer( this.template.getConnectionFactory()); this.directReplyToContainer.setChannelAwareMessageListener(this); } /** * @param autoStartup true for auto start. * @see #isAutoStartup() */ public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } /** * @param phase the phase. * @see #getPhase() */ public void setPhase(int phase) { this.phase = phase; } /** * Set to true to enable the receipt of returned messages that cannot be delivered * in the form of a {@link AmqpMessageReturnedException}. * @param mandatory true to enable returns. */ public void setMandatory(boolean mandatory) { this.template.setReturnCallback(this); this.template.setMandatory(mandatory); } /** * @param mandatoryExpression a SpEL {@link Expression} to evaluate against each request * message. The result of the evaluation must be a {@code boolean} value. * @since 2.0 */ public void setMandatoryExpression(Expression mandatoryExpression) { this.template.setReturnCallback(this); this.template.setMandatoryExpression(mandatoryExpression); } /** * @param mandatoryExpression a SpEL {@link Expression} to evaluate against each request * message. The result of the evaluation must be a {@code boolean} value. * @since 2.0 */ public void setMandatoryExpressionString(String mandatoryExpression) { this.template.setReturnCallback(this); this.template.setMandatoryExpressionString(mandatoryExpression); } /** * Set to true to enable publisher confirms. When enabled, the {@link RabbitFuture} * returned by the send and receive operation will have a * {@code ListenableFuture<Boolean>} in its {@code confirm} property. * @param enableConfirms true to enable publisher confirms. */ public void setEnableConfirms(boolean enableConfirms) { this.enableConfirms = enableConfirms; if (enableConfirms) { this.template.setConfirmCallback(this); } } public String getBeanName() { return this.beanName; } @Override public void setBeanName(String beanName) { this.beanName = beanName; } /** * @return a reference to the underlying connection factory in the * {@link RabbitTemplate}. */ public ConnectionFactory getConnectionFactory() { return this.template.getConnectionFactory(); } /** * Set the receive timeout - the future returned by the send and receive * methods will be canceled when this timeout expires. {@code <= 0} means * futures never expire. Beware that this will cause a memory leak if a * reply is not received. Default: 30000 (30 seconds). * @param receiveTimeout the timeout in milliseconds. */ public void setReceiveTimeout(long receiveTimeout) { this.receiveTimeout = receiveTimeout; } /** * Set the task scheduler to expire timed out futures. * @param taskScheduler the task scheduler * @see #setReceiveTimeout(long) */ public void setTaskScheduler(TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; // NOSONAR synchronization } /** * @return a reference to the underlying {@link RabbitTemplate}'s * {@link MessageConverter}. */ public MessageConverter getMessageConverter() { return this.template.getMessageConverter(); } @Override public RabbitMessageFuture sendAndReceive(Message message) { return sendAndReceive(this.template.getExchange(), this.template.getRoutingKey(), message); } @Override public RabbitMessageFuture sendAndReceive(String routingKey, Message message) { return sendAndReceive(this.template.getExchange(), routingKey, message); } @Override public RabbitMessageFuture sendAndReceive(String exchange, String routingKey, Message message) { String correlationId = getOrSetCorrelationIdAndSetReplyTo(message); RabbitMessageFuture future = new RabbitMessageFuture(correlationId, message); CorrelationData correlationData = null; if (this.enableConfirms) { correlationData = new CorrelationData(correlationId); future.setConfirm(new SettableListenableFuture<Boolean>()); } this.pending.put(correlationId, future); if (this.container != null) { this.template.send(exchange, routingKey, message, correlationData); } else { ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); future.setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); } future.startTimer(); return future; } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(Object object) { return convertSendAndReceive(this.template.getExchange(), this.template.getRoutingKey(), object, null); } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(String routingKey, Object object) { return convertSendAndReceive(this.template.getExchange(), routingKey, object, null); } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(String exchange, String routingKey, Object object) { return convertSendAndReceive(exchange, routingKey, object, null); } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor) { return convertSendAndReceive(this.template.getExchange(), this.template.getRoutingKey(), object, messagePostProcessor); } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor) { return convertSendAndReceive(this.template.getExchange(), routingKey, object, messagePostProcessor); } @Override public <C> RabbitConverterFuture<C> convertSendAndReceive(String exchange, String routingKey, Object object, MessagePostProcessor messagePostProcessor) { CorrelationData correlationData = null; if (this.enableConfirms) { correlationData = new CorrelationData(null); } CorrelationMessagePostProcessor<C> correlationPostProcessor = new CorrelationMessagePostProcessor<C>( messagePostProcessor, correlationData); if (this.container != null) { this.template.convertAndSend(exchange, routingKey, object, correlationPostProcessor, correlationData); } else { MessageConverter converter = this.template.getMessageConverter(); if (converter == null) { throw new AmqpIllegalStateException( "No 'messageConverter' specified. Check configuration of RabbitTemplate."); } Message message = converter.toMessage(object, new MessageProperties()); correlationPostProcessor.postProcessMessage(message); ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); correlationPostProcessor.getFuture().setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); } RabbitConverterFuture<C> future = correlationPostProcessor.getFuture(); future.startTimer(); return future; } private void sendDirect(Channel channel, String exchange, String routingKey, Message message, CorrelationData correlationData) { message.getMessageProperties().setReplyTo(Address.AMQ_RABBITMQ_REPLY_TO); try { if (channel instanceof PublisherCallbackChannel) { this.template.addListener(channel); } this.template.doSend(channel, exchange, routingKey, message, this.template.isMandatoryFor(message), correlationData); } catch (Exception e) { throw new AmqpException("Failed to send request", e); } } @Override public synchronized void start() { if (!this.running) { if (this.taskScheduler == null) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setThreadNamePrefix(getBeanName() == null ? "asyncTemplate-" : (getBeanName() + "-")); scheduler.afterPropertiesSet(); this.taskScheduler = scheduler; } if (this.container != null) { this.container.start(); } if (this.directReplyToContainer != null) { this.directReplyToContainer.setTaskScheduler(this.taskScheduler); this.directReplyToContainer.start(); } } this.running = true; } @Override public synchronized void stop() { if (this.running) { if (this.container != null) { this.container.stop(); } if (this.directReplyToContainer != null) { this.directReplyToContainer.stop(); } for (RabbitFuture<?> future : this.pending.values()) { future.setNackCause("AsyncRabbitTemplate was stopped while waiting for reply"); future.cancel(true); } } this.running = false; } @Override public boolean isRunning() { return this.running; } @Override public int getPhase() { return this.phase; } @Override public boolean isAutoStartup() { return this.autoStartup; } @Override public void stop(Runnable callback) { stop(); callback.run(); } @SuppressWarnings("unchecked") @Override public void onMessage(Message message, Channel channel) { MessageProperties messageProperties = message.getMessageProperties(); if (messageProperties != null) { String correlationId = messageProperties.getCorrelationId(); if (StringUtils.hasText(correlationId)) { if (this.logger.isDebugEnabled()) { this.logger.debug("onMessage: " + message); } RabbitFuture<?> future = this.pending.remove(correlationId); if (future != null) { if (future instanceof AsyncRabbitTemplate.RabbitConverterFuture) { Object converted = this.template.getMessageConverter().fromMessage(message); ((RabbitConverterFuture<Object>) future).set(converted); } else { ((RabbitMessageFuture) future).set(message); } } else { if (this.logger.isWarnEnabled()) { this.logger.warn("No pending reply - perhaps timed out: " + message); } } } } } @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { MessageProperties messageProperties = message.getMessageProperties(); String correlationId = messageProperties.getCorrelationId(); if (StringUtils.hasText(correlationId)) { RabbitFuture<?> future = this.pending.remove(correlationId); if (future != null) { future.setException(new AmqpMessageReturnedException("Message returned", message, replyCode, replyText, exchange, routingKey)); } else { if (this.logger.isWarnEnabled()) { this.logger.warn("No pending reply - perhaps timed out? Message returned: " + message); } } } } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (this.logger.isDebugEnabled()) { this.logger.debug( "Confirm: " + correlationData + ", ack=" + ack + (cause == null ? "" : (", cause: " + cause))); } String correlationId = correlationData.getId(); if (correlationId != null) { RabbitFuture<?> future = this.pending.get(correlationId); if (future != null) { future.setNackCause(cause); ((SettableListenableFuture<Boolean>) future.getConfirm()).set(ack); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Confirm: " + correlationData + ", ack=" + ack + (cause == null ? "" : (", cause: " + cause)) + " no pending future - either canceled or the reply is already received"); } } } } private String getOrSetCorrelationIdAndSetReplyTo(Message message) { String correlationId; MessageProperties messageProperties = message.getMessageProperties(); Assert.notNull(messageProperties, "the message properties cannot be null"); String currentCorrelationId = messageProperties.getCorrelationId(); if (!StringUtils.hasText(currentCorrelationId)) { correlationId = UUID.randomUUID().toString(); messageProperties.setCorrelationId(correlationId); Assert.isNull(messageProperties.getReplyTo(), "'replyTo' property must be null"); } else { correlationId = currentCorrelationId; } messageProperties.setReplyTo(this.replyAddress); return correlationId; } @Override public String toString() { return this.beanName == null ? super.toString() : (this.getClass().getSimpleName() + ": " + this.beanName); } /** * Base class for {@link ListenableFuture}s returned by {@link AsyncRabbitTemplate}. * @since 1.6 */ public abstract class RabbitFuture<T> extends SettableListenableFuture<T> { private final String correlationId; private final Message requestMessage; private ScheduledFuture<?> timeoutTask; private volatile ListenableFuture<Boolean> confirm; private String nackCause; private ChannelHolder channelHolder; public RabbitFuture(String correlationId, Message requestMessage) { this.correlationId = correlationId; this.requestMessage = requestMessage; } void setChannelHolder(ChannelHolder channel) { this.channelHolder = channel; } @Override public boolean cancel(boolean mayInterruptIfRunning) { if (this.timeoutTask != null) { this.timeoutTask.cancel(true); } AsyncRabbitTemplate.this.pending.remove(this.correlationId); if (this.channelHolder != null && AsyncRabbitTemplate.this.directReplyToContainer != null) { AsyncRabbitTemplate.this.directReplyToContainer.releaseConsumerFor(this.channelHolder, false, null); } return super.cancel(mayInterruptIfRunning); } /** * When confirms are enabled contains a {@link ListenableFuture} * for the confirmation. * @return the future. */ public ListenableFuture<Boolean> getConfirm() { return this.confirm; } void setConfirm(ListenableFuture<Boolean> confirm) { this.confirm = confirm; } /** * When confirms are enabled and a nack is received, contains * the cause for the nack, if any. * @return the cause. */ public String getNackCause() { return this.nackCause; } void setNackCause(String nackCause) { this.nackCause = nackCause; } void startTimer() { if (AsyncRabbitTemplate.this.receiveTimeout > 0) { this.timeoutTask = AsyncRabbitTemplate.this.taskScheduler.schedule(new TimeoutTask(), // NOSONAR sync new Date(System.currentTimeMillis() + AsyncRabbitTemplate.this.receiveTimeout)); } else { this.timeoutTask = null; } } private class TimeoutTask implements Runnable { @Override public void run() { AsyncRabbitTemplate.this.pending.remove(RabbitFuture.this.correlationId); if (RabbitFuture.this.channelHolder != null && AsyncRabbitTemplate.this.directReplyToContainer != null) { AsyncRabbitTemplate.this.directReplyToContainer .releaseConsumerFor(RabbitFuture.this.channelHolder, false, null); } setException(new AmqpReplyTimeoutException("Reply timed out", RabbitFuture.this.requestMessage)); } } } /** * A {@link RabbitFuture} with a return type of {@link Message}. * @since 1.6 */ public class RabbitMessageFuture extends RabbitFuture<Message> implements ListenableFuture<Message> { public RabbitMessageFuture(String correlationId, Message requestMessage) { super(correlationId, requestMessage); } } /** * A {@link RabbitFuture} with a return type of the template's * generic parameter. * @since 1.6 */ public class RabbitConverterFuture<C> extends RabbitFuture<C> implements ListenableFuture<C> { public RabbitConverterFuture(String correlationId, Message requestMessage) { super(correlationId, requestMessage); } } private final class CorrelationMessagePostProcessor<C> implements MessagePostProcessor { private final MessagePostProcessor userPostProcessor; private final CorrelationData correlationData; private volatile RabbitConverterFuture<C> future; CorrelationMessagePostProcessor(MessagePostProcessor userPostProcessor, CorrelationData correlationData) { this.userPostProcessor = userPostProcessor; this.correlationData = correlationData; } @Override public Message postProcessMessage(Message message) throws AmqpException { Message messageToSend = message; if (this.userPostProcessor != null) { messageToSend = this.userPostProcessor.postProcessMessage(message); } String correlationId = getOrSetCorrelationIdAndSetReplyTo(messageToSend); this.future = new RabbitConverterFuture<C>(correlationId, message); if (this.correlationData != null && this.correlationData.getId() == null) { this.correlationData.setId(correlationId); this.future.setConfirm(new SettableListenableFuture<Boolean>()); } AsyncRabbitTemplate.this.pending.put(correlationId, this.future); return messageToSend; } private RabbitConverterFuture<C> getFuture() { return this.future; } } }