IGNITE-15674 Migrate CacheSpringStoreSessionListener to ignite-extensions. (#74)

diff --git a/modules/spring-boot-autoconfigure-ext/pom.xml b/modules/spring-boot-autoconfigure-ext/pom.xml
index 9b7657f..d5ce646 100644
--- a/modules/spring-boot-autoconfigure-ext/pom.xml
+++ b/modules/spring-boot-autoconfigure-ext/pom.xml
@@ -71,6 +71,13 @@
         </dependency>
 
         <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+            <version>${spring-5.2.version}</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
             <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-log4j</artifactId>
             <version>${ignite.version}</version>
diff --git a/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml b/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
index 500705c..167230a 100644
--- a/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
+++ b/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
@@ -72,6 +72,13 @@
         </dependency>
 
         <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+            <version>${spring-5.2.version}</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
             <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-log4j</artifactId>
             <version>${ignite.version}</version>
diff --git a/modules/spring-data-2.0-ext/pom.xml b/modules/spring-data-2.0-ext/pom.xml
index 3bc8b92..9fcfda7 100644
--- a/modules/spring-data-2.0-ext/pom.xml
+++ b/modules/spring-data-2.0-ext/pom.xml
@@ -79,48 +79,11 @@
             <exclusions>
                 <exclusion>
                     <groupId>org.springframework</groupId>
-                    <artifactId>spring-core</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-beans</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-aop</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
                     <artifactId>spring-context</artifactId>
                 </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-expression</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-tx</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-jdbc</artifactId>
-                </exclusion>
             </exclusions>
         </dependency>
 
-        <!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.0-->
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-core</artifactId>
-            <version>${spring-5.0.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-beans</artifactId>
-            <version>${spring-5.0.version}</version>
-        </dependency>
-
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
@@ -128,12 +91,6 @@
         </dependency>
 
         <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-tx</artifactId>
-            <version>${spring-5.0.version}</version>
-        </dependency>
-
-        <dependency>
             <groupId>commons-lang</groupId>
             <artifactId>commons-lang</artifactId>
             <version>${commons.lang.version}</version>
diff --git a/modules/spring-data-2.2-ext/pom.xml b/modules/spring-data-2.2-ext/pom.xml
index a30eb6e..3f2ee20 100644
--- a/modules/spring-data-2.2-ext/pom.xml
+++ b/modules/spring-data-2.2-ext/pom.xml
@@ -79,61 +79,19 @@
             <exclusions>
                 <exclusion>
                     <groupId>org.springframework</groupId>
-                    <artifactId>spring-core</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-beans</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-aop</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
                     <artifactId>spring-context</artifactId>
                 </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-expression</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-tx</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-jdbc</artifactId>
-                </exclusion>
             </exclusions>
         </dependency>
 
         <!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.2-->
         <dependency>
             <groupId>org.springframework</groupId>
-            <artifactId>spring-core</artifactId>
-            <version>${spring-5.2.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-beans</artifactId>
-            <version>${spring-5.2.version}</version>
-        </dependency>
-
-        <dependency>
-            <groupId>org.springframework</groupId>
             <artifactId>spring-context</artifactId>
             <version>${spring-5.2.version}</version>
         </dependency>
 
         <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-tx</artifactId>
-            <version>${spring-5.2.version}</version>
-        </dependency>
-
-        <dependency>
             <groupId>commons-lang</groupId>
             <artifactId>commons-lang</artifactId>
             <version>${commons.lang.version}</version>
diff --git a/modules/spring-tx-ext/examples/config/example-default.xml b/modules/spring-tx-ext/examples/config/example-default.xml
new file mode 100644
index 0000000..e6c359d
--- /dev/null
+++ b/modules/spring-tx-ext/examples/config/example-default.xml
@@ -0,0 +1,76 @@
+<?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.
+-->
+
+<!--
+    Ignite configuration with all defaults and enabled p2p deployment and enabled events.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <bean abstract="true" id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <!-- Set to true to enable distributed class loading for examples, default is false. -->
+        <property name="peerClassLoadingEnabled" value="true"/>
+
+        <!-- Enable task execution events for examples. -->
+        <property name="includeEventTypes">
+            <list>
+                <!--Task execution events-->
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_TIMEDOUT"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_SESSION_ATTR_SET"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_REDUCED"/>
+
+                <!--Cache events-->
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_PUT"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_READ"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_REMOVED"/>
+            </list>
+        </property>
+
+        <!-- Explicitly configure TCP discovery SPI to provide list of initial nodes. -->
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <!--
+                        Ignite provides several options for automatic discovery that can be used
+                        instead os static IP based discovery. For information on all options refer
+                        to our documentation: http://apacheignite.readme.io/docs/cluster-config
+                    -->
+                    <!-- Uncomment static IP finder to enable static-based discovery of initial nodes. -->
+                    <!--<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">-->
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500..47509</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/spring-tx-ext/examples/config/example-ignite.xml b/modules/spring-tx-ext/examples/config/example-ignite.xml
new file mode 100644
index 0000000..a3e7e22
--- /dev/null
+++ b/modules/spring-tx-ext/examples/config/example-ignite.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<!--
+    Ignite configuration with all defaults and enabled p2p deployment and enabled events.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+    <!-- Imports default Ignite configuration -->
+    <import resource="example-default.xml"/>
+
+    <bean parent="ignite.cfg"/>
+</beans>
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java
new file mode 100644
index 0000000..88c4c9c
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java
@@ -0,0 +1,36 @@
+/*
+ * 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.examples;
+
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+
+/**
+ * Starts up an empty node with example compute configuration.
+ */
+public class ExampleNodeStartup {
+    /**
+     * Start up an empty node with example compute configuration.
+     *
+     * @param args Command line arguments, none required.
+     * @throws IgniteException If failed.
+     */
+    public static void main(String[] args) throws IgniteException {
+        Ignition.start("modules/spring-tx-ext/examples/config/example-ignite.xml");
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java
new file mode 100644
index 0000000..7b0a1cd
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java
@@ -0,0 +1,131 @@
+/*
+ * 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.examples;
+
+import java.net.URL;
+import java.util.List;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cluster.ClusterGroup;
+
+/**
+ *
+ */
+public class ExamplesUtils {
+    /** */
+    private static final ClassLoader CLS_LDR = ExamplesUtils.class.getClassLoader();
+
+    /**
+     * Exits with code {@code -1} if maximum memory is below 90% of minimally allowed threshold.
+     *
+     * @param min Minimum memory threshold.
+     */
+    public static void checkMinMemory(long min) {
+        long maxMem = Runtime.getRuntime().maxMemory();
+
+        if (maxMem < .85 * min) {
+            System.err.println("Heap limit is too low (" + (maxMem / (1024 * 1024)) +
+                "MB), please increase heap size at least up to " + (min / (1024 * 1024)) + "MB.");
+
+            System.exit(-1);
+        }
+    }
+
+    /**
+     * Returns URL resolved by class loader for classes in examples project.
+     *
+     * @return Resolved URL.
+     */
+    public static URL url(String path) {
+        URL url = CLS_LDR.getResource(path);
+
+        if (url == null)
+            throw new RuntimeException("Failed to resolve resource URL by path: " + path);
+
+        return url;
+    }
+
+    /**
+     * Checks minimum topology size for running a certain example.
+     *
+     * @param grp Cluster to check size for.
+     * @param size Minimum number of nodes required to run a certain example.
+     * @return {@code True} if check passed, {@code false} otherwise.
+     */
+    public static boolean checkMinTopologySize(ClusterGroup grp, int size) {
+        int prjSize = grp.nodes().size();
+
+        if (prjSize < size) {
+            System.err.println(">>> Please start at least " + size + " cluster nodes to run example.");
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if cluster has server nodes.
+     *
+     * @param ignite Ignite instance.
+     * @return {@code True} if cluster has server nodes, {@code false} otherwise.
+     */
+    public static boolean hasServerNodes(Ignite ignite) {
+        if (ignite.cluster().forServers().nodes().isEmpty()) {
+            System.err.println("Server nodes not found (start data nodes with ExampleNodeStartup class)");
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Convenience method for printing query results.
+     *
+     * @param res Query results.
+     */
+    public static void printQueryResults(List<?> res) {
+        if (res == null || res.isEmpty())
+            System.out.println("Query result set is empty.");
+        else {
+            for (Object row : res) {
+                if (row instanceof List) {
+                    System.out.print("(");
+
+                    List<?> l = (List)row;
+
+                    for (int i = 0; i < l.size(); i++) {
+                        Object o = l.get(i);
+
+                        if (o instanceof Double || o instanceof Float)
+                            System.out.printf("%.2f", o);
+                        else
+                            System.out.print(l.get(i));
+
+                        if (i + 1 != l.size())
+                            System.out.print(',');
+                    }
+
+                    System.out.println(')');
+                }
+                else
+                    System.out.println("  " + row);
+            }
+        }
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java
new file mode 100644
index 0000000..c4b52bb
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java
@@ -0,0 +1,118 @@
+/*
+ * 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.examples.datagrid.store.spring;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.cache.Cache;
+import javax.cache.integration.CacheLoaderException;
+import javax.sql.DataSource;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cache.store.CacheStore;
+import org.apache.ignite.cache.store.CacheStoreAdapter;
+import org.apache.ignite.examples.model.Person;
+import org.apache.ignite.lang.IgniteBiInClosure;
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+
+/**
+ * Example of {@link CacheStore} implementation that uses JDBC
+ * transaction with cache transactions and maps {@link Long} to {@link Person}.
+ */
+public class CacheSpringPersonStore extends CacheStoreAdapter<Long, Person> {
+    /** Data source. */
+    public static final DataSource DATA_SRC =
+        JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+    /** Spring JDBC template. */
+    private JdbcTemplate jdbcTemplate;
+
+    /**
+     * Constructor.
+     *
+     * @throws IgniteException If failed.
+     */
+    public CacheSpringPersonStore() throws IgniteException {
+        jdbcTemplate = new JdbcTemplate(DATA_SRC);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Person load(Long key) {
+        System.out.println(">>> Store load [key=" + key + ']');
+
+        try {
+            return jdbcTemplate.queryForObject("select * from PERSON where id = ?", new RowMapper<Person>() {
+                @Override public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
+                    return new Person(rs.getLong(1), rs.getString(2), rs.getString(3));
+                }
+            }, key);
+        }
+        catch (EmptyResultDataAccessException ignored) {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void write(Cache.Entry<? extends Long, ? extends Person> entry) {
+        Long key = entry.getKey();
+        Person val = entry.getValue();
+
+        System.out.println(">>> Store write [key=" + key + ", val=" + val + ']');
+
+        int updated = jdbcTemplate.update("update PERSON set first_name = ?, last_name = ? where id = ?",
+            val.firstName, val.lastName, val.id);
+
+        if (updated == 0) {
+            jdbcTemplate.update("insert into PERSON (id, first_name, last_name) values (?, ?, ?)",
+                val.id, val.firstName, val.lastName);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(Object key) {
+        System.out.println(">>> Store delete [key=" + key + ']');
+
+        jdbcTemplate.update("delete from PERSON where id = ?", key);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void loadCache(final IgniteBiInClosure<Long, Person> clo, Object... args) {
+        if (args == null || args.length == 0 || args[0] == null)
+            throw new CacheLoaderException("Expected entry count parameter is not provided.");
+
+        int entryCnt = (Integer)args[0];
+
+        final AtomicInteger cnt = new AtomicInteger();
+
+        jdbcTemplate.query("select * from PERSON limit ?", new RowCallbackHandler() {
+            @Override public void processRow(ResultSet rs) throws SQLException {
+                Person person = new Person(rs.getLong(1), rs.getString(2), rs.getString(3));
+
+                clo.apply(person.id, person);
+
+                cnt.incrementAndGet();
+            }
+        }, entryCnt);
+
+        System.out.println(">>> Loaded " + cnt + " values into cache.");
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java
new file mode 100644
index 0000000..22bf7ae
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java
@@ -0,0 +1,160 @@
+/*
+ * 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.examples.datagrid.store.spring;
+
+import java.util.UUID;
+import javax.cache.configuration.Factory;
+import javax.cache.configuration.FactoryBuilder;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.cache.store.spring.CacheSpringStoreSessionListener;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.examples.ExampleNodeStartup;
+import org.apache.ignite.examples.ExamplesUtils;
+import org.apache.ignite.examples.model.Person;
+import org.apache.ignite.examples.util.DbH2ServerStartup;
+import org.apache.ignite.transactions.Transaction;
+
+import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
+
+/**
+ * Demonstrates usage of cache with underlying persistent store configured.
+ * <p>
+ * This example uses {@link CacheSpringPersonStore} as a persistent store.
+ * <p>
+ * To start the example, you should:
+ * <ul>
+ *     <li>Start H2 database TCP server using {@link DbH2ServerStartup}.</li>
+ *     <li>Start a few nodes using {@link ExampleNodeStartup}.</li>
+ *     <li>Start example using {@link CacheSpringStoreExample}.</li>
+ * </ul>
+ * <p>
+ * Remote nodes can be started with {@link ExampleNodeStartup} in another JVM which will
+ * start node with {@code examples/config/example-ignite.xml} configuration.
+ */
+public class CacheSpringStoreExample {
+    /** Cache name. */
+    private static final String CACHE_NAME = CacheSpringStoreExample.class.getSimpleName();
+
+    /** Heap size required to run this example. */
+    public static final int MIN_MEMORY = 1024 * 1024 * 1024;
+
+    /** Number of entries to load. */
+    private static final int ENTRY_COUNT = 100_000;
+
+    /** Global person ID to use across entire example. */
+    private static final Long id = Math.abs(UUID.randomUUID().getLeastSignificantBits());
+
+    /**
+     * Executes example.
+     *
+     * @param args Command line arguments, none required.
+     * @throws IgniteException If example execution failed.
+     */
+    public static void main(String[] args) throws IgniteException {
+        ExamplesUtils.checkMinMemory(MIN_MEMORY);
+
+        // To start ignite with desired configuration uncomment the appropriate line.
+        try (Ignite ignite = Ignition.start("modules/spring-tx-ext/examples/config/example-ignite.xml")) {
+            System.out.println();
+            System.out.println(">>> Cache store example started.");
+
+            CacheConfiguration<Long, Person> cacheCfg = new CacheConfiguration<>(CACHE_NAME);
+
+            // Set atomicity as transaction, since we are showing transactions in example.
+            cacheCfg.setAtomicityMode(TRANSACTIONAL);
+
+            // Configure Spring store.
+            cacheCfg.setCacheStoreFactory(FactoryBuilder.factoryOf(CacheSpringPersonStore.class));
+
+            // Configure Spring session listener.
+            cacheCfg.setCacheStoreSessionListenerFactories(new Factory<CacheStoreSessionListener>() {
+                @Override public CacheStoreSessionListener create() {
+                    CacheSpringStoreSessionListener lsnr = new CacheSpringStoreSessionListener();
+
+                    lsnr.setDataSource(CacheSpringPersonStore.DATA_SRC);
+
+                    return lsnr;
+                }
+            });
+
+            cacheCfg.setReadThrough(true);
+            cacheCfg.setWriteThrough(true);
+
+            // Auto-close cache at the end of the example.
+            try (IgniteCache<Long, Person> cache = ignite.getOrCreateCache(cacheCfg)) {
+                // Make initial cache loading from persistent store. This is a
+                // distributed operation and will call CacheStore.loadCache(...)
+                // method on all nodes in topology.
+                loadCache(cache);
+
+                // Start transaction and execute several cache operations with
+                // read/write-through to persistent store.
+                executeTransaction(cache);
+            }
+            finally {
+                // Distributed cache could be removed from cluster only by #destroyCache() call.
+                ignite.destroyCache(CACHE_NAME);
+            }
+        }
+    }
+
+    /**
+     * Makes initial cache loading.
+     *
+     * @param cache Cache to load.
+     */
+    private static void loadCache(IgniteCache<Long, Person> cache) {
+        long start = System.currentTimeMillis();
+
+        // Start loading cache from persistent store on all caching nodes.
+        cache.loadCache(null, ENTRY_COUNT);
+
+        long end = System.currentTimeMillis();
+
+        System.out.println(">>> Loaded " + cache.size() + " keys with backups in " + (end - start) + "ms.");
+    }
+
+    /**
+     * Executes transaction with read/write-through to persistent store.
+     *
+     * @param cache Cache to execute transaction on.
+     */
+    private static void executeTransaction(IgniteCache<Long, Person> cache) {
+        try (Transaction tx = Ignition.ignite().transactions().txStart()) {
+            Person val = cache.get(id);
+
+            System.out.println("Read value: " + val);
+
+            val = cache.getAndPut(id, new Person(id, "Isaac", "Newton"));
+
+            System.out.println("Overwrote old value: " + val);
+
+            val = cache.get(id);
+
+            System.out.println("Read value: " + val);
+
+            tx.commit();
+        }
+
+        System.out.println("Read value after commit: " + cache.get(id));
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java
new file mode 100644
index 0000000..211239f
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Contains Spring-based cache store implementation.
+ */
+package org.apache.ignite.examples.datagrid.store.spring;
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java
new file mode 100644
index 0000000..159a5ef
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java
@@ -0,0 +1,72 @@
+/*
+ * 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.examples.model;
+
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.binary.BinaryReader;
+import org.apache.ignite.binary.BinaryWriter;
+import org.apache.ignite.binary.Binarylizable;
+
+/**
+ * Employee address.
+ * <p>
+ * This class implements {@link org.apache.ignite.binary.Binarylizable} only for example purposes,
+ * in order to show how to customize serialization and deserialization of
+ * binary objects.
+ */
+public class Address implements Binarylizable {
+    /** Street. */
+    private String street;
+
+    /** ZIP code. */
+    private int zip;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Address() {
+        // No-op.
+    }
+
+    /**
+     * @param street Street.
+     * @param zip ZIP code.
+     */
+    public Address(String street, int zip) {
+        this.street = street;
+        this.zip = zip;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(BinaryWriter writer) throws BinaryObjectException {
+        writer.writeString("street", street);
+        writer.writeInt("zip", zip);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(BinaryReader reader) throws BinaryObjectException {
+        street = reader.readString("street");
+        zip = reader.readInt("zip");
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Address [street=" + street +
+            ", zip=" + zip + ']';
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java
new file mode 100644
index 0000000..a59ffce
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java
@@ -0,0 +1,93 @@
+/*
+ * 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.examples.model;
+
+import java.util.Collection;
+
+/**
+ * This class represents employee object.
+ */
+public class Employee {
+    /** Name. */
+    private String name;
+
+    /** Salary. */
+    private long salary;
+
+    /** Address. */
+    private Address addr;
+
+    /** Departments. */
+    private Collection<String> departments;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Employee() {
+        // No-op.
+    }
+
+    /**
+     * @param name Name.
+     * @param salary Salary.
+     * @param addr Address.
+     * @param departments Departments.
+     */
+    public Employee(String name, long salary, Address addr, Collection<String> departments) {
+        this.name = name;
+        this.salary = salary;
+        this.addr = addr;
+        this.departments = departments;
+    }
+
+    /**
+     * @return Name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Salary.
+     */
+    public long salary() {
+        return salary;
+    }
+
+    /**
+     * @return Address.
+     */
+    public Address address() {
+        return addr;
+    }
+
+    /**
+     * @return Departments.
+     */
+    public Collection<String> departments() {
+        return departments;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Employee [name=" + name +
+            ", salary=" + salary +
+            ", address=" + addr +
+            ", departments=" + departments + ']';
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java
new file mode 100644
index 0000000..55e7967
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java
@@ -0,0 +1,93 @@
+/*
+ * 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.examples.model;
+
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/**
+ * This class represents key for employee object.
+ * <p>
+ * Used in query example to collocate employees
+ * with their organizations.
+ */
+public class EmployeeKey {
+    /** ID. */
+    private int id;
+
+    /** Organization ID. */
+    @AffinityKeyMapped
+    private int organizationId;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public EmployeeKey() {
+        // No-op.
+    }
+
+    /**
+     * @param id ID.
+     * @param organizationId Organization ID.
+     */
+    public EmployeeKey(int id, int organizationId) {
+        this.id = id;
+        this.organizationId = organizationId;
+    }
+
+    /**
+     * @return ID.
+     */
+    public int id() {
+        return id;
+    }
+
+    /**
+     * @return Organization ID.
+     */
+    public int organizationId() {
+        return organizationId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        EmployeeKey key = (EmployeeKey)o;
+
+        return id == key.id && organizationId == key.organizationId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + organizationId;
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "EmployeeKey [id=" + id +
+            ", organizationId=" + organizationId + ']';
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java
new file mode 100644
index 0000000..fc90c07
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java
@@ -0,0 +1,132 @@
+/*
+ * 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.examples.model;
+
+import java.sql.Timestamp;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+/**
+ * This class represents organization object.
+ */
+public class Organization {
+    /** */
+    private static final AtomicLong ID_GEN = new AtomicLong();
+
+    /** Organization ID (indexed). */
+    @QuerySqlField(index = true)
+    private Long id;
+
+    /** Organization name (indexed). */
+    @QuerySqlField(index = true)
+    private String name;
+
+    /** Address. */
+    private Address addr;
+
+    /** Type. */
+    private OrganizationType type;
+
+    /** Last update time. */
+    private Timestamp lastUpdated;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Organization() {
+        // No-op.
+    }
+
+    /**
+     * @param name Organization name.
+     */
+    public Organization(String name) {
+        id = ID_GEN.incrementAndGet();
+
+        this.name = name;
+    }
+
+    /**
+     * @param id Organization ID.
+     * @param name Organization name.
+     */
+    public Organization(long id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * @param name Name.
+     * @param addr Address.
+     * @param type Type.
+     * @param lastUpdated Last update time.
+     */
+    public Organization(String name, Address addr, OrganizationType type, Timestamp lastUpdated) {
+        id = ID_GEN.incrementAndGet();
+
+        this.name = name;
+        this.addr = addr;
+        this.type = type;
+
+        this.lastUpdated = lastUpdated;
+    }
+
+    /**
+     * @return Organization ID.
+     */
+    public Long id() {
+        return id;
+    }
+
+    /**
+     * @return Name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Address.
+     */
+    public Address address() {
+        return addr;
+    }
+
+    /**
+     * @return Type.
+     */
+    public OrganizationType type() {
+        return type;
+    }
+
+    /**
+     * @return Last update time.
+     */
+    public Timestamp lastUpdated() {
+        return lastUpdated;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Organization [id=" + id +
+            ", name=" + name +
+            ", address=" + addr +
+            ", type=" + type +
+            ", lastUpdated=" + lastUpdated + ']';
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java
new file mode 100644
index 0000000..8b22600
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java
@@ -0,0 +1,32 @@
+/*
+ * 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.examples.model;
+
+/**
+ * Organization type enum.
+ */
+public enum OrganizationType {
+    /** Non-profit organization. */
+    NON_PROFIT,
+
+    /** Private organization. */
+    PRIVATE,
+
+    /** Government organization. */
+    GOVERNMENT
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java
new file mode 100644
index 0000000..6d3a6df
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java
@@ -0,0 +1,145 @@
+/*
+ * 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.examples.model;
+
+import java.io.Serializable;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.affinity.AffinityKey;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * Person class.
+ */
+public class Person implements Serializable {
+    /** */
+    private static final AtomicLong ID_GEN = new AtomicLong();
+
+    /** Person ID (indexed). */
+    @QuerySqlField(index = true)
+    public Long id;
+
+    /** Organization ID (indexed). */
+    @QuerySqlField(index = true)
+    public Long orgId;
+
+    /** First name (not-indexed). */
+    @QuerySqlField
+    public String firstName;
+
+    /** Last name (not indexed). */
+    @QuerySqlField
+    public String lastName;
+
+    /** Resume text (create LUCENE-based TEXT index for this field). */
+    @QueryTextField
+    public String resume;
+
+    /** Salary (indexed). */
+    @QuerySqlField(index = true)
+    public double salary;
+
+    /** Custom cache key to guarantee that person is always collocated with its organization. */
+    private transient AffinityKey<Long> key;
+
+    /**
+     * Default constructor.
+     */
+    public Person() {
+        // No-op.
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param org       Organization.
+     * @param firstName First name.
+     * @param lastName  Last name.
+     * @param salary    Salary.
+     * @param resume    Resume text.
+     */
+    public Person(Organization org, String firstName, String lastName, double salary, String resume) {
+        // Generate unique ID for this person.
+        id = ID_GEN.incrementAndGet();
+
+        orgId = org.id();
+
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.salary = salary;
+        this.resume = resume;
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param id Person ID.
+     * @param orgId Organization ID.
+     * @param firstName First name.
+     * @param lastName Last name.
+     * @param salary    Salary.
+     * @param resume    Resume text.
+     */
+    public Person(Long id, Long orgId, String firstName, String lastName, double salary, String resume) {
+        this.id = id;
+        this.orgId = orgId;
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.salary = salary;
+        this.resume = resume;
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param id Person ID.
+     * @param firstName First name.
+     * @param lastName Last name.
+     */
+    public Person(Long id, String firstName, String lastName) {
+        this.id = id;
+
+        this.firstName = firstName;
+        this.lastName = lastName;
+    }
+
+    /**
+     * Gets cache affinity key. Since in some examples person needs to be collocated with organization, we create
+     * custom affinity key to guarantee this collocation.
+     *
+     * @return Custom affinity key to guarantee that person is always collocated with organization.
+     */
+    public AffinityKey<Long> key() {
+        if (key == null)
+            key = new AffinityKey<>(id, orgId);
+
+        return key;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return "Person [id=" + id +
+                ", orgId=" + orgId +
+                ", lastName=" + lastName +
+                ", firstName=" + firstName +
+                ", salary=" + salary +
+                ", resume=" + resume + ']';
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java
new file mode 100644
index 0000000..d6e1c4c
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 description. -->
+ * Model classes for Apache Ignite examples.
+ */
+
+package org.apache.ignite.examples.model;
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java
new file mode 100644
index 0000000..f3da07d
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java
@@ -0,0 +1,98 @@
+/*
+ * 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.examples.util;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.sql.SQLException;
+import org.apache.ignite.IgniteException;
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.h2.tools.RunScript;
+import org.h2.tools.Server;
+
+/**
+ * Start H2 database TCP server in order to access sample in-memory database from other processes.
+ */
+public class DbH2ServerStartup {
+    /** Create table script. */
+    private static final String CREATE_PERSON_TABLE =
+        "create table if not exists PERSON(id bigint not null, first_name varchar(50), last_name varchar(50), PRIMARY KEY(id));";
+
+    /** Sample data script. */
+    private static final String POPULATE_PERSON_TABLE =
+        "delete from PERSON;\n" +
+        "insert into PERSON(id, first_name, last_name) values(1, 'Johannes', 'Kepler');\n" +
+        "insert into PERSON(id, first_name, last_name) values(2, 'Galileo', 'Galilei');\n" +
+        "insert into PERSON(id, first_name, last_name) values(3, 'Henry', 'More');\n" +
+        "insert into PERSON(id, first_name, last_name) values(4, 'Polish', 'Brethren');\n" +
+        "insert into PERSON(id, first_name, last_name) values(5, 'Robert', 'Boyle');\n" +
+        "insert into PERSON(id, first_name, last_name) values(6, 'Wilhelm', 'Leibniz');";
+
+    /**
+     * Populate sample database.
+     *
+     * @throws SQLException if
+     */
+    public static void populateDatabase() throws SQLException {
+        // Try to connect to database TCP server.
+        JdbcConnectionPool dataSrc = JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+        // Create Person table in database.
+        RunScript.execute(dataSrc.getConnection(), new StringReader(CREATE_PERSON_TABLE));
+
+        // Populates Person table with sample data in database.
+        RunScript.execute(dataSrc.getConnection(), new StringReader(POPULATE_PERSON_TABLE));
+    }
+
+    /**
+     * Start H2 database TCP server.
+     *
+     * @param args Command line arguments, none required.
+     * @throws IgniteException If start H2 database TCP server failed.
+     */
+    public static void main(String[] args) throws IgniteException {
+        try {
+            // Start H2 database TCP server in order to access sample in-memory database from other processes.
+            Server.createTcpServer("-tcpDaemon").start();
+
+            populateDatabase();
+
+            // Try to connect to database TCP server.
+            JdbcConnectionPool dataSrc = JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+            // Create Person table in database.
+            RunScript.execute(dataSrc.getConnection(), new StringReader(CREATE_PERSON_TABLE));
+
+            // Populates Person table with sample data in database.
+            RunScript.execute(dataSrc.getConnection(), new StringReader(POPULATE_PERSON_TABLE));
+        }
+        catch (SQLException e) {
+            throw new IgniteException("Failed to start database TCP server", e);
+        }
+
+        try {
+            do {
+                System.out.println("Type 'q' and press 'Enter' to stop H2 TCP server...");
+            }
+            while ('q' != System.in.read());
+        }
+        catch (IOException ignored) {
+            // No-op.
+        }
+    }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java
new file mode 100644
index 0000000..1d87d02
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Contains utility classes for examples.
+ */
+package org.apache.ignite.examples.util;
diff --git a/modules/spring-tx-ext/pom.xml b/modules/spring-tx-ext/pom.xml
index 25f26f1..0bced6e 100644
--- a/modules/spring-tx-ext/pom.xml
+++ b/modules/spring-tx-ext/pom.xml
@@ -43,15 +43,16 @@
         </dependency>
 
         <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <version>${spring.version}</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
             <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-spring</artifactId>
             <version>${ignite.version}</version>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.springframework</groupId>
-                    <artifactId>spring-tx</artifactId>
-                </exclusion>
-            </exclusions>
             <scope>provided</scope>
         </dependency>
 
@@ -125,13 +126,13 @@
             <dependencies>
                 <dependency>
                     <groupId>org.apache.ignite</groupId>
-                    <artifactId>ignite-spring</artifactId>
+                    <artifactId>ignite-core</artifactId>
                     <version>${ignite.version}</version>
                 </dependency>
 
                 <dependency>
                     <groupId>org.apache.ignite</groupId>
-                    <artifactId>ignite-core</artifactId>
+                    <artifactId>ignite-spring</artifactId>
                     <version>${ignite.version}</version>
                 </dependency>
 
@@ -140,6 +141,12 @@
                     <artifactId>spring-tx</artifactId>
                     <version>${spring.version}</version>
                 </dependency>
+
+                <dependency>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-jdbc</artifactId>
+                    <version>${spring.version}</version>
+                </dependency>
             </dependencies>
         </profile>
     </profiles>
diff --git a/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java
new file mode 100644
index 0000000..d3a46d8
--- /dev/null
+++ b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java
@@ -0,0 +1,212 @@
+/*
+ * 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.store.spring;
+
+import javax.cache.integration.CacheWriterException;
+import javax.sql.DataSource;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.cache.store.CacheStoreSession;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lifecycle.LifecycleAware;
+import org.apache.ignite.resources.LoggerResource;
+import org.apache.ignite.transactions.Transaction;
+import org.apache.ignite.transactions.TransactionIsolation;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+/**
+ * Cache store session listener based on Spring transaction management.
+ * <p>
+ * This listener starts a new DB transaction for each session and commits
+ * or rolls it back when session ends. If there is no ongoing
+ * cache transaction, this listener is no-op.
+ * <p>
+ * Store implementation can use any Spring APIs like {@link JdbcTemplate}
+ * and others. The listener will guarantee that if there is an
+ * ongoing cache transaction, all store operations within this
+ * transaction will be automatically enlisted in the same database
+ * transaction.
+ * <p>
+ * {@link org.apache.ignite.cache.store.spring.CacheSpringStoreSessionListener} requires that either
+ * {@link #setTransactionManager(PlatformTransactionManager) transaction manager}
+ * or {@link #setDataSource(DataSource) data source} is configured. If non of them is
+ * provided, exception is thrown. Is both are provided, data source will be
+ * ignored.
+ * <p>
+ * If there is a transaction, a {@link TransactionStatus} object will be saved
+ * as a store session {@link CacheStoreSession#attachment() attachment}. It
+ * can be used to acquire current DB transaction status.
+ */
+public class CacheSpringStoreSessionListener implements CacheStoreSessionListener, LifecycleAware {
+    /** Transaction manager. */
+    private PlatformTransactionManager txMgr;
+
+    /** Data source. */
+    private DataSource dataSrc;
+
+    /** Logger. */
+    @LoggerResource
+    private IgniteLogger log;
+
+    /**
+     * Sets transaction manager.
+     * <p>
+     * Either transaction manager or data source is required.
+     * If none is provided, exception will be thrown on startup.
+     *
+     * @param txMgr Transaction manager.
+     */
+    public void setTransactionManager(PlatformTransactionManager txMgr) {
+        this.txMgr = txMgr;
+    }
+
+    /**
+     * Gets transaction manager.
+     *
+     * @return Transaction manager.
+     */
+    public PlatformTransactionManager getTransactionManager() {
+        return txMgr;
+    }
+
+    /**
+     * Sets data source.
+     * <p>
+     * Either transaction manager or data source is required.
+     * If none is provided, exception will be thrown on startup.
+     *
+     * @param dataSrc Data source.
+     */
+    public void setDataSource(DataSource dataSrc) {
+        this.dataSrc = dataSrc;
+    }
+
+    /**
+     * Gets data source.
+     *
+     * @return Data source.
+     */
+    public DataSource getDataSource() {
+        return dataSrc;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void start() throws IgniteException {
+        if (txMgr == null && dataSrc == null)
+            throw new IgniteException("Either transaction manager or data source is required by " +
+                getClass().getSimpleName() + '.');
+
+        if (dataSrc != null) {
+            if (txMgr == null)
+                txMgr = new DataSourceTransactionManager(dataSrc);
+            else
+                U.warn(log, "Data source configured in " + getClass().getSimpleName() +
+                    " will be ignored (transaction manager is already set).");
+        }
+
+        assert txMgr != null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void stop() throws IgniteException {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onSessionStart(CacheStoreSession ses) {
+        if (ses.isWithinTransaction() && ses.attachment() == null) {
+            try {
+                TransactionDefinition def = definition(ses.transaction(), ses.cacheName());
+
+                ses.attach(txMgr.getTransaction(def));
+            }
+            catch (TransactionException e) {
+                throw new CacheWriterException("Failed to start store session [tx=" + ses.transaction() + ']', e);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public void onSessionEnd(CacheStoreSession ses, boolean commit) {
+        if (ses.isWithinTransaction()) {
+            TransactionStatus tx = ses.attach(null);
+
+            if (tx != null) {
+                try {
+                    if (commit)
+                        txMgr.commit(tx);
+                    else
+                        txMgr.rollback(tx);
+                }
+                catch (TransactionException e) {
+                    throw new CacheWriterException("Failed to end store session [tx=" + ses.transaction() + ']', e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets DB transaction isolation level based on ongoing cache transaction isolation.
+     *
+     * @return DB transaction isolation.
+     */
+    private TransactionDefinition definition(Transaction tx, String cacheName) {
+        assert tx != null;
+
+        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
+
+        def.setName("Ignite Tx [cache=" + (cacheName != null ? cacheName : "<default>") + ", id=" + tx.xid() + ']');
+        def.setIsolationLevel(isolationLevel(tx.isolation()));
+
+        long timeoutSec = (tx.timeout() + 500) / 1000;
+
+        if (timeoutSec > 0 && timeoutSec < Integer.MAX_VALUE)
+            def.setTimeout((int)timeoutSec);
+
+        return def;
+    }
+
+    /**
+     * Gets DB transaction isolation level based on ongoing cache transaction isolation.
+     *
+     * @param isolation Cache transaction isolation.
+     * @return DB transaction isolation.
+     */
+    private int isolationLevel(TransactionIsolation isolation) {
+        switch (isolation) {
+            case READ_COMMITTED:
+                return TransactionDefinition.ISOLATION_READ_COMMITTED;
+
+            case REPEATABLE_READ:
+                return TransactionDefinition.ISOLATION_REPEATABLE_READ;
+
+            case SERIALIZABLE:
+                return TransactionDefinition.ISOLATION_SERIALIZABLE;
+
+            default:
+                throw new IllegalStateException(); // Will never happen.
+        }
+    }
+}
diff --git a/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java
new file mode 100644
index 0000000..408ba24
--- /dev/null
+++ b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java
@@ -0,0 +1,22 @@
+/*
+ *  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.
+ */
+
+/**
+ * Contains cache store session listener based on Spring transaction management.
+ */
+
+package org.apache.ignite.cache.store.spring;
diff --git a/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java b/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java
new file mode 100644
index 0000000..d3db02c
--- /dev/null
+++ b/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.store.spring;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import javax.cache.Cache;
+import javax.cache.configuration.Factory;
+import javax.cache.integration.CacheLoaderException;
+import javax.cache.integration.CacheWriterException;
+import javax.sql.DataSource;
+import org.apache.ignite.cache.store.CacheStore;
+import org.apache.ignite.cache.store.CacheStoreAdapter;
+import org.apache.ignite.cache.store.CacheStoreSession;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.cache.store.CacheStoreSessionListenerAbstractSelfTest;
+import org.apache.ignite.cache.store.jdbc.CacheJdbcStoreSessionListener;
+import org.apache.ignite.internal.processors.query.h2.DistributedSqlConfiguration;
+import org.apache.ignite.internal.processors.query.h2.FunctionsManager;
+import org.apache.ignite.lang.IgniteBiInClosure;
+import org.apache.ignite.resources.CacheStoreSessionResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.transaction.TransactionStatus;
+
+/**
+ * Tests for {@link CacheJdbcStoreSessionListener}.
+ */
+public class CacheSpringStoreSessionListenerSelfTest extends CacheStoreSessionListenerAbstractSelfTest {
+    /** */
+    private static final DataSource DATA_SRC = new DriverManagerDataSource(URL);
+
+    /** */
+    private static Method FunctionManager_removeFunctions;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        FunctionManager_removeFunctions = FunctionsManager.class.getDeclaredMethod("removeFunctions", Set.class);
+
+        FunctionManager_removeFunctions.setAccessible(true);
+
+        // Cleanup disabled functions because transaction manager uses LOCK_MODE()
+        FunctionManager_removeFunctions.invoke(FunctionsManager.class, Collections.emptySet());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        FunctionManager_removeFunctions.invoke(FunctionsManager.class, DistributedSqlConfiguration.DFLT_DISABLED_FUNCS);
+
+        super.afterTestsStopped();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Factory<? extends CacheStore<Integer, Integer>> storeFactory() {
+        return new Factory<CacheStore<Integer, Integer>>() {
+            @Override public CacheStore<Integer, Integer> create() {
+                return new Store(new JdbcTemplate(DATA_SRC));
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Factory<CacheStoreSessionListener> sessionListenerFactory() {
+        return new Factory<CacheStoreSessionListener>() {
+            @Override public CacheStoreSessionListener create() {
+                CacheSpringStoreSessionListener lsnr = new CacheSpringStoreSessionListener();
+
+                lsnr.setDataSource(DATA_SRC);
+
+                return lsnr;
+            }
+        };
+    }
+
+    /**
+     */
+    private static class Store extends CacheStoreAdapter<Integer, Integer> {
+        /** */
+        private static String SES_CONN_KEY = "ses_conn";
+
+        /** */
+        private final JdbcTemplate jdbc;
+
+        /** */
+        @CacheStoreSessionResource
+        private CacheStoreSession ses;
+
+        /**
+         * @param jdbc JDBC template.
+         */
+        private Store(JdbcTemplate jdbc) {
+            this.jdbc = jdbc;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void loadCache(IgniteBiInClosure<Integer, Integer> clo, Object... args) {
+            loadCacheCnt.incrementAndGet();
+
+            checkTransaction();
+            checkConnection();
+        }
+
+        /** {@inheritDoc} */
+        @Override public Integer load(Integer key) throws CacheLoaderException {
+            loadCnt.incrementAndGet();
+
+            checkTransaction();
+            checkConnection();
+
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void write(Cache.Entry<? extends Integer, ? extends Integer> entry)
+            throws CacheWriterException {
+            writeCnt.incrementAndGet();
+
+            checkTransaction();
+            checkConnection();
+
+            if (write.get()) {
+                String table;
+
+                switch (ses.cacheName()) {
+                    case "cache1":
+                        table = "Table1";
+
+                        break;
+
+                    case "cache2":
+                        if (fail.get())
+                            throw new CacheWriterException("Expected failure.");
+
+                        table = "Table2";
+
+                        break;
+
+                    default:
+                        throw new CacheWriterException("Wring cache: " + ses.cacheName());
+                }
+
+                jdbc.update("INSERT INTO " + table + " (key, value) VALUES (?, ?)",
+                    entry.getKey(), entry.getValue());
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public void delete(Object key) throws CacheWriterException {
+            deleteCnt.incrementAndGet();
+
+            checkTransaction();
+            checkConnection();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void sessionEnd(boolean commit) {
+            assertNull(ses.attachment());
+        }
+
+        /**
+         */
+        private void checkTransaction() {
+            TransactionStatus tx = ses.attachment();
+
+            if (ses.isWithinTransaction()) {
+                assertNotNull(tx);
+                assertFalse(tx.isCompleted());
+            }
+            else
+                assertNull(tx);
+        }
+
+        /**
+         */
+        private void checkConnection() {
+            Connection conn = DataSourceUtils.getConnection(jdbc.getDataSource());
+
+            assertNotNull(conn);
+
+            try {
+                assertFalse(conn.isClosed());
+                assertEquals(!ses.isWithinTransaction(), conn.getAutoCommit());
+            }
+            catch (SQLException e) {
+                throw new RuntimeException(e);
+            }
+
+            verifySameInstance(conn);
+        }
+
+        /**
+         * @param conn Connection.
+         */
+        private void verifySameInstance(Connection conn) {
+            Map<String, Connection> props = ses.properties();
+
+            Connection sesConn = props.get(SES_CONN_KEY);
+
+            if (sesConn == null)
+                props.put(SES_CONN_KEY, conn);
+            else {
+                assertSame(conn, sesConn);
+
+                reuseCnt.incrementAndGet();
+            }
+        }
+    }
+}