09月02, 2019

Spring cache redis 集成过期时间注解

Spring cache 是一个非常强大的缓存框架,它通过注解的方式来对调用的方法进行缓存,同时它也支持目前的主流缓存框架,但是有一个缺点,就是在注解中不能使用过期时间。至于为什么不支持过期时间,我百度搜索了一下,是因为各种缓存厂商对缓存过期时间支持的不一样,所以目前没有做到统一的支持。

现在进入主题,本次讨论的基于redis来实现缓存,由于 Spring Data Redis的版本不同,咱们实现的方式有略有不同,但是原理是一样的。

实现原理:

  1. 基于 RedisCacheManagerexpires属性,RedisCacheManager是有实现基于key过期的功能的。
  2. 自定义一个 SpringCaCheRedisExpire 注解来存放过期时间
  3. 在Spring应用生命周期的某个时间后,将 SpringCaCheRedisExpire里头的缓存时间放入到RedisCacheManagerexpires属性里头。

基于 spring-data-redis 1.8.X 版本实现

  1. 首先定义一个注解,此注解和Spring cache的Cacheable,CachePut注解一快使用,再不破坏Spring cache原有的功能上添加一个过期时间。
/**
 * <p>为Spring Cache添加过期时间</p>
 *
 * @author 呛水滴鱼
 * @version 1.0
 * @since 1.0
 * @see org.springframework.cache.annotation.Cacheable,org.springframework.cache.annotation.CachePut
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SpringCaCheRedisExpire {

    /**
     * 过期时间(单位:秒)
     * @return
     */
    long value() default 600;
}
  1. 编写一个SpringCaCheRedisExpireRegistry在Spring注册完所有的bean以后,提取所有带有Cacheable或者CachePutSpringCaCheRedisExpire的方法,并把Cacheable或者CachePutvalue属性作为key,把SpringCaCheRedisExpirevalue作为值,存放到一个expiresMap中,最后把这个Map存放或者替换到RedisCacheManagerexpires属性里头。

在spring-data-redis 1.8.X 版本,RedisCacheManager 未采用 Builder 模式构建,所以这里的expires属性是可以修改的,为了减少代码,此处直接在 RedisCacheManager 创建完毕之后修改它的 expires 属性。

/**
 * <p>1.在所有bean注册注册到Spring后,扫面所有的类,提取SpringCaCheRedisExpire的值</p>
 * <p>2.在创建完 RedisCacheManager 后修改它的 Expires</p>
 *
 * @author 呛水滴鱼
 * @version 1.0
 * @since 1.0
 */
@Slf4j
public class SpringCaCheRedisExpireRegistry implements BeanDefinitionRegistryPostProcessor {

    private static Map<String,Long> expiresMap = new ConcurrentHashMap<>();



    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        for (String name : registry.getBeanDefinitionNames()) {
            BeanDefinition definition = registry.getBeanDefinition(name);
            if(Objects.isNull(definition) || StringUtils.isBlank(definition.getBeanClassName())){
                continue;
            }
            Class<?> clazz;
            try {
                clazz = Class.forName(definition.getBeanClassName());
            } catch (ClassNotFoundException e) {
                throw new NoClassDefFoundError("没有找到:"+ definition.getBeanClassName());
            }
            for (Method method : clazz.getMethods()) {
                SpringCaCheRedisExpire springCaCheRedisExpire = method.getAnnotation(SpringCaCheRedisExpire.class);
                if (Objects.isNull(springCaCheRedisExpire)) {
                    continue;
                }
                if (!isCacheAbleMethod(method)) {
                    continue;
                }

                String[] cacheValues = getCacheAbleMethodValue(method);
                if (cacheValues.length == 0) {
                    continue;
                }
                for (String cacheValue : cacheValues) {
                    long expireTime = springCaCheRedisExpire.value();
                    expiresMap.put(cacheValue,expireTime);
                    log.info(" SpringCaCheRedisExpire 添加缓存 {} 过期时间为 {} ", cacheValue, expireTime);
                }
            }
        }
    }


    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        RedisCacheManager redisCacheManager = beanFactory.getBean(RedisCacheManager.class);
        if(redisCacheManager != null){
            redisCacheManager.setExpires(expiresMap);
        }
    }


    private boolean isCacheAbleMethod(Method method) {
        return Objects.nonNull(method.getAnnotation(CachePut.class)) || Objects.nonNull(method.getAnnotation(Cacheable.class));
    }


    private String[] getCacheAbleMethodValue(Method method) {
        if (Objects.nonNull(method.getAnnotation(CachePut.class))) {
            CachePut cachePut = method.getAnnotation(CachePut.class);
            return cachePut.value();
        }

        if (Objects.nonNull(method.getAnnotation(Cacheable.class))) {
            Cacheable cacheable = method.getAnnotation(Cacheable.class);
            return cacheable.value();
        }

        return new String[]{};
    }
}
  1. 启用我们刚刚定义的配置
    @Bean
    public SpringCaCheRedisExpireRegistry springCaCheRedisExpireRegistry(){
        return new SpringCaCheRedisExpireRegistry();
    }

