自定义注解设置缓存有效期的正确姿势

引言
redis缓存的有效期可以通过xml配置文件设置(默认有效期),也可以通过编码的方式手动去设置,但是这两种方式都存在缺陷。xml方式设置的是全局的默认有效期,虽然灵活,但不能给某个缓存设置单独的有效期;硬编码方式虽然可以给不同的缓存设置单独的有效期,但是管理上不够灵活。Spring提供的Cache相关注解中并没有提供有效期的配置参数,so,自定义注解实现缓存有效期的灵活设置诞生了,具体源码前往github下载

Redis缓存
如何使用Redis实现数据缓存,请参考上篇《使用Spring-Data-Redis实现数据缓存》

工具类介绍
1.JedisPoolConfig
jedis连接池配置类,位于jedis包中,用于配置连接池中jedis连接数的个数、是否阻塞、逐出策略等。示例配置如下所示。

    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- maxIdle最大空闲连接数 -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- maxTotal最大连接数 -->
        <property name="maxTotal" value="${redis.maxActive}"/>
        <!-- maxWaitMillis获取连接时的最大等待毫秒数,小于零表示阻塞不确定的时间,默认为-1 -->
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <!-- testOnBorrow在获取连接的时是否检查有效性 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>

2.JedisConnectionFactory
jedis实例的创建工厂,基于连接池创建jedis实例,位于spring-data-redis包中。示例配置如下所示。

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- hostName Redis主机名,默认是localhost -->
        <property name="hostName" value="${redis.host}"/>
        <!-- port Redis提供服务的端口-->
        <property name="port" value="${redis.port}"/>
        <!-- password Redis认证密码 -->
        <property name="password" value="${redis.pass}"/>
        <!-- database 连接工厂使用到的数据库索引,默认是0 -->
        <property name="database" value="${redis.dbIndex}"/>
        <!-- poolConfig 连接池配置 -->
        <property name="poolConfig" ref="poolConfig"/>
    </bean>

3.RedisTemplate
RedisTemplate可以从JedisConnectionFactory中获取jedis实例,封装了jedis的操作,位于spring-data-redis包中,让使用者无需关心连接的获取及释放,集中关注业务处理。示例配置如下所示。

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

4.RedisCacheManager
使用RedisTemplate对Redis缓存进行管理,位于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中一个重要的方法,void setExpires(Map<String, Long> expires),该方法的传入参数是一个Map,Map的key值是@Cacheable(或@CacheEvict或@CachePut)注解的value值,Map的value值是缓存的有效期(单位秒),用于批量设置缓存的有效期。

自定义注解
直接贴代码了,如下。

@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
新写了一个SpringRedisCacheManager,继承自RedisCacheManager,用于对@CacheDuration解析及有效期的设置,代码如下。

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);
        }
        //设置有效期
        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标注的有效期,优先使用方法上标注的有效期
     * @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实例 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- maxIdle最大空闲连接数 -->
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <!-- maxTotal最大连接数 -->
        <property name="maxTotal" value="${redis.maxActive}"/>
        <!-- maxWaitMillis获取连接时的最大等待毫秒数,小于零表示阻塞不确定的时间,默认为-1 -->
        <property name="maxWaitMillis" value="${redis.maxWait}"/>
        <!-- testOnBorrow在获取连接的时是否检查有效性 -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>
    <!-- 配置JedisConnectionFactory -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- hostName Redis主机名,默认是localhost -->
        <property name="hostName" value="${redis.host}"/>
        <!-- port Redis提供服务的端口-->
        <property name="port" value="${redis.port}"/>
        <!-- password Redis认证密码 -->
        <property name="password" value="${redis.pass}"/>
        <!-- database 连接工厂使用到的数据库索引,默认是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("第一次执行查询:" + userService.queryFullNameById(100L));
        System.out.println("----------------------------------");
        System.out.println("第二次执行查询:" + 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("第三次执行查询:" + userService.queryFullNameById(100l));
    }

测试结果

execute queryFullNameById method
第一次执行查询:ZhangSanFeng
----------------------------------
第二次执行查询:ZhangSanFeng
----------------------------------
UserId_100有效期(单位秒):15
----------------------------------
主线程休眠3秒后
----------------------------------
UserId_100有效期(单位秒):12
----------------------------------
第三次执行查询:ZhangSanFeng

结果分析
UserService类上标注的CacheDuration设置有效期是6秒,而方法queryFullNameById上CacheDuration设置的有效期是16秒,最后生效的是16秒。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,680评论 6 342
  • 1 Redis介绍1.1 什么是NoSql为了解决高并发、高可扩展、高可用、大数据存储问题而产生的数据库解决方...
    克鲁德李阅读 5,252评论 0 36
  • 本文将从Redis的基本特性入手,通过讲述Redis的数据结构和主要命令对Redis的基本能力进行直观介绍。之后概...
    kelgon阅读 61,081评论 24 626
  • 很蓝的天,很美的海,很浓的云,相机不够用。 不大不小的山,不繁不简的城,不好不坏的人,围在一座城。 东或西,起或落...
    嗨嗯阅读 167评论 0 1