自定義注解設(shè)置緩存有效期的正確姿勢

引言
redis緩存的有效期可以通過xml配置文件設(shè)置(默認(rèn)有效期),也可以通過編碼的方式手動去設(shè)置,但是這兩種方式都存在缺陷。xml方式設(shè)置的是全局的默認(rèn)有效期,雖然靈活,但不能給某個(gè)緩存設(shè)置單獨(dú)的有效期;硬編碼方式雖然可以給不同的緩存設(shè)置單獨(dú)的有效期,但是管理上不夠靈活。Spring提供的Cache相關(guān)注解中并沒有提供有效期的配置參數(shù),so,自定義注解實(shí)現(xiàn)緩存有效期的靈活設(shè)置誕生了,具體源碼前往github下載。

Redis緩存
如何使用Redis實(shí)現(xiàn)數(shù)據(jù)緩存,請參考上篇《使用Spring-Data-Redis實(shí)現(xiàn)數(shù)據(jù)緩存》。

工具類介紹
1.JedisPoolConfig
jedis連接池配置類,位于jedis包中,用于配置連接池中jedis連接數(shù)的個(gè)數(shù)、是否阻塞、逐出策略等。示例配置如下所示。

    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- maxIdle最大空閑連接數(shù) -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- maxTotal最大連接數(shù) -->
        <property name="maxTotal" value="${redis.maxActive}"/>
        <!-- maxWaitMillis獲取連接時(shí)的最大等待毫秒數(shù),小于零表示阻塞不確定的時(shí)間,默認(rèn)為-1 -->
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <!-- testOnBorrow在獲取連接的時(shí)是否檢查有效性 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>

2.JedisConnectionFactory
jedis實(shí)例的創(chuàng)建工廠,基于連接池創(chuàng)建jedis實(shí)例,位于spring-data-redis包中。示例配置如下所示。

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- hostName Redis主機(jī)名,默認(rèn)是localhost -->
        <property name="hostName" value="${redis.host}"/>
        <!-- port Redis提供服務(wù)的端口-->
        <property name="port" value="${redis.port}"/>
        <!-- password Redis認(rèn)證密碼 -->
        <property name="password" value="${redis.pass}"/>
        <!-- database 連接工廠使用到的數(shù)據(jù)庫索引,默認(rèn)是0 -->
        <property name="database" value="${redis.dbIndex}"/>
        <!-- poolConfig 連接池配置 -->
        <property name="poolConfig" ref="poolConfig"/>
    </bean>

3.RedisTemplate
RedisTemplate可以從JedisConnectionFactory中獲取jedis實(shí)例,封裝了jedis的操作,位于spring-data-redis包中,讓使用者無需關(guān)心連接的獲取及釋放,集中關(guān)注業(yè)務(wù)處理。示例配置如下所示。

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
    </bean>

4.RedisCacheManager
使用RedisTemplate對Redis緩存進(jìn)行管理,位于spring-data-redis包中。示例配置如下所示。

   <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate"/>
        <property name="defaultExpiration" value="${redis.expiration}"/>
    </bean>

這里介紹RedisCacheManager中一個(gè)重要的方法,void setExpires(Map<String, Long> expires),該方法的傳入?yún)?shù)是一個(gè)Map,Map的key值是@Cacheable(或@CacheEvict或@CachePut)注解的value值,Map的value值是緩存的有效期(單位秒),用于批量設(shè)置緩存的有效期。

自定義注解
直接貼代碼了,如下。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface CacheDuration {
    //Sets the expire time (in seconds).
    public long duration() default 60;
}

使用@CacheDuration

@Service("userService")
@CacheDuration(duration = 6)
public class UserService {
    @Cacheable(value = "User", key = "'UserId_' + #id", condition = "#id<=110")
    @CacheDuration(duration = 16)
    public String queryFullNameById(long id) {
        System.out.println("execute queryFullNameById method");
        return "ZhangSanFeng";
    }
}

新RedisCacheManager
新寫了一個(gè)SpringRedisCacheManager,繼承自RedisCacheManager,用于對@CacheDuration解析及有效期的設(shè)置,代碼如下。

public class SpringRedisCacheManager extends RedisCacheManager implements ApplicationContextAware, InitializingBean {
    private ApplicationContext applicationContext;
    public SpringRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    @Override
    public void afterPropertiesSet() {
        parseCacheDuration(applicationContext);
    }
    private void parseCacheDuration(ApplicationContext applicationContext) {
        final Map<String, Long> cacheExpires = new HashMap<>();
        String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
        for (String beanName : beanNames) {
            final Class clazz = applicationContext.getType(beanName);
            Service service = findAnnotation(clazz, Service.class);
            if (null == service) {
                continue;
            }
            addCacheExpires(clazz, cacheExpires);
        }
        //設(shè)置有效期
        super.setExpires(cacheExpires);
    }
    private void addCacheExpires(final Class clazz, final Map<String, Long> cacheExpires) {
        ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
            @Override
            public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
                ReflectionUtils.makeAccessible(method);
                CacheDuration cacheDuration = findCacheDuration(clazz, method);
                Cacheable cacheable = findAnnotation(method, Cacheable.class);
                CacheConfig cacheConfig = findAnnotation(clazz, CacheConfig.class);
                Set<String> cacheNames = findCacheNames(cacheConfig, cacheable);
                for (String cacheName : cacheNames) {
                    cacheExpires.put(cacheName, cacheDuration.duration());
                }
            }
        }, new ReflectionUtils.MethodFilter() {
            @Override
            public boolean matches(Method method) {
                return null != findAnnotation(method, Cacheable.class);
            }
        });
    }
    /**
     * CacheDuration標(biāo)注的有效期,優(yōu)先使用方法上標(biāo)注的有效期
     * @param clazz
     * @param method
     * @return
     */
    private CacheDuration findCacheDuration(Class clazz, Method method) {
        CacheDuration methodCacheDuration = findAnnotation(method, CacheDuration.class);
        if (null != methodCacheDuration) {
            return methodCacheDuration;
        }
        CacheDuration classCacheDuration = findAnnotation(clazz, CacheDuration.class);
        if (null != classCacheDuration) {
            return classCacheDuration;
        }
        throw new IllegalStateException("No CacheDuration config on Class " + clazz.getName() + " and method " + method.toString());
    }
    private Set<String> findCacheNames(CacheConfig cacheConfig, Cacheable cacheable) {
        return isEmpty(cacheable.value()) ?
                newHashSet(cacheConfig.cacheNames()) : newHashSet(cacheable.value());
    }
}

