注解实现SpringCache自定义失效时间

注解实现SpringCache自定义失效时间

SpringCache是一个很方便的缓存框架,但是官方提供的缓存的配置只有全局的缓存失效时间,没有针对某个命名空间做配置,因为工作上业务的关系需要针对某一个缓存做单独的控制,所有想了个办法来实现。大概分为以下步骤:

1)自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
 * 缓存失效的注解,目前只支持在类级别上有效
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheExpire {
    /**
     * 失效时间,默认是60
     * @return
     */
    public long ttl() default 60L;

    /**
     * 单位,默认是秒
     * @return
     */
    public TimeUnit unit() default TimeUnit.SECONDS;
}

2)CacheManagerHelper获得注解的值

import com.spboot.utils.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * CacheManager的辅助类
 */
@Component
public class CacheManagerHelper implements ApplicationContextAware {

    private static ApplicationContext applicationContext;
    private static Map<String , Duration> CACHE_DURATION = new HashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    	CacheManagerHelper.applicationContext = applicationContext;
    }

    /**
     * 根据cacheName获得对应的duration值
     * @param name
     * @return
     */
    public static Duration getByKey(String name) {
        return findAllCacheBean().get(name);
    }

    /**
     * 找到所有的被 @CacheConfig 和 @CacheExpire 修饰的类对象
     */
    public static Map<String , Duration> findAllCacheBean() {
        if(CACHE_DURATION.size() == 0) {
            Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(CacheConfig.class);
            if(beansWithAnnotation != null && beansWithAnnotation.size() > 0) {
                for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
                    Object proxyObject = entry.getValue(); // 代理类
                    Object realObject = BeanUtils.getTarget(proxyObject); //获得真实的对象
                    CacheExpire cacheExpire = realObject.getClass().getAnnotation(CacheExpire.class);
                    if(null != cacheExpire) {
                        CacheConfig cacheConfig = realObject.getClass().getAnnotation(CacheConfig.class);
                        String[] cacheNames = cacheConfig.cacheNames();
                        long convert = TimeUnit.SECONDS.convert(cacheExpire.ttl(), cacheExpire.unit());
                        Duration duration = Duration.ofSeconds(convert);
                        for (String cacheName : cacheNames) {
                            CACHE_DURATION.put(cacheName, duration);
                        }
                    }
                }
            }
        }
        return CACHE_DURATION;
    }
}

3)修改源码org.springframework.data.redis.cache.RedisCache

修改这里是为了改变每次存储之前redis的key的ttl值,通过上面自定义的CacheManagerHelper来获得。

修改源码位置:

  • org.springframework.data.redis.cache.RedisCache#put
  • org.springframework.data.redis.cache.RedisCache#putIfAbsent
  • 添加的方法:
    • getDuration(java.lang.String, org.springframework.data.redis.cache.RedisCacheConfiguration)

因为代码太长,只放出了被修改过的代码,其余的保持不变:

/**
	 *  如果该命名空间使用了@CacheExpire注解就是用自定义的失效时间,否则使用默认的
	 * @param name
	 * @param cacheConfiguration
	 * @return
	 */
private Duration getDuration(String name, RedisCacheConfiguration cacheConfiguration) {
    Duration duration = CacheManagerHelper.getByKey(name);
    if(null != duration) { // 如果当前命名空间配置了自定义失效时间,使用配置值
        return duration;
    }
    return cacheConfig.getTtl(); // 否则使用全局的配置值
}

/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
	 */
@Override
public void put(Object key, @Nullable Object value) {

    Object cacheValue = preProcessCacheValue(value);

    if (!isAllowNullValues() && cacheValue == null) {

        throw new IllegalArgumentException(String.format(
            "Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
            name));
    }

    // 修改的
    cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), getDuration(name, cacheConfig));
    //		默认的
    //		cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}

/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
	 */
@Override
public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {

    Object cacheValue = preProcessCacheValue(value);

    if (!isAllowNullValues() && cacheValue == null) {
        return get(key);
    }

    // 修改后的
    byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),  getDuration(name, cacheConfig));
    // 默认的
    //		byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
    //				cacheConfig.getTtl());

    if (result == null) {
        return null;
    }

    return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
}

4)全局redis cache config

import java.time.Duration;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
@EnableCaching // 开启spring的缓存
public class CacheConfig {

    /**
     * 自定义得缓存管理器
     * @param redisConnectionFactory
     * @return
     */
    @Primary
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {

        //初始化一个RedisCacheWriter
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);

        // key 序列化方式
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // value的序列化机制
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = jackson2JsonRedisSerializer();
        
        // 配置
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)) // 默认1个小时失效时间
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))  // 设置 k v 序列化机制
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));


        //初始化RedisCacheManager
        RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);

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


}

5)使用注解

假设在一个controller使用注解,例如:

@CacheExpire(ttl = 10, unit = TimeUnit.SECONDS) // 自定义注解,10秒钟就过期
@CacheConfig(
     cacheNames = "testApiService")
@RestController
public class TestApi {
    @Cacheable
    @GetMapping("/api/redis")
	public Map<String,String> data() {
		Map<String,String> map = new HashMap<String, String>();
		map.put("k1", "v1");
		map.put("k2", "v2");
		map.put("k3", "v3");
		return map;
	}
}

具体的代码地址

如此一来就实现了使用注解控制缓存失效时间,这里还有优化的空间,比如注解精细到方法粒度的控制,使用aop来替代等,后面有时间再优化实践吧。

posted @ 2020-05-31 10:37  bartggg  阅读(2018)  评论(0编辑  收藏  举报