• 注解实现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来替代等,后面有时间再优化实践吧。

  • 相关阅读:
    cve-2015-1635 poc
    Python实现ORM
    Android完全退出应用的方法
    Java反射理解
    Android动画
    Android进程间通信IPC
    Java的四种引用方式
    Android底部菜单的实现
    Android中AsyncTask使用
    Android自定义控件
  • 原文地址:https://www.cnblogs.com/bartggg/p/12996651.html
Copyright © 2020-2023  润新知