注解实现SpringCache自定义失效时间(升级版)

注解实现SpringCache自定义失效时间(升级版)

之前做过注解实现自定义失效时间,但是需要重写spring-cache中的RedisCache源码,有些不怎么容易扩展,这里使用自定义的CacheManager、和RedisCache类来实现对应的逻辑:
旧版本链接

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获得注解的值

package com.spboot.config.cache.helper;

import com.spboot.config.cache.RedisCacheCustomize;
import com.spboot.utils.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 修改CacheManager的辅助类 调用时机: execute:314, CacheAspectSupport
 * (org.springframework.cache.interceptor) invoke:61, CacheInterceptor
 * (org.springframework.cache.interceptor) proceed:185,
 * ReflectiveMethodInvocation (org.springframework.aop.framework) 由于 SpringAOP
 * 只能切 Spring 容器管理的类,而调用时机本身就是使用反射来实现的 同是全局使用的是同一个 CacheManager,所以无法使用aop
 */
@Component
public class CacheManagerHelper implements ApplicationContextAware {

	private final Logger logger = LoggerFactory.getLogger(CacheManagerHelper.class);

	private static ApplicationContext applicationContext;

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

	/** ttl字段 */
	@Deprecated
	private static Field CACHE_TTL;

	/** ttl字段的修饰器 */
	@Deprecated
	private static Field CACHE_TTL_MODIFIERS;
	static {
		// 缓存以提高反射的性能
		try {
			CACHE_TTL = RedisCacheConfiguration.class.getDeclaredField("ttl");
			CACHE_TTL.setAccessible(true);
			// 获取字段修改器
			CACHE_TTL_MODIFIERS = CACHE_TTL.getClass().getDeclaredField("modifiers");
			CACHE_TTL_MODIFIERS.setAccessible(true);
		} catch (Exception e) {
		}
	}

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

	/**
	 * 获得失效时间,不存在就按照默认的失效时间
	 * @param redisCacheCustomize
	 * @return
	 */
	public static Duration getByCache(RedisCacheCustomize redisCacheCustomize) {
		Duration duration = getByKey(redisCacheCustomize.getName());
		if( null == duration) {
			return redisCacheCustomize.getCacheConfig().getTtl();
		}
		return duration;
	}

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

	/**
	 * 修改redisCache对象中持有的RedisCacheConfiguration的 private final Duration ttl;
	 * 
	 * @param redisCache
	 */
	@Deprecated
	public void modifyRedisCacheConfiguration(RedisCache redisCache) {
		Duration duration = findAllCacheBean().get(redisCache.getName());
		if (null == duration) {
			return;
		}
		// 找到了就修改
		modify(redisCache.getCacheConfiguration(), duration);
	}

	/**
	 * 修改RedisCacheConfiguration的private final Duration ttl;
	 * 
	 * @param cacheConfiguration
	 * @param duration
	 */
	@Deprecated
	private void modify(RedisCacheConfiguration cacheConfiguration, Duration duration) {
		try {
			// 去掉final修饰符
			CACHE_TTL_MODIFIERS.setInt(CACHE_TTL, CACHE_TTL.getModifiers() & ~Modifier.FINAL);
			// 修改字段值
			CACHE_TTL.set(cacheConfiguration, duration);
			// 添加final修饰符
			CACHE_TTL_MODIFIERS.setInt(CACHE_TTL, CACHE_TTL.getModifiers() & ~Modifier.FINAL);
		} catch (Exception e) {
			logger.error("修改cacheConfiguration的ttl失败,原因:{}", e.getMessage());
		}
	}

	/**
	 * 找到所有的被 @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)自定义RedisCacheCustomize

package com.spboot.config.cache;

import com.spboot.config.cache.helper.CacheManagerHelper;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.Assert;

public class RedisCacheCustomize extends RedisCache {

    private final String name;
    private final RedisCacheWriter cacheWriter;
    private final RedisCacheConfiguration cacheConfig;
    private final ConversionService conversionService;

    protected RedisCacheCustomize(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
        super(name, cacheWriter, cacheConfig);
        Assert.notNull(name, "Name must not be null!");
        Assert.notNull(cacheWriter, "CacheWriter must not be null!");
        Assert.notNull(cacheConfig, "CacheConfig must not be null!");

        this.name = name;
        this.cacheWriter = cacheWriter;
        this.cacheConfig = cacheConfig;
        this.conversionService = cacheConfig.getConversionService();
    }

    public RedisCacheConfiguration getCacheConfig() {
        return cacheConfig;
    }

    @Override
    public void put(Object key, 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), CacheManagerHelper.getByCache(this));
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        Object cacheValue = preProcessCacheValue(value);
        if (!isAllowNullValues() && cacheValue == null) {
            return get(key);
        }
        byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
                CacheManagerHelper.getByCache(this));
        if (result == null) {
            return null;
        }
        return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
    }

    private byte[] createAndConvertCacheKey(Object key) {
        return serializeCacheKey(createCacheKey(key));
    }

}

4)自定义RedisCacheManagerCustomize

package com.spboot.config.cache;

import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;

public class RedisCacheManagerCustomize extends RedisCacheManager {

    private final RedisCacheWriter cacheWriter;
    private final RedisCacheConfiguration defaultCacheConfig;

    public RedisCacheManagerCustomize(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
        this.cacheWriter = cacheWriter;
        this.defaultCacheConfig = defaultCacheConfiguration;
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
      // 使用自己的  RedisCacheCustomize
      return new RedisCacheCustomize(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
    }
}

5)缓存配置类

package com.spboot.config.cache;

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))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))  // 设置 k v 序列化机制
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));


        //初始化RedisCacheManager
//        RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
       // 重点在这里,使用自己的CacheManager
        RedisCacheManager cacheManager = new RedisCacheManagerCustomize(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;
    }


}

6) 使用注解

假设在一个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;
	}
}

代码以及封装的starter

posted @ 2021-07-20 21:05  bartggg  阅读(706)  评论(0编辑  收藏  举报