diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc
index 82a9d2506..84e443a86 100644
--- a/src/main/asciidoc/reference/redis.adoc
+++ b/src/main/asciidoc/reference/redis.adoc
@@ -465,30 +465,90 @@ As shown in the example above, the consuming code is decoupled from the actual s
[[redis:support:cache-abstraction]]
=== Support for Spring Cache Abstraction
+NOTE: Changed in 2.0
+
Spring Redis provides an implementation for Spring http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html[cache abstraction] through the `org.springframework.data.redis.cache` package. To use Redis as a backing implementation, simply add `RedisCacheManager` to your configuration:
-[source,xml]
+[source,java]
----
-
-
-
-
-
-
-
-
+@Bean
+public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ return RedisCacheManager.defaultCacheManager(connectionFactory);
+}
----
-NOTE: By default `RedisCacheManager` will lazily initialize `RedisCache` whenever a `Cache` is requested. This can be changed by predefining a `Set` of cache names.
+`RedisCacheManager` behavior can be configured via `RedisCacheManagerConfigurator` allowing to set the default `RedisCacheConfiguration`, transaction behaviour and predefined caches.
-NOTE: By default `RedisCacheManager` will not participate in any ongoing transaction. Use `setTransactionAware` to enable transaction support.
+[source,java]
+----
+RedisCacheManager cm = RedisCacheManager.usingRawConnectionFactory(connectionFactory)
+ .withCacheDefaults(defaultCacheConfig())
+ .withInitialCacheConfigurations(singletonMap("predefined", defaultCacheConfig().disableCachingNullValues()))
+ .transactionAware()
+ .createAndGet();
+----
-NOTE: By default `RedisCacheManager` does not prefix keys for cache regions, which can lead to an unexpected growth of a `ZSET` used to maintain known keys. It's highly recommended to enable the usage of prefixes in order to avoid this unexpected growth and potential key clashes using more than one cache region.
+Behavior of `RedisCache` created via `RedisCacheManager` is defined via `RedisCacheConfiguration`. The configuration allows to set key expiration times, prefixes and ``RedisSerializer``s for converting to and from the binary storage format.
+As shown above `RedisCacheManager` allows definition of configurations on a per cache base.
-NOTE: By default `RedisCache` will not cache any `null` values as keys without a value get dropped by Redis itself. However you can explicitly enable `null` value caching via `RedisCacheManager` which will store `org.springframework.cache.support.NullValue` as a placeholder.
+[source,java]
+----
+RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofSeconds(1))
+ .disableCachingNullValues();
+----
+
+Using default `RedisCacheManager` uses a non locking `RedisCacheWriter` for reading & writing bits.
+While this ensures a max of performance the lack of entry locking can lead to overlapping, non atomic commands, for `putIfAbsent` and `clean` as those methods combine a series of commands sent to Redis.
+The locking counterpart prevents command overlap by setting an explicit lock key and checking against presence of this key which leads to additional requests and potential command wait times.
+
+It is possible to opt in to the locking behavior as follows:
+
+[source,java]
+----
+RedisCacheManager cm = RedisCacheManager.usingCacheWriter(DefaultRedisCacheWriter.lockingRedisCacheWriter())
+ .withCacheDefaults(defaultCacheConfig())
+ ...
+----
+
+.RedisCacheManager defaults
+[width="80%",cols="<1,<2",options="header"]
+|====
+|Setting
+|Value
+
+|Cache Writer
+|non locking
+
+|Cache Configuration
+|`RedisCacheConfiguraiton#defaultConfiguration`
+
+|Initial Caches
+|none
+
+|Trasaction Aware
+|no
+|====
+
+.RedisCacheConfiguration defaults
+[width="80%",cols="<1,<2",options="header"]
+|====
+|Key Expiration
+|none
+
+|Cache `null`
+|yes
+
+|Prefix Keys
+|yes
+
+|Default Prefix
+|the actual cache name
+
+|Key Serializer
+|`StringRedisSerializer`
+
+|Value Serializer
+|`JdkSerializationRedisSerializer`
+|====
diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCachePrefix.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCachePrefix.java
deleted file mode 100644
index 80ecd4b06..000000000
--- a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCachePrefix.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2011-2013 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.data.redis.serializer.RedisSerializer;
-import org.springframework.data.redis.serializer.StringRedisSerializer;
-
-/**
- * Default implementation for {@link RedisCachePrefix} which uses the given cache name and a delimiter for creating the
- * prefix.
- *
- * @author Costin Leau
- */
-public class DefaultRedisCachePrefix implements RedisCachePrefix {
-
- private final RedisSerializer serializer = new StringRedisSerializer();
- private final String delimiter;
-
- public DefaultRedisCachePrefix() {
- this(":");
- }
-
- public DefaultRedisCachePrefix(String delimiter) {
- this.delimiter = delimiter;
- }
-
- public byte[] prefix(String cacheName) {
- return serializer.serialize((delimiter != null ? cacheName.concat(delimiter) : cacheName.concat(":")));
- }
-}
diff --git a/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java
new file mode 100644
index 000000000..b97b77e85
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/cache/DefaultRedisCacheWriter.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2017 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 java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.data.redis.connection.RedisClusterConnection;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
+import org.springframework.data.redis.connection.ReturnType;
+import org.springframework.data.redis.core.types.Expiration;
+import org.springframework.util.Assert;
+
+/**
+ * {@link RedisCacheWriter} implementation capable of reading/writing binary data from/to Redis in {@literal standalone}
+ * and {@literal cluster} environments. Works upon a given {@link RedisConnectionFactory} to obtain the actual
+ * {@link RedisConnection}.
+ * {@link DefaultRedisCacheWriter} can be used in {@link #lockingRedisCacheWriter(RedisConnectionFactory) locking} or
+ * {@link #nonLockingRedisCacheWriter(RedisConnectionFactory) non-locking} mode. While {@literal non-locking} aims for
+ * maximum performance it may result in overlapping, non atomic, command execution for operations spanning multiple
+ * Redis interactions like {@code putIfAbsent}. The {@literal locking} counterpart prevents command overlap by setting
+ * an explicit lock key and checking against presence of this key which leads to additional requests and potential
+ * command wait times.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public class DefaultRedisCacheWriter implements RedisCacheWriter {
+
+ private static final byte[] CLEAN_SCRIPT = "local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;"
+ .getBytes(Charset.forName("UTF-8"));
+
+ private final RedisConnectionFactory connectionFactory;
+ private final Duration sleepTime;
+
+ /**
+ * @param connectionFactory must not be {@literal null}.
+ */
+ DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory) {
+ this(connectionFactory, Duration.ZERO);
+ }
+
+ /**
+ * @param connectionFactory must not be {@literal null}.
+ * @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
+ * to disable locking.
+ */
+ DefaultRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+ Assert.notNull(sleepTime, "SleepTime must not be null!");
+
+ this.connectionFactory = connectionFactory;
+ this.sleepTime = sleepTime;
+ }
+
+ /**
+ * Create new {@link RedisCacheWriter} without locking behavior.
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return new instance of {@link DefaultRedisCacheWriter}.
+ */
+ public static DefaultRedisCacheWriter nonLockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+ return new DefaultRedisCacheWriter(connectionFactory);
+ }
+
+ /**
+ * Create new {@link RedisCacheWriter} with locking behavior.
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return new instance of {@link DefaultRedisCacheWriter}.
+ */
+ public static DefaultRedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+ return new DefaultRedisCacheWriter(connectionFactory, Duration.ofMillis(50));
+ }
+
+ @Override
+ public void put(String name, byte[] key, byte[] value, Duration ttl) {
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(key, "Key must not be null!");
+ Assert.notNull(value, "Value must not be null!");
+ Assert.notNull(ttl, "Ttl must not be null!");
+
+ execute(name, connection -> {
+
+ if (shouldExpireWithin(ttl)) {
+ connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
+ } else {
+ connection.set(key, value);
+ }
+
+ return "OK";
+ });
+ }
+
+ @Override
+ public byte[] get(String name, byte[] key) {
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(key, "Key must not be null!");
+
+ return execute(name, connection -> connection.get(key));
+ }
+
+ @Override
+ public byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl) {
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(key, "Key must not be null!");
+ Assert.notNull(value, "Value must not be null!");
+ Assert.notNull(ttl, "Ttl must not be null!");
+
+ return execute(name, connection -> {
+
+ if (isLockingCacheWriter()) {
+ doLock(name, connection);
+ }
+
+ try {
+ if (connection.setNX(key, value)) {
+
+ if (shouldExpireWithin(ttl)) {
+ connection.pExpire(key, ttl.toMillis());
+ }
+ return null;
+ }
+
+ return connection.get(key);
+ } finally {
+
+ if (isLockingCacheWriter()) {
+ doUnlock(name, connection);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void remove(String name, byte[] key) {
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(key, "Key must not be null!");
+
+ execute(name, connection -> connection.del(key));
+ }
+
+ /**
+ * Explicitly set a write lock on a cache.
+ *
+ * @param name the name of the cache to lock.
+ */
+ public void lock(String name) {
+ execute(name, connection -> doLock(name, connection));
+ }
+
+ private Boolean doLock(String name, RedisConnection connection) {
+ return connection.setNX(createCacheLockKey(name), new byte[] {});
+ }
+
+ /**
+ * Explicitly remove a write lock from a cache.
+ *
+ * @param name the name of the cache to unlock.
+ */
+ public void unlock(String name) {
+ executeWithoutLockCheck(connection -> doUnlock(name, connection));
+ }
+
+ private Long doUnlock(String name, RedisConnection connection) {
+ return connection.del(createCacheLockKey(name));
+ }
+
+ /**
+ * Check if a cache has set a lock.
+ *
+ * @param name the name of the cache to check for presence of a lock.
+ * @return {@literal true} if lock found.
+ */
+ public boolean isLoked(String name) {
+ return executeWithoutLockCheck(connection -> doCheckLock(name, connection));
+ }
+
+ private boolean doCheckLock(String name, RedisConnection connection) {
+ return connection.exists(createCacheLockKey(name));
+ }
+
+ @Override
+ public void clean(String name, byte[] pattern) {
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(pattern, "Pattern must not be null!");
+
+ execute(name, connection -> {
+
+ boolean wasLocked = false;
+
+ try {
+ if (connection instanceof RedisClusterConnection) {
+
+ if (isLockingCacheWriter()) {
+ doLock(name, connection);
+ wasLocked = true;
+ }
+
+ byte[][] keys = connection.keys(pattern).stream().toArray(size -> new byte[size][]);
+ connection.del(keys);
+ } else {
+ connection.eval(CLEAN_SCRIPT, ReturnType.INTEGER, 0, pattern);
+ }
+ } finally {
+
+ if (wasLocked && isLockingCacheWriter()) {
+ doUnlock(name, connection);
+ }
+ }
+
+ return "OK";
+ });
+ }
+
+ /**
+ * @return {@literal true} if {@link RedisCacheWriter} uses locks.
+ */
+ public boolean isLockingCacheWriter() {
+ return !sleepTime.isZero() && !sleepTime.isNegative();
+ }
+
+ T execute(String name, ConnectionCallback callback) {
+
+ RedisConnection connection = connectionFactory.getConnection();
+ try {
+
+ checkAndPotentiallyWaitUntilUnlocked(name, connection);
+ return callback.doWithConnection(connection);
+ } finally {
+ connection.close();
+ }
+ }
+
+ private T executeWithoutLockCheck(ConnectionCallback callback) {
+
+ RedisConnection connection = connectionFactory.getConnection();
+
+ try {
+ return callback.doWithConnection(connection);
+ } finally {
+ connection.close();
+ }
+ }
+
+ private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
+
+ if (isLockingCacheWriter()) {
+
+ long timeout = sleepTime.toMillis();
+
+ while (doCheckLock(name, connection)) {
+ try {
+ Thread.sleep(timeout);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ private boolean shouldExpireWithin(Duration ttl) {
+ return ttl != null && !ttl.isZero() && !ttl.isNegative();
+ }
+
+ byte[] createCacheLockKey(String name) {
+ return (name + "~lock").getBytes(Charset.forName("UTF-8"));
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface ConnectionCallback {
+
+ T doWithConnection(RedisConnection connection);
+ }
+}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCache.java b/src/main/java/org/springframework/data/redis/cache/RedisCache.java
index b507a2bdb..112f498e5 100644
--- a/src/main/java/org/springframework/data/redis/cache/RedisCache.java
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCache.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2011-2017 the original author or authors.
+ * Copyright 2017 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.
@@ -15,918 +15,236 @@
*/
package org.springframework.data.redis.cache;
-import java.lang.reflect.Constructor;
-import java.util.Arrays;
-import java.util.Set;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.cache.support.NullValue;
import org.springframework.cache.support.SimpleValueWrapper;
-import org.springframework.dao.DataAccessException;
-import org.springframework.data.redis.RedisSystemException;
-import org.springframework.data.redis.connection.DecoratedRedisConnection;
-import org.springframework.data.redis.connection.RedisClusterConnection;
-import org.springframework.data.redis.connection.RedisConnection;
-import org.springframework.data.redis.connection.ReturnType;
-import org.springframework.data.redis.core.RedisCallback;
-import org.springframework.data.redis.core.RedisOperations;
-import org.springframework.data.redis.serializer.GenericToStringSerializer;
-import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
-import org.springframework.data.redis.serializer.RedisSerializer;
-import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
/**
- * Cache implementation on top of Redis.
+ * {@link org.springframework.cache.Cache} implementation using for Redis as underlying store.
*
- * @author Costin Leau
* @author Christoph Strobl
- * @author Thomas Darimont
- * @author Mark Paluch
+ * @since 2.0
*/
-@SuppressWarnings("unchecked")
public class RedisCache extends AbstractValueAdaptingCache {
- @SuppressWarnings("rawtypes") //
- private final RedisOperations redisOperations;
- private final RedisCacheMetadata cacheMetadata;
- private final CacheValueAccessor cacheValueAccessor;
+ private static final byte[] BINARY_NULL_VALUE = new JdkSerializationRedisSerializer().serialize(NullValue.INSTANCE);
+
+ private final String name;
+ private final RedisCacheWriter cacheWriter;
+ private final RedisCacheConfiguration cacheConfig;
+ private final ConfigurableConversionService conversionService = new DefaultFormattingConversionService();
/**
- * Constructs a new {@link RedisCache} instance.
+ * Create new {@link RedisCache}.
*
- * @param name cache name
- * @param prefix
- * @param redisOperations
- * @param expiration
+ * @param name must not be {@literal null}.
+ * @param cacheWriter must not be {@literal null}.
+ * @param cacheConfig must not be {@literal null}.
*/
- public RedisCache(String name, byte[] prefix, RedisOperations extends Object, ? extends Object> redisOperations,
- long expiration) {
- this(name, prefix, redisOperations, expiration, false);
+ RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
+
+ super(cacheConfig.getAllowCacheNullValues());
+
+ Assert.notNull(name, "Name must not be null!");
+ Assert.notNull(cacheWriter, "CacheWriter must not be null!");
+ Assert.notNull(cacheConfig, "CacheConfig must not be null!");
+
+ this.name = name;
+ this.cacheWriter = cacheWriter;
+ this.cacheConfig = cacheConfig;
+
+ conversionService.addConverter(String.class, byte[].class, source -> source.getBytes(Charset.forName("UTF-8")));
}
- /**
- * Constructs a new {@link RedisCache} instance.
- *
- * @param name cache name
- * @param prefix must not be {@literal null} or empty.
- * @param redisOperations
- * @param expiration
- * @param allowNullValues
- * @since 1.8
- */
- public RedisCache(String name, byte[] prefix, RedisOperations extends Object, ? extends Object> redisOperations,
- long expiration, boolean allowNullValues) {
-
- super(allowNullValues);
-
- Assert.hasText(name, "CacheName must not be null or empty!");
-
- RedisSerializer> serializer = redisOperations.getValueSerializer() != null ? redisOperations.getValueSerializer()
- : (RedisSerializer>) new JdkSerializationRedisSerializer();
-
- this.cacheMetadata = new RedisCacheMetadata(name, prefix);
- this.cacheMetadata.setDefaultExpiration(expiration);
- this.redisOperations = redisOperations;
- this.cacheValueAccessor = new CacheValueAccessor(serializer);
-
- if (allowNullValues) {
-
- if (redisOperations.getValueSerializer() instanceof StringRedisSerializer
- || redisOperations.getValueSerializer() instanceof GenericToStringSerializer
- || redisOperations.getValueSerializer() instanceof Jackson2JsonRedisSerializer) {
- throw new IllegalArgumentException(String.format(
- "Redis does not allow keys with null value ¯\\_(ツ)_/¯. "
- + "The chosen %s does not support generic type handling and therefore cannot be used with allowNullValues enabled. "
- + "Please use a different RedisSerializer or disable null value support.",
- ClassUtils.getShortName(redisOperations.getValueSerializer().getClass())));
- }
- }
- }
-
- /**
- * Return the value to which this cache maps the specified key, generically specifying a type that return value will
- * be cast to.
- *
- * @param key
- * @param type
- * @return
- */
- public T get(Object key, Class type) {
-
- ValueWrapper wrapper = get(key);
- return wrapper == null ? null : (T) wrapper.get();
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#get(java.lang.Object)
- */
- @Override
- public ValueWrapper get(Object key) {
- return get(getRedisCacheKey(key));
- }
-
- /*
- * @see org.springframework.cache.Cache#get(java.lang.Object, java.util.concurrent.Callable)
- * introduced in springframework 4.3.0.RC1
- */
- public T get(final Object key, final Callable valueLoader) {
-
- RedisCacheElement cacheElement = new RedisCacheElement(getRedisCacheKey(key),
- new StoreTranslatingCallable(valueLoader)).expireAfter(cacheMetadata.getDefaultExpiration());
- BinaryRedisCacheElement rce = new BinaryRedisCacheElement(cacheElement, cacheValueAccessor);
-
- ValueWrapper val = get(key);
- if (val != null) {
- return (T) val.get();
- }
-
- RedisWriteThroughCallback callback = new RedisWriteThroughCallback(rce, cacheMetadata);
-
- try {
- byte[] result = (byte[]) redisOperations.execute(callback);
- return (T) (result == null ? null : fromStoreValue(cacheValueAccessor.deserializeIfNecessary(result)));
- } catch (RuntimeException e) {
- throw CacheValueRetrievalExceptionFactory.INSTANCE.create(key, valueLoader, e);
- }
- }
-
- /**
- * Return the value to which this cache maps the specified key.
- *
- * @param cacheKey the key whose associated value is to be returned via its binary representation.
- * @return the {@link RedisCacheElement} stored at given key or {@literal null} if no value found for key.
- * @since 1.5
- */
- public RedisCacheElement get(final RedisCacheKey cacheKey) {
-
- Assert.notNull(cacheKey, "CacheKey must not be null!");
-
- Boolean exists = (Boolean) redisOperations.execute(new RedisCallback() {
-
- @Override
- public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
- return connection.exists(cacheKey.getKeyBytes());
- }
- });
-
- if (!exists.booleanValue()) {
- return null;
- }
-
- return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
- */
- @Override
- public void put(final Object key, final Object value) {
-
- put(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value))
- .expireAfter(cacheMetadata.getDefaultExpiration()));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.support.AbstractValueAdaptingCache#fromStoreValue(java.lang.Object)
- */
- @Override
- protected Object fromStoreValue(Object storeValue) {
-
- // we need this override for the GenericJackson2JsonRedisSerializer support.
- if (isAllowNullValues() && storeValue instanceof NullValue) {
- return null;
- }
-
- return super.fromStoreValue(storeValue);
- }
-
- /**
- * Add the element by adding {@link RedisCacheElement#get()} at {@link RedisCacheElement#getKeyBytes()}. If the cache
- * previously contained a mapping for this {@link RedisCacheElement#getKeyBytes()}, the old value is replaced by
- * {@link RedisCacheElement#get()}.
- *
- * @param element must not be {@literal null}.
- * @since 1.5
- */
- public void put(RedisCacheElement element) {
-
- Assert.notNull(element, "Element must not be null!");
-
- redisOperations
- .execute(new RedisCachePutCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
- */
- public ValueWrapper putIfAbsent(Object key, final Object value) {
-
- return putIfAbsent(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value))
- .expireAfter(cacheMetadata.getDefaultExpiration()));
- }
-
- /**
- * Add the element as long as no element exists at {@link RedisCacheElement#getKeyBytes()}. If a value is present for
- * {@link RedisCacheElement#getKeyBytes()} this one is returned.
- *
- * @param element must not be {@literal null}.
- * @return
- * @since 1.5
- */
- public ValueWrapper putIfAbsent(RedisCacheElement element) {
-
- Assert.notNull(element, "Element must not be null!");
-
- new RedisCachePutIfAbsentCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata);
-
- return toWrapper(cacheValueAccessor.deserializeIfNecessary((byte[]) redisOperations.execute(
- new RedisCachePutIfAbsentCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata))));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#evict(java.lang.Object)
- */
- public void evict(Object key) {
- evict(new RedisCacheElement(getRedisCacheKey(key), null));
- }
-
- /**
- * @param element {@link RedisCacheElement#getKeyBytes()}
- * @since 1.5
- */
- public void evict(final RedisCacheElement element) {
-
- Assert.notNull(element, "Element must not be null!");
- redisOperations
- .execute(new RedisCacheEvictCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#clear()
- */
- public void clear() {
- redisOperations.execute(cacheMetadata.usesKeyPrefix() ? new RedisCacheCleanByPrefixCallback(cacheMetadata)
- : new RedisCacheCleanByKeysCallback(cacheMetadata));
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.Cache#getName()
- */
- public String getName() {
- return cacheMetadata.getCacheName();
- }
-
- /**
- * {@inheritDoc} This implementation simply returns the RedisTemplate used for configuring the cache, giving access to
- * the underlying Redis store.
- */
- public Object getNativeCache() {
- return redisOperations;
- }
-
- private ValueWrapper toWrapper(Object value) {
- return (value != null ? new SimpleValueWrapper(value) : null);
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.support.AbstractValueAdaptingCache#lookup(java.lang.Object)
- */
@Override
protected Object lookup(Object key) {
- RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key);
+ byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
- byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback(
- new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
+ if (value == null) {
+ return null;
+ }
- @Override
- public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
- return connection.get(element.getKeyBytes());
- }
- });
-
- return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes);
+ return deserializeCacheValue(value);
}
- private RedisCacheKey getRedisCacheKey(Object key) {
- return new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix())
- .withKeySerializer(redisOperations.getKeySerializer());
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public RedisCacheWriter getNativeCache() {
+ return this.cacheWriter;
+ }
+
+ @Override
+ public synchronized T get(Object key, Callable valueLoader) {
+
+ ValueWrapper result = get(key);
+
+ if (result != null) {
+ return (T) result.get();
+ }
+
+ T value = valueFromLoader(key, valueLoader);
+ put(key, value);
+ return value;
+ }
+
+ @Override
+ public void put(Object key, Object value) {
+
+ Object cacheValue = preProcessCacheValue(value);
+
+ if (!isAllowNullValues() && cacheValue == null) {
+
+ throw new IllegalArgumentException(String.format(
+ "Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
+ name));
+ }
+
+ cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
+ }
+
+ @Override
+ public ValueWrapper putIfAbsent(Object key, Object value) {
+
+ Object cacheValue = preProcessCacheValue(value);
+
+ if (!isAllowNullValues() && cacheValue == null) {
+ return get(key);
+ }
+
+ byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
+ cacheConfig.getTtl());
+
+ if (result == null) {
+ return null;
+ }
+
+ return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
+ }
+
+ @Override
+ public void evict(Object key) {
+ cacheWriter.remove(name, createAndConvertCacheKey(key));
+ }
+
+ @Override
+ public void clear() {
+
+ byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
+ cacheWriter.clean(name, pattern);
}
/**
- * {@link Callable} to transform a value obtained from another {@link Callable} to its store value.
- *
- * @author Mark Paluch
- * @since 1.8
- * @see #toStoreValue(Object)
- */
- private class StoreTranslatingCallable implements Callable {
-
- private Callable> valueLoader;
-
- public StoreTranslatingCallable(Callable> valueLoader) {
- this.valueLoader = valueLoader;
- }
-
- @Override
- public Object call() throws Exception {
- return toStoreValue(valueLoader.call());
- }
- }
-
- /**
- * Metadata required to maintain {@link RedisCache}. Keeps track of additional data structures required for processing
- * cache operations.
+ * Get {@link RedisCacheConfiguration} used.
*
- * @author Christoph Strobl
- * @since 1.5
+ * @return immutable {@link RedisCacheConfiguration}. Never {@literal null}.
*/
- static class RedisCacheMetadata {
-
- private final String cacheName;
- private final byte[] keyPrefix;
- private final byte[] setOfKnownKeys;
- private final byte[] cacheLockName;
- private long defaultExpiration = 0;
-
- /**
- * @param cacheName must not be {@literal null} or empty.
- * @param keyPrefix can be {@literal null}.
- */
- public RedisCacheMetadata(String cacheName, byte[] keyPrefix) {
-
- Assert.hasText(cacheName, "CacheName must not be null or empty!");
- this.cacheName = cacheName;
- this.keyPrefix = keyPrefix;
-
- StringRedisSerializer stringSerializer = new StringRedisSerializer();
-
- // name of the set holding the keys
- this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys");
- this.cacheLockName = stringSerializer.serialize(cacheName + "~lock");
- }
-
- /**
- * @return true if the {@link RedisCache} uses a prefix for key ranges.
- */
- public boolean usesKeyPrefix() {
- return (keyPrefix != null && keyPrefix.length > 0);
- }
-
- /**
- * Get the binary representation of the key prefix.
- *
- * @return never {@literal null}.
- */
- public byte[] getKeyPrefix() {
- return this.keyPrefix;
- }
-
- /**
- * Get the binary representation of the key identifying the data structure used to maintain known keys.
- *
- * @return never {@literal null}.
- */
- public byte[] getSetOfKnownKeysKey() {
- return setOfKnownKeys;
- }
-
- /**
- * Get the binary representation of the key identifying the data structure used to lock the cache.
- *
- * @return never {@literal null}.
- */
- public byte[] getCacheLockKey() {
- return cacheLockName;
- }
-
- /**
- * Get the name of the cache.
- *
- * @return
- */
- public String getCacheName() {
- return cacheName;
- }
-
- /**
- * Set the default expiration time in seconds
- *
- * @param seconds
- */
- public void setDefaultExpiration(long seconds) {
- this.defaultExpiration = seconds;
- }
-
- /**
- * Get the default expiration time in seconds.
- *
- * @return
- */
- public long getDefaultExpiration() {
- return defaultExpiration;
- }
-
+ public RedisCacheConfiguration getCacheConfiguration() {
+ return cacheConfig;
}
/**
- * @author Christoph Strobl
- * @since 1.5
+ * Customization hook called before passing object to
+ * {@link org.springframework.data.redis.serializer.RedisSerializer}.
+ *
+ * @param value can be {@literal null}.
+ * @return preprocessed value. Can be {@literal null}.
*/
- static class CacheValueAccessor {
- @SuppressWarnings("rawtypes") //
- private final RedisSerializer valueSerializer;
-
- @SuppressWarnings("rawtypes")
- CacheValueAccessor(RedisSerializer valueRedisSerializer) {
- valueSerializer = valueRedisSerializer;
- }
-
- byte[] convertToBytesIfNecessary(Object value) {
-
- if (value == null) {
- return new byte[0];
- }
-
- if (valueSerializer == null && value instanceof byte[]) {
- return (byte[]) value;
- }
-
- return valueSerializer.serialize(value);
- }
-
- Object deserializeIfNecessary(byte[] value) {
-
- if (valueSerializer != null) {
- return valueSerializer.deserialize(value);
- }
+ protected Object preProcessCacheValue(Object value) {
+ if (value != null) {
return value;
}
+
+ return isAllowNullValues() ? NullValue.INSTANCE : null;
}
/**
- * @author Christoph Strobl
- * @since 1.6
+ * Serialize the key.
+ *
+ * @param cacheKey must not be {@literal null}.
+ * @return never {@literal null}.
*/
- static class BinaryRedisCacheElement extends RedisCacheElement {
-
- private byte[] keyBytes;
- private byte[] valueBytes;
- private RedisCacheElement element;
- private boolean lazyLoad;
- private CacheValueAccessor accessor;
-
- public BinaryRedisCacheElement(RedisCacheElement element, CacheValueAccessor accessor) {
-
- super(element.getKey(), element.get());
- this.element = element;
- this.keyBytes = element.getKeyBytes();
- this.accessor = accessor;
-
- lazyLoad = element.get() instanceof Callable;
- this.valueBytes = lazyLoad ? null : accessor.convertToBytesIfNecessary(element.get());
- }
-
- @Override
- public byte[] getKeyBytes() {
- return keyBytes;
- }
-
- public long getTimeToLive() {
- return element.getTimeToLive();
- }
-
- public boolean hasKeyPrefix() {
- return element.hasKeyPrefix();
- }
-
- public boolean isEternal() {
- return element.isEternal();
- }
-
- public RedisCacheElement expireAfter(long seconds) {
- return element.expireAfter(seconds);
- }
-
- @Override
- public byte[] get() {
-
- if (lazyLoad && valueBytes == null) {
- try {
- valueBytes = accessor.convertToBytesIfNecessary(((Callable>) element.get()).call());
- } catch (Exception e) {
- throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e.getMessage(), e);
- }
- }
- return valueBytes;
- }
+ protected byte[] serializeCacheKey(String cacheKey) {
+ return cacheConfig.getKeySerializationPair().write(cacheKey).array();
}
/**
- * @author Christoph Strobl
- * @since 1.5
- * @param
+ * Serialize the value to cache.
+ *
+ * @param value must not be {@literal null}.
+ * @return never {@literal null}.
*/
- static abstract class AbstractRedisCacheCallback implements RedisCallback {
+ protected byte[] serializeCacheValue(Object value) {
- private long WAIT_FOR_LOCK_TIMEOUT = 300;
- private final BinaryRedisCacheElement element;
- private final RedisCacheMetadata cacheMetadata;
-
- public AbstractRedisCacheCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
- this.element = element;
- this.cacheMetadata = metadata;
+ if (isAllowNullValues() && value instanceof NullValue) {
+ return BINARY_NULL_VALUE;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.core.RedisCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public T doInRedis(RedisConnection connection) throws DataAccessException {
- waitForLock(connection);
- return doInRedis(element, connection);
- }
-
- public abstract T doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException;
-
- protected void processKeyExpiration(RedisCacheElement element, RedisConnection connection) {
- if (!element.isEternal()) {
- connection.expire(element.getKeyBytes(), element.getTimeToLive());
- }
- }
-
- protected void maintainKnownKeys(RedisCacheElement element, RedisConnection connection) {
-
- if (!element.hasKeyPrefix()) {
-
- connection.zAdd(cacheMetadata.getSetOfKnownKeysKey(), 0, element.getKeyBytes());
-
- if (!element.isEternal()) {
- connection.expire(cacheMetadata.getSetOfKnownKeysKey(), element.getTimeToLive());
- }
- }
- }
-
- protected void cleanKnownKeys(RedisCacheElement element, RedisConnection connection) {
-
- if (!element.hasKeyPrefix()) {
- connection.zRem(cacheMetadata.getSetOfKnownKeysKey(), element.getKeyBytes());
- }
- }
-
- protected boolean waitForLock(RedisConnection connection) {
-
- boolean retry;
- boolean foundLock = false;
- do {
- retry = false;
- if (connection.exists(cacheMetadata.getCacheLockKey())) {
- foundLock = true;
- try {
- Thread.sleep(WAIT_FOR_LOCK_TIMEOUT);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- retry = true;
- }
- } while (retry);
-
- return foundLock;
- }
-
- protected void lock(RedisConnection connection) {
- waitForLock(connection);
- connection.set(cacheMetadata.getCacheLockKey(), "locked".getBytes());
- }
-
- protected void unlock(RedisConnection connection) {
- connection.del(cacheMetadata.getCacheLockKey());
- }
+ return cacheConfig.getValueSerializationPair().write(value).array();
}
/**
- * @author Christoph Strobl
- * @param
- * @since 1.5
+ * Deserialize the given value to the actual cache value.
+ *
+ * @param value must not be {@literal null}.
+ * @return can be {@literal null}.
*/
- static abstract class LockingRedisCacheCallback implements RedisCallback {
- private final RedisCacheMetadata metadata;
+ protected Object deserializeCacheValue(byte[] value) {
- public LockingRedisCacheCallback(RedisCacheMetadata metadata) {
- this.metadata = metadata;
+ if (isAllowNullValues() && ObjectUtils.nullSafeEquals(value, BINARY_NULL_VALUE)) {
+ return NullValue.INSTANCE;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.core.RedisCallback#doInRedis(org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public T doInRedis(RedisConnection connection) throws DataAccessException {
-
- if (connection.exists(metadata.getCacheLockKey())) {
- return null;
- }
- try {
- connection.set(metadata.getCacheLockKey(), metadata.getCacheLockKey());
- return doInLock(connection);
- } finally {
- connection.del(metadata.getCacheLockKey());
- }
- }
-
- public abstract T doInLock(RedisConnection connection);
+ return cacheConfig.getValueSerializationPair().read(ByteBuffer.wrap(value));
}
/**
- * @author Christoph Strobl
- * @since 1.5
+ * Customization hook for creating cache key before it gets serialized.
+ *
+ * @param key will never be {@literal null}.
+ * @return never {@literal null}.
*/
- static class RedisCacheCleanByKeysCallback extends LockingRedisCacheCallback {
+ protected String createCacheKey(Object key) {
- private static final int PAGE_SIZE = 128;
- private final RedisCacheMetadata metadata;
-
- RedisCacheCleanByKeysCallback(RedisCacheMetadata metadata) {
- super(metadata);
- this.metadata = metadata;
+ String convertedKey = conversionService.convert(key, String.class);
+ if (!cacheConfig.usePrefix()) {
+ return convertedKey;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.cache.RedisCache.LockingRedisCacheCallback#doInLock(org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public Void doInLock(RedisConnection connection) {
-
- int offset = 0;
- boolean finished = false;
-
- do {
- // need to paginate the keys
- Set keys = connection.zRange(metadata.getSetOfKnownKeysKey(), (offset) * PAGE_SIZE,
- (offset + 1) * PAGE_SIZE - 1);
- finished = keys.size() < PAGE_SIZE;
- offset++;
- if (!keys.isEmpty()) {
- connection.del(keys.toArray(new byte[keys.size()][]));
- }
- } while (!finished);
-
- connection.del(metadata.getSetOfKnownKeysKey());
- return null;
- }
+ return prefixCacheKey(convertedKey);
}
- /**
- * @author Christoph Strobl
- * @since 1.5
- */
- static class RedisCacheCleanByPrefixCallback extends LockingRedisCacheCallback {
-
- private static final byte[] REMOVE_KEYS_BY_PATTERN_LUA = new StringRedisSerializer().serialize(
- "local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;");
- private static final byte[] WILD_CARD = new StringRedisSerializer().serialize("*");
- private final RedisCacheMetadata metadata;
-
- public RedisCacheCleanByPrefixCallback(RedisCacheMetadata metadata) {
- super(metadata);
- this.metadata = metadata;
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.cache.RedisCache.LockingRedisCacheCallback#doInLock(org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public Void doInLock(RedisConnection connection) throws DataAccessException {
-
- byte[] prefixToUse = Arrays.copyOf(metadata.getKeyPrefix(), metadata.getKeyPrefix().length + WILD_CARD.length);
- System.arraycopy(WILD_CARD, 0, prefixToUse, metadata.getKeyPrefix().length, WILD_CARD.length);
-
- if (isClusterConnection(connection)) {
-
- // load keys to the client because currently Redis Cluster connections do not allow eval of lua scripts.
- Set keys = connection.keys(prefixToUse);
- if (!keys.isEmpty()) {
- connection.del(keys.toArray(new byte[keys.size()][]));
- }
- } else {
- connection.eval(REMOVE_KEYS_BY_PATTERN_LUA, ReturnType.INTEGER, 0, prefixToUse);
- }
-
- return null;
- }
+ private byte[] createAndConvertCacheKey(Object key) {
+ return serializeCacheKey(createCacheKey(key));
}
- /**
- * @author Christoph Strobl
- * @since 1.5
- */
- static class RedisCacheEvictCallback extends AbstractRedisCacheCallback {
-
- public RedisCacheEvictCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
- super(element, metadata);
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisCacheCallback#doInRedis(org.springframework.data.redis.cache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public Void doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
-
- connection.del(element.getKeyBytes());
- cleanKnownKeys(element, connection);
- return null;
- }
+ private String prefixCacheKey(String key) {
+ return cacheConfig.getKeyPrefix().orElseGet(() -> name + "::") + key;
}
- /**
- * @author Christoph Strobl
- * @since 1.5
- */
- static class RedisCachePutCallback extends AbstractRedisCacheCallback {
+ private T valueFromLoader(Object key, Callable valueLoader) {
- public RedisCachePutCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
-
- super(element, metadata);
+ try {
+ return valueLoader.call();
+ } catch (Exception e) {
+ throw new ValueRetrievalException(key, valueLoader, e);
}
-
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisPutCallback#doInRedis(org.springframework.data.redis.cache.RedisCache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public Void doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
-
- if (!isClusterConnection(connection)) {
- connection.multi();
- }
-
- if (element.get().length == 0) {
- connection.del(element.getKeyBytes());
- } else {
- connection.set(element.getKeyBytes(), element.get());
-
- processKeyExpiration(element, connection);
- maintainKnownKeys(element, connection);
- }
-
- if (!isClusterConnection(connection)) {
- connection.exec();
- }
- return null;
- }
- }
-
- /**
- * @author Christoph Strobl
- * @since 1.5
- */
- static class RedisCachePutIfAbsentCallback extends AbstractRedisCacheCallback {
-
- public RedisCachePutIfAbsentCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
- super(element, metadata);
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.data.redis.cache.RedisCache.AbstractRedisPutCallback#doInRedis(org.springframework.data.redis.cache.RedisCache.RedisCacheElement, org.springframework.data.redis.connection.RedisConnection)
- */
- @Override
- public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
-
- waitForLock(connection);
-
- byte[] keyBytes = element.getKeyBytes();
- byte[] value = element.get();
-
- if (!connection.setNX(keyBytes, value)) {
- return connection.get(keyBytes);
- }
-
- maintainKnownKeys(element, connection);
- processKeyExpiration(element, connection);
-
- return null;
- }
- }
-
- /**
- * @author Christoph Strobl
- * @since 1.7
- */
- static class RedisWriteThroughCallback extends AbstractRedisCacheCallback {
-
- public RedisWriteThroughCallback(BinaryRedisCacheElement element, RedisCacheMetadata metadata) {
- super(element, metadata);
- }
-
- @Override
- public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
-
- try {
-
- lock(connection);
-
- try {
-
- byte[] value = connection.get(element.getKeyBytes());
-
- if (value != null) {
- return value;
- }
-
- if (!isClusterConnection(connection)) {
-
- connection.watch(element.getKeyBytes());
- connection.multi();
- }
-
- value = element.get();
-
- if (value.length == 0) {
- connection.del(element.getKeyBytes());
- } else {
- connection.set(element.getKeyBytes(), value);
- processKeyExpiration(element, connection);
- maintainKnownKeys(element, connection);
- }
-
- if (!isClusterConnection(connection)) {
- connection.exec();
- }
-
- return value;
- } catch (RuntimeException e) {
- if (!isClusterConnection(connection)) {
- connection.discard();
- }
- throw e;
- }
- } finally {
- unlock(connection);
- }
- }
- };
-
- /**
- * @author Christoph Strobl
- * @since 1.7 (TODO: remove when upgrading to spring 4.3)
- */
- private static enum CacheValueRetrievalExceptionFactory {
-
- INSTANCE;
-
- private static boolean isSpring43;
-
- static {
- isSpring43 = ClassUtils.isPresent("org.springframework.cache.Cache$ValueRetrievalException",
- ClassUtils.getDefaultClassLoader());
- }
-
- public RuntimeException create(Object key, Callable> valueLoader, Throwable cause) {
-
- if (isSpring43) {
- try {
- Class> execption = ClassUtils.forName("org.springframework.cache.Cache$ValueRetrievalException",
- this.getClass().getClassLoader());
- Constructor> c = ClassUtils.getConstructorIfAvailable(execption, Object.class, Callable.class,
- Throwable.class);
- return (RuntimeException) c.newInstance(key, valueLoader, cause);
- } catch (Exception ex) {
- // ignore
- }
- }
-
- return new RedisSystemException(
- String.format("Value for key '%s' could not be loaded using '%s'.", key, valueLoader), cause);
- }
- }
-
- private static boolean isClusterConnection(RedisConnection connection) {
-
- while (connection instanceof DecoratedRedisConnection) {
- connection = ((DecoratedRedisConnection) connection).getDelegate();
- }
-
- return connection instanceof RedisClusterConnection;
}
}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java
new file mode 100644
index 000000000..1c71e9bb8
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheConfiguration.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2017 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 java.time.Duration;
+import java.util.Optional;
+
+import org.springframework.cache.Cache;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.util.Assert;
+
+/**
+ * Immutable {@link RedisCacheConfiguration} helps customizing {@link RedisCache} behaviour such as caching
+ * {@literal null} values, cache key prefixes and binary serialization.
+ * Start with {@link RedisCacheConfiguration#defaultCacheConfig()} and customize {@link RedisCache} behaviour from
+ * there on.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public class RedisCacheConfiguration {
+
+ private final Duration ttl;
+ private final boolean cacheNullValues;
+ private final String keyPrefix;
+ private final boolean usePrefix;
+
+ private final SerializationPair keySerializationPair;
+ private final SerializationPair> valueSerializationPair;
+
+ private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, String keyPrefix,
+ SerializationPair keySerializationPair, SerializationPair> valueSerializationPair) {
+
+ this.ttl = ttl;
+ this.cacheNullValues = cacheNullValues;
+ this.usePrefix = usePrefix;
+ this.keyPrefix = keyPrefix;
+ this.keySerializationPair = keySerializationPair;
+ this.valueSerializationPair = valueSerializationPair;
+ }
+
+ /**
+ * Default {@link RedisCacheConfiguration} using the following:
+ *
+ * key expiration
+ * eternal
+ * cache null values
+ * yes
+ * prefix cache keys
+ * yes
+ * default prefix
+ * [the actual cache name]
+ * key serializer
+ * StringRedisSerializer.class
+ * value serializer
+ * JdkSerializationRedisSerializer.class
+ *
+ *
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public static RedisCacheConfiguration defaultCacheConfig() {
+
+ return new RedisCacheConfiguration(Duration.ZERO, true, true, null,
+ SerializationPair.fromSerializer(new StringRedisSerializer()),
+ SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
+ }
+
+ /**
+ * Set the ttl to apply for cache entries. Use {@link Duration#ZERO} to have an eternal cache.
+ *
+ * @param ttl must not be {@literal null}.
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration entryTtl(Duration ttl) {
+
+ Assert.notNull(ttl, "Ttl must not be null!");
+
+ return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
+ valueSerializationPair);
+ }
+
+ /**
+ * Use the given prefix instead of the default one.
+ *
+ * @param prefix must not be {@literal null}.
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration prefixKeysWith(String prefix) {
+
+ Assert.notNull(prefix, "Prefix must not be null!");
+
+ return new RedisCacheConfiguration(ttl, cacheNullValues, true, prefix, keySerializationPair,
+ valueSerializationPair);
+ }
+
+ /**
+ * Disable caching {@literal null} values.
+ * NOTE any {@link org.springframework.cache.Cache#put(Object, Object)} operation involving
+ * {@literal null} value will error. Nothing will be written to Redis, nothing will be removed. An already existing
+ * key will still be there afterwards with the very same value as before.
+ *
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration disableCachingNullValues() {
+ return new RedisCacheConfiguration(ttl, false, usePrefix, keyPrefix, keySerializationPair, valueSerializationPair);
+ }
+
+ /**
+ * Disable using cache key prefixes.
+ * NOTE : {@link Cache#clear()} might result in unintended removal of {@literal key}s in Redis. Make
+ * sure to use a dedicated Redis instance when disabling prefixes.
+ *
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration disableKeyPrefix() {
+
+ return new RedisCacheConfiguration(ttl, cacheNullValues, false, keyPrefix, keySerializationPair,
+ valueSerializationPair);
+ }
+
+ /**
+ * Define the {@link SerializationPair} used for de-/serializing cache keys.
+ *
+ * @param keySerializationPair must not be {@literal null}.
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration serializeKeysWith(SerializationPair keySerializationPair) {
+
+ Assert.notNull(keySerializationPair, "KeySerializationPair must not be null!");
+
+ return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
+ valueSerializationPair);
+ }
+
+ /**
+ * Define the {@link SerializationPair} used for de-/serializing cache values.
+ *
+ * @param valueSerializationPair must not be {@literal null}.
+ * @return new {@link RedisCacheConfiguration}.
+ */
+ public RedisCacheConfiguration serializeValuesWith(SerializationPair> valueSerializationPair) {
+
+ Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null!");
+
+ return new RedisCacheConfiguration(ttl, cacheNullValues, usePrefix, keyPrefix, keySerializationPair,
+ valueSerializationPair);
+ }
+
+ /**
+ * @return never {@literal null}.
+ */
+ public Optional getKeyPrefix() {
+ return Optional.ofNullable(keyPrefix);
+ }
+
+ /**
+ * @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefix()} if present or the
+ * default which resolves to {@link Cache#getName()}.
+ */
+ public boolean usePrefix() {
+ return usePrefix;
+ }
+
+ /**
+ * @return {@literal true} if caching {@literal null} is allowed.
+ */
+ public boolean getAllowCacheNullValues() {
+ return cacheNullValues;
+ }
+
+ /**
+ * @return never {@literal null}.
+ */
+ public SerializationPair getKeySerializationPair() {
+ return keySerializationPair;
+ }
+
+ /**
+ * @return never {@literal null}.
+ */
+ public SerializationPair getValueSerializationPair() {
+ return valueSerializationPair;
+ }
+
+ /**
+ * @return The expiration time (ttl) for cache entries. Never {@literal null}.
+ */
+ public Duration getTtl() {
+ return ttl != null ? ttl : Duration.ZERO;
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java
deleted file mode 100644
index b9b7a1568..000000000
--- a/src/main/java/org/springframework/data/redis/cache/RedisCacheElement.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright 2015 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.springframework.util.Assert.*;
-
-import org.springframework.cache.support.SimpleValueWrapper;
-
-/**
- * Element to be stored inside {@link RedisCache}.
- *
- * @author Christoph Strobl
- * @since 1.5
- */
-public class RedisCacheElement extends SimpleValueWrapper {
-
- private final RedisCacheKey cacheKey;
- private long timeToLive;
-
- /**
- * @param cacheKey the key to be used for storing value in {@link RedisCache}. Must not be {@literal null}.
- * @param value
- */
- public RedisCacheElement(RedisCacheKey cacheKey, Object value) {
- super(value);
-
- notNull(cacheKey, "CacheKey must not be null!");
- this.cacheKey = cacheKey;
- }
-
- /**
- * Get the binary key representation.
- *
- * @return
- */
- public byte[] getKeyBytes() {
- return cacheKey.getKeyBytes();
- }
-
- /**
- * @return
- */
- public RedisCacheKey getKey() {
- return cacheKey;
- }
-
- /**
- * Set the elements time to live. Use {@literal zero} to store eternally.
- *
- * @param timeToLive
- */
- public void setTimeToLive(long timeToLive) {
- this.timeToLive = timeToLive;
- }
-
- /**
- * @return
- */
- public long getTimeToLive() {
- return timeToLive;
- }
-
- /**
- * @return true in case {@link RedisCacheKey} is prefixed.
- */
- public boolean hasKeyPrefix() {
- return cacheKey.hasPrefix();
- }
-
- /**
- * @return true if timeToLive is 0
- */
- public boolean isEternal() {
- return 0 == timeToLive;
- }
-
- /**
- * Expire the element after given seconds.
- *
- * @param seconds
- * @return
- */
- public RedisCacheElement expireAfter(long seconds) {
-
- setTimeToLive(seconds);
- return this;
- }
-}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java
deleted file mode 100644
index 8185894ed..000000000
--- a/src/main/java/org/springframework/data/redis/cache/RedisCacheKey.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.springframework.util.Assert.*;
-
-import java.util.Arrays;
-
-import org.springframework.data.redis.serializer.RedisSerializer;
-
-/**
- * @author Christoph Strobl
- * @since 1.5
- */
-public class RedisCacheKey {
-
- private final Object keyElement;
- private byte[] prefix;
- @SuppressWarnings("rawtypes")//
- private RedisSerializer serializer;
-
- /**
- * @param keyElement must not be {@literal null}.
- */
- public RedisCacheKey(Object keyElement) {
-
- notNull(keyElement, "KeyElement must not be null!");
- this.keyElement = keyElement;
- }
-
- /**
- * Get the {@link Byte} representation of the given key element using prefix if available.
- */
- public byte[] getKeyBytes() {
-
- byte[] rawKey = serializeKeyElement();
- if (!hasPrefix()) {
- return rawKey;
- }
-
- byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
- System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);
-
- return prefixedKey;
- }
-
- /**
- * @return
- */
- public Object getKeyElement() {
- return keyElement;
- }
-
- @SuppressWarnings("unchecked")
- private byte[] serializeKeyElement() {
-
- if (serializer == null && keyElement instanceof byte[]) {
- return (byte[]) keyElement;
- }
-
- return serializer.serialize(keyElement);
- }
-
- /**
- * Set the {@link RedisSerializer} used for converting the key into its {@link Byte} representation.
- *
- * @param serializer can be {@literal null}.
- */
- public void setSerializer(RedisSerializer> serializer) {
- this.serializer = serializer;
- }
-
- /**
- * @return true if prefix is not empty.
- */
- public boolean hasPrefix() {
- return (prefix != null && prefix.length > 0);
- }
-
- /**
- * Use the given prefix when generating key.
- *
- * @param prefix can be {@literal null}.
- * @return
- */
- public RedisCacheKey usePrefix(byte[] prefix) {
- this.prefix = prefix;
- return this;
- }
-
- /**
- * Use {@link RedisSerializer} for converting the key into its {@link Byte} representation.
- *
- * @param serializer can be {@literal null}.
- * @return
- */
- public RedisCacheKey withKeySerializer(RedisSerializer serializer) {
-
- this.serializer = serializer;
- return this;
- }
-
-}
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 df5feaf5a..f0f87552d 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-2016 the original author or authors.
+ * Copyright 2017 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.
@@ -15,332 +15,306 @@
*/
package org.springframework.data.redis.cache;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-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.support.NullValue;
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.RedisOperations;
-import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
- * {@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
+ * @since 2.0
*/
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
- private final Log logger = LogFactory.getLog(RedisCacheManager.class);
+ private final RedisCacheWriter cacheWriter;
+ private final RedisCacheConfiguration defaultCacheConfig;
+ private final Map initialCacheConfiguration;
- @SuppressWarnings("rawtypes") //
- private final RedisOperations redisOperations;
+ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
- private boolean usePrefix = false;
- private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
- private boolean loadRemoteCachesOnStartup = false;
- private boolean dynamic = true;
+ Assert.notNull(cacheWriter, "CacheWriter must not be null!");
+ Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
- // 0 - never expire
- private long defaultExpiration = 0;
- private Map expires = null;
-
- private Set configuredCacheNames;
-
- private final boolean cacheNullValues;
-
- /**
- * Construct a {@link RedisCacheManager}.
- *
- * @param redisOperations
- */
- @SuppressWarnings("rawtypes")
- public RedisCacheManager(RedisOperations redisOperations) {
- this(redisOperations, Collections. emptyList());
+ this.cacheWriter = cacheWriter;
+ this.defaultCacheConfig = defaultCacheConfiguration;
+ this.initialCacheConfiguration = new LinkedHashMap<>();
}
/**
- * Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only.
- *
- * @param redisOperations
- * @param cacheNames
- * @since 1.2
- */
- @SuppressWarnings("rawtypes")
- public RedisCacheManager(RedisOperations redisOperations, Collection cacheNames) {
- this(redisOperations, cacheNames, false);
- }
-
- /**
- * Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only.
- *
- * NOTE When enabling {@code cacheNullValues} please make sure the {@link RedisSerializer} used by
- * {@link RedisOperations} is capable of serializing {@link NullValue}.
+ * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default
+ * {@link RedisCacheConfiguration}.
*
- * @param redisOperations {@link RedisOperations} to work upon.
- * @param cacheNames {@link Collection} of known cache names.
- * @param cacheNullValues set to {@literal true} to allow caching {@literal null}.
- * @since 1.8
+ * @param cacheWriter must not be {@literal null}.
+ * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use
+ * {@link RedisCacheConfiguration#defaultCacheConfig()}.
+ * @param initialCacheNames optional set of known cache names that will be created with given
+ * {@literal defaultCacheConfiguration}.
*/
- @SuppressWarnings("rawtypes")
- public RedisCacheManager(RedisOperations redisOperations, Collection cacheNames, boolean cacheNullValues) {
+ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
+ String... initialCacheNames) {
- this.redisOperations = redisOperations;
- this.cacheNullValues = cacheNullValues;
- setCacheNames(cacheNames);
- }
+ this(cacheWriter, defaultCacheConfiguration);
- /**
- * 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.
- * Calling this with a {@code null} or empty collection argument resets the mode to 'dynamic', allowing for further
- * creation of caches again.
- */
- public void setCacheNames(Collection cacheNames) {
-
- Set newCacheNames = CollectionUtils.isEmpty(cacheNames) ? Collections. emptySet()
- : new HashSet(cacheNames);
-
- this.configuredCacheNames = newCacheNames;
- this.dynamic = newCacheNames.isEmpty();
- }
-
- public void setUsePrefix(boolean usePrefix) {
- this.usePrefix = usePrefix;
- }
-
- /**
- * Sets the cachePrefix. Defaults to 'DefaultRedisCachePrefix').
- *
- * @param cachePrefix the cachePrefix to set
- */
- public void setCachePrefix(RedisCachePrefix cachePrefix) {
- this.cachePrefix = cachePrefix;
- }
-
- /**
- * Sets the default expire time (in seconds).
- *
- * @param defaultExpireTime time in seconds.
- */
- public void setDefaultExpiration(long defaultExpireTime) {
- this.defaultExpiration = defaultExpireTime;
- }
-
- /**
- * Sets the expire time (in seconds) for cache regions (by key).
- *
- * @param expires time in seconds
- */
- 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 extends Cache> loadCaches() {
-
- Assert.notNull(this.redisOperations, "A redis template is required in order to interact with data store");
-
- Set caches = new LinkedHashSet(
- loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : new ArrayList());
-
- Set cachesToLoad = new LinkedHashSet(this.configuredCacheNames);
- cachesToLoad.addAll(this.getCacheNames());
-
- if (!CollectionUtils.isEmpty(cachesToLoad)) {
-
- for (String cacheName : cachesToLoad) {
- caches.add(createCache(cacheName));
- }
+ for (String cacheName : initialCacheNames) {
+ this.initialCacheConfiguration.put(cacheName, defaultCacheConfiguration);
}
+ }
+ /**
+ * Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default
+ * {@link RedisCacheConfiguration}.
+ *
+ * @param cacheWriter must not be {@literal null}.
+ * @param defaultCacheConfiguration must not be {@literal null}. Maybe just use
+ * {@link RedisCacheConfiguration#defaultCacheConfig()}.
+ * @param initialCacheConfigurations Map of known cache names along with the configuration to use for those caches.
+ * Must not be {@literal null}.
+ */
+ public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
+ Map initialCacheConfigurations) {
+
+ this(cacheWriter, defaultCacheConfiguration);
+
+ Assert.notNull(initialCacheConfigurations, "InitialCacheConfigurations must not be null!");
+ this.initialCacheConfiguration.putAll(initialCacheConfigurations);
+ }
+
+ /**
+ * Create a new {@link RedisCacheManager} with defaults applied.
+ *
+ * locking
+ * disabled
+ * cache configuration
+ * {@link RedisCacheConfiguration#defaultCacheConfig()}
+ * initial caches
+ * none
+ * transaction aware
+ * no
+ *
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return new instance of {@link RedisCacheManager}.
+ */
+ public static RedisCacheManager defaultCacheManager(RedisConnectionFactory connectionFactory) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+
+ return new RedisCacheManager(new DefaultRedisCacheWriter(connectionFactory),
+ RedisCacheConfiguration.defaultCacheConfig());
+ }
+
+ /**
+ * Entry point for builder style {@link RedisCacheManager} configuration.
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return new {@link RedisCacheManagerConfigurator}.
+ */
+ public static RedisCacheManagerConfigurator usingRawConnectionFactory(RedisConnectionFactory connectionFactory) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+
+ return RedisCacheManagerConfigurator.usingRawFactory(connectionFactory);
+ }
+
+ /**
+ * Entry point for builder style {@link RedisCacheManager} configuration.
+ *
+ * @param cacheWriter must not be {@literal null}.
+ * @return new {@link RedisCacheManagerConfigurator}.
+ */
+ public static RedisCacheManagerConfigurator usingCacheWriter(RedisCacheWriter cacheWriter) {
+
+ Assert.notNull(cacheWriter, "CacheWriter must not be null!");
+
+ return RedisCacheManagerConfigurator.usingCacheWriter(cacheWriter);
+ }
+
+ @Override
+ protected Collection loadCaches() {
+
+ List caches = new LinkedList<>();
+ for (Map.Entry entry : initialCacheConfiguration.entrySet()) {
+ caches.add(createRedisCache(entry.getKey(), entry.getValue()));
+ }
return caches;
}
- /**
- * Returns a new {@link Collection} of {@link Cache} from the given caches collection and adds the configured
- * {@link Cache}s of they are not already present.
- *
- * @param caches must not be {@literal null}
- * @return
- */
- protected Collection extends Cache> addConfiguredCachesIfNecessary(Collection extends Cache> caches) {
-
- Assert.notNull(caches, "Caches must not be null!");
-
- Collection result = new ArrayList(caches);
-
- for (String cacheName : getCacheNames()) {
-
- boolean configuredCacheAlreadyPresent = false;
-
- for (Cache cache : caches) {
-
- if (cache.getName().equals(cacheName)) {
- configuredCacheAlreadyPresent = true;
- break;
- }
- }
-
- if (!configuredCacheAlreadyPresent) {
- result.add(getCache(cacheName));
- }
- }
-
- return result;
- }
-
- /**
- * Will no longer add the cache to the set of
- *
- * @param cacheName
- * @return
- * @deprecated since 1.8 - please use {@link #getCache(String)}.
- */
- @Deprecated
- protected Cache createAndAddCache(String cacheName) {
-
- Cache cache = super.getCache(cacheName);
- return cache != null ? cache : createCache(cacheName);
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.cache.support.AbstractCacheManager#getMissingCache(java.lang.String)
- */
@Override
- protected Cache getMissingCache(String name) {
- return this.dynamic ? createCache(name) : null;
+ protected RedisCache getMissingCache(String name) {
+ return createRedisCache(name, defaultCacheConfig);
}
- @SuppressWarnings("unchecked")
- protected RedisCache createCache(String cacheName) {
- long expiration = computeExpiration(cacheName);
- return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
- cacheNullValues);
- }
+ /**
+ * @return unmodifiable {@link Map} containing cache name / configuration pairs. Never {@literal null}.
+ */
+ public Map getCacheConfigurations() {
- protected long computeExpiration(String name) {
- Long expiration = null;
- if (expires != null) {
- expiration = expires.get(name);
- }
- return (expiration != null ? expiration.longValue() : defaultExpiration);
- }
+ Map configurationMap = new HashMap<>(getCacheNames().size());
+ getCacheNames().forEach(it -> {
- protected 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")
- protected Set loadRemoteCacheKeys() {
- return (Set) redisOperations.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(redisOperations.getKeySerializer().serialize("*~keys"));
- Set cacheKeys = new LinkedHashSet();
-
- if (!CollectionUtils.isEmpty(keys)) {
- for (byte[] key : keys) {
- cacheKeys.add(redisOperations.getKeySerializer().deserialize(key).toString().replace("~keys", ""));
- }
- }
-
- return cacheKeys;
- }
+ RedisCache cache = RedisCache.class.cast(lookupCache(it));
+ configurationMap.put(it, cache != null ? cache.getCacheConfiguration() : null);
});
+ return Collections.unmodifiableMap(configurationMap);
}
- @SuppressWarnings("rawtypes")
- protected RedisOperations getRedisOperations() {
- return redisOperations;
+ /**
+ * Configuration hook for creating {@link RedisCache} with given name and cacheConfig.
+ *
+ * @param name must not be {@literal null}.
+ * @param cacheConfig can be {@literal null}.
+ * @return never {@literal null}.
+ */
+ protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
+ return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
- protected RedisCachePrefix getCachePrefix() {
- return cachePrefix;
- }
+ /**
+ * Configurator for creating {@link RedisCacheManager}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ public static class RedisCacheManagerConfigurator {
- protected boolean isUsePrefix() {
- return usePrefix;
- }
+ private final RedisCacheConfiguration defaultCacheConfiguration;
+ private final RedisCacheWriter cacheWriter;
+ private final boolean enableTransactions;
+ private final Map intialCaches = new LinkedHashMap<>();
- /* (non-Javadoc)
- * @see
- org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager#decorateCache(org.springframework.cache.Cache)
- */
- @Override
- protected Cache decorateCache(Cache cache) {
+ private RedisCacheManagerConfigurator(RedisCacheWriter cacheWriter,
+ RedisCacheConfiguration defaultCacheConfiguration, Map intialCaches,
+ boolean enableTransactions) {
- if (isCacheAlreadyDecorated(cache)) {
- return cache;
+ this.cacheWriter = cacheWriter;
+ this.defaultCacheConfiguration = defaultCacheConfiguration;
+
+ if (!CollectionUtils.isEmpty(intialCaches)) {
+ this.intialCaches.putAll(intialCaches);
+ }
+
+ this.enableTransactions = enableTransactions;
}
- return super.decorateCache(cache);
- }
+ /**
+ * Entry point for builder style {@link RedisCacheManager} configuration.
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return new {@link RedisCacheManagerConfigurator}.
+ */
+ public static RedisCacheManagerConfigurator usingRawFactory(RedisConnectionFactory connectionFactory) {
- protected boolean isCacheAlreadyDecorated(Cache cache) {
- return isTransactionAware() && cache instanceof TransactionAwareCacheDecorator;
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
+
+ return usingCacheWriter(new DefaultRedisCacheWriter(connectionFactory));
+ }
+
+ /**
+ * Entry point for builder style {@link RedisCacheManager} configuration.
+ *
+ * @param cacheWriter must not be {@literal null}.
+ * @return new {@link RedisCacheManagerConfigurator}.
+ */
+ public static RedisCacheManagerConfigurator usingCacheWriter(RedisCacheWriter cacheWriter) {
+
+ Assert.notNull(cacheWriter, "CacheWriter must not be null!");
+
+ return new RedisCacheManagerConfigurator(cacheWriter, RedisCacheConfiguration.defaultCacheConfig(), null, false);
+ }
+
+ /**
+ * Define a default {@link RedisCacheConfiguration} applied to dynamically created {@link RedisCache}s.
+ *
+ * @param defaultCacheConfiguration must not be {@literal null}.
+ * @return new instance of {@link RedisCacheManagerConfigurator}.
+ */
+ public RedisCacheManagerConfigurator withCacheDefaults(RedisCacheConfiguration defaultCacheConfiguration) {
+
+ Assert.notNull(defaultCacheConfiguration, "DefaultCacheConfiguration must not be null!");
+
+ return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, intialCaches,
+ enableTransactions);
+ }
+
+ /**
+ * Enable {@link RedisCache}s to synchronize cache put/evict operations with ongoing Spring-managed transactions.
+ *
+ * @return new instance of {@link RedisCacheManagerConfigurator}.
+ */
+ public RedisCacheManagerConfigurator transactionAware() {
+ return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, intialCaches, true);
+ }
+
+ /**
+ * Append a {@link Set} of cache names to be pre initialized with current {@link RedisCacheConfiguration}.
+ * NOTE: This calls depends on {@link #withCacheDefaults(RedisCacheConfiguration)} using whatever
+ * default {@link RedisCacheConfiguration} is present at the time of invoking this method.
+ *
+ * @param cacheNames must not be {@literal null}.
+ * @return new instance of {@link RedisCacheManagerConfigurator}.
+ */
+ public RedisCacheManagerConfigurator withInitialCacheNames(Set cacheNames) {
+
+ Assert.notNull(cacheNames, "CacheNames must not be null!");
+
+ Map cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
+ cacheNames.forEach(it -> cacheConfigMap.put(it, defaultCacheConfiguration));
+ return withInitialCacheConfigurations(cacheConfigMap);
+ }
+
+ /**
+ * Append a {@link Map} of cache name/{@link RedisCacheConfiguration} pairs to be pre initialized.
+ *
+ * @param cacheConfigurations must not be {@literal null}.
+ * @return new instance of {@link RedisCacheManagerConfigurator}.
+ */
+ public RedisCacheManagerConfigurator withInitialCacheConfigurations(
+ Map cacheConfigurations) {
+
+ Assert.notNull(cacheConfigurations, "CacheConfigurations must not be null!");
+
+ Map cacheConfigMap = new LinkedHashMap<>(intialCaches);
+ cacheConfigMap.putAll(cacheConfigurations);
+ return new RedisCacheManagerConfigurator(cacheWriter, defaultCacheConfiguration, cacheConfigMap,
+ enableTransactions);
+ }
+
+ /**
+ * Create new instance of {@link RedisCacheManager} with configuration options applied.
+ *
+ * @return new instance of {@link RedisCacheManager}.
+ */
+ public RedisCacheManager createAndGet() {
+
+ RedisCacheManager cm = new RedisCacheManager(cacheWriter, defaultCacheConfiguration, intialCaches) {
+
+ boolean hasAlreadyBeenSet;
+
+ @Override
+ public void setTransactionAware(boolean transactionAware) {
+
+ if (hasAlreadyBeenSet) {
+ throw new IllegalStateException(
+ String.format("CacheManager transaction awareness has already been set to %s.", isTransactionAware()));
+ }
+
+ super.setTransactionAware(transactionAware);
+ hasAlreadyBeenSet = true;
+ }
+ };
+
+ cm.setTransactionAware(enableTransactions);
+ cm.afterPropertiesSet(); // is this a good idea? Still need to think about it.
+ return cm;
+ }
}
}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCachePrefix.java b/src/main/java/org/springframework/data/redis/cache/RedisCachePrefix.java
deleted file mode 100644
index 8a7aef4cb..000000000
--- a/src/main/java/org/springframework/data/redis/cache/RedisCachePrefix.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2011-2013 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;
-
-/**
- * Contract for generating 'prefixes' for Cache keys saved in Redis. Due to the 'flat' nature of the Redis storage, the
- * prefix is used as a 'namespace' for grouping the key/values inside a cache (and to avoid collision with other caches
- * or keys inside Redis).
- *
- * @author Costin Leau
- */
-public interface RedisCachePrefix {
-
- /**
- * Returns the prefix for the given cache (identified by name). Note the prefix is returned in raw form so it can be
- * saved directly to Redis without any serialization.
- *
- * @param cacheName the name of the cache using the prefix
- * @return the prefix for the given cache.
- */
- byte[] prefix(String cacheName);
-
-}
diff --git a/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java
new file mode 100644
index 000000000..87dee818d
--- /dev/null
+++ b/src/main/java/org/springframework/data/redis/cache/RedisCacheWriter.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 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 java.time.Duration;
+
+/**
+ * {@link RedisCacheWriter} provides low level access to redis commands ({@code SET, SETNX, GET, EXPIRE,...} used for
+ * caching.
+ * The {@link RedisCacheWriter} may be shared by multiple cache implementations and is responsible for writing / reading
+ * binary data to / from Redis. The implementation honors potential cache lock flags that might be set.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface RedisCacheWriter {
+
+ /**
+ * Write the given key/value pair to Redis an set the expiration time if defined.
+ *
+ * @param name The cache name must not be {@literal null}.
+ * @param key The key for the cache entry. Must not be {@literal null}.
+ * @param value The value stored for the key. Must not be {@literal null}.
+ * @param ttl Optional expiration time. Can be {@literal null}.
+ */
+ void put(String name, byte[] key, byte[] value, Duration ttl);
+
+ /**
+ * Get the binary value representation from Redis stored for the given key.
+ *
+ * @param name must not be {@literal null}.
+ * @param key must not be {@literal null}.
+ * @return {@literal null} if key does not exist.
+ */
+
+ byte[] get(String name, byte[] key);
+
+ /**
+ * Write the given value to Redis if the key does not already exist.
+ *
+ * @param name The cache name must not be {@literal null}.
+ * @param key The key for the cache entry. Must not be {@literal null}.
+ * @param value The value stored for the key. Must not be {@literal null}.
+ * @param ttl Optional expiration time. Can be {@literal null}.
+ * @return {@literal null} if the value has been written, the value stored for the key if it already exists.
+ */
+
+ byte[] putIfAbsent(String name, byte[] key, byte[] value, Duration ttl);
+
+ /**
+ * Remove the given key from Redis.
+ *
+ * @param name The cache name must not be {@literal null}.
+ * @param key The key for the cache entry. Must not be {@literal null}.
+ */
+ void remove(String name, byte[] key);
+
+ /**
+ * Remove all keys following the given pattern.
+ *
+ * @param name The cache name must not be {@literal null}.
+ * @param pattern The pattern for the keys to remove. Must not be {@literal null}.
+ */
+ void clean(String name, byte[] pattern);
+
+}
diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
index 227822456..d81ed9986 100644
--- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
+++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
@@ -101,6 +101,13 @@ public class LettuceConnectionFactory
this(new MutableLettuceClientConfiguration());
}
+ /**
+ * Constructs a new {@link LettuceConnectionFactory} instance with default settings.
+ */
+ public LettuceConnectionFactory(RedisStandaloneConfiguration config) {
+ this(config, new MutableLettuceClientConfiguration());
+ }
+
/**
* Constructs a new {@link LettuceConnectionFactory} instance given {@link LettuceClientConfiguration}.
*
diff --git a/src/test/java/org/springframework/data/redis/cache/AbstractNativeCacheTest.java b/src/test/java/org/springframework/data/redis/cache/AbstractNativeCacheTest.java
deleted file mode 100644
index 57a73db26..000000000
--- a/src/test/java/org/springframework/data/redis/cache/AbstractNativeCacheTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2011-2013 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.junit.Assert.*;
-import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.springframework.cache.Cache;
-import org.springframework.cache.Cache.ValueWrapper;
-
-/**
- * Test for native cache implementations.
- *
- * @author Costin Leau
- */
-public abstract class AbstractNativeCacheTest {
-
- private T nativeCache;
- protected Cache cache;
- protected final static String CACHE_NAME = "testCache";
- private final boolean allowCacheNullValues;
-
- protected AbstractNativeCacheTest(boolean allowCacheNullValues) {
- this.allowCacheNullValues = allowCacheNullValues;
- }
-
- @Before
- public void setUp() throws Exception {
- nativeCache = createNativeCache();
- cache = createCache(nativeCache, allowCacheNullValues);
- cache.clear();
- }
-
- protected abstract T createNativeCache() throws Exception;
-
- protected abstract Cache createCache(T nativeCache, boolean allowCacheNullValues);
-
- protected abstract Object getKey();
-
- protected abstract Object getValue();
-
- protected boolean getAllowCacheNullValues() {
- return allowCacheNullValues;
- }
-
- @Test
- public void testCacheName() throws Exception {
- assertEquals(CACHE_NAME, cache.getName());
- }
-
- @Test
- public void testNativeCache() throws Exception {
- assertSame(nativeCache, cache.getNativeCache());
- }
-
- @Test
- public void testCachePut() throws Exception {
- Object key = getKey();
- Object value = getValue();
-
- assertNotNull(value);
- assertNull(cache.get(key));
- cache.put(key, value);
- ValueWrapper valueWrapper = cache.get(key);
- if (valueWrapper != null) {
- assertThat(valueWrapper.get(), isEqual(value));
- }
- // keeps failing on the CI server so do
- else {
- // Thread.sleep(200);
- // assertNotNull(cache.get(key));
- // ignore for now
- }
- }
-
- @Test
- public void testCacheClear() throws Exception {
- Object key1 = getKey();
- Object value1 = getValue();
-
- Object key2 = getKey();
- Object value2 = getValue();
-
- assertNull(cache.get(key1));
- cache.put(key1, value1);
- assertNull(cache.get(key2));
- cache.put(key2, value2);
- cache.clear();
- assertNull(cache.get(key2));
- assertNull(cache.get(key1));
- }
-}
diff --git a/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java b/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java
new file mode 100644
index 000000000..aa494b989
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/CacheTestParams.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017 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.springframework.data.redis.connection.ClusterTestVariables.*;
+
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.Delegate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.runners.model.Statement;
+import org.springframework.data.redis.SettingsUtils;
+import org.springframework.data.redis.connection.RedisClusterConfiguration;
+import org.springframework.data.redis.connection.RedisClusterNode;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.OxmSerializer;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.test.util.RedisClusterRule;
+import org.springframework.oxm.xstream.XStreamMarshaller;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Christoph Strobl
+ */
+class CacheTestParams {
+
+ private static Collection connectionFactories() {
+
+ RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
+ config.setHostName(SettingsUtils.getHost());
+ config.setPort(SettingsUtils.getPort());
+
+ List factoryList = new ArrayList<>(3);
+
+ // Jedis Standalone
+ JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(config);
+ jedisConnectionFactory.afterPropertiesSet();
+ factoryList.add(new FixDamnedJunitParameterizedNameForConnectionFactory(jedisConnectionFactory, ""));
+
+ // Lettuce Standalone
+ LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(config);
+ lettuceConnectionFactory.afterPropertiesSet();
+ factoryList.add(new FixDamnedJunitParameterizedNameForConnectionFactory(lettuceConnectionFactory, ""));
+
+ if (clusterAvailable()) {
+
+ RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
+ clusterConfiguration.addClusterNode(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_1_PORT));
+
+ // Jedis Cluster
+ JedisConnectionFactory jedisClusterConnectionFactory = new JedisConnectionFactory(clusterConfiguration);
+ jedisClusterConnectionFactory.afterPropertiesSet();
+ factoryList
+ .add(new FixDamnedJunitParameterizedNameForConnectionFactory(jedisClusterConnectionFactory, "cluster"));
+
+ // Lettuce Cluster
+ LettuceConnectionFactory lettuceClusterConnectionFactory = new LettuceConnectionFactory(clusterConfiguration);
+ lettuceClusterConnectionFactory.afterPropertiesSet();
+
+ factoryList
+ .add(new FixDamnedJunitParameterizedNameForConnectionFactory(lettuceClusterConnectionFactory, "cluster"));
+ }
+
+ return factoryList;
+ }
+
+ static Collection justConnectionFactories() {
+ return connectionFactories().stream().map(factory -> new Object[] { factory }).collect(Collectors.toList());
+ }
+
+ static Collection connectionFactoriesAndSerializers() {
+
+ // XStream serializer
+ XStreamMarshaller xstream = new XStreamMarshaller();
+ xstream.afterPropertiesSet();
+
+ OxmSerializer oxmSerializer = new OxmSerializer(xstream, xstream);
+ GenericJackson2JsonRedisSerializer jackson2Serializer = new GenericJackson2JsonRedisSerializer();
+ JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
+
+ return connectionFactories()
+ .stream().flatMap(factory -> Arrays
+ .asList( //
+ new Object[] { factory, new FixDamnedJunitParameterizedNameForRedisSerializer(jdkSerializer) }, //
+ new Object[] { factory, new FixDamnedJunitParameterizedNameForRedisSerializer(jackson2Serializer) }, //
+ new Object[] { factory, new FixDamnedJunitParameterizedNameForRedisSerializer(oxmSerializer) })
+ .stream())
+ .collect(Collectors.toList());
+ }
+
+ @RequiredArgsConstructor
+ static class FixDamnedJunitParameterizedNameForConnectionFactory/* ¯\_(ツ)_/¯ */ implements RedisConnectionFactory {
+
+ final @Delegate RedisConnectionFactory connectionFactory;
+ final String addon;
+
+ @Override // Why Junit? Why?
+ public String toString() {
+ return connectionFactory.getClass().getSimpleName() + (StringUtils.hasText(addon) ? " - [" + addon + "]" : "");
+ }
+ }
+
+ @RequiredArgsConstructor
+ static class FixDamnedJunitParameterizedNameForRedisSerializer/* ¯\_(ツ)_/¯ */ implements RedisSerializer {
+
+ final @Delegate RedisSerializer serializer;
+
+ @Override // Why Junit? Why?
+ public String toString() {
+ return serializer.getClass().getSimpleName();
+ }
+ }
+
+ private static boolean clusterAvailable() {
+
+ try {
+ new RedisClusterRule().apply(new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+
+ }
+ }, null).evaluate();
+ } catch (Throwable throwable) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java
new file mode 100644
index 000000000..811e64269
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/DefaultRedisCacheWriterTests.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+import static org.springframework.data.redis.cache.DefaultRedisCacheWriter.*;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.springframework.data.redis.ConnectionFactoryTracker;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.types.Expiration;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(Parameterized.class)
+public class DefaultRedisCacheWriterTests {
+
+ static final String CACHE_NAME = "default-redis-cache-writer-tests";
+
+ String key = "key-1";
+ String cacheKey = CACHE_NAME + "::" + key;
+ byte[] binaryCacheKey = cacheKey.getBytes(Charset.forName("UTF-8"));
+
+ byte[] binaryCacheValue = "value".getBytes(Charset.forName("UTF-8"));
+
+ RedisConnectionFactory connectionFactory;
+
+ public DefaultRedisCacheWriterTests(RedisConnectionFactory connectionFactory) {
+
+ this.connectionFactory = connectionFactory;
+
+ ConnectionFactoryTracker.add(connectionFactory);
+ }
+
+ @Parameters(name = "{index}: {0}")
+ public static Collection testParams() {
+ return CacheTestParams.justConnectionFactories();
+ }
+
+ @AfterClass
+ public static void cleanUpResources() {
+ ConnectionFactoryTracker.cleanUp();
+ }
+
+ @Before
+ public void setUp() {
+
+ JedisConnectionFactory cf = new JedisConnectionFactory();
+ cf.afterPropertiesSet();
+
+ connectionFactory = cf;
+
+ doWithConnection(RedisConnection::flushAll);
+ }
+
+ @Test // DATAREDIS-481
+ public void putShouldAddEternalEntry() {
+
+ nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ assertThat(connection.ttl(binaryCacheKey)).isEqualTo(-1);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putShouldAddExpiringEntry() {
+
+ nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue,
+ Duration.ofSeconds(1));
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(0);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putShouldOverwriteExistingEternalEntry() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, "foo".getBytes()));
+
+ nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ assertThat(connection.ttl(binaryCacheKey)).isEqualTo(-1);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putShouldOverwriteExistingExpiringEntryAndResetTtl() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, "foo".getBytes(),
+ Expiration.from(1, TimeUnit.MINUTES), SetOption.upsert()));
+
+ nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue,
+ Duration.ofSeconds(5));
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(3).isLessThan(6);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void getShouldReturnValue() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binaryCacheValue));
+
+ assertThat(nonLockingRedisCacheWriter(connectionFactory).get(CACHE_NAME, binaryCacheKey))
+ .isEqualTo(binaryCacheValue);
+ }
+
+ @Test // DATAREDIS-481
+ public void getShouldReturnNullWhenKeyDoesNotExist() {
+ assertThat(nonLockingRedisCacheWriter(connectionFactory).get(CACHE_NAME, binaryCacheKey)).isNull();
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldAddEternalEntryWhenKeyDoesNotExist() {
+
+ assertThat(nonLockingRedisCacheWriter(connectionFactory).putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue,
+ Duration.ZERO)).isNull();
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldNotAddEternalEntryWhenKeyAlreadyExist() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binaryCacheValue));
+
+ assertThat(nonLockingRedisCacheWriter(connectionFactory).putIfAbsent(CACHE_NAME, binaryCacheKey, "foo".getBytes(),
+ Duration.ZERO)).isEqualTo(binaryCacheValue);
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryCacheValue);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldAddExpiringEntryWhenKeyDoesNotExist() {
+
+ assertThat(nonLockingRedisCacheWriter(connectionFactory).putIfAbsent(CACHE_NAME, binaryCacheKey, binaryCacheValue,
+ Duration.ofSeconds(5))).isNull();
+
+ doWithConnection(connection -> {
+ assertThat(connection.ttl(binaryCacheKey)).isGreaterThan(3).isLessThan(6);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void removeShouldDeleteEntry() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binaryCacheValue));
+
+ nonLockingRedisCacheWriter(connectionFactory).remove(CACHE_NAME, binaryCacheKey);
+
+ doWithConnection(connection -> assertThat(connection.exists(binaryCacheKey)).isFalse());
+ }
+
+ @Test // DATAREDIS-418
+ public void cleanShouldRemoveAllKeysByPattern() {
+
+ doWithConnection(connection -> {
+ connection.set(binaryCacheKey, binaryCacheValue);
+ connection.set("foo".getBytes(), "bar".getBytes());
+ });
+
+ nonLockingRedisCacheWriter(connectionFactory).clean(CACHE_NAME,
+ (CACHE_NAME + "::*").getBytes(Charset.forName("UTF-8")));
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isFalse();
+ assertThat(connection.exists("foo".getBytes())).isTrue();
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void nonLockingCacheWriterShouldIgnoreExistingLock() {
+
+ lockingRedisCacheWriter(connectionFactory).lock(CACHE_NAME);
+
+ nonLockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void lockingCacheWriterShouldIgnoreExistingLockOnDifferenceCache() {
+
+ lockingRedisCacheWriter(connectionFactory).lock(CACHE_NAME);
+
+ lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME + "-no-the-other-cache", binaryCacheKey, binaryCacheValue,
+ Duration.ZERO);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void lockingCacheWriterShouldWaitForLockRelease() throws InterruptedException {
+
+ DefaultRedisCacheWriter cw = lockingRedisCacheWriter(connectionFactory);
+ cw.lock(CACHE_NAME);
+
+ Thread th = new Thread(() -> {
+ lockingRedisCacheWriter(connectionFactory).put(CACHE_NAME, binaryCacheKey, binaryCacheValue, Duration.ZERO);
+ });
+ th.start();
+
+ try {
+ Thread.sleep(200);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isFalse();
+ });
+
+ cw.unlock(CACHE_NAME);
+
+ Thread.sleep(200);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ });
+ } finally {
+ th.interrupt();
+ }
+ }
+
+ void doWithConnection(Consumer callback) {
+ RedisConnection connection = connectionFactory.getConnection();
+ try {
+ callback.accept(connection);
+ } finally {
+ connection.close();
+ }
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java b/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java
similarity index 68%
rename from src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java
rename to src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java
index cea72645c..450698181 100644
--- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java
+++ b/src/test/java/org/springframework/data/redis/cache/LegacyRedisCacheTests.java
@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.springframework.data.redis.cache;
import static edu.umd.cs.mtc.TestFramework.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsEqual.*;
-import static org.hamcrest.core.IsInstanceOf.*;
import static org.hamcrest.core.IsNot.*;
import static org.hamcrest.core.IsNull.*;
import static org.hamcrest.core.IsSame.*;
@@ -27,18 +25,18 @@ import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
+import edu.umd.cs.mtc.MultithreadedTestCase;
+
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.hamcrest.core.IsInstanceOf;
import org.junit.AfterClass;
-import org.junit.AssumptionViolatedException;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -48,16 +46,14 @@ import org.springframework.cache.Cache.ValueRetrievalException;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.data.redis.ConnectionFactoryTracker;
import org.springframework.data.redis.ObjectFactory;
-import org.springframework.data.redis.StringObjectFactory;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.AbstractOperationsTestParams;
import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
-import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
-import org.springframework.data.redis.serializer.StringRedisSerializer;
-
-import edu.umd.cs.mtc.MultithreadedTestCase;
/**
+ * Tests moved over from 1.x line RedisCache implementation. Just removed somme of the limitations/assumtions previously
+ * required.
+ *
* @author Costin Leau
* @author Jennifer Hickey
* @author Christoph Strobl
@@ -65,21 +61,26 @@ import edu.umd.cs.mtc.MultithreadedTestCase;
*/
@SuppressWarnings("rawtypes")
@RunWith(Parameterized.class)
-public class RedisCacheTest extends AbstractNativeCacheTest {
+public class LegacyRedisCacheTests {
- private ObjectFactory keyFactory;
- private ObjectFactory valueFactory;
- private RedisTemplate template;
+ final static String CACHE_NAME = "testCache";
+ ObjectFactory keyFactory;
+ ObjectFactory valueFactory;
+ RedisConnectionFactory connectionFactory;
+ final boolean allowCacheNullValues;
- public RedisCacheTest(RedisTemplate template, ObjectFactory keyFactory, ObjectFactory valueFactory,
- boolean allowCacheNullValues) {
+ RedisCache cache;
- super(allowCacheNullValues);
+ public LegacyRedisCacheTests(RedisTemplate template, ObjectFactory keyFactory,
+ ObjectFactory valueFactory, boolean allowCacheNullValues) {
+ this.connectionFactory = template.getConnectionFactory();
this.keyFactory = keyFactory;
this.valueFactory = valueFactory;
- this.template = template;
- ConnectionFactoryTracker.add(template.getConnectionFactory());
+ this.allowCacheNullValues = allowCacheNullValues;
+ ConnectionFactoryTracker.add(connectionFactory);
+
+ cache = createCache();
}
@Parameters
@@ -103,35 +104,23 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
return target;
}
- @SuppressWarnings("unchecked")
- protected RedisCache createCache(RedisTemplate nativeCache, boolean allowCacheNullValues) {
-
- return new RedisCache(CACHE_NAME, CACHE_NAME.concat(":").getBytes(), nativeCache, TimeUnit.MINUTES.toSeconds(10),
- allowCacheNullValues);
- }
-
- protected RedisTemplate createNativeCache() throws Exception {
- return template;
- }
-
- @Before
- public void setUp() throws Exception {
-
- if (!(template.getValueSerializer() instanceof JdkSerializationRedisSerializer
- || template.getValueSerializer() instanceof GenericJackson2JsonRedisSerializer
- || template.getValueSerializer() == null) && getAllowCacheNullValues()) {
- throw new AssumptionViolatedException(
- "Null values can only be cachend with the Jdk or GenericJackson2 serialization");
- }
- ConnectionFactoryTracker.add(template.getConnectionFactory());
- super.setUp();
- }
-
@AfterClass
public static void cleanUp() {
ConnectionFactoryTracker.cleanUp();
}
+ @SuppressWarnings("unchecked")
+ private RedisCache createCache() {
+
+ RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofSeconds(10));
+ if (!allowCacheNullValues) {
+ cacheConfiguration = cacheConfiguration.disableCachingNullValues();
+ }
+
+ return new RedisCache(CACHE_NAME, new DefaultRedisCacheWriter(connectionFactory), cacheConfiguration);
+ }
+
protected Object getValue() {
return valueFactory.instance();
}
@@ -140,8 +129,40 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
return keyFactory.instance();
}
+ @Test
+ public void testCachePut() throws Exception {
+ Object key = getKey();
+ Object value = getValue();
+
+ assertNotNull(value);
+ assertNull(cache.get(key));
+ cache.put(key, value);
+ ValueWrapper valueWrapper = cache.get(key);
+ if (valueWrapper != null) {
+ assertThat(valueWrapper.get(), isEqual(value));
+ }
+ }
+
+ @Test
+ public void testCacheClear() throws Exception {
+ Object key1 = getKey();
+ Object value1 = getValue();
+
+ Object key2 = getKey();
+ Object value2 = getValue();
+
+ assertNull(cache.get(key1));
+ cache.put(key1, value1);
+ assertNull(cache.get(key2));
+ cache.put(key2, value2);
+ cache.clear();
+ assertNull(cache.get(key2));
+ assertNull(cache.get(key1));
+ }
+
@Test
public void testConcurrentRead() throws Exception {
+
final Object key1 = getKey();
final Object value1 = getValue();
@@ -187,20 +208,9 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
assertThat(valueWrapper.get(), isEqual(v1));
}
- @Test
- public void testCacheName() throws Exception {
-
- RedisCacheManager redisCM = new RedisCacheManager(template);
- redisCM.afterPropertiesSet();
-
- String cacheName = "s2gx11";
- Cache cache = redisCM.getCache(cacheName);
- assertNotNull(cache);
- assertTrue(redisCM.getCacheNames().contains(cacheName));
- }
-
@Test
public void testGetWhileClear() throws InterruptedException {
+
final Object key1 = getKey();
final Object value1 = getValue();
int numTries = 10;
@@ -232,69 +242,56 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@Test // DATAREDIS-243
public void testCacheGetShouldReturnCachedInstance() {
- assumeThat(cache, instanceOf(RedisCache.class));
Object key = getKey();
Object value = getValue();
cache.put(key, value);
- assertThat(value, isEqual(((RedisCache) cache).get(key, Object.class)));
+ assertThat(value, isEqual(cache.get(key, Object.class)));
}
@Test // DATAREDIS-243
public void testCacheGetShouldRetunInstanceOfCorrectType() {
- assumeThat(cache, instanceOf(RedisCache.class));
Object key = getKey();
Object value = getValue();
cache.put(key, value);
- RedisCache redisCache = (RedisCache) cache;
- assertThat(redisCache.get(key, value.getClass()), IsInstanceOf.instanceOf(value.getClass()));
+ assertThat(cache.get(key, value.getClass()), IsInstanceOf. instanceOf(value.getClass()));
}
- @Test(expected = ClassCastException.class) // DATAREDIS-243
+ @Test(expected = IllegalStateException.class) // DATAREDIS-243
public void testCacheGetShouldThrowExceptionOnInvalidType() {
- assumeThat(cache, instanceOf(RedisCache.class));
Object key = getKey();
Object value = getValue();
cache.put(key, value);
- RedisCache redisCache = (RedisCache) cache;
@SuppressWarnings("unused")
- Cache retrievedObject = redisCache.get(key, Cache.class);
+ Cache retrievedObject = cache.get(key, Cache.class);
}
@Test // DATAREDIS-243
public void testCacheGetShouldReturnNullIfNoCachedValueFound() {
- assumeThat(cache, instanceOf(RedisCache.class));
Object key = getKey();
Object value = getValue();
cache.put(key, value);
- RedisCache redisCache = (RedisCache) cache;
-
- Object invalidKey = template.getKeySerializer() == null ? "spring-data-redis".getBytes() : "spring-data-redis";
- assertThat(redisCache.get(invalidKey, value.getClass()), nullValue());
+ Object invalidKey = "spring-data-redis".getBytes();
+ assertThat(cache.get(invalidKey, value.getClass()), nullValue());
}
@Test // DATAREDIS-344, DATAREDIS-416
public void putIfAbsentShouldSetValueOnlyIfNotPresent() {
- assumeThat(cache, instanceOf(RedisCache.class));
-
- RedisCache redisCache = (RedisCache) cache;
-
Object key = getKey();
- template.delete(key);
Object value = getValue();
- assertThat(redisCache.putIfAbsent(key, value), nullValue());
+ assertThat(cache.putIfAbsent(key, value), nullValue());
- ValueWrapper wrapper = redisCache.putIfAbsent(key, value);
+ ValueWrapper wrapper = cache.putIfAbsent(key, value);
if (!(value instanceof Number)) {
assertThat(wrapper.get(), not(sameInstance(value)));
@@ -306,7 +303,7 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@Test(expected = IllegalArgumentException.class) // DATAREDIS-510, DATAREDIS-606
public void cachePutWithNullShouldNotAddStuffToRedis() {
- assumeThat(getAllowCacheNullValues(), is(false));
+ assumeThat("Only suitable when cache does NOT allow null values.", allowCacheNullValues, is(false));
Object key = getKey();
@@ -316,7 +313,7 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@Test // DATAREDIS-510, DATAREDIS-606
public void cachePutWithNullShouldErrorAndLeaveExistingKeyUntouched() {
- assumeThat(getAllowCacheNullValues(), is(false));
+ assumeThat("Only suitable when cache does NOT allow null values.", allowCacheNullValues, is(false));
Object key = getKey();
Object value = getValue();
@@ -336,17 +333,13 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@Test // DATAREDIS-443, DATAREDIS-452
public void testCacheGetSynchronized() throws Throwable {
-
- assumeThat(cache, instanceOf(RedisCache.class));
- assumeThat(valueFactory, instanceOf(StringObjectFactory.class));
-
- runOnce(new CacheGetWithValueLoaderIsThreadSafe((RedisCache) cache));
+ runOnce(new CacheGetWithValueLoaderIsThreadSafe(cache));
}
@Test // DATAREDIS-553
public void cachePutWithNullShouldAddStuffToRedisWhenCachingNullIsEnabled() {
- assumeThat(getAllowCacheNullValues(), is(true));
+ assumeThat("Only suitable when cache does allow null values.", allowCacheNullValues, is(true));
Object key = getKey();
Object value = getValue();
@@ -359,8 +352,7 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@Test // DATAREDIS-553
public void testCacheGetSynchronizedNullAllowingNull() {
- assumeThat(getAllowCacheNullValues(), is(true));
- assumeThat(cache, instanceOf(RedisCache.class));
+ assumeThat("Only suitable when cache does allow null values.", allowCacheNullValues, is(true));
Object key = getKey();
Object value = cache.get(key, new Callable() {
@@ -374,12 +366,10 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
assertThat(cache.get(key).get(), is(nullValue()));
}
- @Test(expected = ValueRetrievalException.class) // DATAREDIS-553, DATAREDIS-606
+ @Test(expected = IllegalArgumentException.class) // DATAREDIS-553, DATAREDIS-606
public void testCacheGetSynchronizedNullNotAllowingNull() {
- assumeThat(getAllowCacheNullValues(), is(false));
- assumeThat(cache, instanceOf(RedisCache.class));
- assumeThat(template.getValueSerializer(), not(instanceOf(StringRedisSerializer.class)));
+ assumeThat("Only suitable when cache does NOT allow null values.", allowCacheNullValues, is(false));
Object key = getKey();
Object value = cache.get(key, new Callable() {
@@ -390,11 +380,22 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
});
}
+ @Test(expected = ValueRetrievalException.class)
+ public void testCacheGetSynchronizedThrowsExceptionInValueLoader() {
+
+ Object key = getKey();
+ Object value = cache.get(key, new Callable() {
+ @Override
+ public Object call() throws Exception {
+ throw new RuntimeException("doh!");
+ }
+ });
+ }
+
@Test // DATAREDIS-553
public void testCacheGetSynchronizedNullWithStoredNull() {
- assumeThat(getAllowCacheNullValues(), is(true));
- assumeThat(cache, instanceOf(RedisCache.class));
+ assumeThat("Only suitable when cache does allow null values.", allowCacheNullValues, is(true));
Object key = getKey();
cache.put(key, null);
@@ -412,10 +413,10 @@ public class RedisCacheTest extends AbstractNativeCacheTest {
@SuppressWarnings("unused")
private static class CacheGetWithValueLoaderIsThreadSafe extends MultithreadedTestCase {
- RedisCache redisCache;
+ Cache redisCache;
TestCacheLoader cacheLoader;
- public CacheGetWithValueLoaderIsThreadSafe(RedisCache redisCache) {
+ public CacheGetWithValueLoaderIsThreadSafe(Cache redisCache) {
this.redisCache = redisCache;
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerTransactionalUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerTransactionalUnitTests.java
deleted file mode 100644
index 7980e39a0..000000000
--- a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerTransactionalUnitTests.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2015-2017 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.junit.Assert.*;
-import static org.mockito.Mockito.*;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Arrays;
-
-import javax.sql.DataSource;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.springframework.beans.DirectFieldAccessor;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.cache.Cache;
-import org.springframework.cache.CacheManager;
-import org.springframework.cache.annotation.EnableCaching;
-import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-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.test.util.RelaxedJUnit4ClassRunner;
-import org.springframework.jdbc.datasource.DataSourceTransactionManager;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * @author Thomas Darimont
- */
-@RunWith(RelaxedJUnit4ClassRunner.class)
-@ContextConfiguration
-@Transactional(transactionManager = "transactionManager")
-public class RedisCacheManagerTransactionalUnitTests {
-
- @Autowired protected CacheManager cacheManager;
-
- private final static String cacheName = "cache-name";
-
- @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);
- cacheManager.setCacheNames(Arrays.asList(cacheName));
- 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;
- }
- }
-
- @Test // DATAREDIS-375
- public void testCacheIsNotDecoratedTwiceWithTransactionAwareCacheDecorator() {
-
- Cache cache = cacheManager.getCache(cacheName);
-
- // the cache must be decorated with the TransactionAwareCacheDecorator
- assertTrue(cache instanceof TransactionAwareCacheDecorator);
- TransactionAwareCacheDecorator transactionalCache = (TransactionAwareCacheDecorator) cache;
-
- // get the target cache via reflection
- Object targetCache = new DirectFieldAccessor(transactionalCache).getPropertyValue("targetCache");
-
- // the target cache must be an instance of RedisCache and not TransactionAwareCacheDecorator
- assertTrue(targetCache instanceof RedisCache);
- }
-}
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java
index 34933436d..4f8fdfc27 100644
--- a/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheManagerUnitTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2017 the original author or authors.
+ * Copyright 2017 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.
@@ -15,181 +15,66 @@
*/
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.Mockito.*;
+import static org.assertj.core.api.Assertions.*;
-import java.util.Arrays;
import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-import org.hamcrest.core.IsCollectionContaining;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.cache.Cache;
-import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
+import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
-import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.test.util.ReflectionTestUtils;
/**
* @author Christoph Strobl
- * @author Thomas Darimont
*/
@RunWith(MockitoJUnitRunner.class)
public class RedisCacheManagerUnitTests {
- private @Mock RedisConnection redisConnectionMock;
- private @Mock RedisConnectionFactory redisConnectionFactoryMock;
+ @Mock RedisCacheWriter cacheWriter;
- @SuppressWarnings("rawtypes")//
- private RedisTemplate redisTemplate;
- private RedisCacheManager cacheManager;
+ @Test // DATAREDIS-481
+ public void missingCacheShouldBeCreatedWithDefaultConfiguration() {
- @SuppressWarnings("rawtypes")
- @Before
- public void setUp() {
+ RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
- when(redisConnectionFactoryMock.getConnection()).thenReturn(redisConnectionMock);
-
- redisTemplate = new RedisTemplate();
- redisTemplate.setConnectionFactory(redisConnectionFactoryMock);
- redisTemplate.afterPropertiesSet();
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.afterPropertiesSet();
+ RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter).withCacheDefaults(configuration).createAndGet();
+ assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isEqualTo(configuration);
}
- @Test // DATAREDIS-246
- public void testGetCacheReturnsNewCacheWhenRequestedCacheIsNotAvailable() {
+ @Test // DATAREDIS-481
+ public void predefinedCacheShouldBeCreatedWithSpecificConfig() {
- Cache cache = cacheManager.getCache("not-available");
- assertThat(cache, notNullValue());
+ RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().disableKeyPrefix();
+
+ RedisCacheManager cm = RedisCacheManager.usingCacheWriter(cacheWriter)
+ .withInitialCacheConfigurations(Collections.singletonMap("predefined-cache", configuration)).createAndGet();
+
+ assertThat(((RedisCache) cm.getCache("predefined-cache")).getCacheConfiguration()).isEqualTo(configuration);
+ assertThat(cm.getMissingCache("new-cache").getCacheConfiguration()).isNotEqualTo(configuration);
}
- @Test // DATAREDIS-246
- public void testGetCacheReturnsExistingCacheWhenRequested() {
+ @Test // DATAREDIS-481
+ public void transactionAwareCacheManagerShouldDecoracteCache() {
- Cache cache = cacheManager.getCache("cache");
- assertThat(cacheManager.getCache("cache"), sameInstance(cache));
+ Cache cache = RedisCacheManager.usingCacheWriter(cacheWriter).transactionAware().createAndGet()
+ .getCache("decoracted-cache");
+
+ assertThat(cache).isInstanceOfAny(TransactionAwareCacheDecorator.class);
+ assertThat(ReflectionTestUtils.getField(cache, "targetCache")).isInstanceOf(RedisCache.class);
}
- @Test // DATAREDIS-246
- public void testCacheInitSouldNotRequestRemoteKeysByDefault() {
- Mockito.verifyZeroInteractions(redisConnectionMock);
+ @Bean
+ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ return RedisCacheManager.usingRawConnectionFactory(connectionFactory)
+ .withCacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
+ .withInitialCacheConfigurations(Collections.singletonMap("predefined", RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()))
+ .transactionAware()
+ .createAndGet();
}
- @Test // DATAREDIS-246
- public void testCacheInitShouldFetchAllCacheKeysWhenLoadingRemoteCachesOnStartupIsEnabled() {
-
- cacheManager = new RedisCacheManager(redisTemplate);
- 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"));
- }
-
- @SuppressWarnings("unchecked")
- @Test // DATAREDIS-246
- public void testCacheInitShouldInitializeRemoteCachesCorrectlyWhenLoadingRemoteCachesOnStartupIsEnabled() {
-
- Set keys = new HashSet(Arrays.asList(redisTemplate.getKeySerializer()
- .serialize("remote-cache~keys")));
- when(redisConnectionMock.keys(any(byte[].class))).thenReturn(keys);
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setLoadRemoteCachesOnStartup(true);
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCacheNames(), IsCollectionContaining.hasItem("remote-cache"));
- }
-
- @Test // DATAREDIS-246
- public void testCacheInitShouldNotInitialzeCachesWhenLoadingRemoteCachesOnStartupIsEnabledAndNoCachesAvailableOnRemoteServer() {
-
- when(redisConnectionMock.keys(any(byte[].class))).thenReturn(Collections. emptySet());
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setLoadRemoteCachesOnStartup(true);
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCacheNames().isEmpty(), is(true));
- }
-
- /**
- * see DATAREDIS-246
- */
- @Test
- public void testCacheManagerShouldNotDynamicallyCreateCachesWhenInStaticMode() {
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setCacheNames(Arrays.asList("spring", "data"));
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCache("redis"), nullValue());
- }
-
- /**
- * see DATAREDIS-246
- */
- @Test
- public void testCacheManagerShouldRetrunRegisteredCacheWhenInStaticMode() {
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setCacheNames(Arrays.asList("spring", "data"));
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCache("spring"), notNullValue());
- }
-
- /**
- * see DATAREDIS-246
- */
- @Test
- public void testPuttingCacheManagerIntoStaticModeShouldNotRemoveAlreadyRegisteredCaches() {
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.getCache("redis");
- cacheManager.setCacheNames(Arrays.asList("spring", "data"));
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCache("redis"), notNullValue());
- }
-
- @Test // DATAREDIS-283
- public void testRetainConfiguredCachesAfterBeanInitialization() {
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setCacheNames(Arrays.asList("spring", "data"));
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCache("spring"), notNullValue());
- assertThat(cacheManager.getCache("data"), notNullValue());
- }
-
- @Test // DATAREDIS-283
- public void testRetainConfiguredCachesAfterBeanInitializationWithLoadingOfRemoteKeys() {
-
- Set keys = new HashSet(Arrays.asList(redisTemplate.getKeySerializer()
- .serialize("remote-cache~keys")));
- when(redisConnectionMock.keys(any(byte[].class))).thenReturn(keys);
-
- cacheManager = new RedisCacheManager(redisTemplate);
- cacheManager.setCacheNames(Arrays.asList("spring", "data"));
- cacheManager.setLoadRemoteCachesOnStartup(true);
- cacheManager.afterPropertiesSet();
-
- assertThat(cacheManager.getCache("spring"), notNullValue());
- assertThat(cacheManager.getCache("data"), notNullValue());
- assertThat(cacheManager.getCacheNames(), IsCollectionContaining.hasItem("remote-cache"));
- }
}
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java
new file mode 100644
index 000000000..dadda93b7
--- /dev/null
+++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTests.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Date;
+import java.util.function.Consumer;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.springframework.cache.Cache.ValueWrapper;
+import org.springframework.cache.support.NullValue;
+import org.springframework.data.redis.ConnectionFactoryTracker;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.test.util.ReflectionTestUtils;
+
+/**
+ * Tests for {@link RedisCache} with {@link DefaultRedisCacheWriter} using different {@link RedisSerializer} and
+ * {@link RedisConnectionFactory} pairs.
+ *
+ * @author Christoph Strobl
+ */
+@RunWith(Parameterized.class)
+public class RedisCacheTests {
+
+ String key = "key-1";
+ String cacheKey = "cache::" + key;
+ byte[] binaryCacheKey = cacheKey.getBytes(Charset.forName("UTF-8"));
+
+ Person sample = new Person("calmity", new Date());
+ byte[] binarySample;
+
+ NullValue nullValue = NullValue.class.cast(ReflectionTestUtils.getField(NullValue.class, "INSTANCE"));
+ byte[] binaryNullValue = new JdkSerializationRedisSerializer().serialize(nullValue);
+
+ RedisConnectionFactory connectionFactory;
+ RedisSerializer serializer;
+ RedisCache cache;
+
+ public RedisCacheTests(RedisConnectionFactory connectionFactory, RedisSerializer serializer) {
+
+ this.connectionFactory = connectionFactory;
+ this.serializer = serializer;
+ this.binarySample = serializer.serialize(sample);
+
+ ConnectionFactoryTracker.add(connectionFactory);
+ }
+
+ @Parameters(name = "{index}: {0} & {1}")
+ public static Collection testParams() {
+ return CacheTestParams.connectionFactoriesAndSerializers();
+ }
+
+ @AfterClass
+ public static void cleanUpResources() {
+ ConnectionFactoryTracker.cleanUp();
+ }
+
+ @Before
+ public void setUp() {
+
+ doWithConnection(RedisConnection::flushAll);
+
+ cache = new RedisCache("cache", new DefaultRedisCacheWriter(connectionFactory),
+ RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(SerializationPair.fromSerializer(serializer)));
+ }
+
+ @Test // DATAREDIS-481
+ public void putShouldAddEntry() {
+
+ cache.put("key-1", sample);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putNullShouldAddEntryForNullValue() {
+
+ cache.put("key-1", null);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldAddEntryIfNotExists() {
+
+ cache.putIfAbsent("key-1", sample);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binarySample);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentWithNullShouldAddNullValueEntryIfNotExists() {
+
+ assertThat(cache.putIfAbsent("key-1", null)).isNull();
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldReturnExistingIfExists() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binarySample));
+
+ ValueWrapper result = cache.putIfAbsent("key-1", "this-should-not-be-set");
+
+ assertThat(result).isNotNull();
+ assertThat(result.get()).isEqualTo(sample);
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binarySample);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void putIfAbsentShouldReturnExistingNullValueIfExists() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binaryNullValue));
+
+ ValueWrapper result = cache.putIfAbsent("key-1", "this-should-not-be-set");
+
+ assertThat(result).isNotNull();
+ assertThat(result.get()).isNull();
+
+ doWithConnection(connection -> {
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void getShouldRetrieveEntry() {
+
+ doWithConnection(connection -> {
+ connection.set(binaryCacheKey, binarySample);
+ });
+
+ ValueWrapper result = cache.get(key);
+ assertThat(result).isNotNull();
+ assertThat(result.get()).isEqualTo(sample);
+ }
+
+ @Test // DATAREDIS-481
+ public void getShouldReturnNullWhenKeyDoesNotExist() {
+ assertThat(cache.get(key)).isNull();
+ }
+
+ @Test // DATAREDIS-481
+ public void getShouldReturnValueWrapperHoldingNullIfNullValueStored() {
+
+ doWithConnection(connection -> {
+ connection.set(binaryCacheKey, binaryNullValue);
+ });
+
+ ValueWrapper result = cache.get(key);
+ assertThat(result).isNotNull();
+ assertThat(result.get()).isEqualTo(null);
+ }
+
+ @Test // DATAREDIS-481
+ public void evictShouldRemoveKey() {
+
+ doWithConnection(connection -> {
+ connection.set(binaryCacheKey, binaryNullValue);
+ });
+
+ cache.evict(key);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isFalse();
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void getWithCallableShouldResolveValueIfNotPresent() {
+
+ assertThat(cache.get(key, () -> sample)).isEqualTo(sample);
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binarySample);
+ });
+ }
+
+ @Test // DATAREDIS-481
+ public void getWithCallableShouldNotResolveValueIfPresent() {
+
+ doWithConnection(connection -> connection.set(binaryCacheKey, binaryNullValue));
+
+ cache.get(key, () -> {
+ throw new IllegalStateException("Why call the value loader when we've got a cache entry?");
+ });
+
+ doWithConnection(connection -> {
+ assertThat(connection.exists(binaryCacheKey)).isTrue();
+ assertThat(connection.get(binaryCacheKey)).isEqualTo(binaryNullValue);
+ });
+ }
+
+ void doWithConnection(Consumer callback) {
+ RedisConnection connection = connectionFactory.getConnection();
+ try {
+ callback.accept(connection);
+ } finally {
+ connection.close();
+ }
+ }
+
+ @Data
+ @NoArgsConstructor
+ @AllArgsConstructor
+ static class Person implements Serializable {
+ String firstame;
+ Date birthdate;
+ }
+
+}
diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java
deleted file mode 100644
index 7e296abaa..000000000
--- a/src/test/java/org/springframework/data/redis/cache/RedisCacheUnitTests.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Copyright 2015-2017 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.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
-import static org.hamcrest.core.IsEqual.*;
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
-import static org.springframework.util.ClassUtils.*;
-
-import java.util.concurrent.Callable;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.springframework.cache.Cache;
-import org.springframework.cache.Cache.ValueRetrievalException;
-import org.springframework.cache.support.NullValue;
-import org.springframework.data.redis.RedisSystemException;
-import org.springframework.data.redis.connection.RedisClusterConnection;
-import org.springframework.data.redis.connection.RedisConnection;
-import org.springframework.data.redis.connection.RedisConnectionFactory;
-import org.springframework.data.redis.connection.ReturnType;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.serializer.RedisSerializer;
-
-/**
- * @author Christoph Strobl
- * @author Mark Paluch
- */
-@SuppressWarnings("rawtypes")
-@RunWith(MockitoJUnitRunner.Silent.class)
-public class RedisCacheUnitTests {
-
- private static final String CACHE_NAME = "foo";
- private static final String PREFIX = "prefix:";
- private static final byte[] PREFIX_BYTES = "prefix:".getBytes();
- private static final byte[] KNOWN_KEYS_SET_NAME_BYTES = (CACHE_NAME + "~keys").getBytes();
-
- private static final String KEY = "key";
- private static final byte[] KEY_BYTES = KEY.getBytes();
- private static final byte[] KEY_WITH_PREFIX_BYTES = (PREFIX + KEY).getBytes();
-
- private static final String VALUE = "value";
- private static final byte[] VALUE_BYTES = VALUE.getBytes();
-
- private static final byte[] NO_PREFIX_BYTES = new byte[] {};
- private static final long EXPIRATION = 1000;
-
- RedisTemplate, ?> templateSpy;
- @Mock RedisSerializer keySerializerMock;
- @Mock RedisSerializer valueSerializerMock;
- @Mock RedisConnectionFactory connectionFactoryMock;
- @Mock RedisConnection connectionMock;
-
- RedisCache cache;
-
- public @Rule ExpectedException exception = ExpectedException.none();
-
- @SuppressWarnings("unchecked")
- @Before
- public void setUp() {
-
- RedisTemplate template = new RedisTemplate();
- template.setConnectionFactory(connectionFactoryMock);
- template.setKeySerializer(keySerializerMock);
- template.setValueSerializer(valueSerializerMock);
- template.afterPropertiesSet();
-
- templateSpy = spy(template);
-
- when(connectionFactoryMock.getConnection()).thenReturn(connectionMock);
-
- when(keySerializerMock.serialize(any())).thenReturn(KEY_BYTES);
- when(valueSerializerMock.serialize(any())).thenReturn(VALUE_BYTES);
- when(valueSerializerMock.deserialize(eq(VALUE_BYTES))).thenReturn(VALUE);
- }
-
- @Test // DATAREDIS-369
- public void putShouldNotKeepTrackOfKnownKeysWhenPrefixIsSet() {
-
- cache = new RedisCache(CACHE_NAME, PREFIX_BYTES, templateSpy, EXPIRATION);
- cache.put(KEY, VALUE);
-
- verify(connectionMock).set(eq(KEY_WITH_PREFIX_BYTES), eq(VALUE_BYTES));
- verify(connectionMock).expire(eq(KEY_WITH_PREFIX_BYTES), eq(EXPIRATION));
- verify(connectionMock, never()).zAdd(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0D), any(byte[].class));
- }
-
- @Test // DATAREDIS-369
- public void putShouldKeepTrackOfKnownKeysWhenNoPrefixIsSet() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, EXPIRATION);
- cache.put(KEY, VALUE);
-
- verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
- verify(connectionMock).expire(eq(KEY_BYTES), eq(EXPIRATION));
- verify(connectionMock).zAdd(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0D), eq(KEY_BYTES));
- }
-
- @Test // DATAREDIS-369
- public void clearShouldRemoveKeysUsingKnownKeysWhenNoPrefixIsSet() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, EXPIRATION);
- cache.clear();
-
- verify(connectionMock).zRange(eq(KNOWN_KEYS_SET_NAME_BYTES), eq(0L), eq(127L));
- }
-
- @Test // DATAREDIS-369
- public void clearShouldCallLuaScriptToRemoveKeysWhenPrefixIsSet() {
-
- cache = new RedisCache(CACHE_NAME, PREFIX_BYTES, templateSpy, EXPIRATION);
- cache.clear();
-
- verify(connectionMock).eval(any(byte[].class), eq(ReturnType.INTEGER), eq(0),
- eq((PREFIX + "*").getBytes()));
- }
-
- @Test // DATAREDIS-402
- public void putShouldNotExpireKnownKeysSetWhenTtlIsZero() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
- cache.put(KEY, VALUE);
-
- verify(connectionMock, never()).expire(eq(KNOWN_KEYS_SET_NAME_BYTES), anyLong());
- }
-
- @Test // DATAREDIS-542
- public void putIfAbsentShouldExpireWhenValueWasSet() {
-
- when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(true);
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
- Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
-
- assertThat(valueWrapper, is(nullValue()));
- verify(connectionMock).setNX(KEY_BYTES, VALUE_BYTES);
- verify(connectionMock).expire(eq(KEY_BYTES), anyLong());
- }
-
- @Test // DATAREDIS-542
- public void putIfAbsentShouldNotExpireWhenValueWasNotSetAndRedisContainsOtherData() {
-
- String other = "other";
- when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(false);
- when(connectionMock.get(KEY_BYTES)).thenReturn(other.getBytes());
- when(valueSerializerMock.deserialize(eq(other.getBytes()))).thenReturn(other);
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
- Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
-
- assertThat(valueWrapper, is(notNullValue()));
- verify(connectionMock, never()).expire(eq(KEY_BYTES), anyLong());
- }
-
- @Test // DATAREDIS-542
- public void putIfAbsentShouldNotSetExpireWhenValueWasNotSetAndRedisContainsSameData() {
-
- when(connectionMock.setNX(KEY_BYTES, VALUE_BYTES)).thenReturn(false);
- when(connectionMock.get(KEY_BYTES)).thenReturn(VALUE_BYTES);
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 10L);
- Cache.ValueWrapper valueWrapper = cache.putIfAbsent(KEY, VALUE);
-
- assertThat(valueWrapper, is(notNullValue()));
- verify(connectionMock, never()).expire(eq(KEY_BYTES), anyLong());
- }
-
- @Test // DATAREDIS-443
- @SuppressWarnings("unchecked")
- public void getWithCallable() throws ClassNotFoundException {
-
- if (isPresent("org.springframework.cache.Cache$ValueRetrievalException", getDefaultClassLoader())) {
- exception.expect((Class extends Throwable>) forName("org.springframework.cache.Cache$ValueRetrievalException",
- getDefaultClassLoader()));
- } else {
- exception.expect(RedisSystemException.class);
- }
-
- exception.expectMessage("Value for key 'key' could not be loaded");
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- throw new UnsupportedOperationException("Expected exception");
- }
- });
- }
-
- @Test(expected = ValueRetrievalException.class) // DATAREDIS-553, DATAREDIS-606
- @SuppressWarnings("unchecked")
- public void getWithCallableShouldThrowExceptionSotringNullWhenNotAllowingNull() throws ClassNotFoundException {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L, false);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- return null;
- }
- });
- }
-
- @Test // DATAREDIS-553
- @SuppressWarnings("unchecked")
- public void getWithCallableShouldStoreNullAllowingNull() throws ClassNotFoundException {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L, true);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- return null;
- }
- });
-
- verify(valueSerializerMock).serialize(isA(NullValue.class));
- verify(connectionMock).get(eq(KEY_BYTES));
- verify(connectionMock).multi();
- verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
- verify(connectionMock).exec();
- }
-
- @Test // DATAREDIS-443, DATAREDIS-592
- public void getWithCallableShouldReadValueFromCallableAddToCache() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- return VALUE;
- }
- });
-
- verify(connectionMock).get(eq(KEY_BYTES));
- verify(connectionMock).multi();
- verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
- verify(connectionMock, never()).expire(any(byte[].class), anyLong());
- verify(connectionMock).exec();
- }
-
- @Test // DATAREDIS-592
- public void getWithCallableShouldReadValueFromCallableAddToCacheWithTtl() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 100L);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- return VALUE;
- }
- });
-
- verify(connectionMock).get(eq(KEY_BYTES));
- verify(connectionMock).multi();
- verify(connectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
- verify(connectionMock).expire(eq(KEY_BYTES), eq(100L));
- verify(connectionMock).exec();
- }
-
- @Test // DATAREDIS-443
- @SuppressWarnings("unchecked")
- public void getWithCallableShouldNotReadValueFromCallableWhenAlreadyPresent() {
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
- Callable callableMock = mock(Callable.class);
-
- when(connectionMock.exists(KEY_BYTES)).thenReturn(true);
- when(connectionMock.get(KEY_BYTES)).thenReturn(VALUE_BYTES);
-
- assertThat((String) cache.get(KEY, callableMock), equalTo(VALUE));
- verifyZeroInteractions(callableMock);
- }
-
- @Test // DATAREDIS-468
- public void noMultiExecForCluster() {
-
- RedisClusterConnection clusterConnectionMock = mock(RedisClusterConnection.class);
- when(connectionFactoryMock.getConnection()).thenReturn(clusterConnectionMock);
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
-
- when(connectionMock.exists(KEY_BYTES)).thenReturn(true);
- when(connectionMock.get(KEY_BYTES)).thenReturn(null).thenReturn(VALUE_BYTES);
-
- cache.put(KEY, VALUE);
-
- verify(clusterConnectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
- verify(clusterConnectionMock, never()).multi();
- verify(clusterConnectionMock, never()).exec();
- verifyZeroInteractions(connectionMock);
- }
-
- @Test // DATAREDIS-468
- public void getWithCallableForCluster() {
-
- RedisClusterConnection clusterConnectionMock = mock(RedisClusterConnection.class);
- when(connectionFactoryMock.getConnection()).thenReturn(clusterConnectionMock);
-
- cache = new RedisCache(CACHE_NAME, NO_PREFIX_BYTES, templateSpy, 0L);
-
- cache.get(KEY, new Callable() {
- @Override
- public Object call() throws Exception {
- return VALUE;
- }
- });
-
- verify(clusterConnectionMock).get(eq(KEY_BYTES));
- verify(clusterConnectionMock).set(eq(KEY_BYTES), eq(VALUE_BYTES));
-
- verify(clusterConnectionMock, never()).multi();
- verify(clusterConnectionMock, never()).exec();
- verifyZeroInteractions(connectionMock);
- }
-
-}
diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java
deleted file mode 100644
index 508f03007..000000000
--- a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerTestBase.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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
deleted file mode 100644
index f781a64ff..000000000
--- a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2014-2017 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.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.data.redis.test.util.RelaxedJUnit4ClassRunner;
-import org.springframework.jdbc.datasource.DataSourceTransactionManager;
-import org.springframework.test.annotation.Rollback;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.transaction.AfterTransaction;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * @author Christoph Strobl
- */
-@RunWith(RelaxedJUnit4ClassRunner.class)
-@ContextConfiguration
-@Transactional(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"));
- }
-
- @Rollback(false)
- @Test // DATAREDIS-246
- 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
deleted file mode 100644
index 2dd628e38..000000000
--- a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2014-2017 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.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.data.redis.test.util.RelaxedJUnit4ClassRunner;
-import org.springframework.jdbc.datasource.DataSourceTransactionManager;
-import org.springframework.test.annotation.Rollback;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.transaction.AfterTransaction;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.annotation.Transactional;
-
-/**
- * @author Christoph Strobl
- */
-@RunWith(RelaxedJUnit4ClassRunner.class)
-@ContextConfiguration
-@Transactional(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));
- }
-
- @Rollback(true)
- @Test // DATAREDIS-246
- public void tesValuesNotAddedToCacheWhenTransactionIsRolledBack() {
- transactionalService.foo();
- }
-
-}