基于 spring-data-redis 2.X 版本实现

在 spring-data-redis 2.x 版本以后,RedisCacheManager 有了比较大的变化,改成了使用Builder方式或者直接使用构造方法来创建对象,此时expires属性也换成了initialCaches。原理上和上边的相同,不过不能直接在创建完对象之后去修改属性,而是在它初始化之前把属性配置好,然后在初始化RedisCacheManager传递给它。

  1. 自定义一个注解 SpringCaCheRedisExpire,同上
  2. 编写一个SpringCaCheRedisExpiresMap来存放缓存key和过期时间的对应关系。(上边的是直接内置在一个类里头)
/**
 * 存放缓存key和过期时间的对应关系
 * @author wenpengyuan
 * @version 1.0
 * @since 1.0
 */
public class SpringCaCheRedisExpiresMap {

    private static Map<String, Long> expiresMap = new ConcurrentHashMap<>();

    static void register(String key,Long expire){
        expiresMap.put(key,expire);
    }

    public static Map<String, Long> getExpiresMap(){
        return expiresMap;
    }
}
  1. 编写一个SpringCaCheRedisExpireRegistry在Spring应该注册时去获取SpringCaCheRedisExpiresMap中需要的值,然后存放在里头。(这里稍微和上边的不一样)
/**
 * <p>1.在所有bean注册注册到Spring后,扫面所有的类,提取SpringCaCheRedisExpire的值</p>
 *
 * @author wenpengyuan
 * @version 1.0
 * @since 1.0
 */
@Slf4j
public class SpringCaCheRedisExpireRegistry implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        for (String name : registry.getBeanDefinitionNames()) {
            BeanDefinition definition = registry.getBeanDefinition(name);
            if(Objects.isNull(definition) || StringUtils.isBlank(definition.getBeanClassName())){
                continue;
            }
            Class<?> clazz;
            try {
                clazz = Class.forName(definition.getBeanClassName());
            } catch (ClassNotFoundException e) {
                throw new NoClassDefFoundError("没有找到:"+ definition.getBeanClassName());
            }
            for (Method method : clazz.getMethods()) {
                SpringCaCheRedisExpire springCaCheRedisExpire = method.getAnnotation(SpringCaCheRedisExpire.class);
                if (Objects.isNull(springCaCheRedisExpire)) {
                    continue;
                }
                if (!isCacheAbleMethod(method)) {
                    continue;
                }

                String[] cacheValues = getCacheAbleMethodValue(method);
                if (cacheValues.length == 0) {
                    continue;
                }
                for (String cacheValue : cacheValues) {
                    long expireTime = springCaCheRedisExpire.value();
                    SpringCaCheRedisExpiresMap.register(cacheValue,expireTime);
                    log.info(" SpringCaCheRedisExpire 添加缓存 {} 过期时间为 {} ", cacheValue, expireTime);
                }
            }
        }
    }


    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }


    private boolean isCacheAbleMethod(Method method) {
        return Objects.nonNull(method.getAnnotation(CachePut.class)) || Objects.nonNull(method.getAnnotation(Cacheable.class));
    }


    private String[] getCacheAbleMethodValue(Method method) {
        if (Objects.nonNull(method.getAnnotation(CachePut.class))) {
            CachePut cachePut = method.getAnnotation(CachePut.class);
            return cachePut.value();
        }

        if (Objects.nonNull(method.getAnnotation(Cacheable.class))) {
            Cacheable cacheable = method.getAnnotation(Cacheable.class);
            return cacheable.value();
        }

        return new String[]{};
    }
}
  1. SpringCaCheRedisExpires中的对应关系配置到RedisCacheManager里头
