目前公司业务需要根据当前时间,动态缓存时间,缓存失效时间是根据业务动态计算出来的。
解决方法
- 自定义aop缓存
- 修改spring自带的@EnableCaching缓存
我选择了方法2,毕竟我也是熟读源码的人,这一点点问题还是要挑战一下的,完全自定义了还有啥意思。
Spring缓存源码加载流程
-
缓存入口类
@EnableCaching
-
@Import自动导入
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(CachingConfigurationSelector.class) public @interface EnableCaching { /** * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed * to standard Java interface-based proxies. The default is {@code false}. <strong> * Applicable only if {@link #mode()} is set to {@link AdviceMode#PROXY}</strong>. * <p>Note that setting this attribute to {@code true} will affect <em>all</em> * Spring-managed beans requiring proxying, not just those marked with {@code @Cacheable}. * For example, other beans marked with Spring's {@code @Transactional} annotation will * be upgraded to subclass proxying at the same time. This approach has no negative * impact in practice unless one is explicitly expecting one type of proxy vs another, * e.g. in tests. */ boolean proxyTargetClass() default false; /** * Indicate how caching advice should be applied. * <p><b>The default is {@link AdviceMode#PROXY}.</b> * Please note that proxy mode allows for interception of calls through the proxy * only. Local calls within the same class cannot get intercepted that way; * a caching annotation on such a method within a local call will be ignored * since Spring's interceptor does not even kick in for such a runtime scenario. * For a more advanced mode of interception, consider switching this to * {@link AdviceMode#ASPECTJ}. */ AdviceMode mode() default AdviceMode.PROXY; /** * Indicate the ordering of the execution of the caching advisor * when multiple advices are applied at a specific joinpoint. * <p>The default is {@link Ordered#LOWEST_PRECEDENCE}. */ int order() default Ordered.LOWEST_PRECEDENCE; }
-
Import导入类CachingConfigurationSelector,是实现ImportSelector接口的,那我们看selectImports方法
/** * Returns {@link ProxyCachingConfiguration} or {@code AspectJCachingConfiguration} * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableCaching#mode()}, * respectively. Potentially includes corresponding JCache configuration as well. */ @Override public String[] selectImports(AdviceMode adviceMode) { switch (adviceMode) { case PROXY: return getProxyImports(); // 走这个,看注解默认值 case ASPECTJ: return getAspectJImports(); default: return null; } } /** * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#PROXY}. * <p>Take care of adding the necessary JSR-107 import if it is available. */ private String[] getProxyImports() { List<String> result = new ArrayList<>(3); result.add(AutoProxyRegistrar.class.getName()); result.add(ProxyCachingConfiguration.class.getName()); if (jsr107Present && jcacheImplPresent) { result.add(PROXY_JCACHE_CONFIGURATION_CLASS); } return StringUtils.toStringArray(result); }
-
看ProxyCachingConfiguration实现,肯定是生成缓存代理的类,重要的三元素(Advisor、Pointcut、Advice),具体逻辑在Advice里面,我们只要看Advice的invoke方法即可。
@Configuration(proxyBeanMethods = false) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ProxyCachingConfiguration extends AbstractCachingConfiguration { @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor( CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) { BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor(); advisor.setCacheOperationSource(cacheOperationSource); advisor.setAdvice(cacheInterceptor); // 这里说明cacheInterceptor是advice if (this.enableCaching != null) { advisor.setOrder(this.enableCaching.<Integer>getNumber("order")); } return advisor; } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public CacheOperationSource cacheOperationSource() { return new AnnotationCacheOperationSource(); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) { CacheInterceptor interceptor = new CacheInterceptor(); //这是是创建advice interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager); interceptor.setCacheOperationSource(cacheOperationSource); return interceptor; } }
-
我们直接看CacheInterceptor的invoke方法即可
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable { @Override @Nullable public Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); CacheOperationInvoker aopAllianceInvoker = () -> { try { return invocation.proceed(); } catch (Throwable ex) { throw new CacheOperationInvoker.ThrowableWrapper(ex); } }; Object target = invocation.getThis(); Assert.state(target != null, "Target must not be null"); try { return execute(aopAllianceInvoker, target, method, invocation.getArguments());// 核心方法 } catch (CacheOperationInvoker.ThrowableWrapper th) { throw th.getOriginal(); } } }
-
我们看execute核心方法
@Nullable protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically) if (this.initialized) { Class<?> targetClass = getTargetClass(target); CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { // 获取方法缓存注解等信息包装类 Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));// 核心方法 } } } return invoker.invoke(); }
-
最最最核心方法
@Nullable private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { // Special handling of synchronized invocation if (contexts.isSynchronized()) { CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next(); if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); Cache cache = context.getCaches().iterator().next(); try { return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker)))); } catch (Cache.ValueRetrievalException ex) { // Directly propagate ThrowableWrapper from the invoker, // or potentially also an IllegalArgumentException etc. ReflectionUtils.rethrowRuntimeException(ex.getCause()); } } else { // No caching required, only call the underlying method return invokeOperation(invoker); } } //先处理@CacheEvicts的逻辑,,其实就是掉clear方法 // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); //处理@Cacheable的逻辑,,其实就是掉get方法 // Check if we have a cached item matching the conditions Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); // Collect puts from any @Cacheable miss, if no cached item is found List<CachePutRequest> cachePutRequests = new LinkedList<>(); //如果缓存没命中或者不是使用的@Cacheable注解 if (cacheHit == null) { //处理@Cacheable的逻辑,收集插入请求,插入缓存的值需要调用被代理方法 collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); } Object cacheValue; Object returnValue; //如果缓存命中了 if (cacheHit != null && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit cacheValue = cacheHit.get(); //直接返回缓存中的值 returnValue = wrapCacheValue(method, cacheValue); } else { //在这里调用被代理方法 // Invoke the method if we don't have a cache hit returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); } //处理@CachePut注解,收集put请求 // Collect any explicit @CachePuts collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); //处理put请求,其实就是掉put方法 // Process any collected put requests, either from @CachePut or a @Cacheable miss for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); // 主要看这个方法 } // Process any late evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; }
-
通过上面代码,我们主要集中看apply方法,因为这边是设置缓存
public void apply(@Nullable Object result) { if (this.context.canPutToCache(result)) { for (Cache cache : this.context.getCaches()) { doPut(cache, this.key, result); // 核心方法 } } } /** * Execute {@link Cache#put(Object, Object)} on the specified {@link Cache} * and invoke the error handler if an exception occurs. */ protected void doPut(Cache cache, Object key, @Nullable Object result) { try { cache.put(key, result); // 核心方法 } catch (RuntimeException ex) { getErrorHandler().handleCachePutError(ex, cache, key, result); } }
-
这边我们主要看一下cache对象基于RedisCache的实现,毕竟我们用的是redis的缓存。下面方法中的getTtl就是我们的突破口,如果我们能将getTtl方法返回一个动态的值,那我们的业务不就实现了吗???
哇喔,想想就激动!!!
@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), cacheConfig.getTtl()); // 获取过期时间 }
-
那问题来了,这个过期时间怎么来的?我们只要分析一下RedisCacheConfiguration这个对象中的ttl属性怎么来的即可。
public class RedisCacheConfiguration { private final Duration ttl; private final boolean cacheNullValues; private final CacheKeyPrefix keyPrefix; private final boolean usePrefix; private final SerializationPair<String> keySerializationPair; private final SerializationPair<Object> valueSerializationPair; private final ConversionService conversionService; // 有且仅有一个构造方法,还是私有的,那肯定有static方法 @SuppressWarnings("unchecked") private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair, ConversionService conversionService) { this.ttl = ttl; this.cacheNullValues = cacheNullValues; this.usePrefix = usePrefix; this.keyPrefix = keyPrefix; this.keySerializationPair = keySerializationPair; this.valueSerializationPair = (SerializationPair<Object>) valueSerializationPair; this.conversionService = conversionService; } }
-
我们看下这个类结构,有大量的static,并且返回值是本实例对象,那不久就是类似于builder的建造器么?
[图片上传失败...(image-e74fa-1679482374169)]
-
那我们接下来肯定要搞明白哪里调用初始化了RedisCacheConfiguration对象,其实想也不用想,肯定是利用的springboot的spi机制,而且一般就在sprin-boot-autoconfigure.jar里面。
我们在RedisCacheConfiguration的defaultCacheConfig方法上点一下,果然找到了RedisCacheConfiguration类。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisConnectionFactory.class) @AutoConfigureAfter(RedisAutoConfiguration.class) @ConditionalOnBean(RedisConnectionFactory.class) @ConditionalOnMissingBean(CacheManager.class)// 如果spring容器中有一个CacheManager对象,这个类就不加载 @Conditional(CacheCondition.class) class RedisCacheConfiguration { // 定义了RedisCacheManager对象 @Bean // cacheProperties 配置缓存属性 RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults( determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader())); List<String> cacheNames = cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); } if (cacheProperties.getRedis().isEnableStatistics()) { builder.enableStatistics(); } redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return cacheManagerCustomizers.customize(builder.build()); } private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration( CacheProperties cacheProperties, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ClassLoader classLoader) { return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader)); } // 我们的RedisCacheConfiguration对象就在这。 private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration( CacheProperties cacheProperties, ClassLoader classLoader) { Redis redisProperties = cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration .defaultCacheConfig(); config = config.serializeValuesWith( SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
-
我们在步骤9看到,RedisCache对象是持有RedisCacheConfiguration对象的,那什么时候放进去的呢?想必一定和RedisCacheManager有关,因为我们RedisCacheConfiguration是赋值给了RedisCacheManager。
RedisCacheManager类的代码,我们可以看出在createRedisCache方法,源码将defaultCacheConfig放进了RedisCache对象
/** * Configuration hook for creating {@link RedisCache} with given name and {@code cacheConfig}. * * @param name must not be {@literal null}. * @param cacheConfig can be {@literal null}. * @return never {@literal null}. */ protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) { return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig); }
修改思路
- 首先我们要自定义类,继承RedisCache,并重写put方法,将ttl设置成我们的业务逻辑(参考步骤9)
- 要自定义RedisCache,也必须自定义一个类继承RedisCacheManager,修改RedisCacheManager的createRedisCache方法,返回自定义的RedisCache对象。(参考步骤9)
- 最后我们必须自定义RedisCacheManager,覆盖springboot中默认的RedisCacheManager。
实现代码
-
自定义RedisCache
public class CGRedisCache extends RedisCache { /** * Create new {@link RedisCache}. * * @param redisCache must not be {@literal null}. */ protected CGRedisCache(RedisCache redisCache) { super(redisCache.getName(), redisCache.getNativeCache(), redisCache.getCacheConfiguration()); } @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.", getName())); } getNativeCache().put(getName(), serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue), getTtl()); } private Duration getTtl() { // 实现自己的业务逻辑吧 return null; } @Override public ValueWrapper putIfAbsent(Object key, Object value) { Object cacheValue = preProcessCacheValue(value); if (!isAllowNullValues() && cacheValue == null) { return get(key); } byte[] result = getNativeCache().putIfAbsent(getName(), serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue), getTtl()); if (result == null) { return null; } return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))); } }
-
自定义RedisCacheManager
public class CGRedisCacheManager extends RedisCacheManager { public CGRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, Map<String, RedisCacheConfiguration> initialCacheConfigurations) { super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations); } @Override protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { RedisCache redisCache = super.createRedisCache(name, cacheConfig); return new CGRedisCache(redisCache); } }
-
讲自定义的RedisCacheManager加入到spring容器中,覆盖默认配置
@Configuration @EnableCaching public class RedisCacheConfig { /** * 自定义缓存管理器 */ @Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { Jackson2JsonRedisSerializer valueRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); valueRedisSerializer.setObjectMapper(objectMapper); RedisSerializationContext.SerializationPair<Object> v = RedisSerializationContext.SerializationPair.fromSerializer(valueRedisSerializer); CacheKeyPrefix cacheKeyPrefix = cacheName -> "Children:api:cache:"; RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(1L)) .computePrefixWith(cacheKeyPrefix) .serializeValuesWith(v) .disableCachingNullValues(); // 针对不同cacheName,设置不同的过期时间 Map<String, RedisCacheConfiguration> initialCacheConfiguration = new HashMap<String, RedisCacheConfiguration>() {{ put("ttl1", RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheKeyPrefix).serializeValuesWith(v).entryTtl(Duration.ofSeconds(1L))); put("ttl3", RedisCacheConfiguration.defaultCacheConfig().computePrefixWith(cacheKeyPrefix).serializeValuesWith(v).entryTtl(Duration.ofSeconds(3L))); }}; return new CGRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory), defaultCacheConfig, initialCacheConfiguration); } }
总结
读懂源码很重要,整体修改还是比较简单的,就是说一下spring-boot源码写的有一点点坑,各种private,不过也还好。