diff --git a/build.gradle b/build.gradle index 0ddf12f22..8251ad57a 100644 --- a/build.gradle +++ b/build.gradle @@ -65,9 +65,11 @@ dependencies { // Testing testCompile "junit:junit:$junitVersion" testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.springframework:spring-jdbc:$springVersion" testCompile "org.mockito:mockito-all:$mockitoVersion" testCompile("javax.annotation:jsr250-api:1.0", optional) testCompile("com.thoughtworks.xstream:xstream:1.4.4", optional) + testCompile("javax.transaction:jta:1.1") } sourceCompatibility = 1.6 diff --git a/docs/src/reference/docbook/index.xml b/docs/src/reference/docbook/index.xml index f319bd27e..33376a1d0 100644 --- a/docs/src/reference/docbook/index.xml +++ b/docs/src/reference/docbook/index.xml @@ -21,6 +21,11 @@ Hickey SpringSource + + Christoph + Strobl + Pivotal + diff --git a/docs/src/reference/docbook/reference/redis.xml b/docs/src/reference/docbook/reference/redis.xml index 273b647d8..0840f4008 100644 --- a/docs/src/reference/docbook/reference/redis.xml +++ b/docs/src/reference/docbook/reference/redis.xml @@ -450,7 +450,7 @@
Support for Spring Cache Abstraction - Spring Redis provides an implementation for Spring 3.1 cache abstraction + Spring Redis provides an implementation for Spring cache abstraction through the org.springframework.data.redis.cache package. To use Redis as a backing implementation, simply add RedisCacheManager to your configuration: ]]> + + + By default RedisCacheManager will lazily initialize RedisCache whenever a Cache is requested. This can be changed by predefining a Set of cache names. + + + By default RedisCacheManager will not participate in any ongoing transaction. Use setTransactionAware to enable transaction support. +
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java index 2238dcdde..93831c3b5 100644 --- a/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java +++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,65 +13,100 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.redis.cache; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.cache.transaction.TransactionAwareCacheDecorator; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** - * CacheManager implementation for Redis. By default saves the keys directly, without appending a prefix (which acts as - * a namespace). To avoid clashes, it is recommended to change this (by setting 'usePrefix' to 'true'). For performance - * reasons, the current implementation uses a set for the keys in each cache. + * {@link CacheManager} implementation for Redis. By default saves the keys directly, without appending a prefix (which + * acts as a namespace). To avoid clashes, it is recommended to change this (by setting 'usePrefix' to 'true').
+ * By default {@link RedisCache}s will be lazily initialized for each {@link #getCache(String)} request unless a set of + * predefined cache names is provided.
+ *
+ * Setting {@link #setTransactionAware(boolean)} to {@code true} will force Caches to be decorated as + * {@link TransactionAwareCacheDecorator} so values will only be written to the cache after successful commit of + * surrounding transaction. * * @author Costin Leau + * @author Christoph Strobl + * @author Thomas Darimont */ -public class RedisCacheManager implements CacheManager { +public class RedisCacheManager extends AbstractTransactionSupportingCacheManager { - // fast lookup by name map - private final ConcurrentMap caches = new ConcurrentHashMap(); - private final Collection names = Collections.unmodifiableSet(caches.keySet()); + private final Log logger = LogFactory.getLog(RedisCacheManager.class); + + @SuppressWarnings("rawtypes")// private final RedisTemplate template; private boolean usePrefix = false; private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix(); + private boolean loadRemoteCachesOnStartup = false; + private boolean dynamic = true; // 0 - never expire private long defaultExpiration = 0; private Map expires = null; + /** + * Construct a {@link RedisCacheManager}. + * + * @param template + */ + @SuppressWarnings("rawtypes") public RedisCacheManager(RedisTemplate template) { + this(template, Collections. emptyList()); + } + + /** + * Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only. + * + * @param template + * @param cacheNames + * @since 1.2 + */ + @SuppressWarnings("rawtypes") + public RedisCacheManager(RedisTemplate template, Collection cacheNames) { this.template = template; + setCacheNames(cacheNames); } + @Override public Cache getCache(String name) { - Cache c = caches.get(name); - if (c == null) { - long expiration = computeExpiration(name); - c = new RedisCache(name, (usePrefix ? cachePrefix.prefix(name) : null), template, expiration); - caches.put(name, c); + Cache cache = super.getCache(name); + if (cache == null && this.dynamic) { + return createAndAddCache(name); } - return c; + return cache; } - private long computeExpiration(String name) { - Long expiration = null; - if (expires != null) { - expiration = expires.get(name); + /** + * Specify the set of cache names for this CacheManager's 'static' mode.
+ * The number of caches and their names will be fixed after a call to this method, with no creation of further cache + * regions at runtime. + */ + public void setCacheNames(Collection cacheNames) { + + if (!CollectionUtils.isEmpty(cacheNames)) { + for (String cacheName : cacheNames) { + createAndAddCache(cacheName); + } + this.dynamic = false; } - return (expiration != null ? expiration.longValue() : defaultExpiration); - } - - public Collection getCacheNames() { - return names; } public void setUsePrefix(boolean usePrefix) { @@ -104,4 +139,89 @@ public class RedisCacheManager implements CacheManager { public void setExpires(Map expires) { this.expires = (expires != null ? new ConcurrentHashMap(expires) : null); } + + /** + * If set to {@code true} {@link RedisCacheManager} will try to retrieve cache names from redis server using + * {@literal KEYS} command and initialize {@link RedisCache} for each of them. + * + * @param loadRemoteCachesOnStartup + * @since 1.2 + */ + public void setLoadRemoteCachesOnStartup(boolean loadRemoteCachesOnStartup) { + this.loadRemoteCachesOnStartup = loadRemoteCachesOnStartup; + } + + /* + * (non-Javadoc) + * @see org.springframework.cache.support.AbstractCacheManager#loadCaches() + */ + @Override + protected Collection loadCaches() { + + Assert.notNull(this.template, "A redis template is required in order to interact with data store"); + return loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : Collections. emptyList(); + } + + private Cache createAndAddCache(String cacheName) { + addCache(createCache(cacheName)); + return super.getCache(cacheName); + } + + @SuppressWarnings("unchecked") + private RedisCache createCache(String cacheName) { + long expiration = computeExpiration(cacheName); + return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), template, expiration); + } + + private long computeExpiration(String name) { + Long expiration = null; + if (expires != null) { + expiration = expires.get(name); + } + return (expiration != null ? expiration.longValue() : defaultExpiration); + } + + private List loadAndInitRemoteCaches() { + + List caches = new ArrayList(); + + try { + Set cacheNames = loadRemoteCacheKeys(); + if (!CollectionUtils.isEmpty(cacheNames)) { + for (String cacheName : cacheNames) { + if (null == super.getCache(cacheName)) { + caches.add(createCache(cacheName)); + } + } + } + } catch (Exception e) { + if(logger.isWarnEnabled()){ + logger.warn("Failed to initialize cache with remote cache keys.",e); + } + } + + return caches; + } + + @SuppressWarnings("unchecked") + private Set loadRemoteCacheKeys() { + return (Set) template.execute(new RedisCallback>() { + + @Override + public Set doInRedis(RedisConnection connection) throws DataAccessException { + + // we are using the ~keys postfix as defined in RedisCache#setName + Set keys = connection.keys(template.getKeySerializer().serialize("*~keys")); + Set cacheKeys = new LinkedHashSet(); + + if (!CollectionUtils.isEmpty(keys)) { + for (byte[] key : keys) { + cacheKeys.add(template.getKeySerializer().deserialize(key).toString().replace("~keys", "")); + } + } + + return cacheKeys; + } + }); + } } diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java new file mode 100644 index 000000000..38edef84c --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsNull.*; +import static org.hamcrest.core.IsSame.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.internal.matchers.IsCollectionContaining; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.cache.Cache; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class RedisCacheManagerUnitTests { + + private @Mock RedisConnection redisConnectionMock; + private @Mock RedisConnectionFactory redisConnectionFactoryMock; + + @SuppressWarnings("rawtypes")// + private RedisTemplate redisTemplate; + private RedisCacheManager cacheManager; + + @SuppressWarnings("rawtypes") + @Before + public void setUp() { + + when(redisConnectionFactoryMock.getConnection()).thenReturn(redisConnectionMock); + + redisTemplate = new RedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactoryMock); + redisTemplate.afterPropertiesSet(); + + cacheManager = new RedisCacheManager(redisTemplate); + } + + /** + * @see DATAREDIS-246 + */ + @Test + public void testGetCacheReturnsNewCacheWhenRequestedCacheIsNotAvailable() { + + Cache cache = cacheManager.getCache("not-available"); + assertThat(cache, notNullValue()); + } + + /** + * @see DATAREDIS-246 + */ + @Test + public void testGetCacheReturnsExistingCacheWhenRequested() { + + Cache cache = cacheManager.getCache("cache"); + assertThat(cacheManager.getCache("cache"), sameInstance(cache)); + } + + /** + * @see DATAREDIS-246 + */ + @Test + public void testCacheInitSouldNotRequestRemoteKeysByDefault() { + + cacheManager.afterPropertiesSet(); + Mockito.verifyZeroInteractions(redisConnectionMock); + } + + /** + * @see DATAREDIS-246 + */ + @Test + public void testCacheInitShouldFetchAllCacheKeysWhenLoadingRemoteCachesOnStartupIsEnabled() { + + cacheManager.setLoadRemoteCachesOnStartup(true); + cacheManager.afterPropertiesSet(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + verify(redisConnectionMock, times(1)).keys(captor.capture()); + assertThat(redisTemplate.getKeySerializer().deserialize(captor.getValue()).toString(), is("*~keys")); + } + + /** + * @see DATAREDIS-246 + */ + @SuppressWarnings("unchecked") + @Test + public void testCacheInitShouldInitializeRemoteCachesCorrectlyWhenLoadingRemoteCachesOnStartupIsEnabled() { + + Set keys = new HashSet(Arrays.asList(redisTemplate.getKeySerializer() + .serialize("remote-cache~keys"))); + when(redisConnectionMock.keys(any(byte[].class))).thenReturn(keys); + + cacheManager.setLoadRemoteCachesOnStartup(true); + cacheManager.afterPropertiesSet(); + + assertThat(cacheManager.getCacheNames(), IsCollectionContaining.hasItem("remote-cache")); + } + + /** + * @see DATAREDIS-246 + */ + @Test + public void testCacheInitShouldNotInitialzeCachesWhenLoadingRemoteCachesOnStartupIsEnabledAndNoCachesAvailableOnRemoteServer() { + + when(redisConnectionMock.keys(any(byte[].class))).thenReturn(Collections. emptySet()); + cacheManager.setLoadRemoteCachesOnStartup(true); + cacheManager.afterPropertiesSet(); + + assertThat(cacheManager.getCacheNames().isEmpty(), is(true)); + } + + /** + * see DATAREDIS-246 + */ + @Test + public void testCacheManagerShouldNotDynamicallyCreateCachesWhenInStaticMode() { + + cacheManager.setCacheNames(Arrays.asList("spring", "data")); + assertThat(cacheManager.getCache("redis"), nullValue()); + } + + /** + * see DATAREDIS-246 + */ + @Test + public void testCacheManagerShouldRetrunRegisteredCacheWhenInStaticMode() { + + cacheManager.setCacheNames(Arrays.asList("spring", "data")); + assertThat(cacheManager.getCache("spring"), notNullValue()); + } + + /** + * see DATAREDIS-246 + */ + @Test + public void testPuttingCacheManagerIntoStaticModeShouldNotRemoveAlreadyRegisteredCaches() { + + cacheManager.getCache("redis"); + cacheManager.setCacheNames(Arrays.asList("spring", "data")); + assertThat(cacheManager.getCache("redis"), notNullValue()); + } + +} diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java new file mode 100644 index 000000000..508f03007 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +/** + * The idea would have been to provide {@link Configuration} here and just set {@link Rollback} accordingly in extending + * classes. Unfortunately this does not work when running build with gradle under java6. It was also not possible to add + * {@link Configuration} here and import it using {@link ContextConfiguration#classes()}.
+ *
+ * Therefore {@link ContextConfiguration} had to be duplicated in + * {@link TransactionalRedisCacheManagerWithCommitUnitTests} and + * {@link TransactionalRedisCacheManagerWithRollbackUnitTests}. + * + * @author Christoph Strobl + */ +public abstract class TransactionalRedisCacheManagerTestBase { + + static class FooService { + + private @Autowired BarRepository repo; + + @Transactional + public String foo() { + return "foo" + repo.bar(); + } + } + + static class BarRepository { + + @Cacheable("bar") + public String bar() { + return "bar"; + } + + } + +} diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java new file mode 100644 index 000000000..3cd6be73e --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.hamcrest.core.IsEqual; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository; +import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +@TransactionConfiguration(transactionManager = "transactionManager") +public class TransactionalRedisCacheManagerWithCommitUnitTests { + + @SuppressWarnings("rawtypes")// + protected @Autowired RedisTemplate redisTemplate; + protected @Autowired FooService transactionalService; + + @Configuration + @EnableCaching + public static class Config { + + @Bean + public PlatformTransactionManager transactionManager() throws SQLException { + + DataSourceTransactionManager txmgr = new DataSourceTransactionManager(); + txmgr.setDataSource(dataSource()); + txmgr.afterPropertiesSet(); + + return txmgr; + } + + @Bean + public DataSource dataSource() throws SQLException { + + DataSource dataSourceMock = mock(DataSource.class); + when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class)); + + return dataSourceMock; + } + + @Bean + public CacheManager cacheManager() { + + RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate()); + cacheManager.setTransactionAware(true); + return cacheManager; + } + + @SuppressWarnings({ "rawtypes" }) + @Bean + public RedisTemplate redisTemplate() { + + RedisConnection connectionMock = mock(RedisConnection.class); + RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class); + + when(factoryMock.getConnection()).thenReturn(connectionMock); + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factoryMock); + + return template; + } + + @Bean + public FooService fooService() { + return new FooService(); + } + + @Bean + public BarRepository barRepository() { + return new BarRepository(); + } + } + + @AfterTransaction + public void assertThatValuesHaveBeenAddedToRedis() { + + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(byte[].class); + + verify(redisTemplate.getConnectionFactory().getConnection(), times(1)).zAdd(keyCaptor.capture(), eq(0D), + valueCaptor.capture()); + + Assert.assertThat(new StringRedisSerializer().deserialize(keyCaptor.getValue()).toString(), + IsEqual.equalTo("bar~keys")); + } + + /** + * @see DATAREDIS-246 + */ + @Rollback(false) + @Test + public void testValuesAddedToCacheWhenTransactionIsCommited() { + transactionalService.foo(); + } +} diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java new file mode 100644 index 000000000..ad32c6252 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.cache; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.BarRepository; +import org.springframework.data.redis.cache.TransactionalRedisCacheManagerTestBase.FooService; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.AfterTransaction; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +@TransactionConfiguration(transactionManager = "transactionManager") +public class TransactionalRedisCacheManagerWithRollbackUnitTests { + + @SuppressWarnings("rawtypes")// + protected @Autowired RedisTemplate redisTemplate; + protected @Autowired FooService transactionalService; + + @Configuration + @EnableCaching + public static class Config { + + @Bean + public PlatformTransactionManager transactionManager() throws SQLException { + + DataSourceTransactionManager txmgr = new DataSourceTransactionManager(); + txmgr.setDataSource(dataSource()); + txmgr.afterPropertiesSet(); + + return txmgr; + } + + @Bean + public DataSource dataSource() throws SQLException { + + DataSource dataSourceMock = mock(DataSource.class); + when(dataSourceMock.getConnection()).thenReturn(mock(Connection.class)); + + return dataSourceMock; + } + + @Bean + public CacheManager cacheManager() { + + RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate()); + cacheManager.setTransactionAware(true); + return cacheManager; + } + + @SuppressWarnings({ "rawtypes" }) + @Bean + public RedisTemplate redisTemplate() { + + RedisConnection connectionMock = mock(RedisConnection.class); + RedisConnectionFactory factoryMock = mock(RedisConnectionFactory.class); + + when(factoryMock.getConnection()).thenReturn(connectionMock); + + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factoryMock); + + return template; + } + + @Bean + public FooService fooService() { + return new FooService(); + } + + @Bean + public BarRepository barRepository() { + return new BarRepository(); + } + } + + @AfterTransaction + public void assertThatValuesNeverAddedToRedis() { + + verify(redisTemplate.getConnectionFactory().getConnection(), times(0)).zAdd(any(byte[].class), eq(0D), + any(byte[].class)); + } + + /** + * @see DATAREDIS-246 + */ + @Rollback(true) + @Test + public void tesValuesNotAddedToCacheWhenTransactionIsRolledBack() { + transactionalService.foo(); + } + +}