/**
 * 缓存配置
 * @author 呛水滴鱼
 * @version 1.0
 * @since 1.0
 */
@Configuration
@EnableCaching
public class CacheConfig {


    /**
         * 启用 SpringCaCheRedisExpire
     */
    @Bean
    public SpringCaCheRedisExpireRegistry springCaCheRedisExpireRegistry(){
        return new SpringCaCheRedisExpireRegistry();
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 获取过期时间的对应关系
        Map<String, Long> expiresMap = SpringCaCheRedisExpires.getExpiresMap();
        // 转换成 RedisCacheManager 能接受的对象
        Map<String,RedisCacheConfiguration> initialCacheConfigurations = new ConcurrentHashMap<>();
        expiresMap.forEach((s, aLong) -> initialCacheConfigurations.put(s,this.getRedisCacheConfigurationWithTtl(aLong)));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(this.getRedisCacheConfigurationWithTtl(10L))
                .disableCreateOnMissingCache()
                         // 配置到 RedisCacheManager 里头
                .withInitialCacheConfigurations(initialCacheConfigurations)
                .initialCacheNames(expiresMap.keySet())
                .build();

    }


    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Long seconds) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = getObjectJackson2JsonRedisSerializer();

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

        redisCacheConfiguration.getValueSerializationPair();

        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer)
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }


    private Jackson2JsonRedisSerializer<Object> getObjectJackson2JsonRedisSerializer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        return jackson2JsonRedisSerializer;
    }
}

使用和测试,启动应用后使用 http://{{apphost}}/test/cache?key=haha 测试一下

/**
 * 测试
 *
 * @author 呛水滴鱼
 * @version 1.0
 * @since 1.0
 */
@RequestMapping("/test")
@RestController
public class TestController {

    @Resource
    private TestProcess testProcess;

    @GetMapping("/cache")
    public String testCache(HttpServletRequest request) {
        return testProcess.testCache(request.getParameter("key"));
    }

}


/**
 * 测试
 *
 * @author 呛水滴鱼
 * @version 1.0
 * @since 1.0
 */
@Component
public class TestProcess {
    private final static String DEFAULT = "default";
    private final Map<String,AtomicInteger> countMap = new ConcurrentHashMap<>();


    @SpringCaCheRedisExpire(10)
    @Cacheable(value = "test_cache",key = "#key")
    public String testCache(String key){

        String keySafe = Optional.ofNullable(key).orElse(DEFAULT);
        if(Objects.isNull(countMap.get(keySafe))){
            countMap.put(keySafe,new AtomicInteger(0));
        }
        String result = "================key:"+keySafe+",count:"+countMap.get(keySafe).incrementAndGet()+"===============";
        System.out.println(result);
        return result;
    }
}

关于 @Controller 和 @RestController 过期时间不生效的问题

  1. 这是因为在registry.getBeanDefinitionNames()里头没有@Controller @RestController标记的类,当然并不是代表他们没有注册到 Spring 里头,应该是 SpringMVC 做了某些特殊的处理。
  2. 可以在BeanDefinitionRegistryPostProcessor.postProcessBeanFactory里头进行初始化我们的RedisCacheManager,然后把映射关系放进去。这个方法是在 Spring 创建完对象之后,并在它把对象进行依赖注入之前回调的方法。

本文链接:https://www.putin.ink/post/spring-cache-expires.html

-- EOF --

Comments