/*
* Copyright 2011-2016 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
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.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'). <br>
* By default {@link RedisCache}s will be lazily initialized for each {@link #getCache(String)} request unless a set of
* predefined cache names is provided. <br>
* <br>
* Setting {@link #setTransactionAware(boolean)} to {@code true} will force Caches to be decorated as
* {@link TransactionAwareCacheDecorator} so values will only be written to the cache after successful commit of
* surrounding transaction.
*
* @author Costin Leau
* @author Christoph Strobl
* @author Thomas Darimont
*/
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
private final Log logger = LogFactory.getLog(RedisCacheManager.class);
@SuppressWarnings("rawtypes") //
private final RedisOperations redisOperations;
private boolean usePrefix = false;
private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();
private boolean loadRemoteCachesOnStartup = false;
private boolean dynamic = true;
// 0 - never expire
private long defaultExpiration = 0;
private Map<String, Long> expires = null;
private Set<String> configuredCacheNames;
private final boolean cacheNullValues;
/**
* Construct a {@link RedisCacheManager}.
*
* @param redisOperations
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations) {
this(redisOperations, Collections.<String> emptyList());
}
/**
* 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<String> cacheNames) {
this(redisOperations, cacheNames, false);
}
/**
* Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only. <br />
* <br />
* <strong>NOTE</strong> When enabling {@code cacheNullValues} please make sure the {@link RedisSerializer} used by
* {@link RedisOperations} is capable of serializing {@link NullValue}.
*
* @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
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean cacheNullValues) {
this.redisOperations = redisOperations;
this.cacheNullValues = cacheNullValues;
setCacheNames(cacheNames);
}
/**
* Specify the set of cache names for this CacheManager's 'static' mode. <br>
* 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. <br>
* 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<String> cacheNames) {
Set<String> newCacheNames = CollectionUtils.isEmpty(cacheNames) ? Collections.<String> emptySet()
: new HashSet<String>(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<String, Long> expires) {
this.expires = (expires != null ? new ConcurrentHashMap<String, Long>(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<Cache> caches = new LinkedHashSet<Cache>(
loadRemoteCachesOnStartup ? loadAndInitRemoteCaches() : new ArrayList<Cache>());
Set<String> cachesToLoad = new LinkedHashSet<String>(this.configuredCacheNames);
cachesToLoad.addAll(this.getCacheNames());
if (!CollectionUtils.isEmpty(cachesToLoad)) {
for (String cacheName : cachesToLoad) {
caches.add(createCache(cacheName));
}
}
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<Cache> result = new ArrayList<Cache>(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;
}
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
long expiration = computeExpiration(cacheName);
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
cacheNullValues);
}
protected long computeExpiration(String name) {
Long expiration = null;
if (expires != null) {
expiration = expires.get(name);
}
return (expiration != null ? expiration.longValue() : defaultExpiration);
}
protected List<Cache> loadAndInitRemoteCaches() {
List<Cache> caches = new ArrayList<Cache>();
try {
Set<String> 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<String> loadRemoteCacheKeys() {
return (Set<String>) redisOperations.execute(new RedisCallback<Set<String>>() {
@Override
public Set<String> doInRedis(RedisConnection connection) throws DataAccessException {
// we are using the ~keys postfix as defined in RedisCache#setName
Set<byte[]> keys = connection.keys(redisOperations.getKeySerializer().serialize("*~keys"));
Set<String> cacheKeys = new LinkedHashSet<String>();
if (!CollectionUtils.isEmpty(keys)) {
for (byte[] key : keys) {
cacheKeys.add(redisOperations.getKeySerializer().deserialize(key).toString().replace("~keys", ""));
}
}
return cacheKeys;
}
});
}
@SuppressWarnings("rawtypes")
protected RedisOperations getRedisOperations() {
return redisOperations;
}
protected RedisCachePrefix getCachePrefix() {
return cachePrefix;
}
protected boolean isUsePrefix() {
return usePrefix;
}
/* (non-Javadoc)
* @see
org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager#decorateCache(org.springframework.cache.Cache)
*/
@Override
protected Cache decorateCache(Cache cache) {
if (isCacheAlreadyDecorated(cache)) {
return cache;
}
return super.decorateCache(cache);
}
protected boolean isCacheAlreadyDecorated(Cache cache) {
return isTransactionAware() && cache instanceof TransactionAwareCacheDecorator;
}
}