From 82578066e8370832f8833693d5f0ceef7c998110 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 7 Feb 2014 10:01:11 +0100 Subject: [PATCH] DATAREDIS-246 - RedisCacheManager.getCache() does not match the logical of CompositeCacheManager. By default RedisCacheManager lazily initializes RedisCaches when requested. To put it into static mode a defined set of cache names has to be passed to the CacheManager. We changed the implementation so it makes use of AbstractTransactionalCache introduced in Spring 3.2 which properly registers caches. Additionally the loadRemoteCachesOnStartup switch allows to retrieve and initialize existing caches by loading keys form redis server. Further on RedisCacheManager can be set in transaction aware mode so that values are only put into cache after successful commit of surrounding transaction. Original pull request: #33 --- build.gradle | 2 + docs/src/reference/docbook/index.xml | 5 + .../src/reference/docbook/reference/redis.xml | 9 +- .../data/redis/cache/RedisCacheManager.java | 176 +++++++++++++++--- .../cache/RedisCacheManagerUnitTests.java | 174 +++++++++++++++++ ...ransactionalRedisCacheManagerTestBase.java | 57 ++++++ ...lRedisCacheManagerWithCommitUnitTests.java | 142 ++++++++++++++ ...edisCacheManagerWithRollbackUnitTests.java | 133 +++++++++++++ 8 files changed, 669 insertions(+), 29 deletions(-) create mode 100644 src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java create mode 100644 src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java create mode 100644 src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java 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(); + } + +}