SpringBoot之@Cacheable注解改造实现自定义缓存过期时间配置

SpringBoot之redis缓存的基本配置及使用一文,我们可以使用@Cacheable注解很方便的使用redis缓存,但是对于缓存的过期时间配置,需要针对cacheNames单独额外配置,有点麻烦。于是我就想有没有什么办法可以加以改造使之更加方便?

考虑到同一模块的业务一般使用同一个cacheName,且同一个cacheName的过期时间一般不变。那么能不能在cacheName上进行一些改造呢?于是我分析了下源码,从RedisCacheManager这个类开始着手,通过源码我们发现,在RedisCacheManager中有这么一个方法:

protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
    return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}

通过名字以及返回值我们可以猜测,我们所使用的的缓存都是一个个由该方法创建出来的RedisCache对象管理。通过对该方法进行Find Usages分析,我们发现有2个地方使用到这个方法:

/*
    * (non-Javadoc)
    * @see org.springframework.cache.support.AbstractCacheManager#loadCaches()
    */
@Override
protected Collection<RedisCache> loadCaches() {

    List<RedisCache> caches = new LinkedList<>();

    for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
        caches.add(createRedisCache(entry.getKey(), entry.getValue()));
    }

    return caches;
}

/*
    * (non-Javadoc)
    * @see org.springframework.cache.support.AbstractCacheManager#getMissingCache(java.lang.String)
    */
@Override
protected RedisCache getMissingCache(String name) {
    return allowInFlightCacheCreation ? createRedisCache(name, defaultCacheConfig) : null;
}

其中loadCaches()猜测是如果有初始化配置时,用来初始化已经配置的缓存;getMissingCache()猜测是在使用过程中,如果根据name获取不到缓存对象则进行创建(allowInFlightCacheCreation需要为true才有效,该值默认是true)。我们在AbstractCacheManager这个抽象类中的这两个方法可以证实我们的猜测没错:

public void initializeCaches() {
    Collection<? extends Cache> caches = this.loadCaches();
    synchronized(this.cacheMap) {
        this.cacheNames = Collections.emptySet();
        this.cacheMap.clear();
        Set<String> cacheNames = new LinkedHashSet(caches.size());
        Iterator var4 = caches.iterator();

        while(var4.hasNext()) {
            Cache cache = (Cache)var4.next();
            String name = cache.getName();
            this.cacheMap.put(name, this.decorateCache(cache));
            cacheNames.add(name);
        }

        this.cacheNames = Collections.unmodifiableSet(cacheNames);
    }
}

@Nullable
public Cache getCache(String name) {
    Cache cache = (Cache)this.cacheMap.get(name);
    if (cache != null) {
        return cache;
    } else {
        synchronized(this.cacheMap) {
            cache = (Cache)this.cacheMap.get(name);
            if (cache == null) {
                cache = this.getMissingCache(name);
                if (cache != null) {
                    cache = this.decorateCache(cache);
                    this.cacheMap.put(name, cache);
                    this.updateCacheNames(name);
                }
            }

            return cache;
        }
    }
}

根据上面分析,createRedisCache()是最终创建缓存对象的地方,而且该方法入参有:缓存名,缓存配置。也刚好符合我们的需要,所以我们在这里进行改造最合适不过。

我们可以创建一个自定义的RedisCacheManager命名为CustomTtlRedisCacheManager并且继承RedisCacheManager,然后在新类中对createRedisCache进行重载改造。具体参见代码:

/**
 * 通过cacheName自定义过期时间的RedisCacheManager
 * 支持直接使用cacheName来定义过期时间
 * cacheName:name#time   time为过期时间,单位秒,0为不过期
 * 例如:test#100  意思是定义一个名为test#100的缓存,且过期时间为100秒
 */
public class CustomTtlRedisCacheManager extends RedisCacheManager {

    public CustomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    public CustomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
    }

    public CustomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, boolean allowInFlightCacheCreation, String... initialCacheNames) {
        super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
    }

    public CustomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
    }

    public CustomTtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
        super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        Duration ttl = getTtlByName(name);
        if (ttl != null) {
            //证明在cacheName上使用了过期时间,需要修改配置中的ttl
            cacheConfig = cacheConfig.entryTtl(ttl);
        }
        //修改缓存key和value值的序列化方式
        cacheConfig = cacheConfig.computePrefixWith(DEFAULT_CACHE_KEY_PREFIX)
                .serializeValuesWith(DEFAULT_PAIR);
        return super.createRedisCache(name, cacheConfig);
    }

    /**
     * 缓存参数的分隔符
     * 数组元素0=缓存的名称
     * 数组元素1=缓存过期时间TTL
     */
    private static final String DEFAULT_SEPARATOR = "#";

    /**
     * 通过name获取过期时间
     * @param name
     * @return
     */
    private Duration getTtlByName(String name) {
        if (name == null) {
            return null;
        }
        //根据分隔符拆分字符串,并进行过期时间ttl的解析
        String[] cacheParams = name.split(DEFAULT_SEPARATOR);
        if (cacheParams.length > 1) {
            String ttl = cacheParams[1];
            if (!StringUtils.isEmpty(ttl)) {
                try {
                    return Duration.ofSeconds(Long.parseLong(ttl));
                } catch (Exception e) {
                }
            }
        }
        return null;
    }

    /**
     * 默认的key前缀
     */
    private static final CacheKeyPrefix DEFAULT_CACHE_KEY_PREFIX = new CacheKeyPrefix() {
        @Override
        public String compute(String cacheName) {
            return cacheName+":";
        }
    };

    /**
     * 默认序列化方式为json
     */
    private static final RedisSerializationContext.SerializationPair<Object> DEFAULT_PAIR = RedisSerializationContext.SerializationPair
            .fromSerializer(new GenericJackson2JsonRedisSerializer());

}

注释写的挺详细,具体逻辑参考代码及注释。因为我们自定义了一个自己的RedisCacheManager类。所以我这里顺便把上文中对缓存key和value序列化的修改也集成了,这样的话我们RedisConfig类中需要修改为使用我们自定义的RedisCacheManager,代码如下:

@Configuration
public class RedisConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //初始化一个RedisCacheConfiguration
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        //返回一个自定义的CacheManager
        return new CustomTtlRedisCacheManager(redisCacheWriter, defaultCacheConfig);
    }

}

到此我们的改造就完成了,现在我们可以轻松的通过cacheName来对缓存的过期时间进行设置。如:

@Cacheable(cacheNames = "testCache#5", key = "'testKey'")

表示缓存名为testCache#5,且缓存在5秒之后自动过期。如果没有带#号,或者#号后面的不是合法的数字,则过期时间不生效,跟原版的RedisCacheManager的配置一样。

代码:https://gitee.com/lqccan/blog-demo/tree/master/SpringBoot/redis-custom-ttl

相关链接:SpringBoot之RedisTemplate操作redis出现\xAC\xED\x00\x05t\x00\x08乱码问题