目前公司業(yè)務(wù)需要根據(jù)當(dāng)前時(shí)間腰耙,動(dòng)態(tài)緩存時(shí)間死姚,緩存失效時(shí)間是根據(jù)業(yè)務(wù)動(dòng)態(tài)計(jì)算出來(lái)的。
解決方法
- 自定義aop緩存
- 修改spring自帶的@EnableCaching緩存
我選擇了方法2,畢竟我也是熟讀源碼的人,這一點(diǎn)點(diǎn)問(wèn)題還是要挑戰(zhàn)一下的听皿,完全自定義了還有啥意思。
Spring緩存源碼加載流程
-
緩存入口類(lèi)
@EnableCaching
-
@Import自動(dòng)導(dǎo)入
@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導(dǎo)入類(lèi)CachingConfigurationSelector,是實(shí)現(xiàn)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(); // 走這個(gè),看注解默認(rèn)值 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實(shí)現(xiàn)悼院,肯定是生成緩存代理的類(lèi)伤为,重要的三元素(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); // 這里說(shuō)明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(); //這是是創(chuàng)建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) { // 獲取方法緩存注解等信息包裝類(lèi) 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的邏輯,,其實(shí)就是掉clear方法 // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); //處理@Cacheable的邏輯,,其實(shí)就是掉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<>(); //如果緩存沒(méi)命中或者不是使用的@Cacheable注解 if (cacheHit == null) { //處理@Cacheable的邏輯位衩,收集插入請(qǐng)求,插入緩存的值需要調(diào)用被代理方法 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 { //在這里調(diào)用被代理方法 // Invoke the method if we don't have a cache hit returnValue = invokeOperation(invoker); cacheValue = unwrapReturnValue(returnValue); } //處理@CachePut注解,收集put請(qǐng)求 // Collect any explicit @CachePuts collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); //處理put請(qǐng)求,其實(shí)就是掉put方法 // Process any collected put requests, either from @CachePut or a @Cacheable miss for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); // 主要看這個(gè)方法 } // Process any late evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; }
-
通過(guò)上面代碼熔萧,我們主要集中看apply方法糖驴,因?yàn)檫@邊是設(shè)置緩存
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對(duì)象基于RedisCache的實(shí)現(xiàn),畢竟我們用的是redis的緩存佛致。下面方法中的getTtl就是我們的突破口贮缕,如果我們能將getTtl方法返回一個(gè)動(dòng)態(tài)的值,那我們的業(yè)務(wù)不就實(shí)現(xiàn)了嗎俺榆?感昼??
哇喔罐脊,想想就激動(dòng)6ㄉぁ!萍桌!
@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()); // 獲取過(guò)期時(shí)間 }
-
那問(wèn)題來(lái)了宵溅,這個(gè)過(guò)期時(shí)間怎么來(lái)的?我們只要分析一下RedisCacheConfiguration這個(gè)對(duì)象中的ttl屬性怎么來(lái)的即可上炎。
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; // 有且僅有一個(gè)構(gòu)造方法层玲,還是私有的,那肯定有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; } }
-
我們看下這個(gè)類(lèi)結(jié)構(gòu)反症,有大量的static辛块,并且返回值是本實(shí)例對(duì)象,那不久就是類(lèi)似于builder的建造器么铅碍?
[圖片上傳失敗...(image-e74fa-1679482374169)]
-
那我們接下來(lái)肯定要搞明白哪里調(diào)用初始化了RedisCacheConfiguration對(duì)象润绵,其實(shí)想也不用想,肯定是利用的springboot的spi機(jī)制胞谈,而且一般就在sprin-boot-autoconfigure.jar里面尘盼。
我們?cè)赗edisCacheConfiguration的defaultCacheConfig方法上點(diǎn)一下憨愉,果然找到了RedisCacheConfiguration類(lèi)。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisConnectionFactory.class) @AutoConfigureAfter(RedisAutoConfiguration.class) @ConditionalOnBean(RedisConnectionFactory.class) @ConditionalOnMissingBean(CacheManager.class)// 如果spring容器中有一個(gè)CacheManager對(duì)象卿捎,這個(gè)類(lèi)就不加載 @Conditional(CacheCondition.class) class RedisCacheConfiguration { // 定義了RedisCacheManager對(duì)象 @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對(duì)象就在這配紫。 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; } }
-
我們?cè)诓襟E9看到,RedisCache對(duì)象是持有RedisCacheConfiguration對(duì)象的午阵,那什么時(shí)候放進(jìn)去的呢躺孝?想必一定和RedisCacheManager有關(guān),因?yàn)槲覀僐edisCacheConfiguration是賦值給了RedisCacheManager底桂。
RedisCacheManager類(lèi)的代碼植袍,我們可以看出在createRedisCache方法,源碼將defaultCacheConfig放進(jìn)了RedisCache對(duì)象
/** * 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); }
修改思路
- 首先我們要自定義類(lèi)籽懦,繼承RedisCache于个,并重寫(xiě)put方法,將ttl設(shè)置成我們的業(yè)務(wù)邏輯(參考步驟9)
- 要自定義RedisCache暮顺,也必須自定義一個(gè)類(lèi)繼承RedisCacheManager厅篓,修改RedisCacheManager的createRedisCache方法,返回自定義的RedisCache對(duì)象捶码。(參考步驟9)
- 最后我們必須自定義RedisCacheManager羽氮,覆蓋springboot中默認(rèn)的RedisCacheManager。
實(shí)現(xiàn)代碼
-
自定義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() { // 實(shí)現(xiàn)自己的業(yè)務(wù)邏輯吧 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容器中宙项,覆蓋默認(rèn)配置
@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(); // 針對(duì)不同cacheName乏苦,設(shè)置不同的過(guò)期時(shí)間 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); } }
總結(jié)
讀懂源碼很重要株扛,整體修改還是比較簡(jiǎn)單的尤筐,就是說(shuō)一下spring-boot源碼寫(xiě)的有一點(diǎn)點(diǎn)坑,各種private洞就,不過(guò)也還好盆繁。