org.springframework.amqp.rabbit.connection.CachingConnectionFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.amqp.rabbit.connection.CachingConnectionFactory.java

Source

/*
 * Copyright 2002-2019 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
 *
 *      https://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.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;

import org.springframework.amqp.AmqpApplicationContextClosedException;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.AmqpTimeoutException;
import org.springframework.amqp.rabbit.support.ActiveObjectCounter;
import org.springframework.amqp.support.ConditionalExceptionLogger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import com.rabbitmq.client.AlreadyClosedException;
import com.rabbitmq.client.BlockedListener;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ShutdownListener;
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;

/**
 * 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
 * {@link #createConnection()};
 * connections are cached according to the {@link #setConnectionCacheSize(int) "connectionCacheSize" value}.
 * Both connections and channels are cached in this mode.
 * <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 Connection(s).</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. {@link Channel#close()} returns the channel to the
 * cache, if there is room, or physically closes the channel otherwise.
 *
 * @author Mark Pollack
 * @author Mark Fisher
 * @author Dave Syer
 * @author Gary Russell
 * @author Artem Bilan
 * @author Steve Powell
 * @author Will Droste
 */
@ManagedResource
public class CachingConnectionFactory extends AbstractConnectionFactory
        implements InitializingBean, ShutdownListener {

    private static final String UNUSED = "unused";

    private static final int DEFAULT_CHANNEL_CACHE_SIZE = 25;

    private static final String DEFAULT_DEFERRED_POOL_PREFIX = "spring-rabbit-deferred-pool-";

    private static final int CHANNEL_EXEC_SHUTDOWN_TIMEOUT = 30;

    /**
     * Create a unique ID for the pool.
     */
    private static final AtomicInteger threadPoolId = new AtomicInteger(); // NOSONAR lower case

    private static final Set<String> txStarts = new HashSet<>(Arrays.asList("basicPublish", "basicAck", // NOSONAR
            "basicNack", "basicReject"));

    private static final Set<String> ackMethods = new HashSet<>(Arrays.asList("basicAck", // NOSONAR
            "basicNack", "basicReject"));

    private static final Set<String> txEnds = new HashSet<>(Arrays.asList("txCommit", "txRollback")); // NOSONAR

    private final ChannelCachingConnectionProxy connection = new ChannelCachingConnectionProxy(null);

    /**
     * The cache mode.
     */
    public enum CacheMode {
        /**
         * Cache channels - single connection.
         */
        CHANNEL,
        /**
         * Cache connections and channels within each connection.
         */
        CONNECTION
    }

    private final Set<ChannelCachingConnectionProxy> allocatedConnections = new HashSet<>();

    private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> allocatedConnectionNonTransactionalChannels = new HashMap<>();

    private final Map<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> allocatedConnectionTransactionalChannels = new HashMap<>();

    private final BlockingDeque<ChannelCachingConnectionProxy> idleConnections = new LinkedBlockingDeque<>();

    private final LinkedList<ChannelProxy> cachedChannelsNonTransactional = new LinkedList<>(); // NOSONAR removeFirst()

    private final LinkedList<ChannelProxy> cachedChannelsTransactional = new LinkedList<>(); // NOSONAR removeFirst()

    private final Map<Connection, Semaphore> checkoutPermits = new HashMap<>();

    private final Map<String, AtomicInteger> channelHighWaterMarks = new HashMap<>();

    private final AtomicInteger connectionHighWaterMark = new AtomicInteger();

    private final CachingConnectionFactory publisherConnectionFactory;

    /** Synchronization monitor for the shared Connection. */
    private final Object connectionMonitor = new Object();

    private final ActiveObjectCounter<Channel> inFlightAsyncCloses = new ActiveObjectCounter<>();

    private long channelCheckoutTimeout = 0;

    private CacheMode cacheMode = CacheMode.CHANNEL;

    private int channelCacheSize = DEFAULT_CHANNEL_CACHE_SIZE;

    private int connectionCacheSize = 1;

    private int connectionLimit = Integer.MAX_VALUE;

    private boolean publisherConfirms;

    private boolean simplePublisherConfirms;

    private boolean publisherReturns;

    private ConditionalExceptionLogger closeExceptionLogger = new DefaultChannelCloseLogger();

    private PublisherCallbackChannelFactory publisherChannelFactory = PublisherCallbackChannelImpl.factory();

    private volatile boolean active = true;

    private volatile boolean initialized;

    /**
     * Executor used for channels if no explicit executor set.
     */
    private volatile ExecutorService channelsExecutor;

    private volatile boolean stopped;

    /**
     * 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.
     * @param hostname the host name to connect to
     */
    public CachingConnectionFactory(@Nullable String hostname) {
        this(hostname, com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_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
     * and port.
     * @param hostNameArg the host name to connect to
     * @param port the port number
     */
    public CachingConnectionFactory(@Nullable String hostNameArg, int port) {
        super(newRabbitConnectionFactory());
        String hostname = hostNameArg;
        if (!StringUtils.hasText(hostname)) {
            hostname = getDefaultHostName();
        }
        setHost(hostname);
        setPort(port);
        this.publisherConnectionFactory = new CachingConnectionFactory(getRabbitConnectionFactory(), true);
        setPublisherConnectionFactory(this.publisherConnectionFactory);
    }

    /**
     * Create a new CachingConnectionFactory given a {@link URI}.
     * @param uri the amqp uri configuring the connection
     * @since 1.5
     */
    public CachingConnectionFactory(URI uri) {
        super(newRabbitConnectionFactory());
        setUri(uri);
        this.publisherConnectionFactory = new CachingConnectionFactory(getRabbitConnectionFactory(), true);
        setPublisherConnectionFactory(this.publisherConnectionFactory);
    }

    /**
     * Create a new CachingConnectionFactory for the given target ConnectionFactory.
     * @param rabbitConnectionFactory the target ConnectionFactory
     */
    public CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) {
        this(rabbitConnectionFactory, false);
    }

    /**
     * Create a new CachingConnectionFactory for the given target ConnectionFactory.
     * @param rabbitConnectionFactory the target ConnectionFactory
     * @param isPublisherFactory true if this is the publisher sub-factory.
     */
    private CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory,
            boolean isPublisherFactory) {
        super(rabbitConnectionFactory);
        if (!isPublisherFactory) {
            if (rabbitConnectionFactory.isAutomaticRecoveryEnabled()) {
                rabbitConnectionFactory.setAutomaticRecoveryEnabled(false);
                logger.warn("***\nAutomatic Recovery was Enabled in the provided connection factory;\n"
                        + "while Spring AMQP is generally compatible with this feature, there\n"
                        + "are some corner cases where problems arise. Spring AMQP\n"
                        + "prefers to use its own recovery mechanisms; when this option is true, you may receive\n"
                        + "'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered.\n"
                        + "It has therefore been disabled; if you really wish to enable it, use\n"
                        + "'getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true)',\n"
                        + "but this is discouraged.");
            }
            this.publisherConnectionFactory = new CachingConnectionFactory(getRabbitConnectionFactory(), true);
            setPublisherConnectionFactory(this.publisherConnectionFactory);
        } else {
            this.publisherConnectionFactory = null;
        }
    }

    private static com.rabbitmq.client.ConnectionFactory newRabbitConnectionFactory() {
        com.rabbitmq.client.ConnectionFactory connectionFactory = new com.rabbitmq.client.ConnectionFactory();
        connectionFactory.setAutomaticRecoveryEnabled(false);
        return connectionFactory;
    }

    /**
     * The number of channels to maintain in the cache. By default, channels are allocated on
     * demand (unbounded) and this represents the maximum cache size. To limit the available
     * channels, see {@link #setChannelCheckoutTimeout(long)}.
     * @param sessionCacheSize the channel cache size.
     * @see #setChannelCheckoutTimeout(long)
     */
    public void setChannelCacheSize(int sessionCacheSize) {
        Assert.isTrue(sessionCacheSize >= 1, "Channel cache size must be 1 or higher");
        this.channelCacheSize = sessionCacheSize;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setChannelCacheSize(sessionCacheSize);
        }
    }

    public int getChannelCacheSize() {
        return this.channelCacheSize;
    }

    public CacheMode getCacheMode() {
        return this.cacheMode;
    }

    public void setCacheMode(CacheMode cacheMode) {
        Assert.isTrue(!this.initialized, "'cacheMode' cannot be changed after initialization.");
        Assert.notNull(cacheMode, "'cacheMode' must not be null.");
        this.cacheMode = cacheMode;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setCacheMode(cacheMode);
        }
    }

    public int getConnectionCacheSize() {
        return this.connectionCacheSize;
    }

    public void setConnectionCacheSize(int connectionCacheSize) {
        Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher.");
        this.connectionCacheSize = connectionCacheSize;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setConnectionCacheSize(connectionCacheSize);
        }
    }

    /**
     * Set the connection limit when using cache mode CONNECTION. When the limit is
     * reached and there are no idle connections, the
     * {@link #setChannelCheckoutTimeout(long) channelCheckoutTimeLimit} is used to wait
     * for a connection to become idle.
     * @param connectionLimit the limit.
     * @since 1.5.5
     */
    public void setConnectionLimit(int connectionLimit) {
        Assert.isTrue(connectionLimit >= 1, "Connection limit must be 1 or higher.");
        this.connectionLimit = connectionLimit;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setConnectionLimit(connectionLimit);
        }
    }

    @Override
    public boolean isPublisherConfirms() {
        return this.publisherConfirms;
    }

    @Override
    public boolean isPublisherReturns() {
        return this.publisherReturns;
    }

    public void setPublisherReturns(boolean publisherReturns) {
        this.publisherReturns = publisherReturns;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setPublisherReturns(publisherReturns);
        }
    }

    /**
     * Use full publisher confirms, with correlation data and a callback for each message.
     * @param publisherConfirms true for full publisher returns,
     * @since 1.1
     * @see #setSimplePublisherConfirms(boolean)
     */
    public void setPublisherConfirms(boolean publisherConfirms) {
        Assert.isTrue(!this.simplePublisherConfirms,
                "Cannot set both publisherConfirms and simplePublisherConfirms");
        this.publisherConfirms = publisherConfirms;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setPublisherConfirms(publisherConfirms);
        }
    }

    /**
     * Use simple publisher confirms where the template simply waits for completion.
     * @param simplePublisherConfirms true for confirms.
     * @since 2.1
     * @see #setPublisherConfirms(boolean)
     */
    public void setSimplePublisherConfirms(boolean simplePublisherConfirms) {
        Assert.isTrue(!this.publisherConfirms, "Cannot set both publisherConfirms and simplePublisherConfirms");
        this.simplePublisherConfirms = simplePublisherConfirms;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setSimplePublisherConfirms(simplePublisherConfirms);
        }
    }

    @Override
    public boolean isSimplePublisherConfirms() {
        return this.simplePublisherConfirms;
    }

    /**
     * Sets the channel checkout timeout. When greater than 0, enables channel limiting
     * in that the {@link #channelCacheSize} becomes the total number of available channels per
     * connection rather than a simple cache size. Note that changing the {@link #channelCacheSize}
     * does not affect the limit on existing connection(s), invoke {@link #destroy()} to cause a
     * new connection to be created with the new limit.
     * <p>
     * Since 1.5.5, also applies to getting a connection when the cache mode is CONNECTION.
     * @param channelCheckoutTimeout the timeout in milliseconds; default 0 (channel limiting not enabled).
     * @since 1.4.2
     * @see #setConnectionLimit(int)
     */
    public void setChannelCheckoutTimeout(long channelCheckoutTimeout) {
        this.channelCheckoutTimeout = channelCheckoutTimeout;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setChannelCheckoutTimeout(channelCheckoutTimeout);
        }
    }

    /**
     * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed
     * passive queue declaration, it is logged at debug level. Normal channel closes (200 OK) are not
     * logged. All others are logged at ERROR level (unless access is refused due to an exclusive consumer
     * condition, in which case, it is logged at INFO level).
     * @param closeExceptionLogger the {@link ConditionalExceptionLogger}.
     * @since 1.5
     */
    public void setCloseExceptionLogger(ConditionalExceptionLogger closeExceptionLogger) {
        Assert.notNull(closeExceptionLogger, "'closeExceptionLogger' cannot be null");
        this.closeExceptionLogger = closeExceptionLogger;
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.setCloseExceptionLogger(closeExceptionLogger);
        }
    }

    /**
     * Set the factory to use to create {@link PublisherCallbackChannel} instances.
     * @param publisherChannelFactory the factory.
     * @since 2.1.6
     */
    public void setPublisherChannelFactory(PublisherCallbackChannelFactory publisherChannelFactory) {
        Assert.notNull(publisherChannelFactory, "'publisherChannelFactory' cannot be null");
        this.publisherChannelFactory = publisherChannelFactory;
    }

    @Override
    public void afterPropertiesSet() {
        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.");
        }
        initCacheWaterMarks();
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.afterPropertiesSet();
        }
    }

    private void initCacheWaterMarks() {
        this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional),
                new AtomicInteger());
        this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsTransactional),
                new AtomicInteger());
    }

    @Override
    public void setConnectionListeners(List<? extends ConnectionListener> listeners) {
        super.setConnectionListeners(listeners); // handles publishing sub-factory
        // If the connection is already alive we assume that the new listeners want to be notified
        if (this.connection.target != null) {
            this.getConnectionListener().onCreate(this.connection);
        }
    }

    @Override
    public void addConnectionListener(ConnectionListener listener) {
        super.addConnectionListener(listener); // handles publishing sub-factory
        // If the connection is already alive we assume that the new listener wants to be notified
        if (this.connection.target != null) {
            listener.onCreate(this.connection);
        }
    }

    @Override
    public void shutdownCompleted(ShutdownSignalException cause) {
        this.closeExceptionLogger.log(logger, "Channel shutdown", cause);
        int protocolClassId = cause.getReason().protocolClassId();
        if (protocolClassId == RabbitUtils.CHANNEL_PROTOCOL_CLASS_ID_20) {
            getChannelListener().onShutDown(cause);
        } else if (protocolClassId == RabbitUtils.CONNECTION_PROTOCOL_CLASS_ID_10) {
            getConnectionListener().onShutDown(cause);
        }

    }

    private Channel getChannel(ChannelCachingConnectionProxy connection, boolean transactional) {
        Semaphore permits = null;
        if (this.channelCheckoutTimeout > 0) {
            permits = obtainPermits(connection);
        }
        LinkedList<ChannelProxy> channelList = determineChannelList(connection, transactional);
        ChannelProxy channel = null;
        if (connection.isOpen()) {
            channel = findOpenChannel(channelList, channel);
            if (channel != null && logger.isTraceEnabled()) {
                logger.trace("Found cached Rabbit Channel: " + channel.toString());
            }
        }
        if (channel == null) {
            try {
                channel = getCachedChannelProxy(connection, channelList, transactional);
            } catch (RuntimeException e) {
                if (permits != null) {
                    permits.release();
                    if (logger.isDebugEnabled()) {
                        logger.debug("Could not get channel; released permit for " + connection + ", remaining:"
                                + permits.availablePermits());
                    }
                }
                throw e;
            }
        }
        return channel;
    }

    private Semaphore obtainPermits(ChannelCachingConnectionProxy connection) {
        Semaphore permits;
        permits = this.checkoutPermits.get(connection);
        if (permits != null) {
            try {
                if (!permits.tryAcquire(this.channelCheckoutTimeout, TimeUnit.MILLISECONDS)) {
                    throw new AmqpTimeoutException("No available channels");
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Acquired permit for " + connection + ", remaining:" + permits.availablePermits());
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new AmqpTimeoutException("Interrupted while acquiring a channel", e);
            }
        } else {
            throw new IllegalStateException("No permits map entry for " + connection);
        }
        return permits;
    }

    private ChannelProxy findOpenChannel(LinkedList<ChannelProxy> channelList, // NOSONAR LinkedList.removeFirst()
            ChannelProxy channelArg) {
        ChannelProxy channel = channelArg;
        synchronized (channelList) {
            while (!channelList.isEmpty()) {
                channel = channelList.removeFirst();
                if (logger.isTraceEnabled()) {
                    logger.trace(channel + " retrieved from cache");
                }
                if (channel.isOpen()) {
                    break;
                } else {
                    cleanUpClosedChannel(channel);
                    channel = null;
                }
            }
        }
        return channel;
    }

    private void cleanUpClosedChannel(ChannelProxy channel) {
        try {
            Channel target = channel.getTargetChannel();
            if (target != null) {
                target.close();
                /*
                 *  To remove it from auto-recovery if so configured,
                 *  and nack any pending confirms if PublisherCallbackChannel.
                 */
            }
        } catch (AlreadyClosedException e) {
            if (logger.isTraceEnabled()) {
                logger.trace(channel + " is already closed");
            }
        } catch (IOException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Unexpected Exception closing channel " + e.getMessage());
            }
        } catch (TimeoutException e) {
            if (logger.isWarnEnabled()) {
                logger.warn("TimeoutException closing channel " + e.getMessage());
            }
        }
    }

    private LinkedList<ChannelProxy> determineChannelList(ChannelCachingConnectionProxy connection, // NOSONAR LL
            boolean transactional) {
        LinkedList<ChannelProxy> channelList; // NOSONAR must be LinkedList
        if (this.cacheMode == CacheMode.CHANNEL) {
            channelList = transactional ? this.cachedChannelsTransactional : this.cachedChannelsNonTransactional;
        } else {
            channelList = transactional ? this.allocatedConnectionTransactionalChannels.get(connection)
                    : this.allocatedConnectionNonTransactionalChannels.get(connection);
        }
        if (channelList == null) {
            throw new IllegalStateException("No channel list for connection " + connection);
        }
        return channelList;
    }

    private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection,
            LinkedList<ChannelProxy> channelList, boolean transactional) { //NOSONAR LinkedList for addLast()

        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.isOpen()) {
                synchronized (this.connectionMonitor) {
                    if (!this.connection.isOpen()) {
                        this.connection.notifyCloseIfNecessary();
                    }
                    if (!this.connection.isOpen()) {
                        this.connection.target = null;
                        createConnection();
                    }
                }
            }
            return doCreateBareChannel(this.connection, transactional);
        } else if (this.cacheMode == CacheMode.CONNECTION) {
            if (!connection.isOpen()) {
                synchronized (this.connectionMonitor) {
                    this.allocatedConnectionNonTransactionalChannels.get(connection).clear();
                    this.allocatedConnectionTransactionalChannels.get(connection).clear();
                    connection.notifyCloseIfNecessary();
                    refreshProxyConnection(connection);
                }
            }
            return doCreateBareChannel(connection, transactional);
        }
        return null; // NOSONAR doCreate will throw an exception
    }

    private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean transactional) {
        Channel channel = conn.createBareChannel(transactional);
        if (this.publisherConfirms || this.simplePublisherConfirms) {
            try {
                channel.confirmSelect();
            } catch (IOException e) {
                logger.error("Could not configure the channel to receive publisher confirms", e);
            }
        }
        if ((this.publisherConfirms || this.publisherReturns)
                && !(channel instanceof PublisherCallbackChannelImpl)) {
            channel = this.publisherChannelFactory.createChannel(channel, getChannelsExecutor());
        }
        if (channel != null) {
            channel.addShutdownListener(this);
        }
        return channel; // NOSONAR - Simple connection throws exception
    }

    @Override
    public final Connection createConnection() throws AmqpException {
        if (this.stopped) {
            throw new AmqpApplicationContextClosedException(
                    "The ApplicationContext is closed and the ConnectionFactory can no longer create connections.");
        }
        synchronized (this.connectionMonitor) {
            if (this.cacheMode == CacheMode.CHANNEL) {
                if (this.connection.target == null) {
                    this.connection.target = super.createBareConnection();
                    // invoke the listener *after* this.connection is assigned
                    if (!this.checkoutPermits.containsKey(this.connection)) {
                        this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize));
                    }
                    this.connection.closeNotified.set(false);
                    getConnectionListener().onCreate(this.connection);
                }
                return this.connection;
            } else if (this.cacheMode == CacheMode.CONNECTION) {
                return connectionFromCache();
            }
        }
        return null; // NOSONAR - never reach here - exceptions
    }

    private Connection connectionFromCache() {
        ChannelCachingConnectionProxy cachedConnection = findIdleConnection();
        long now = System.currentTimeMillis();
        if (cachedConnection == null && countOpenConnections() >= this.connectionLimit) {
            cachedConnection = waitForConnection(now);
        }
        if (cachedConnection == null) {
            if (countOpenConnections() >= this.connectionLimit
                    && System.currentTimeMillis() - now >= this.channelCheckoutTimeout) {
                throw new AmqpTimeoutException("Timed out attempting to get a connection");
            }
            cachedConnection = new ChannelCachingConnectionProxy(super.createBareConnection());
            if (logger.isDebugEnabled()) {
                logger.debug("Adding new connection '" + cachedConnection + "'");
            }
            this.allocatedConnections.add(cachedConnection);
            this.allocatedConnectionNonTransactionalChannels.put(cachedConnection, new LinkedList<ChannelProxy>());
            this.channelHighWaterMarks.put(
                    ObjectUtils.getIdentityHexString(
                            this.allocatedConnectionNonTransactionalChannels.get(cachedConnection)),
                    new AtomicInteger());
            this.allocatedConnectionTransactionalChannels.put(cachedConnection, new LinkedList<ChannelProxy>());
            this.channelHighWaterMarks.put(
                    ObjectUtils.getIdentityHexString(
                            this.allocatedConnectionTransactionalChannels.get(cachedConnection)),
                    new AtomicInteger());
            this.checkoutPermits.put(cachedConnection, new Semaphore(this.channelCacheSize));
            getConnectionListener().onCreate(cachedConnection);
        } else if (!cachedConnection.isOpen()) {
            try {
                refreshProxyConnection(cachedConnection);
            } catch (Exception e) {
                this.idleConnections.addLast(cachedConnection);
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Obtained connection '" + cachedConnection + "' from cache");
            }
        }
        return cachedConnection;
    }

    @Nullable
    private ChannelCachingConnectionProxy waitForConnection(long now) {
        ChannelCachingConnectionProxy cachedConnection = null;
        while (cachedConnection == null && System.currentTimeMillis() - now < this.channelCheckoutTimeout) {
            if (countOpenConnections() >= this.connectionLimit) {
                try {
                    this.connectionMonitor.wait(this.channelCheckoutTimeout);
                    cachedConnection = findIdleConnection();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new AmqpException("Interrupted while waiting for a connection", e);
                }
            }
        }
        return cachedConnection;
    }

    /*
     * Iterate over the idle connections looking for an open one. If there are no idle,
     * return null, if there are no open idle, return the first closed idle so it can
     * be reopened.
     */
    @Nullable
    private ChannelCachingConnectionProxy findIdleConnection() {
        ChannelCachingConnectionProxy cachedConnection = null;
        ChannelCachingConnectionProxy lastIdle = this.idleConnections.peekLast();
        while (cachedConnection == null) {
            cachedConnection = this.idleConnections.poll();
            if (cachedConnection != null) {
                if (!cachedConnection.isOpen()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipping closed connection '" + cachedConnection + "'");
                    }
                    cachedConnection.notifyCloseIfNecessary();
                    this.idleConnections.addLast(cachedConnection);
                    if (cachedConnection.equals(lastIdle)) {
                        // all of the idle connections are closed.
                        cachedConnection = this.idleConnections.poll();
                        break;
                    }
                    cachedConnection = null;
                }
            } else {
                break;
            }
        }
        return cachedConnection;
    }

    private void refreshProxyConnection(ChannelCachingConnectionProxy connection) {
        connection.destroy();
        connection.notifyCloseIfNecessary();
        connection.target = super.createBareConnection();
        connection.closeNotified.set(false);
        getConnectionListener().onCreate(connection);
        if (logger.isDebugEnabled()) {
            logger.debug("Refreshed existing connection '" + connection + "'");
        }
    }

    /**
     * Close the underlying shared connection. Use {@link #resetConnection()} to close the
     * connection while the application is still running.
     * <p>
     * As this bean implements DisposableBean, a bean factory will automatically invoke
     * this on destruction of its cached singletons.
     * <p>
     * If called after the context is closed, the connection factory can no longer server
     * up connections.
     */
    @Override
    public final void destroy() {
        super.destroy();
        resetConnection();
        if (getContextStopped()) {
            this.stopped = true;
            if (this.channelsExecutor != null) {
                try {
                    if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) {
                        this.logger
                                .warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount());
                    }
                    this.channelsExecutor.shutdown();
                    if (!this.channelsExecutor.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) {
                        this.logger.warn("Channel executor failed to shut down");
                    }
                } catch (@SuppressWarnings(UNUSED) InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /**
     * Close the connection(s). This will impact any in-process operations. New
     * connection(s) will be created on demand after this method returns. This might be
     * used to force a reconnect to the primary broker after failing over to a secondary
     * broker.
     */
    public void resetConnection() {
        synchronized (this.connectionMonitor) {
            if (this.connection.target != null) {
                this.connection.destroy();
            }
            this.allocatedConnections.forEach(c -> c.destroy());
            this.channelHighWaterMarks.values().forEach(count -> count.set(0));
            this.connectionHighWaterMark.set(0);
        }
        if (this.publisherConnectionFactory != null) {
            this.publisherConnectionFactory.resetConnection();
        }
    }

    /*
     * Reset the Channel cache and underlying shared Connection, to be reinitialized on next access.
     */
    protected void reset(List<ChannelProxy> channels, List<ChannelProxy> txChannels,
            Map<Channel, ChannelProxy> channelsAwaitingAcks) {

        this.active = false;
        closeAndClear(channels);
        closeAndClear(txChannels);
        closeChannels(channelsAwaitingAcks.values());
        channelsAwaitingAcks.clear();
        this.active = true;
    }

    protected void closeAndClear(Collection<ChannelProxy> theChannels) {
        synchronized (theChannels) {
            closeChannels(theChannels);
            theChannels.clear();
        }
    }

    protected void closeChannels(Collection<ChannelProxy> theChannels) {
        for (ChannelProxy channel : theChannels) {
            try {
                channel.close();
            } catch (Exception ex) {
                logger.trace("Could not close cached Rabbit Channel", ex);
            }
        }
    }

    @ManagedAttribute
    public Properties getCacheProperties() {
        Properties props = new Properties();
        props.setProperty("cacheMode", this.cacheMode.name());
        synchronized (this.connectionMonitor) {
            props.setProperty("channelCacheSize", Integer.toString(this.channelCacheSize));
            if (this.cacheMode.equals(CacheMode.CONNECTION)) {
                props.setProperty("connectionCacheSize", Integer.toString(this.connectionCacheSize));
                props.setProperty("openConnections", Integer.toString(countOpenConnections()));
                props.setProperty("idleConnections", Integer.toString(this.idleConnections.size()));
                props.setProperty("idleConnectionsHighWater", Integer.toString(this.connectionHighWaterMark.get()));
                for (ChannelCachingConnectionProxy proxy : this.allocatedConnections) {
                    putConnectionName(props, proxy, ":" + proxy.getLocalPort());
                }
                for (Entry<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> entry : this.allocatedConnectionTransactionalChannels
                        .entrySet()) {
                    int port = entry.getKey().getLocalPort();
                    if (port > 0 && entry.getKey().isOpen()) {
                        LinkedList<ChannelProxy> channelList = entry.getValue();
                        props.put("idleChannelsTx:" + port, Integer.toString(channelList.size()));
                        props.put("idleChannelsTxHighWater:" + port, Integer.toString(this.channelHighWaterMarks
                                .get(ObjectUtils.getIdentityHexString(channelList)).get()));
                    }
                }
                for (Entry<ChannelCachingConnectionProxy, LinkedList<ChannelProxy>> entry : this.allocatedConnectionNonTransactionalChannels
                        .entrySet()) {
                    int port = entry.getKey().getLocalPort();
                    if (port > 0 && entry.getKey().isOpen()) {
                        LinkedList<ChannelProxy> channelList = entry.getValue();
                        props.put("idleChannelsNotTx:" + port, Integer.toString(channelList.size()));
                        props.put("idleChannelsNotTxHighWater:" + port, Integer.toString(this.channelHighWaterMarks
                                .get(ObjectUtils.getIdentityHexString(channelList)).get()));
                    }
                }
            } else {
                props.setProperty("localPort",
                        Integer.toString(this.connection.target == null ? 0 : this.connection.getLocalPort()));
                props.setProperty("idleChannelsTx", Integer.toString(this.cachedChannelsTransactional.size()));
                props.setProperty("idleChannelsNotTx",
                        Integer.toString(this.cachedChannelsNonTransactional.size()));
                props.setProperty("idleChannelsTxHighWater", Integer.toString(this.channelHighWaterMarks
                        .get(ObjectUtils.getIdentityHexString(this.cachedChannelsTransactional)).get()));
                props.setProperty("idleChannelsNotTxHighWater", Integer.toString(this.channelHighWaterMarks
                        .get(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional)).get()));
                putConnectionName(props, this.connection, "");
            }
        }
        return props;
    }

    /**
     * Return the cache properties from the underlying publisher sub-factory.
     * @return the properties.
     * @since 2.0.2
     */
    @ManagedAttribute
    public Properties getPublisherConnectionFactoryCacheProperties() {
        if (this.publisherConnectionFactory != null) {
            return this.publisherConnectionFactory.getCacheProperties();
        }
        return new Properties();
    }

    private void putConnectionName(Properties props, ConnectionProxy connection, String keySuffix) {
        Connection targetConnection = connection.getTargetConnection(); // NOSONAR (close())
        if (targetConnection != null) {
            com.rabbitmq.client.Connection delegate = targetConnection.getDelegate();
            if (delegate != null) {
                String name = delegate.getClientProvidedName();
                if (name != null) {
                    props.put("connectionName" + keySuffix, name);
                }
            }
        }
    }

    private int countOpenConnections() {
        int n = 0;
        for (ChannelCachingConnectionProxy proxy : this.allocatedConnections) {
            if (proxy.isOpen()) {
                n++;
            }
        }
        return n;
    }

    /**
     * Determine the executor service used for target channels.
     * @return specified executor service otherwise the default one is created and returned.
     * @since 1.7.9
     */
    protected ExecutorService getChannelsExecutor() {
        if (getExecutorService() != null) {
            return getExecutorService(); // NOSONAR never null
        }
        if (this.channelsExecutor == null) {
            synchronized (this.connectionMonitor) {
                if (this.channelsExecutor == null) {
                    final String threadPrefix = getBeanName() == null
                            ? DEFAULT_DEFERRED_POOL_PREFIX + threadPoolId.incrementAndGet()
                            : getBeanName();
                    ThreadFactory threadPoolFactory = new CustomizableThreadFactory(threadPrefix); // NOSONAR never null
                    this.channelsExecutor = Executors.newCachedThreadPool(threadPoolFactory);
                }
            }
        }
        return this.channelsExecutor;
    }

    @Override
    public String toString() {
        return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + ", host=" + getHost()
                + ", port=" + getPort() + ", active=" + this.active + " " + super.toString() + "]";
    }

    private final class CachedChannelInvocationHandler implements InvocationHandler {

        private static final int ASYNC_CLOSE_TIMEOUT = 5_000;

        private final ChannelCachingConnectionProxy theConnection;

        private final LinkedList<ChannelProxy> channelList; // NOSONAR addLast()

        private final String channelListIdentity;

        private final Object targetMonitor = new Object();

        private final boolean transactional;

        private final boolean confirmSelected = CachingConnectionFactory.this.simplePublisherConfirms;

        private final boolean publisherConfirms = CachingConnectionFactory.this.publisherConfirms;

        private volatile Channel target;

        private volatile boolean txStarted;

        CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection, Channel target,
                LinkedList<ChannelProxy> channelList, // NOSONAR addLast()
                boolean transactional) {

            this.theConnection = connection;
            this.target = target;
            this.channelList = channelList;
            this.channelListIdentity = ObjectUtils.getIdentityHexString(channelList);
            this.transactional = transactional;
        }

        @Override // NOSONAR complexity
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines
            if (logger.isTraceEnabled() && !method.getName().equals("toString")
                    && !method.getName().equals("hashCode") && !method.getName().equals("equals")) {
                try {
                    logger.trace(this.target + " channel." + method.getName() + "("
                            + (args != null ? Arrays.toString(args) : "") + ")");
                } catch (Exception e) {
                    // empty - some mocks fail here
                }
            }
            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 + ", conn: " + this.theConnection;
            } else if (methodName.equals("close")) {
                // Handle close method: don't pass the call on.
                if (CachingConnectionFactory.this.active) {
                    synchronized (this.channelList) {
                        if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()
                                && (this.channelList.size() < getChannelCacheSize()
                                        || this.channelList.contains(proxy))) {
                            logicalClose((ChannelProxy) proxy);
                            return null;
                        }
                    }
                }

                // If we get here, we're supposed to shut down.
                physicalClose(proxy);
                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();
            } else if (methodName.equals("isTransactional")) {
                return this.transactional;
            } else if (methodName.equals("isConfirmSelected")) {
                return this.confirmSelected;
            }
            try {
                if (this.target == null || !this.target.isOpen()) {
                    if (this.target instanceof PublisherCallbackChannel) {
                        this.target.close();
                        throw new InvocationTargetException(
                                new AmqpException("PublisherCallbackChannel is closed"));
                    } else if (this.txStarted) {
                        this.txStarted = false;
                        throw new InvocationTargetException(
                                new IllegalStateException("Channel closed during transaction"));
                    } else if (ackMethods.contains(methodName)) {
                        throw new InvocationTargetException(
                                new IllegalStateException("Channel closed; cannot ack/nack"));
                    }
                    this.target = null;
                }
                synchronized (this.targetMonitor) {
                    if (this.target == null) {
                        this.target = createBareChannel(this.theConnection, this.transactional);
                    }
                    Object result = method.invoke(this.target, args);
                    if (this.transactional) {
                        if (txStarts.contains(methodName)) {
                            this.txStarted = true;
                        } else if (txEnds.contains(methodName)) {
                            this.txStarted = false;
                        }
                    }
                    return result;
                }
            } catch (InvocationTargetException ex) {
                if (this.target == null || !this.target.isOpen()) {
                    // Basic re-connection logic...
                    if (logger.isDebugEnabled()) {
                        logger.debug("Detected closed channel on exception.  Re-initializing: " + this.target);
                    }
                    this.target = null;
                    synchronized (this.targetMonitor) {
                        if (this.target == null) {
                            this.target = createBareChannel(this.theConnection, this.transactional);
                        }
                    }
                }
                throw ex.getTargetException();
            }
        }

        private void releasePermitIfNecessary(Object proxy) {
            if (CachingConnectionFactory.this.channelCheckoutTimeout > 0) {
                /*
                 *  Only release a permit if this is a normal close; if the channel is
                 *  in the list, it means we're closing a cached channel (for which a permit
                 *  has already been released).
                 */
                synchronized (this.channelList) {
                    if (this.channelList.contains(proxy)) {
                        return;
                    }
                }
                Semaphore permits = CachingConnectionFactory.this.checkoutPermits.get(this.theConnection);
                if (permits != null) {
                    permits.release();
                    if (logger.isDebugEnabled()) {
                        logger.debug("Released permit for '" + this.theConnection + "', remaining: "
                                + permits.availablePermits());
                    }
                } else {
                    logger.error("LEAKAGE: No permits map entry for " + this.theConnection);
                }
            }
        }

        /**
         * GUARDED by channelList.
         * @param proxy the channel to close.
         * @throws TimeoutException time out on close.
         * @throws IOException  exception on close.
         */
        private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutException {
            if (this.target == null) {
                return;
            }
            if (this.target != null && !this.target.isOpen()) {
                synchronized (this.targetMonitor) {
                    if (this.target != null && !this.target.isOpen()) {
                        if (this.target instanceof PublisherCallbackChannel) {
                            this.target.close(); // emit nacks if necessary
                        }
                        if (this.channelList.contains(proxy)) {
                            this.channelList.remove(proxy);
                        } else {
                            releasePermitIfNecessary(proxy);
                        }
                        this.target = null;
                        return;
                    }
                }
            }
            returnToCache(proxy);
        }

        private void returnToCache(ChannelProxy proxy) {
            if (CachingConnectionFactory.this.active && this.publisherConfirms
                    && proxy instanceof PublisherCallbackChannel) {

                this.theConnection.channelsAwaitingAcks.put(this.target, proxy);
                ((PublisherCallbackChannel) proxy).setAfterAckCallback(
                        c -> doReturnToCache(this.theConnection.channelsAwaitingAcks.remove(c)));
            } else {
                doReturnToCache(proxy);
            }
        }

        private void doReturnToCache(Channel proxy) {
            if (proxy != null) {
                synchronized (this.channelList) {
                    // Allow for multiple close calls...
                    if (CachingConnectionFactory.this.active) {
                        if (!this.channelList.contains(proxy)) {
                            if (logger.isTraceEnabled()) {
                                logger.trace("Returning cached Channel: " + this.target);
                            }
                            releasePermitIfNecessary(proxy);
                            this.channelList.addLast((ChannelProxy) proxy);
                            setHighWaterMark();
                        }
                    } else {
                        if (proxy.isOpen()) {
                            try {
                                physicalClose(proxy);
                            } catch (@SuppressWarnings(UNUSED) Exception e) {
                            }
                        }
                    }
                }
            }
        }

        private void setHighWaterMark() {
            AtomicInteger hwm = CachingConnectionFactory.this.channelHighWaterMarks.get(this.channelListIdentity);
            if (hwm != null) {
                // No need for atomicity since we're sync'd on the channel list
                int prev = hwm.get();
                int size = this.channelList.size();
                if (size > prev) {
                    hwm.set(size);
                }
            }
        }

        private void physicalClose(Object proxy) throws IOException, TimeoutException {
            if (logger.isDebugEnabled()) {
                logger.debug("Closing cached Channel: " + this.target);
            }
            if (this.target == null) {
                return;
            }
            boolean async = false;
            try {
                if (CachingConnectionFactory.this.active && (CachingConnectionFactory.this.publisherConfirms
                        || CachingConnectionFactory.this.publisherReturns)) {
                    async = true;
                    asyncClose(proxy);
                } else {
                    this.target.close();
                    if (this.target instanceof AutorecoveringChannel) {
                        ClosingRecoveryListener.removeChannel((AutorecoveringChannel) this.target);
                    }
                }
            } catch (AlreadyClosedException e) {
                if (logger.isTraceEnabled()) {
                    logger.trace(this.target + " is already closed", e);
                }
            } finally {
                this.target = null;
                if (!async) {
                    releasePermitIfNecessary(proxy);
                }
            }
        }

        private void asyncClose(Object proxy) {
            ExecutorService executorService = getChannelsExecutor();
            final Channel channel = CachedChannelInvocationHandler.this.target;
            CachingConnectionFactory.this.inFlightAsyncCloses.add(channel);
            try {
                executorService.execute(() -> {
                    try {
                        if (CachingConnectionFactory.this.publisherConfirms) {
                            channel.waitForConfirmsOrDie(ASYNC_CLOSE_TIMEOUT);
                        } else {
                            Thread.sleep(ASYNC_CLOSE_TIMEOUT);
                        }
                    } catch (@SuppressWarnings(UNUSED) InterruptedException e1) {
                        Thread.currentThread().interrupt();
                    } catch (@SuppressWarnings(UNUSED) Exception e2) {
                    } finally {
                        try {
                            channel.close();
                        } catch (@SuppressWarnings(UNUSED) IOException e3) {
                        } catch (@SuppressWarnings(UNUSED) AlreadyClosedException e4) {
                        } catch (@SuppressWarnings(UNUSED) TimeoutException e5) {
                        } catch (ShutdownSignalException e6) {
                            if (!RabbitUtils.isNormalShutdown(e6)) {
                                logger.debug("Unexpected exception on deferred close", e6);
                            }
                        } finally {
                            CachingConnectionFactory.this.inFlightAsyncCloses.release(channel);
                            releasePermitIfNecessary(proxy);
                        }
                    }
                });
            } catch (@SuppressWarnings(UNUSED) RuntimeException e) {
                CachingConnectionFactory.this.inFlightAsyncCloses.release(channel);
            }
        }

    }

    private class ChannelCachingConnectionProxy implements ConnectionProxy { // NOSONAR - final (tests spy)

        private final AtomicBoolean closeNotified = new AtomicBoolean(false);

        private final ConcurrentMap<Channel, ChannelProxy> channelsAwaitingAcks = new ConcurrentHashMap<>();

        private volatile Connection target;

        ChannelCachingConnectionProxy(@Nullable Connection target) {
            this.target = target;
        }

        private Channel createBareChannel(boolean transactional) {
            Assert.state(this.target != null, "Can't create channel - no target connection.");
            return this.target.createChannel(transactional);
        }

        @Override
        public Channel createChannel(boolean transactional) {
            return getChannel(this, transactional);
        }

        @Override
        public void addBlockedListener(BlockedListener listener) {
            Assert.state(this.target != null, "Can't add blocked listener - no target connection.");
            this.target.addBlockedListener(listener);
        }

        @Override
        public boolean removeBlockedListener(BlockedListener listener) {
            Assert.state(this.target != null, "Can't remove blocked listener - no target connection.");
            return this.target.removeBlockedListener(listener);
        }

        @Override
        public void close() {
            if (CachingConnectionFactory.this.cacheMode == CacheMode.CONNECTION) {
                synchronized (CachingConnectionFactory.this.connectionMonitor) {
                    /*
                     * Only connectionCacheSize open idle connections are allowed.
                     */
                    if (!CachingConnectionFactory.this.idleConnections.contains(this)) {
                        if (!isOpen()
                                || countOpenIdleConnections() >= CachingConnectionFactory.this.connectionCacheSize) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Completely closing connection '" + this + "'");
                            }
                            destroy();
                        }
                        if (logger.isDebugEnabled()) {
                            logger.debug("Returning connection '" + this + "' to cache");
                        }
                        CachingConnectionFactory.this.idleConnections.add(this);
                        if (CachingConnectionFactory.this.connectionHighWaterMark
                                .get() < CachingConnectionFactory.this.idleConnections.size()) {
                            CachingConnectionFactory.this.connectionHighWaterMark
                                    .set(CachingConnectionFactory.this.idleConnections.size());
                        }
                        CachingConnectionFactory.this.connectionMonitor.notifyAll();
                    }
                }
            }
        }

        private int countOpenIdleConnections() {
            int n = 0;
            for (ChannelCachingConnectionProxy proxy : CachingConnectionFactory.this.idleConnections) {
                if (proxy.isOpen()) {
                    n++;
                }
            }
            return n;
        }

        public void destroy() {
            if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) {
                reset(CachingConnectionFactory.this.cachedChannelsNonTransactional,
                        CachingConnectionFactory.this.cachedChannelsTransactional, this.channelsAwaitingAcks);
            } else {
                reset(CachingConnectionFactory.this.allocatedConnectionNonTransactionalChannels.get(this),
                        CachingConnectionFactory.this.allocatedConnectionTransactionalChannels.get(this),
                        this.channelsAwaitingAcks);
            }
            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 this.target != null && this.target.isOpen();
        }

        @Override
        public Connection getTargetConnection() {
            return this.target;
        }

        @Override
        public com.rabbitmq.client.Connection getDelegate() {
            return this.target.getDelegate();
        }

        @Override
        public int getLocalPort() {
            Connection target = this.target; // NOSONAR (close)
            if (target != null) {
                return target.getLocalPort();
            }
            return 0;
        }

        @Override
        public String toString() {
            return "Proxy@" + ObjectUtils.getIdentityHexString(this) + " "
                    + (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL ? "Shared " : "Dedicated ")
                    + "Rabbit Connection: " + this.target;
        }

    }

    /**
     * Default implementation of {@link ConditionalExceptionLogger} for logging channel
     * close exceptions.
     * @since 1.5
     */
    private static class DefaultChannelCloseLogger implements ConditionalExceptionLogger {

        DefaultChannelCloseLogger() {
            super();
        }

        @Override
        public void log(Log logger, String message, Throwable t) {
            if (t instanceof ShutdownSignalException) {
                ShutdownSignalException cause = (ShutdownSignalException) t;
                if (RabbitUtils.isPassiveDeclarationChannelClose(cause)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(message + ": " + cause.getMessage());
                    }
                } else if (RabbitUtils.isExclusiveUseChannelClose(cause)) {
                    if (logger.isInfoEnabled()) {
                        logger.info(message + ": " + cause.getMessage());
                    }
                } else if (!RabbitUtils.isNormalChannelClose(cause)) {
                    logger.error(message + ": " + cause.getMessage());
                }
            } else {
                logger.error("Unexpected invocation of " + this.getClass() + ", with message: " + message, t);
            }
        }

    }

}