注釈ベースspringキャッシュを拡張し、注釈サポートメソッドレベルのカスタム有効期間-redis編

10841 ワード

ここで使用するspringはredisのパッケージspring-data-redisに対して、主にRedisCacheManagerに対して二次パッケージを作ります.
主な依存パッケージ:
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.7.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.6.0.RELEASE</version>
</dependency>

以下、拡張クラスcom.caiya.cache.redis.ExtendedRedisCacheManager:
package com.caiya.cache.redis;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCachePrefix;
import org.springframework.data.redis.core.RedisOperations;

import java.util.Collection;
import java.util.Collections;

/**
 * CacheManager backed by a Simple Spring Redis (SSR) {@link org.springframework.data.redis.cache.RedisCache}. Spring Cache and
 * CacheManager doesn't support configuring expiration time per method (there is no dedicated parameter in cache
 * annotation to pass expiration time). This extension of {@link RedisCacheManager} overcomes this limitation and allow to
 * pass expiration time as a part of cache name. To define custom expiration on method as a cache name use concatenation
 * of specific cache name, separator and expiration e.g.
 * <p/>
 * <pre>
 * public class UserDAO {
 *
 *     // cache name: userCache, expiration: 300s
 *     &#064;Cacheable(&quot;userCache#300&quot;)
 *     public User getUser(String name) {
 *
 *     }
 * }
 * </pre>
 *
 * @author caiya
 * @since 16/3/18
 */
public class ExtendedRedisCacheManager extends RedisCacheManager {

    private static final Logger logger = Logger.getLogger(ExtendedRedisCacheManager.class);

    private String defaultCacheName;

    private char separator = '#';

    public ExtendedRedisCacheManager(RedisOperations redisOperations) {
        this(redisOperations, Collections.<String>emptyList());
    }

    public ExtendedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        super(redisOperations, cacheNames);
    }

    @Override
    public Cache getCache(String name) {
        // try to get cache by name
        RedisCache cache = (RedisCache) super.getCache(name);
        if (cache != null) {
            return cache;
        }

        // there's no cache which has given name
        // find separator in cache name
        int index = name.lastIndexOf(getSeparator());
        if (index < 0) {
            return null;
        }

        // split name by the separator
        String cacheName = name.substring(0, index);
        if(StringUtils.isBlank(cacheName)){
            cacheName = defaultCacheName;
        }
        cache = (RedisCache) super.getCache(cacheName);
        if (cache == null) {
            return null;
        }

        // get expiration from name
        Integer expiration = getExpiration(name, index);
        if (expiration == null || expiration < 0) {
            logger.warn("Default expiration time will be used for cache '{}' because cannot parse '{}', cacheName : " + cacheName + ", name : " + name);
            return cache;
        }

        return new RedisCache(cacheName, (isUsePrefix() ? getCachePrefix().prefix(cacheName) : null), getRedisOperations(), expiration);
    }


    public char getSeparator() {
        return separator;
    }

    /**
     * Char that separates cache name and expiration time, default: #.
     *
     * @param separator
     */
    public void setSeparator(char separator) {
        this.separator = separator;
    }

    private Integer getExpiration(final String name, final int separatorIndex) {
        Integer expiration = null;
        String expirationAsString = name.substring(separatorIndex + 1);
        try {
            expiration = Integer.parseInt(expirationAsString);
        } catch (NumberFormatException ex) {
            logger.error(String.format("Cannnot separate expiration time from cache: '%s'", name), ex);
        }

        return expiration;
    }

    @Override
    public void setUsePrefix(boolean usePrefix) {
        super.setUsePrefix(usePrefix);
    }

    @Override
    public void setCachePrefix(RedisCachePrefix cachePrefix) {
        super.setCachePrefix(cachePrefix);
    }

    public void setDefaultCacheName(String defaultCacheName) {
        this.defaultCacheName = defaultCacheName;
    }
}

spring-redis.xmlプロファイル:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.0.xsd">

    <!-- Jedis PoolConfig -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.pool.maxTotal}"/>
        <property name="maxIdle" value="${redis.pool.maxIdle}"/>
        <property name="minIdle" value="${redis.pool.minIdle}"/>
        <property name="maxWaitMillis" value="${redis.pool.maxWait}"/>
        <!--    connection  validateObject  -->
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/>
        <!--   returnObject    connection  validateObject  -->
        <property name="testOnReturn" value="${redis.pool.testOnReturn}"/>
        <!--              validateObject  -->
        <property name="testWhileIdle" value="${redis.pool.testWhileIdle}"/>
    </bean>

    <!-- Redis SentinelConfig-->
    <bean id="redisSentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
        <property name="master">
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <property name="name" value="mymaster"/>
            </bean>
        </property>
        <property name="sentinels">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel1.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel1.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel2.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel2.port}"></constructor-arg>
                </bean>
            </set>
        </property>
    </bean>

    <!-- Jedis ConnectionFactory -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <constructor-arg ref="redisSentinelConfiguration"></constructor-arg>
    </bean>

    <!-- Redis Template -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
    </bean>

    <!--        -->
    <cache:annotation-driven/>

    <!--    CacheManager,           -->
    <bean name="cacheManager" class="com.caiya.cache.redis.ExtendedRedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate"/>
        <constructor-arg name="cacheNames">
            <set>
                <value>caiya_a</value>
                <value>caiya_test</value>
            </set>
        </constructor-arg>
        <!--        -->
        <property name="defaultCacheName" value="caiya_a"/>
        <!--             -->
        <property name="loadRemoteCachesOnStartup" value="true"/>
        <!--        -->
        <property name="usePrefix" value="true"/>
        <!--     ,  usePrefix true     -->
        <property name="cachePrefix">
            <bean class="org.springframework.data.redis.cache.DefaultRedisCachePrefix">
                <constructor-arg name="delimiter" value=":"/>
            </bean>
        </property>
        <!--              -->
        <property name="separator" value="#"/>
        <!--      1h -->
        <property name="defaultExpiration" value="3600"/>
        <!--        -->
        <property name="transactionAware" value="false"/>
    </bean>

</beans>

注記使用(keyの生成ポリシーに注意):
/**
 * @description     redis    key :"caiya_a:querySoftwareList_null_null_null_null_1_10",    cacheName      .
 *       
 * @param cid   ID
 * @param origin   
 * @param sortField     
 * @param sortType     
 * @param pageNo    
 * @param pageSize     
 * @return      
 */
@Cacheable(value = "caiya_a#600", key = "'querySoftwareList_' + #cid + '_' + #origin + '_' + #sortField + '_' + #sortType + '_' + #pageNo + '_' + #pageSize")
public SoftwareWrapper querySoftwareList(Long cid, String origin, String sortField, String sortType, Integer pageNo, Integer pageSize) {
   ......
   return ...;
}