/* * Copyright 2002-2017 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.core; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Declarable; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.connection.ChannelProxy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.util.Assert; import com.rabbitmq.client.AMQP.Queue.DeclareOk; import com.rabbitmq.client.Channel; /** * RabbitMQ implementation of portable AMQP administrative operations for AMQP >= 0.9.1. * * @author Mark Pollack * @author Mark Fisher * @author Dave Syer * @author Ed Scriven * @author Gary Russell * @author Artem Bilan */ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, InitializingBean { /** * The default exchange name. */ public static final String DEFAULT_EXCHANGE_NAME = ""; /** * Property key for the queue name in the {@link Properties} returned by * {@link #getQueueProperties(String)}. */ public static final Object QUEUE_NAME = "QUEUE_NAME"; /** * Property key for the message count in the {@link Properties} returned by * {@link #getQueueProperties(String)}. */ public static final Object QUEUE_MESSAGE_COUNT = "QUEUE_MESSAGE_COUNT"; /** * Property key for the consumer count in the {@link Properties} returned by * {@link #getQueueProperties(String)}. */ public static final Object QUEUE_CONSUMER_COUNT = "QUEUE_CONSUMER_COUNT"; private static final String DELAYED_MESSAGE_EXCHANGE = "x-delayed-message"; /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); private final RabbitTemplate rabbitTemplate; private volatile boolean running = false; private volatile boolean autoStartup = true; private volatile ApplicationContext applicationContext; private volatile boolean ignoreDeclarationExceptions; private final Object lifecycleMonitor = new Object(); private final ConnectionFactory connectionFactory; private ApplicationEventPublisher applicationEventPublisher; private volatile DeclarationExceptionEvent lastDeclarationExceptionEvent; public RabbitAdmin(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); this.rabbitTemplate = new RabbitTemplate(connectionFactory); } public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) { this.ignoreDeclarationExceptions = ignoreDeclarationExceptions; } /** * @return the last {@link DeclarationExceptionEvent} that was detected in this admin. * * @since 1.6 */ public DeclarationExceptionEvent getLastDeclarationExceptionEvent() { return this.lastDeclarationExceptionEvent; } public RabbitTemplate getRabbitTemplate() { return this.rabbitTemplate; } // Exchange operations @Override public void declareExchange(final Exchange exchange) { try { this.rabbitTemplate.execute(channel -> { declareExchanges(channel, exchange); return null; }); } catch (AmqpException e) { logOrRethrowDeclarationException(exchange, "exchange", e); } } @Override @ManagedOperation public boolean deleteExchange(final String exchangeName) { return this.rabbitTemplate.execute(channel -> { if (isDeletingDefaultExchange(exchangeName)) { return true; } try { channel.exchangeDelete(exchangeName); } catch (IOException e) { return false; } return true; }); } // Queue operations /** * Declare the given queue. * If the queue doesn't have a value for 'name' property, * the queue name will be generated by Broker and returned from this method. * But the 'name' property of the queue remains as is. * @param queue the queue * @return the queue name if successful, null if not successful and * {@link #setIgnoreDeclarationExceptions(boolean) ignoreDeclarationExceptions} is * true. */ @Override @ManagedOperation public String declareQueue(final Queue queue) { try { return this.rabbitTemplate.execute(channel -> { DeclareOk[] declared = declareQueues(channel, queue); return declared.length > 0 ? declared[0].getQueue() : null; }); } catch (AmqpException e) { logOrRethrowDeclarationException(queue, "queue", e); return null; } } /** * Declares a server-named exclusive, autodelete, non-durable queue. * * @return the queue or null if an exception occurred and * {@link #setIgnoreDeclarationExceptions(boolean) ignoreDeclarationExceptions} * is true. */ @Override @ManagedOperation public Queue declareQueue() { try { DeclareOk declareOk = this.rabbitTemplate.execute(Channel::queueDeclare); return new Queue(declareOk.getQueue(), false, true, true); } catch (AmqpException e) { logOrRethrowDeclarationException(null, "queue", e); return null; } } @Override @ManagedOperation public boolean deleteQueue(final String queueName) { return this.rabbitTemplate.execute(channel -> { try { channel.queueDelete(queueName); } catch (IOException e) { return false; } return true; }); } @Override @ManagedOperation public void deleteQueue(final String queueName, final boolean unused, final boolean empty) { this.rabbitTemplate.execute(channel -> { channel.queueDelete(queueName, unused, empty); return null; }); } @Override @ManagedOperation public void purgeQueue(final String queueName, final boolean noWait) { this.rabbitTemplate.execute(channel -> { channel.queuePurge(queueName); return null; }); } // Binding @Override @ManagedOperation public void declareBinding(final Binding binding) { try { this.rabbitTemplate.execute(channel -> { declareBindings(channel, binding); return null; }); } catch (AmqpException e) { logOrRethrowDeclarationException(binding, "binding", e); } } @Override @ManagedOperation public void removeBinding(final Binding binding) { this.rabbitTemplate.execute(channel -> { if (binding.isDestinationQueue()) { if (isRemovingImplicitQueueBinding(binding)) { return null; } channel.queueUnbind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } else { channel.exchangeUnbind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } return null; }); } /** * Returns 3 properties {@link #QUEUE_NAME}, {@link #QUEUE_MESSAGE_COUNT}, * {@link #QUEUE_CONSUMER_COUNT}, or null if the queue doesn't exist. */ @Override public Properties getQueueProperties(final String queueName) { Assert.hasText(queueName, "'queueName' cannot be null or empty"); return this.rabbitTemplate.execute(channel -> { try { DeclareOk declareOk = channel.queueDeclarePassive(queueName); Properties props = new Properties(); props.put(QUEUE_NAME, declareOk.getQueue()); props.put(QUEUE_MESSAGE_COUNT, declareOk.getMessageCount()); props.put(QUEUE_CONSUMER_COUNT, declareOk.getConsumerCount()); return props; } catch (IllegalArgumentException e) { if (RabbitAdmin.this.logger.isDebugEnabled()) { RabbitAdmin.this.logger.error("Exception while fetching Queue properties: '" + queueName + "'", e); } try { if (channel instanceof ChannelProxy) { ((ChannelProxy) channel).getTargetChannel().close(); } } catch (TimeoutException e1) { } return null; } catch (Exception e) { if (RabbitAdmin.this.logger.isDebugEnabled()) { RabbitAdmin.this.logger.debug("Queue '" + queueName + "' does not exist"); } return null; } }); } // Lifecycle implementation public boolean isAutoStartup() { return this.autoStartup; } /** * If {@link #setAutoStartup(boolean) autoStartup} is set to true, registers a callback on the * {@link ConnectionFactory} to declare all exchanges and queues in the enclosing application context. If the * callback fails then it may cause other clients of the connection factory to fail, but since only exchanges, * queues and bindings are declared failure is not expected. * * @see InitializingBean#afterPropertiesSet() * @see #initialize() */ @Override public void afterPropertiesSet() { synchronized (this.lifecycleMonitor) { if (this.running || !this.autoStartup) { return; } if (this.connectionFactory instanceof CachingConnectionFactory && ((CachingConnectionFactory) this.connectionFactory).getCacheMode() == CacheMode.CONNECTION) { this.logger.warn("RabbitAdmin auto declaration is not supported with CacheMode.CONNECTION"); return; } // Prevent stack overflow... final AtomicBoolean initializing = new AtomicBoolean(false); this.connectionFactory.addConnectionListener(connection -> { if (!initializing.compareAndSet(false, true)) { // If we are already initializing, we don't need to do it again... return; } try { /* * ...but it is possible for this to happen twice in the same ConnectionFactory (if more than * one concurrent Connection is allowed). It's idempotent, so no big deal (a bit of network * chatter). In fact it might even be a good thing: exclusive queues only make sense if they are * declared for every connection. If anyone has a problem with it: use auto-startup="false". */ initialize(); } finally { initializing.compareAndSet(true, false); } }); this.running = true; } } /** * Declares all the exchanges, queues and bindings in the enclosing application context, if any. It should be safe * (but unnecessary) to call this method more than once. */ public void initialize() { if (this.applicationContext == null) { this.logger.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); return; } this.logger.debug("Initializing declarations"); Collection<Exchange> contextExchanges = new LinkedList<Exchange>( this.applicationContext.getBeansOfType(Exchange.class).values()); Collection<Queue> contextQueues = new LinkedList<Queue>( this.applicationContext.getBeansOfType(Queue.class).values()); Collection<Binding> contextBindings = new LinkedList<Binding>( this.applicationContext.getBeansOfType(Binding.class).values()); @SuppressWarnings("rawtypes") Collection<Collection> collections = this.applicationContext.getBeansOfType(Collection.class, false, false) .values(); for (Collection<?> collection : collections) { if (collection.size() > 0 && collection.iterator().next() instanceof Declarable) { for (Object declarable : collection) { if (declarable instanceof Exchange) { contextExchanges.add((Exchange) declarable); } else if (declarable instanceof Queue) { contextQueues.add((Queue) declarable); } else if (declarable instanceof Binding) { contextBindings.add((Binding) declarable); } } } } final Collection<Exchange> exchanges = filterDeclarables(contextExchanges); final Collection<Queue> queues = filterDeclarables(contextQueues); final Collection<Binding> bindings = filterDeclarables(contextBindings); for (Exchange exchange : exchanges) { if ((!exchange.isDurable() || exchange.isAutoDelete()) && this.logger.isInfoEnabled()) { this.logger.info("Auto-declaring a non-durable or auto-delete Exchange (" + exchange.getName() + ") durable:" + exchange.isDurable() + ", auto-delete:" + exchange.isAutoDelete() + ". " + "It will be deleted by the broker if it shuts down, and can be redeclared by closing and " + "reopening the connection."); } } for (Queue queue : queues) { if ((!queue.isDurable() || queue.isAutoDelete() || queue.isExclusive()) && this.logger.isInfoEnabled()) { this.logger.info("Auto-declaring a non-durable, auto-delete, or exclusive Queue (" + queue.getName() + ") durable:" + queue.isDurable() + ", auto-delete:" + queue.isAutoDelete() + ", exclusive:" + queue.isExclusive() + ". " + "It will be redeclared if the broker stops and is restarted while the connection factory is " + "alive, but all messages will be lost."); } } this.rabbitTemplate.execute(channel -> { declareExchanges(channel, exchanges.toArray(new Exchange[exchanges.size()])); declareQueues(channel, queues.toArray(new Queue[queues.size()])); declareBindings(channel, bindings.toArray(new Binding[bindings.size()])); return null; }); this.logger.debug("Declarations finished"); } /** * Remove any instances that should not be declared by this admin. * @param declarables the collection of {@link Declarable}s. * @param <T> the declarable type. * @return a new collection containing {@link Declarable}s that should be declared by this * admin. */ private <T extends Declarable> Collection<T> filterDeclarables(Collection<T> declarables) { return declarables.stream() .filter(d -> d.shouldDeclare() && (d.getDeclaringAdmins().isEmpty() || d.getDeclaringAdmins().contains(this))) .collect(Collectors.toList()); } // private methods for declaring Exchanges, Queues, and Bindings on a Channel private void declareExchanges(final Channel channel, final Exchange... exchanges) throws IOException { for (final Exchange exchange : exchanges) { if (this.logger.isDebugEnabled()) { this.logger.debug("declaring Exchange '" + exchange.getName() + "'"); } if (!isDeclaringDefaultExchange(exchange)) { try { if (exchange.isDelayed()) { Map<String, Object> arguments = exchange.getArguments(); if (arguments == null) { arguments = new HashMap<String, Object>(); } else { arguments = new HashMap<String, Object>(arguments); } arguments.put("x-delayed-type", exchange.getType()); channel.exchangeDeclare(exchange.getName(), DELAYED_MESSAGE_EXCHANGE, exchange.isDurable(), exchange.isAutoDelete(), exchange.isInternal(), arguments); } else { channel.exchangeDeclare(exchange.getName(), exchange.getType(), exchange.isDurable(), exchange.isAutoDelete(), exchange.isInternal(), exchange.getArguments()); } } catch (IOException e) { logOrRethrowDeclarationException(exchange, "exchange", e); } } } } private DeclareOk[] declareQueues(final Channel channel, final Queue... queues) throws IOException { List<DeclareOk> declareOks = new ArrayList<DeclareOk>(queues.length); for (int i = 0; i < queues.length; i++) { Queue queue = queues[i]; if (!queue.getName().startsWith("amq.")) { if (this.logger.isDebugEnabled()) { this.logger.debug("declaring Queue '" + queue.getName() + "'"); } try { try { DeclareOk declareOk = channel.queueDeclare(queue.getName(), queue.isDurable(), queue.isExclusive(), queue.isAutoDelete(), queue.getArguments()); declareOks.add(declareOk); } catch (IllegalArgumentException e) { if (this.logger.isDebugEnabled()) { this.logger.error("Exception while declaring queue: '" + queue.getName() + "'"); } try { if (channel instanceof ChannelProxy) { ((ChannelProxy) channel).getTargetChannel().close(); } } catch (TimeoutException e1) { } throw new IOException(e); } } catch (IOException e) { logOrRethrowDeclarationException(queue, "queue", e); } } this.logger.debug("Queue with name that starts with 'amq.' cannot be declared."); } return declareOks.toArray(new DeclareOk[declareOks.size()]); } private void declareBindings(final Channel channel, final Binding... bindings) throws IOException { for (Binding binding : bindings) { if (this.logger.isDebugEnabled()) { this.logger.debug("Binding destination [" + binding.getDestination() + " (" + binding.getDestinationType() + ")] to exchange [" + binding.getExchange() + "] with routing key [" + binding.getRoutingKey() + "]"); } try { if (binding.isDestinationQueue()) { if (!isDeclaringImplicitQueueBinding(binding)) { channel.queueBind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } } else { channel.exchangeBind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } } catch (IOException e) { logOrRethrowDeclarationException(binding, "binding", e); } } } private <T extends Throwable> void logOrRethrowDeclarationException(Declarable element, String elementType, T t) throws T { DeclarationExceptionEvent event = new DeclarationExceptionEvent(this, element, t); this.lastDeclarationExceptionEvent = event; if (this.applicationEventPublisher != null) { this.applicationEventPublisher.publishEvent(event); } if (this.ignoreDeclarationExceptions || (element != null && element.isIgnoreDeclarationExceptions())) { if (this.logger.isDebugEnabled()) { this.logger.debug("Failed to declare " + elementType + ": " + (element == null ? "broker-generated" : element) + ", continuing...", t); } else if (this.logger.isWarnEnabled()) { Throwable cause = t; if (t instanceof IOException && t.getCause() != null) { cause = t.getCause(); } this.logger.warn("Failed to declare " + elementType + ": " + (element == null ? "broker-generated" : element) + ", continuing... " + cause); } } else { throw t; } } private boolean isDeclaringDefaultExchange(Exchange exchange) { if (isDefaultExchange(exchange.getName())) { this.logger.debug("Default exchange is pre-declared by server."); return true; } return false; } private boolean isDeletingDefaultExchange(String exchangeName) { if (isDefaultExchange(exchangeName)) { this.logger.debug("Default exchange cannot be deleted."); return true; } return false; } private boolean isDefaultExchange(String exchangeName) { return DEFAULT_EXCHANGE_NAME.equals(exchangeName); } private boolean isDeclaringImplicitQueueBinding(Binding binding) { if (isImplicitQueueBinding(binding)) { this.logger.debug("The default exchange is implicitly bound to every queue," + " with a routing key equal to the queue name."); return true; } return false; } private boolean isRemovingImplicitQueueBinding(Binding binding) { if (isImplicitQueueBinding(binding)) { this.logger.debug("Cannot remove implicit default exchange binding to queue."); return true; } return false; } private boolean isImplicitQueueBinding(Binding binding) { return isDefaultExchange(binding.getExchange()) && binding.getDestination().equals(binding.getRoutingKey()); } }