Spring的xml配置
完整配置redisCacheContext.xml如下所示。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
    <context:component-scan base-package="redis.cache"/>
    <context:annotation-config/>
    <cache:annotation-driven cache-manager="redisCacheManager"/>
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:redis.properties</value>
            </list>
        </property>
    </bean>
    <!-- 配置JedisPoolConfig實(shí)例 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- maxIdle最大空閑連接數(shù) -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- maxTotal最大連接數(shù) -->
        <property name="maxTotal" value="${redis.maxActive}"/>
        <!-- maxWaitMillis獲取連接時(shí)的最大等待毫秒數(shù),小于零表示阻塞不確定的時(shí)間,默認(rèn)為-1 -->
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <!-- testOnBorrow在獲取連接的時(shí)是否檢查有效性 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>
    <!-- 配置JedisConnectionFactory -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- hostName Redis主機(jī)名,默認(rèn)是localhost -->
        <property name="hostName" value="${redis.host}"/>
        <!-- port Redis提供服務(wù)的端口-->
        <property name="port" value="${redis.port}"/>
        <!-- password Redis認(rèn)證密碼 -->
        <property name="password" value="${redis.pass}"/>
        <!-- database 連接工廠使用到的數(shù)據(jù)庫索引,默認(rèn)是0 -->
        <property name="database" value="${redis.dbIndex}"/>
        <!-- poolConfig 連接池配置 -->
        <property name="poolConfig" ref="poolConfig"/>
    </bean>
    <!-- 配置RedisTemplate -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
    </bean>
    <!-- 配置RedisCacheManager -->
    <bean id="redisCacheManager" class="redis.cache.SpringRedisCacheManager">
        <constructor-arg name="redisOperations" ref="redisTemplate"/>
        <property name="defaultExpiration" value="${redis.expiration}"/>
    </bean>
</beans>

Redis連接配置
完整配置如下。

redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.dbIndex=0
redis.expiration=3000
redis.maxIdle=300
redis.maxActive=600
redis.maxWait=1000
redis.testOnBorrow=true

測試代碼

    @Test
    public void testRedisCacheManager() {
        ApplicationContext context = new ClassPathXmlApplicationContext("redisCacheContext.xml");
        UserService userService = (UserService) context.getBean("userService");
        RedisTemplate redisTemplate = (RedisTemplate) context.getBean("redisTemplate");
        System.out.println("第一次執(zhí)行查詢:" + userService.queryFullNameById(100L));
        System.out.println("----------------------------------");
        System.out.println("第二次執(zhí)行查詢:" + userService.queryFullNameById(100L));
        System.out.println("----------------------------------");
        System.out.println("UserId_100有效期(單位秒):" + redisTemplate.getExpire("UserId_100", TimeUnit.SECONDS));
        System.out.println("----------------------------------");
        try {
            Thread.sleep(3000);
            System.out.println("主線程休眠3秒后");
            System.out.println("----------------------------------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("UserId_100有效期(單位秒):" + redisTemplate.getExpire("UserId_100", TimeUnit.SECONDS));
        System.out.println("----------------------------------");
        System.out.println("第三次執(zhí)行查詢:" + userService.queryFullNameById(100l));
    }

測試結(jié)果

execute queryFullNameById method
第一次執(zhí)行查詢:ZhangSanFeng
----------------------------------
第二次執(zhí)行查詢:ZhangSanFeng
----------------------------------
UserId_100有效期(單位秒):15
----------------------------------
主線程休眠3秒后
----------------------------------
UserId_100有效期(單位秒):12
----------------------------------
第三次執(zhí)行查詢:ZhangSanFeng

結(jié)果分析
UserService類上標(biāo)注的CacheDuration設(shè)置有效期是6秒,而方法queryFullNameById上CacheDuration設(shè)置的有效期是16秒,最后生效的是16秒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,992評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,972評論 6 342
  • 1 Redis介紹1.1 什么是NoSql為了解決高并發(fā)、高可擴(kuò)展、高可用、大數(shù)據(jù)存儲問題而產(chǎn)生的數(shù)據(jù)庫解決方...
    克魯?shù)吕?/span>閱讀 5,371評論 0 36
  • 本文將從Redis的基本特性入手,通過講述Redis的數(shù)據(jù)結(jié)構(gòu)和主要命令對Redis的基本能力進(jìn)行直觀介紹。之后概...
    kelgon閱讀 61,286評論 23 625
  • 很藍(lán)的天,很美的海,很濃的云,相機(jī)不夠用。 不大不小的山,不繁不簡的城,不好不壞的人,圍在一座城。 東或西,起或落...
    嗨嗯閱讀 212評論 0 1