IGNITE-13809 Add thin client support to ignite-spring-cache - Fixes #46.

Signed-off-by: Aleksey Plekhanov <plehanov.alex@gmail.com>
diff --git a/modules/spring-cache-ext/pom.xml b/modules/spring-cache-ext/pom.xml
index 0b5a190..05e0204 100644
--- a/modules/spring-cache-ext/pom.xml
+++ b/modules/spring-cache-ext/pom.xml
@@ -50,6 +50,12 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring-data-commons</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
             <groupId>com.thoughtworks.xstream</groupId>
             <artifactId>xstream</artifactId>
             <version>${xstream.version}</version>
diff --git a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/AbstractCacheManager.java b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/AbstractCacheManager.java
new file mode 100644
index 0000000..5974806
--- /dev/null
+++ b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/AbstractCacheManager.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.cache.spring;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+
+/**
+ * Represents abstract {@link CacheManager} implementation that hand over responsibility to create new cache instances
+ * and synchronization objects for cache value computations to its inheritors.
+ */
+public abstract class AbstractCacheManager implements CacheManager {
+    /** Caches mapped to their names. */
+    private final Map<String, SpringCache> caches = new ConcurrentHashMap<>();
+
+    /** {@inheritDoc} */
+    @Override public Cache getCache(String name) {
+        return caches.computeIfAbsent(name, k -> createCache(name));
+    }
+
+    /** {@inheritDoc} */
+    @Override public Collection<String> getCacheNames() {
+        return Collections.unmodifiableSet(caches.keySet());
+    }
+
+    /** Creates {@link SpringCache} instance with specified name. */
+    protected abstract SpringCache createCache(String name);
+
+    /** Gets {@link Lock} to synchronize value calculation for specified cache and key. */
+    protected abstract Lock getSyncLock(String cache, Object key);
+}
diff --git a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManager.java b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManager.java
new file mode 100644
index 0000000..49ed32b
--- /dev/null
+++ b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManager.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.cache.spring;
+
+import java.util.concurrent.locks.Lock;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.client.ClientCache;
+import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.internal.util.typedef.internal.A;
+import org.apache.ignite.springdata.proxy.IgniteCacheClientProxy;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+
+/**
+ * Represents implementation of {@link AbstractCacheManager} that uses thin client to connect to an Ignite cluster
+ * and obtain an Ignite cache instance. It requires {@link IgniteClient} instance or {@link ClientConfiguration} to be
+ * set before manager use (see {@link #setClientInstance(IgniteClient),
+ * {@link #setClientConfiguration(ClientConfiguration)}}).
+ *
+ *
+ * Note that Spring Cache synchronous mode ({@link Cacheable#sync}) is not supported by the current manager
+ * implementation. Instead, use an {@link SpringCacheManager} that uses an Ignite thick client to connect to Ignite cluster.
+ *
+ * You can provide Ignite client instance to a Spring configuration XML file, like below:
+ *
+ * <pre name="code" class="xml">
+ * &lt;beans xmlns="http://www.springframework.org/schema/beans"
+ *        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *        xmlns:cache="http://www.springframework.org/schema/cache"
+ *        xsi:schemaLocation="
+ *            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ *            http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"&gt;
+ *     &lt;!--
+ *              Note that org.apache.ignite.IgniteClientSpringBean is available since Ignite 2.11.0 version.
+ *              For Ignite versions earlier than 2.11.0 org.apache.ignite.client.IgniteClient bean should be created
+ *              manually with concern of its connection to the Ignite cluster.
+ *     --&gt;
+ *     &lt;bean id="igniteClient" class="org.apache.ignite.IgniteClientSpringBean"&gt;
+ *         &lt;property name="clientConfiguration"&gt;
+ *             &lt;bean class="org.apache.ignite.configuration.ClientConfiguration"&gt;
+ *             &lt;property name="addresses"&gt;
+ *                 &lt;list&gt;
+ *                     &lt;value&gt;127.0.0.1:10800&lt;/value&gt;
+ *                 &lt;/list&gt;
+ *             &lt;/property&gt;
+ *         &lt;/bean&gt;
+ *         &lt;/property&gt;
+ *     &lt;/bean>
+ *
+ *     &lt;!-- Provide Ignite client instance. --&gt;
+ *     &lt;bean id="cacheManager" class="org.apache.ignite.cache.spring.IgniteClientSpringCacheManager"&gt;
+ *         &lt;property name="clientInstance" ref="igniteClient"/&gt;
+ *     &lt;/bean>
+ *
+ *     &lt;!-- Use annotation-driven cache configuration. --&gt;
+ *     &lt;cache:annotation-driven/&gt;
+ * &lt;/beans&gt;
+ * </pre>
+ *
+ * Or you can provide Ignite client configuration, like below:
+ *
+ * <pre name="code" class="xml">
+ * &lt;beans xmlns="http://www.springframework.org/schema/beans"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *        xmlns:cache="http://www.springframework.org/schema/cache"
+ *        xsi:schemaLocation="
+ *            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ *            http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"&gt;
+ *     &lt;!-- Provide Ignite client instance. --&gt;
+ *     &lt;bean id="cacheManager" class="org.apache.ignite.cache.spring.IgniteClientSpringCacheManager"&gt;
+ *         &lt;property name="clientConfiguration"&gt;
+ *             &lt;bean class="org.apache.ignite.configuration.ClientConfiguration"&gt;
+ *             &lt;property name="addresses"&gt;
+ *                 &lt;list&gt;
+ *                     &lt;value&gt;127.0.0.1:10800&lt;/value&gt;
+ *                 &lt;/list&gt;
+ *             &lt;/property&gt;
+ *         &lt;/bean&gt;
+ *         &lt;/property&gt;
+ *     &lt;/bean>
+ *
+ *     &lt;!-- Use annotation-driven cache configuration. --&gt;
+ *     &lt;cache:annotation-driven/&gt;
+ * &lt;/beans&gt;
+ * </pre>
+ */
+public class IgniteClientSpringCacheManager extends AbstractCacheManager implements DisposableBean,
+    ApplicationListener<ContextRefreshedEvent>
+{
+    /** Ignite client instance. */
+    private IgniteClient cli;
+
+    /** Ignite client configuration. */
+    private ClientConfiguration cliCfg;
+
+    /** Dynamic Ignite cache configuration template. */
+    private ClientCacheConfiguration dynamicCacheCfg;
+
+    /** Flag that indicates whether Ignite client instance was set explicitly. */
+    private boolean externalCliInstance;
+
+    /** Gets Ignite client instance. */
+    public IgniteClient getClientInstance() {
+        return cli;
+    }
+
+    /** Sets Ignite client instance. */
+    public IgniteClientSpringCacheManager setClientInstance(IgniteClient cli) {
+        A.notNull(cli, "cli");
+
+        this.cli = cli;
+
+        externalCliInstance = true;
+
+        return this;
+    }
+
+    /** Gets Ignite client configuration. */
+    public ClientConfiguration getClientConfiguration() {
+        return cliCfg;
+    }
+
+    /** Sets Ignite client configuration that will be used to start the client instance by the manager. */
+    public IgniteClientSpringCacheManager setClientConfiguration(ClientConfiguration cliCfg) {
+        this.cliCfg = cliCfg;
+
+        return this;
+    }
+
+    /** Gets dynamic Ignite cache configuration template. */
+    public ClientCacheConfiguration getDynamicCacheConfiguration() {
+        // To avoid copying the dynamic cache configuration each time as we only change its name.
+        return dynamicCacheCfg == null ? null : dynamicCacheCfg.setName(null);
+    }
+
+    /**
+     * Sets the Ignite cache configurations template that will be used to start the Ignite cache with the name
+     * requested by the Spring Cache Framework if one does not exist. Note that provided cache name will be erased and
+     * replaced with requested one.
+     */
+    public IgniteClientSpringCacheManager setDynamicCacheConfiguration(ClientCacheConfiguration dynamicCacheCfg) {
+        this.dynamicCacheCfg = dynamicCacheCfg;
+
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected synchronized SpringCache createCache(String name) {
+        ClientCacheConfiguration ccfg = dynamicCacheCfg == null ? new ClientCacheConfiguration() : dynamicCacheCfg;
+
+        ClientCache<Object, Object> cache = cli.getOrCreateCache(ccfg.setName(name));
+
+        return new SpringCache(new IgniteCacheClientProxy<>(cache), this);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void destroy() throws Exception {
+        if (!externalCliInstance)
+            cli.close();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Lock getSyncLock(String cache, Object key) {
+        throw new UnsupportedOperationException("Synchronous mode is not supported for the Ignite Spring Cache" +
+            " implementation that uses a thin client to connecting to an Ignite cluster.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onApplicationEvent(ContextRefreshedEvent evt) {
+        if (cli != null)
+            return;
+
+        if (cliCfg == null) {
+            throw new IllegalArgumentException("Neither client instance nor client configuration is specified." +
+                " Set the 'clientInstance' property if you already have an Ignite client instance running," +
+                " or set the 'clientConfiguration' property if an Ignite client instance need to be started" +
+                " implicitly by the manager.");
+        }
+
+        cli = Ignition.startClient(cliCfg);
+    }
+}
diff --git a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCache.java b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCache.java
index 9a8f2a8..0052b9e 100644
--- a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCache.java
+++ b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCache.java
@@ -19,8 +19,8 @@
 
 import java.io.Serializable;
 import java.util.concurrent.Callable;
-import org.apache.ignite.IgniteCache;
-import org.apache.ignite.IgniteLock;
+import java.util.concurrent.locks.Lock;
+import org.apache.ignite.springdata.proxy.IgniteCacheProxy;
 import org.springframework.cache.Cache;
 import org.springframework.cache.support.SimpleValueWrapper;
 
@@ -32,16 +32,16 @@
     private static final Object NULL = new NullValue();
 
     /** */
-    private final IgniteCache<Object, Object> cache;
+    private final IgniteCacheProxy<Object, Object> cache;
 
     /** */
-    private final SpringCacheManager mgr;
+    private final AbstractCacheManager mgr;
 
     /**
      * @param cache Cache.
      * @param mgr Manager
      */
-    SpringCache(IgniteCache<Object, Object> cache, SpringCacheManager mgr) {
+    SpringCache(IgniteCacheProxy<Object, Object> cache, AbstractCacheManager mgr) {
         assert cache != null;
 
         this.cache = cache;
@@ -55,7 +55,7 @@
 
     /** {@inheritDoc} */
     @Override public Object getNativeCache() {
-        return cache;
+        return cache.delegate();
     }
 
     /** {@inheritDoc} */
@@ -66,7 +66,6 @@
     }
 
     /** {@inheritDoc} */
-    @SuppressWarnings("unchecked")
     @Override public <T> T get(Object key, Class<T> type) {
         Object val = cache.get(key);
 
@@ -81,12 +80,11 @@
     }
 
     /** {@inheritDoc} */
-    @SuppressWarnings("unchecked")
     @Override public <T> T get(final Object key, final Callable<T> valLdr) {
         Object val = cache.get(key);
 
         if (val == null) {
-            IgniteLock lock = mgr.getSyncLock(cache.getName(), key);
+            Lock lock = mgr.getSyncLock(cache.getName(), key);
 
             lock.lock();
 
@@ -154,11 +152,13 @@
         return new SimpleValueWrapper(unwrapNull(val));
     }
 
+    /** */
     private static Object unwrapNull(Object val) {
         return NULL.equals(val) ? null : val;
     }
 
-    private <T> Object wrapNull(T val) {
+    /** */
+    private static <T> Object wrapNull(T val) {
         return val == null ? NULL : val;
     }
 
diff --git a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCacheManager.java b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCacheManager.java
index 80f62d7..62622f6 100644
--- a/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCacheManager.java
+++ b/modules/spring-cache-ext/src/main/java/org/apache/ignite/cache/spring/SpringCacheManager.java
@@ -17,12 +17,10 @@
 
 package org.apache.ignite.cache.spring;
 
-import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLock;
 import org.apache.ignite.IgniteSpring;
@@ -31,8 +29,8 @@
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.configuration.NearCacheConfiguration;
 import org.apache.ignite.internal.util.typedef.internal.U;
-import org.springframework.cache.Cache;
-import org.springframework.cache.CacheManager;
+import org.apache.ignite.springdata.proxy.IgniteCacheProxyImpl;
+import org.springframework.beans.factory.DisposableBean;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.context.ApplicationListener;
@@ -142,16 +140,15 @@
  * Ignite distribution, and all these nodes will participate
  * in caching the data.
  */
-public class SpringCacheManager implements CacheManager, ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
+public class SpringCacheManager extends AbstractCacheManager implements ApplicationListener<ContextRefreshedEvent>,
+    ApplicationContextAware, DisposableBean
+{
     /** Default locks count. */
     private static final int DEFAULT_LOCKS_COUNT = 512;
 
     /** IgniteLock name prefix. */
     private static final String SPRING_LOCK_NAME_PREFIX = "springSync";
 
-    /** Caches map. */
-    private final ConcurrentMap<String, SpringCache> caches = new ConcurrentHashMap<>();
-
     /** Grid configuration file path. */
     private String cfgPath;
 
@@ -179,6 +176,9 @@
     /** Locks for value loading to support sync option. */
     private ConcurrentHashMap<Integer, IgniteLock> locks = new ConcurrentHashMap<>();
 
+    /** Flag that indicates whether external Ignite instance is configured. */
+    private boolean externalIgniteInstance;
+
     /** {@inheritDoc} */
     @Override public void setApplicationContext(ApplicationContext ctx) {
         this.springCtx = ctx;
@@ -329,8 +329,11 @@
                 }
                 else if (cfg != null)
                     ignite = IgniteSpring.start(cfg, springCtx);
-                else
+                else {
                     ignite = Ignition.ignite(igniteInstanceName);
+
+                    externalIgniteInstance = true;
+                }
             }
             catch (IgniteCheckedException e) {
                 throw U.convertException(e);
@@ -339,51 +342,34 @@
     }
 
     /** {@inheritDoc} */
-    @Override public Cache getCache(String name) {
-        assert ignite != null;
+    @Override protected SpringCache createCache(String name) {
+        CacheConfiguration<Object, Object> cacheCfg = dynamicCacheCfg != null ?
+            new CacheConfiguration<>(dynamicCacheCfg) : new CacheConfiguration<>();
 
-        SpringCache cache = caches.get(name);
+        NearCacheConfiguration<Object, Object> nearCacheCfg = dynamicNearCacheCfg != null ?
+            new NearCacheConfiguration<>(dynamicNearCacheCfg) : null;
 
-        if (cache == null) {
-            CacheConfiguration<Object, Object> cacheCfg = dynamicCacheCfg != null ?
-                new CacheConfiguration<>(dynamicCacheCfg) : new CacheConfiguration<>();
+        cacheCfg.setName(name);
 
-            NearCacheConfiguration<Object, Object> nearCacheCfg = dynamicNearCacheCfg != null ?
-                new NearCacheConfiguration<>(dynamicNearCacheCfg) : null;
+        IgniteCache<Object, Object> cache = nearCacheCfg != null
+            ? ignite.getOrCreateCache(cacheCfg, nearCacheCfg)
+            : ignite.getOrCreateCache(cacheCfg);
 
-            cacheCfg.setName(name);
-
-            cache = new SpringCache(nearCacheCfg != null ? ignite.getOrCreateCache(cacheCfg, nearCacheCfg) :
-                ignite.getOrCreateCache(cacheCfg), this);
-
-            SpringCache old = caches.putIfAbsent(name, cache);
-
-            if (old != null)
-                cache = old;
-        }
-
-        return cache;
+        return new SpringCache(new IgniteCacheProxyImpl<>(cache), this);
     }
 
     /** {@inheritDoc} */
-    @Override public Collection<String> getCacheNames() {
-        assert ignite != null;
-
-        return new ArrayList<>(caches.keySet());
-    }
-
-    /**
-     * Provides {@link org.apache.ignite.IgniteLock} for specified cache name and key.
-     *
-     * @param name cache name
-     * @param key  key
-     * @return {@link org.apache.ignite.IgniteLock}
-     */
-    IgniteLock getSyncLock(String name, Object key) {
+    @Override protected IgniteLock getSyncLock(String name, Object key) {
         int hash = Objects.hash(name, key);
 
         final int idx = hash % getLocksCount();
 
         return locks.computeIfAbsent(idx, i -> ignite.reentrantLock(SPRING_LOCK_NAME_PREFIX + idx, true, false, true));
     }
+
+    /** {@inheritDoc} */
+    @Override public void destroy() {
+        if (!externalIgniteInstance)
+            ignite.close();
+    }
 }
diff --git a/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManagerTest.java b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManagerTest.java
new file mode 100644
index 0000000..8f84e55
--- /dev/null
+++ b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/IgniteClientSpringCacheManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.ignite.cache.spring;
+
+import org.apache.ignite.Ignition;
+import org.apache.ignite.client.ClientCacheConfiguration;
+import org.apache.ignite.client.IgniteClient;
+import org.apache.ignite.configuration.BinaryConfiguration;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.ClientConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.junit.Test;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.KeyGenerator;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import static org.apache.ignite.configuration.ClientConnectorConfiguration.DFLT_PORT;
+
+/** Tests Spring Cache manager implementation that uses thin client to connect to the Ignite cluster. */
+public class IgniteClientSpringCacheManagerTest extends GridSpringCacheManagerAbstractTest {
+    /** */
+    private AbstractApplicationContext ctx;
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        cfg.setCacheConfiguration(new CacheConfiguration<>(CACHE_NAME));
+        cfg.setBinaryConfiguration(new BinaryConfiguration().setCompactFooter(true));
+
+        return cfg;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        startGrid();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        ctx = new AnnotationConfigApplicationContext(ClientInstanceApplicationContext.class);
+
+        svc = ctx.getBean(GridSpringCacheTestService.class);
+        dynamicSvc = ctx.getBean(GridSpringDynamicCacheTestService.class);
+
+        svc.reset();
+        dynamicSvc.reset();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() {
+        grid().cache(CACHE_NAME).removeAll();
+
+        grid().destroyCache(DYNAMIC_CACHE_NAME);
+
+        ctx.stop();
+    }
+
+    /**
+     * Tests that {@link IgniteClientSpringCacheManager} successfully creates {@link IgniteClient} instance with
+     * provided {@link ClientConfiguration}.
+     */
+    @Test
+    public void testClientConfiguration() {
+        try (
+            AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(
+                ClientConfigurationApplicationContext.class)
+        ) {
+            IgniteClient cli = ctx.getBean(IgniteClientSpringCacheManager.class).getClientInstance();
+
+            assertNotNull(cli);
+            assertEquals(1, cli.cluster().nodes().size());
+        }
+    }
+
+    /** Tests {@link IgniteClientSpringCacheManager} behaviour in case no connection configuration is specified. */
+    @Test
+    @SuppressWarnings("EmptyTryBlock")
+    public void testOmittedConnectionConfiguration() {
+        GridTestUtils.assertThrowsAnyCause(
+            log,
+            () -> {
+                try (
+                    AbstractApplicationContext ignored = new AnnotationConfigApplicationContext(
+                        InvalidConnectionConfigurationApplicationContext.class)
+                ) {
+                    // No-op.
+                }
+
+                return null;
+            },
+            IllegalArgumentException.class,
+            "Neither client instance nor client configuration is specified.");
+    }
+
+    /** Tests {@link IgniteClientSpringCacheManager} configuration approach through XML file. */
+    @Test
+    public void testCacheManagerXmlConfiguration() {
+        try (
+            AbstractApplicationContext ctx = new ClassPathXmlApplicationContext(
+                "org/apache/ignite/cache/spring/ignite-client-spring-caching.xml")
+        ) {
+            IgniteClientSpringCacheManager mgr = ctx.getBean(IgniteClientSpringCacheManager.class);
+
+            assertNotNull(mgr.getClientConfiguration());
+
+            IgniteClient cli = mgr.getClientInstance();
+
+            assertNotNull(cli);
+            assertEquals(1, cli.cluster().nodes().size());
+
+            ClientCacheConfiguration cfg = mgr.getDynamicCacheConfiguration();
+
+            assertEquals(2, cfg.getBackups());
+        }
+    }
+
+    /** Test {@link Cacheable} annotation with {@code sync} mode enabled. */
+    @Test
+    public void testSyncMode() {
+        GridTestUtils.assertThrowsAnyCause(
+            log,
+            () -> {
+                dynamicSvc.cacheableSync(0);
+
+                return null;
+            },
+            UnsupportedOperationException.class,
+            "Synchronous mode is not supported for the Ignite Spring Cache implementation that uses a thin client" +
+                " to connecting to an Ignite cluster."
+        );
+    }
+
+    /** */
+    @Configuration
+    @EnableCaching
+    public static class ClientInstanceApplicationContext extends CachingConfigurerSupport {
+        /** */
+        @Bean
+        public GridSpringCacheTestService cacheService() {
+            return new GridSpringCacheTestService();
+        }
+
+        /** */
+        @Bean
+        public GridSpringDynamicCacheTestService dynamicCacheService() {
+            return new GridSpringDynamicCacheTestService();
+        }
+
+        /** */
+        @Bean
+        public IgniteClient igniteClient() {
+            return Ignition.startClient(new ClientConfiguration()
+                .setAddresses("127.0.0.1:" + DFLT_PORT)
+                .setBinaryConfiguration(new BinaryConfiguration().setCompactFooter(true)));
+        }
+
+        /** */
+        @Bean
+        public AbstractCacheManager cacheManager(IgniteClient cli) {
+            return new IgniteClientSpringCacheManager()
+                .setClientInstance(cli)
+                .setDynamicCacheConfiguration(new ClientCacheConfiguration().setBackups(2));
+        }
+
+        /** {@inheritDoc} */
+        @Override public KeyGenerator keyGenerator() {
+            return new GridSpringCacheTestKeyGenerator();
+        }
+    }
+
+    /** */
+    @Configuration
+    @EnableCaching
+    public static class ClientConfigurationApplicationContext {
+        /** */
+        @Bean
+        public AbstractCacheManager cacheManager() {
+            return new IgniteClientSpringCacheManager()
+                .setClientConfiguration(new ClientConfiguration().setAddresses("127.0.0.1:" + DFLT_PORT));
+        }
+    }
+
+    /** */
+    @Configuration
+    @EnableCaching
+    public static class InvalidConnectionConfigurationApplicationContext {
+        /** */
+        @Bean
+        public AbstractCacheManager cacheManager() {
+            return new IgniteClientSpringCacheManager();
+        }
+    }
+}
diff --git a/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/SpringCacheTest.java b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/SpringCacheTest.java
index 8710273..06298b5 100644
--- a/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/SpringCacheTest.java
+++ b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/SpringCacheTest.java
@@ -20,6 +20,7 @@
 import org.apache.ignite.Ignite;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.internal.util.typedef.G;
+import org.apache.ignite.springdata.proxy.IgniteCacheProxyImpl;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.junit.Test;
 
@@ -58,7 +59,7 @@
 
         cacheName = String.valueOf(System.currentTimeMillis());
         nativeCache = ignite.getOrCreateCache(cacheName);
-        springCache = new SpringCache(nativeCache, null);
+        springCache = new SpringCache(new IgniteCacheProxyImpl<>(nativeCache), null);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/ignite-client-spring-caching.xml b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/ignite-client-spring-caching.xml
new file mode 100644
index 0000000..c35c12f
--- /dev/null
+++ b/modules/spring-cache-ext/src/test/java/org/apache/ignite/cache/spring/ignite-client-spring-caching.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You 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.
+-->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:cache="http://www.springframework.org/schema/cache"
+       xsi:schemaLocation="
+            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+            http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
+    <bean id="cacheManager" class="org.apache.ignite.cache.spring.IgniteClientSpringCacheManager">
+        <property name="clientConfiguration">
+            <bean class="org.apache.ignite.configuration.ClientConfiguration">
+                <property name="addresses">
+                    <list>
+                        <value>127.0.0.1:10801</value>
+                    </list>
+                </property>
+            </bean>
+        </property>
+
+        <property name="dynamicCacheConfiguration">
+            <bean class="org.apache.ignite.client.ClientCacheConfiguration">
+                <property name="backups" value="2"/>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="ignite" class="org.apache.ignite.IgniteSpringBean">
+        <property name="configuration">
+            <bean class="org.apache.ignite.configuration.IgniteConfiguration">
+                <property name="clientConnectorConfiguration">
+                    <bean class="org.apache.ignite.configuration.ClientConnectorConfiguration">
+                        <property name="host" value="127.0.0.1"/>
+                        <property name="port" value="10801"/>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+
+    <cache:annotation-driven/>
+</beans>
diff --git a/modules/spring-cache-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringCacheTestSuite.java b/modules/spring-cache-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringCacheTestSuite.java
index 6d7a797..0c5dc9e 100644
--- a/modules/spring-cache-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringCacheTestSuite.java
+++ b/modules/spring-cache-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringCacheTestSuite.java
@@ -20,6 +20,7 @@
 import org.apache.ignite.cache.spring.GridSpringCacheManagerMultiJvmSelfTest;
 import org.apache.ignite.cache.spring.GridSpringCacheManagerSelfTest;
 import org.apache.ignite.cache.spring.GridSpringCacheManagerSpringBeanSelfTest;
+import org.apache.ignite.cache.spring.IgniteClientSpringCacheManagerTest;
 import org.apache.ignite.cache.spring.SpringCacheManagerContextInjectionTest;
 import org.apache.ignite.cache.spring.SpringCacheTest;
 import org.junit.runner.RunWith;
@@ -34,7 +35,8 @@
     GridSpringCacheManagerSpringBeanSelfTest.class,
     SpringCacheManagerContextInjectionTest.class,
     SpringCacheTest.class,
-    GridSpringCacheManagerMultiJvmSelfTest.class
+    GridSpringCacheManagerMultiJvmSelfTest.class,
+    IgniteClientSpringCacheManagerTest.class
 })
 public class IgniteSpringCacheTestSuite {
 }
diff --git a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheClientProxy.java b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheClientProxy.java
index 10fef14..5d7be5c 100644
--- a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheClientProxy.java
+++ b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheClientProxy.java
@@ -99,4 +99,29 @@
     @Override public @NotNull Iterator<Cache.Entry<K, V>> iterator() {
         return cache.<Cache.Entry<K, V>>query(new ScanQuery<>()).getAll().iterator();
     }
+
+    /** {@inheritDoc} */
+    @Override public String getName() {
+        return cache.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteCacheProxy<K, V> withSkipStore() {
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public V getAndPutIfAbsent(K key, V val) {
+        return cache.getAndPutIfAbsent(key, val);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void removeAll() {
+        cache.removeAll();
+    }
+
+    /** {@inheritDoc} */
+    @Override public ClientCache<K, V> delegate() {
+        return cache;
+    }
 }
diff --git a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxy.java b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxy.java
index 3c5c4d2..530b086 100644
--- a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxy.java
+++ b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxy.java
@@ -108,4 +108,26 @@
      * @return <tt>true</tt> if this map contains a mapping for the specified key.
      */
     public boolean containsKey(K key);
+
+    /** @return The name of the cache. */
+    public String getName();
+
+    /** @return Cache with read-through write-through behavior disabled. */
+    public IgniteCacheProxy<K, V> withSkipStore();
+
+    /**
+     * Atomically associates the specified key with the given value if it is not already associated with a value.
+     *
+     * @param key Key with which the specified value is to be associated.
+     * @param val Value to be associated with the specified key.
+     * @return Value that is already associated with the specified key, or {@code null} if no value was associated
+     * with the specified key and a value was set.
+     */
+    public V getAndPutIfAbsent(K key, V val);
+
+    /** Removes all of the mappings from this cache. */
+    public void removeAll();
+
+    /** @return Cache instance to which the current proxy delegates operations. */
+    public Object delegate();
 }
diff --git a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxyImpl.java b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxyImpl.java
index 7a4c716..2c96506 100644
--- a/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxyImpl.java
+++ b/modules/spring-data-commons/src/main/java/org/apache/ignite/springdata/proxy/IgniteCacheProxyImpl.java
@@ -100,8 +100,28 @@
         return cache.<Cache.Entry<K, V>>query(new ScanQuery<>()).getAll().iterator();
     }
 
+    /** {@inheritDoc} */
+    @Override public String getName() {
+        return cache.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteCacheProxy<K, V> withSkipStore() {
+        return new IgniteCacheProxyImpl<>(cache.withSkipStore());
+    }
+
+    /** {@inheritDoc} */
+    @Override public V getAndPutIfAbsent(K key, V val) {
+        return cache.getAndPutIfAbsent(key, val);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void removeAll() {
+        cache.removeAll();
+    }
+
     /** @return {@link IgniteCache} instance to which operations are delegated. */
-    public IgniteCache<K, V> delegate() {
+    @Override public IgniteCache<K, V> delegate() {
         return cache;
     }
 }