/* * Copyright 2015-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.connection; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.amqp.AmqpException; import org.springframework.beans.factory.DisposableBean; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import com.rabbitmq.http.client.Client; import com.rabbitmq.http.client.domain.QueueInfo; /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and * returns a factory that connects directly to that node. * The RabbitMQ management plugin is called over REST to determine the node and the corresponding * address for that node is injected into the connection factory. * A single instance of each connection factory is retained in a cache. * If the location cannot be determined, the default connection factory is returned. This connection * factory is typically configured to connect to all the servers in a fail-over mode. * <p>{@link #getTargetConnectionFactory(Object)} is invoked by the * {@code SimpleMessageListenerContainer}, when establishing a connection, with the lookup key having * the format {@code '[queueName]'}. * <p>All {@link ConnectionFactory} methods delegate to the default * * @author Gary Russell * @since 1.2 */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean { private final Log logger = LogFactory.getLog(getClass()); private final Map<String, ConnectionFactory> nodeFactories = new HashMap<String, ConnectionFactory>(); private final ConnectionFactory defaultConnectionFactory; private final String[] adminUris; private final Map<String, String> nodeToAddress = new HashMap<String, String>(); private final String vhost; private final String username; private final String password; private final boolean useSSL; private final Resource sslPropertiesLocation; private final String keyStore; private final String trustStore; private final String keyStorePassPhrase; private final String trustStorePassPhrase; /** * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) * @param adminUris the rabbitmq admin addresses (http://host:port, ...) must be the * same length as addresses. * @param vhost the virtual host. * @param username the user name. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. */ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory, Map<String, String> nodeToAddress, String[] adminUris, String vhost, String username, String password, boolean useSSL, Resource sslPropertiesLocation) { Assert.notNull(defaultConnectionFactory, "'defaultConnectionFactory' cannot be null"); this.defaultConnectionFactory = defaultConnectionFactory; this.adminUris = Arrays.copyOf(adminUris, adminUris.length); this.nodeToAddress.putAll(nodeToAddress); this.vhost = vhost; this.username = username; this.password = password; this.useSSL = useSSL; this.sslPropertiesLocation = sslPropertiesLocation; this.keyStore = null; this.trustStore = null; this.keyStorePassPhrase = null; this.trustStorePassPhrase = null; } /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) * @param adminUris the rabbitmq admin addresses (http://host:port, ...) must be the same length * as addresses. * @param vhost the virtual host. * @param username the user name. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). * @param trustStore the trust store resource (e.g. "file:/foo/truststore"). * @param keyStorePassPhrase the pass phrase for the key store. * @param trustStorePassPhrase the pass phrase for the trust store. */ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory, Map<String, String> nodeToAddress, String[] adminUris, String vhost, String username, String password, boolean useSSL, String keyStore, String trustStore, String keyStorePassPhrase, String trustStorePassPhrase) { Assert.notNull(defaultConnectionFactory, "'defaultConnectionFactory' cannot be null"); this.defaultConnectionFactory = defaultConnectionFactory; this.adminUris = Arrays.copyOf(adminUris, adminUris.length); this.nodeToAddress.putAll(nodeToAddress); this.vhost = vhost; this.username = username; this.password = password; this.useSSL = useSSL; this.sslPropertiesLocation = null; this.keyStore = keyStore; this.trustStore = trustStore; this.keyStorePassPhrase = keyStorePassPhrase; this.trustStorePassPhrase = trustStorePassPhrase; } /** * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). * @param adminUris the rabbitmq admin addresses (http://host:port, ...) * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) * must be the same length as addresses. * @param vhost the virtual host. * @param username the user name. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. */ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory, String[] addresses, String[] adminUris, String[] nodes, String vhost, String username, String password, boolean useSSL, Resource sslPropertiesLocation) { Assert.notNull(defaultConnectionFactory, "'defaultConnectionFactory' cannot be null"); Assert.isTrue(addresses.length == nodes.length, "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); this.defaultConnectionFactory = defaultConnectionFactory; this.adminUris = Arrays.copyOf(adminUris, adminUris.length); for (int i = 0; i < addresses.length; i++) { this.nodeToAddress.put(nodes[i], addresses[i]); } this.vhost = vhost; this.username = username; this.password = password; this.useSSL = useSSL; this.sslPropertiesLocation = sslPropertiesLocation; this.keyStore = null; this.trustStore = null; this.keyStorePassPhrase = null; this.trustStorePassPhrase = null; } /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). * @param adminUris the rabbitmq admin addresses (http://host:port, ...). * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) must be the same length * as addresses. * @param vhost the virtual host. * @param username the user name. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). * @param trustStore the trust store resource (e.g. "file:/foo/truststore"). * @param keyStorePassPhrase the pass phrase for the key store. * @param trustStorePassPhrase the pass phrase for the trust store. */ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactory, String[] addresses, String[] adminUris, String[] nodes, String vhost, String username, String password, boolean useSSL, String keyStore, String trustStore, String keyStorePassPhrase, String trustStorePassPhrase) { Assert.notNull(defaultConnectionFactory, "'defaultConnectionFactory' cannot be null"); Assert.isTrue(addresses.length == nodes.length, "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); this.defaultConnectionFactory = defaultConnectionFactory; this.adminUris = Arrays.copyOf(adminUris, adminUris.length); for (int i = 0; i < addresses.length; i++) { this.nodeToAddress.put(nodes[i], addresses[i]); } this.vhost = vhost; this.username = username; this.password = password; this.useSSL = useSSL; this.sslPropertiesLocation = null; this.keyStore = keyStore; this.trustStore = trustStore; this.keyStorePassPhrase = keyStorePassPhrase; this.trustStorePassPhrase = trustStorePassPhrase; } @Override public Connection createConnection() throws AmqpException { return this.defaultConnectionFactory.createConnection(); } @Override public String getHost() { return this.defaultConnectionFactory.getHost(); } @Override public int getPort() { return this.defaultConnectionFactory.getPort(); } @Override public String getVirtualHost() { return this.vhost; } @Override public String getUsername() { return this.username; } @Override public void addConnectionListener(ConnectionListener listener) { this.defaultConnectionFactory.addConnectionListener(listener); } @Override public boolean removeConnectionListener(ConnectionListener listener) { return this.defaultConnectionFactory.removeConnectionListener(listener); } @Override public void clearConnectionListeners() { this.defaultConnectionFactory.clearConnectionListeners(); } @Override public ConnectionFactory getTargetConnectionFactory(Object key) { String queue = ((String) key); queue = queue.substring(1, queue.length() - 1); Assert.isTrue(!queue.contains(","), () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); ConnectionFactory connectionFactory = determineConnectionFactory(queue); if (connectionFactory == null) { return this.defaultConnectionFactory; } else { return connectionFactory; } } private ConnectionFactory determineConnectionFactory(String queue) { for (int i = 0; i < this.adminUris.length; i++) { String adminUri = this.adminUris[i]; if (!adminUri.endsWith("/api/")) { adminUri += "/api/"; } try { Client client = createClient(adminUri, this.username, this.password); QueueInfo queueInfo = client.getQueue(this.vhost, queue); if (queueInfo != null) { String node = queueInfo.getNode(); if (node != null) { String uri = this.nodeToAddress.get(node); if (uri != null) { return nodeConnectionFactory(queue, node, uri); } if (this.logger.isDebugEnabled()) { this.logger.debug("No match for node: " + node); } } } else { throw new AmqpException("Admin returned null QueueInfo"); } } catch (Exception e) { this.logger.warn("Failed to determine queue location for: " + queue + " at: " + adminUri + ": " + e.getMessage()); } } this.logger.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); return null; } /** * Create a client instance. * @param adminUri the admin URI. * @param username the username * @param password the password. * @return The client. * @throws MalformedURLException if the URL is malformed * @throws URISyntaxException if there is a syntax error. */ protected Client createClient(String adminUri, String username, String password) throws MalformedURLException, URISyntaxException { return new Client(adminUri, username, password); } private synchronized ConnectionFactory nodeConnectionFactory(String queue, String node, String address) throws Exception { if (this.logger.isInfoEnabled()) { this.logger.info("Queue: " + queue + " is on node: " + node + " at: " + address); } ConnectionFactory cf = this.nodeFactories.get(node); if (cf == null) { cf = createConnectionFactory(address, node); if (this.logger.isInfoEnabled()) { this.logger.info("Created new connection factory: " + cf); } this.nodeFactories.put(node, cf); } return cf; } /** * Create a dedicated connection factory for the address. * @param address the address to which the factory should connect. * @param node the node. * @return the connection factory. * @throws Exception if errors occur during creation. */ protected ConnectionFactory createConnectionFactory(String address, String node) throws Exception { RabbitConnectionFactoryBean rcfb = new RabbitConnectionFactoryBean(); rcfb.setUseSSL(this.useSSL); rcfb.setSslPropertiesLocation(this.sslPropertiesLocation); rcfb.setKeyStore(this.keyStore); rcfb.setTrustStore(this.trustStore); rcfb.setKeyStorePassphrase(this.keyStorePassPhrase); rcfb.setTrustStorePassphrase(this.trustStorePassPhrase); rcfb.afterPropertiesSet(); CachingConnectionFactory ccf = new CachingConnectionFactory(rcfb.getObject()); ccf.setAddresses(address); ccf.setUsername(this.username); ccf.setPassword(this.password); ccf.setVirtualHost(this.vhost); ccf.setBeanName("node:" + node); return ccf; } @Override public void destroy() throws Exception { for (ConnectionFactory connectionFactory : this.nodeFactories.values()) { if (connectionFactory instanceof DisposableBean) { ((DisposableBean) connectionFactory).destroy(); } } if (this.defaultConnectionFactory instanceof DisposableBean) { ((DisposableBean) this.defaultConnectionFactory).destroy(); } } }