Java tutorial
/* * Copyright 2002-2014 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.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.PublisherCallbackChannel; import org.springframework.amqp.rabbit.support.PublisherCallbackChannelImpl; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; /** * A {@link ConnectionFactory} implementation that (when the cache mode is {@link CacheMode#CHANNEL} (default) * returns the same Connection from all {@link #createConnection()} * calls, and ignores calls to {@link com.rabbitmq.client.Connection#close()} and caches * {@link com.rabbitmq.client.Channel}. * * <p> * By default, only one Channel will be cached, with further requested Channels being created and disposed on demand. * Consider raising the {@link #setChannelCacheSize(int) "channelCacheSize" value} in case of a high-concurrency * environment. * * <p> * When the cache mode is {@link CacheMode#CONNECTION}, a new (or cached) connection is used for each request. In this case, * no channels are cached, just connections. The intended use case is a dedicated connection for long-lived * channels, such as those used in listener container threads. In those cases, the channel must be closed * anyway in order to re-queue any un-acked messages. * <p> * <b>{@link CacheMode#CONNECTION} is not compatible with a Rabbit Admin that auto-declares queues etc.</b> * <p> * <b>NOTE: This ConnectionFactory requires explicit closing of all Channels obtained form its shared Connection.</b> * This is the usual recommendation for native Rabbit access code anyway. However, with this ConnectionFactory, its use * is mandatory in order to actually allow for Channel reuse. * * @author Mark Pollack * @author Mark Fisher * @author Dave Syer * @author Gary Russell * @author Artem Bilan */ public class CachingConnectionFactory extends AbstractConnectionFactory implements InitializingBean, ShutdownListener { public enum CacheMode { /** * Cache channels - single connection */ CHANNEL, /** * Cache connections - no channel caching */ CONNECTION } private volatile CacheMode cacheMode = CacheMode.CHANNEL; private final Set<ChannelCachingConnectionProxy> openConnections = new HashSet<ChannelCachingConnectionProxy>(); private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> openConnectionNonTransactionalChannels = new HashMap<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>(); private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> openConnectionTransactionalChannels = new HashMap<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>>(); private final BlockingQueue<ChannelCachingConnectionProxy> idleConnections = new LinkedBlockingQueue<ChannelCachingConnectionProxy>(); private volatile int channelCacheSize = 1; private volatile int connectionCacheSize = 1; private final LinkedList<ChannelProxy> cachedChannelsNonTransactional = new LinkedList<ChannelProxy>(); private final LinkedList<ChannelProxy> cachedChannelsTransactional = new LinkedList<ChannelProxy>(); private volatile boolean active = true; private volatile ChannelCachingConnectionProxy connection; private volatile boolean publisherConfirms; private volatile boolean publisherReturns; private volatile boolean initialized; /** Synchronization monitor for the shared Connection */ private final Object connectionMonitor = new Object(); /** * Create a new CachingConnectionFactory initializing the hostname to be the value returned from * InetAddress.getLocalHost(), or "localhost" if getLocalHost() throws an exception. */ public CachingConnectionFactory() { this((String) null); } /** * Create a new CachingConnectionFactory given a host name * and port. * * @param hostname the host name to connect to * @param port the port number */ public CachingConnectionFactory(String hostname, int port) { super(new com.rabbitmq.client.ConnectionFactory()); if (!StringUtils.hasText(hostname)) { hostname = getDefaultHostName(); } setHost(hostname); setPort(port); } /** * Create a new CachingConnectionFactory given a port on the hostname returned from * InetAddress.getLocalHost(), or "localhost" if getLocalHost() throws an exception. * * @param port the port number */ public CachingConnectionFactory(int port) { this(null, port); } /** * Create a new CachingConnectionFactory given a host name. * * @param hostname the host name to connect to */ public CachingConnectionFactory(String hostname) { this(hostname, com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT); } /** * Create a new CachingConnectionFactory for the given target ConnectionFactory. * * @param rabbitConnectionFactory the target ConnectionFactory */ public CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) { super(rabbitConnectionFactory); } public void setChannelCacheSize(int sessionCacheSize) { Assert.isTrue(sessionCacheSize >= 1, "Channel cache size must be 1 or higher"); this.channelCacheSize = sessionCacheSize; } public int getChannelCacheSize() { return this.channelCacheSize; } public CacheMode getCacheMode() { return cacheMode; } public void setCacheMode(CacheMode cacheMode) { Assert.isTrue(!initialized, "'cacheMode' cannot be changed after initialization."); Assert.notNull(cacheMode, "'cacheMode' must not be null."); this.cacheMode = cacheMode; } public int getConnectionCachesize() { return connectionCacheSize; } public void setConnectionCacheSize(int connectionCacheSize) { Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher."); this.connectionCacheSize = connectionCacheSize; } public boolean isPublisherConfirms() { return publisherConfirms; } public boolean isPublisherReturns() { return publisherReturns; } public void setPublisherReturns(boolean publisherReturns) { this.publisherReturns = publisherReturns; } public void setPublisherConfirms(boolean publisherConfirms) { this.publisherConfirms = publisherConfirms; } @Override public void afterPropertiesSet() throws Exception { this.initialized = true; if (this.cacheMode == CacheMode.CHANNEL) { Assert.isTrue(this.connectionCacheSize == 1, "When the cache mode is 'CHANNEL', the connection cache size cannot be configured."); } } @Override public void setConnectionListeners(List<? extends ConnectionListener> listeners) { super.setConnectionListeners(listeners); // If the connection is already alive we assume that the new listeners want to be notified if (this.connection != null) { this.getConnectionListener().onCreate(this.connection); } } @Override public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // If the connection is already alive we assume that the new listener wants to be notified if (this.connection != null) { listener.onCreate(this.connection); } } @Override public void shutdownCompleted(ShutdownSignalException cause) { if (!RabbitUtils.isNormalChannelClose(cause)) { logger.error("Channel shutdown: " + cause.getMessage()); } } private Channel getChannel(ChannelCachingConnectionProxy connection, boolean transactional) { LinkedList<ChannelProxy> channelList; if (this.cacheMode == CacheMode.CHANNEL) { channelList = transactional ? this.cachedChannelsTransactional : this.cachedChannelsNonTransactional; } else { channelList = transactional ? this.openConnectionTransactionalChannels.get(connection) : this.openConnectionNonTransactionalChannels.get(connection); } if (channelList == null) { channelList = new LinkedList<ChannelProxy>(); if (transactional) { this.openConnectionTransactionalChannels.put(connection, channelList); } else { this.openConnectionNonTransactionalChannels.put(connection, channelList); } } Channel channel = null; if (connection.isOpen()) { synchronized (channelList) { while (!channelList.isEmpty()) { channel = channelList.removeFirst(); if (channel.isOpen()) { break; } else { channel = null; } } } if (channel != null) { if (logger.isTraceEnabled()) { logger.trace("Found cached Rabbit Channel: " + channel.toString()); } } } if (channel == null) { channel = getCachedChannelProxy(connection, channelList, transactional); } return channel; } private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection, LinkedList<ChannelProxy> channelList, boolean transactional) { Channel targetChannel = createBareChannel(connection, transactional); if (logger.isDebugEnabled()) { logger.debug("Creating cached Rabbit Channel from " + targetChannel); } getChannelListener().onCreate(targetChannel, transactional); Class<?>[] interfaces; if (this.publisherConfirms || this.publisherReturns) { interfaces = new Class<?>[] { ChannelProxy.class, PublisherCallbackChannel.class }; } else { interfaces = new Class<?>[] { ChannelProxy.class }; } return (ChannelProxy) Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(), interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channelList, transactional)); } private Channel createBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) { if (this.cacheMode == CacheMode.CHANNEL) { if (this.connection == null || !this.connection.isOpen()) { synchronized (this.connectionMonitor) { if (this.connection != null && !this.connection.isOpen()) { this.connection.notifyCloseIfNecessary(); } if (this.connection == null || !this.connection.isOpen()) { this.connection = null; createConnection(); } } } return doCreateBareChannel(this.connection, transactional); } else if (this.cacheMode == CacheMode.CONNECTION) { if (!connection.isOpen()) { synchronized (connectionMonitor) { this.openConnectionNonTransactionalChannels.get(connection).clear(); this.openConnectionTransactionalChannels.get(connection).clear(); connection.notifyCloseIfNecessary(); ChannelCachingConnectionProxy newConnection = (ChannelCachingConnectionProxy) createConnection(); /* * Applications already have a reference to the proxy, so we steal the new (or idle) connection's * target and remove the connection from the open list. */ connection.target = newConnection.target; connection.closeNotified.set(false); this.openConnections.remove(newConnection); } } return doCreateBareChannel(connection, transactional); } return null; } private Channel doCreateBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) { Channel channel = connection.createBareChannel(transactional); if (this.publisherConfirms) { try { channel.confirmSelect(); } catch (IOException e) { logger.error("Could not configure the channel to receive publisher confirms", e); } } if (this.publisherConfirms || this.publisherReturns) { if (!(channel instanceof PublisherCallbackChannelImpl)) { channel = new PublisherCallbackChannelImpl(channel); } } if (channel != null) { channel.addShutdownListener(this); } return channel; } @Override public final Connection createConnection() throws AmqpException { synchronized (this.connectionMonitor) { if (this.cacheMode == CacheMode.CHANNEL) { if (this.connection == null) { this.connection = new ChannelCachingConnectionProxy(super.createBareConnection()); // invoke the listener *after* this.connection is assigned getConnectionListener().onCreate(connection); } return this.connection; } else if (this.cacheMode == CacheMode.CONNECTION) { ChannelCachingConnectionProxy connection = null; while (connection == null && !this.idleConnections.isEmpty()) { connection = this.idleConnections.poll(); if (connection != null) { if (!connection.isOpen()) { if (logger.isDebugEnabled()) { logger.debug("Removing closed connection '" + connection + "'"); } connection.notifyCloseIfNecessary(); this.openConnections.remove(connection); this.openConnectionNonTransactionalChannels.remove(connection); this.openConnectionTransactionalChannels.remove(connection); connection = null; } } } if (connection == null) { connection = new ChannelCachingConnectionProxy(super.createBareConnection()); getConnectionListener().onCreate(connection); if (logger.isDebugEnabled()) { logger.debug("Adding new connection '" + connection + "'"); } this.openConnections.add(connection); this.openConnectionNonTransactionalChannels.put(connection, new LinkedList<ChannelProxy>()); this.openConnectionTransactionalChannels.put(connection, new LinkedList<ChannelProxy>()); } else { if (logger.isDebugEnabled()) { logger.debug("Obtained connection '" + connection + "' from cache"); } } return connection; } } return null; } /** * Close the underlying shared connection. The provider of this ConnectionFactory needs to care for proper shutdown. * <p> * As this bean implements DisposableBean, a bean factory will automatically invoke this on destruction of its * cached singletons. */ @Override public final void destroy() { synchronized (this.connectionMonitor) { if (connection != null) { this.connection.destroy(); this.connection = null; } for (ChannelCachingConnectionProxy connection : this.openConnections) { connection.destroy(); } this.openConnections.clear(); this.idleConnections.clear(); this.openConnectionNonTransactionalChannels.clear(); this.openConnectionTransactionalChannels.clear(); } reset(); } /** * Reset the Channel cache and underlying shared Connection, to be reinitialized on next access. */ protected void reset() { this.active = false; if (this.cacheMode == CacheMode.CHANNEL) { synchronized (this.cachedChannelsNonTransactional) { for (ChannelProxy channel : cachedChannelsNonTransactional) { try { channel.getTargetChannel().close(); } catch (Throwable ex) { logger.trace("Could not close cached Rabbit Channel", ex); } } this.cachedChannelsNonTransactional.clear(); } synchronized (this.cachedChannelsTransactional) { for (ChannelProxy channel : cachedChannelsTransactional) { try { channel.getTargetChannel().close(); } catch (Throwable ex) { logger.trace("Could not close cached Rabbit Channel", ex); } } this.cachedChannelsTransactional.clear(); } } this.active = true; this.connection = null; } @Override public String toString() { return "CachingConnectionFactory [channelCacheSize=" + channelCacheSize + ", host=" + this.getHost() + ", port=" + this.getPort() + ", active=" + active + "]"; } private class CachedChannelInvocationHandler implements InvocationHandler { private final ChannelCachingConnectionProxy theConnection; private volatile Channel target; private final LinkedList<ChannelProxy> channelList; private final Object targetMonitor = new Object(); private final boolean transactional; public CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection, Channel target, LinkedList<ChannelProxy> channelList, boolean transactional) { this.theConnection = connection; this.target = target; this.channelList = channelList; this.transactional = transactional; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("txSelect") && !this.transactional) { throw new UnsupportedOperationException("Cannot start transaction on non-transactional channel"); } if (methodName.equals("equals")) { // Only consider equal when proxies are identical. return (proxy == args[0]); } else if (methodName.equals("hashCode")) { // Use hashCode of Channel proxy. return System.identityHashCode(proxy); } else if (methodName.equals("toString")) { return "Cached Rabbit Channel: " + this.target; } else if (methodName.equals("close")) { // Handle close method: don't pass the call on. if (active) { synchronized (this.channelList) { if (!RabbitUtils.isPhysicalCloseRequired() && this.channelList.size() < getChannelCacheSize()) { logicalClose((ChannelProxy) proxy); // Remain open in the channel list. return null; } } } // If we get here, we're supposed to shut down. physicalClose(); return null; } else if (methodName.equals("getTargetChannel")) { // Handle getTargetChannel method: return underlying Channel. return this.target; } else if (methodName.equals("isOpen")) { // Handle isOpen method: we are closed if the target is closed return this.target != null && this.target.isOpen(); } try { if (this.target == null || !this.target.isOpen()) { this.target = null; } synchronized (targetMonitor) { if (this.target == null) { this.target = createBareChannel(theConnection, transactional); } return method.invoke(this.target, args); } } catch (InvocationTargetException ex) { if (this.target == null || !this.target.isOpen()) { // Basic re-connection logic... this.target = null; if (logger.isDebugEnabled()) { logger.debug("Detected closed channel on exception. Re-initializing: " + target); } synchronized (targetMonitor) { if (this.target == null) { this.target = createBareChannel(theConnection, transactional); } } } throw ex.getTargetException(); } } /** * GUARDED by channelList * * @param proxy the channel to close */ private void logicalClose(ChannelProxy proxy) throws Exception { if (this.target != null && !this.target.isOpen()) { synchronized (targetMonitor) { if (this.target != null && !this.target.isOpen()) { this.target = null; return; } } } // Allow for multiple close calls... if (!this.channelList.contains(proxy)) { if (logger.isTraceEnabled()) { logger.trace("Returning cached Channel: " + this.target); } this.channelList.addLast(proxy); } } private void physicalClose() throws Exception { if (logger.isDebugEnabled()) { logger.debug("Closing cached Channel: " + this.target); } if (this.target == null) { return; } if (this.target.isOpen()) { synchronized (targetMonitor) { if (this.target.isOpen()) { this.target.close(); } this.target = null; } } } } private class ChannelCachingConnectionProxy implements Connection, ConnectionProxy { private volatile Connection target; private final AtomicBoolean closeNotified = new AtomicBoolean(false); public ChannelCachingConnectionProxy(Connection target) { this.target = target; } private Channel createBareChannel(boolean transactional) { return target.createChannel(transactional); } @Override public Channel createChannel(boolean transactional) { Channel channel = getChannel(this, transactional); return channel; } @Override public void close() { if (cacheMode == CacheMode.CONNECTION) { synchronized (connectionMonitor) { if (!this.target.isOpen() || idleConnections.size() >= connectionCacheSize) { if (logger.isDebugEnabled()) { logger.debug("Completely closing connection '" + this + "'"); } if (this.target.isOpen()) { RabbitUtils.closeConnection(this.target); } this.notifyCloseIfNecessary(); openConnections.remove(this); openConnectionNonTransactionalChannels.remove(this); openConnectionTransactionalChannels.remove(this); } else { if (!idleConnections.contains(this)) { if (logger.isDebugEnabled()) { logger.debug("Returning connection '" + this + "' to cache"); } idleConnections.add(this); } } } } } public void destroy() { if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) { reset(); } if (this.target != null) { RabbitUtils.closeConnection(this.target); this.notifyCloseIfNecessary(); } this.target = null; } private void notifyCloseIfNecessary() { if (!(this.closeNotified.getAndSet(true))) { getConnectionListener().onClose(this); } } @Override public boolean isOpen() { return target != null && target.isOpen(); } @Override public Connection getTargetConnection() { return target; } @Override public int hashCode() { return 31 + ((target == null) ? 0 : target.hashCode()); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } ChannelCachingConnectionProxy other = (ChannelCachingConnectionProxy) obj; if (target == null) { if (other.target != null) { return false; } } else if (!target.equals(other.target)) { return false; } return true; } @Override public String toString() { return cacheMode == CacheMode.CHANNEL ? "Shared " : "Dedicated " + "Rabbit Connection: " + this.target; } } }