ProxyCachingConfiguration总的来说就是声明了一个SpringCacheAnnotationParser和一个CacheInterceptor。在使用自定义的切点类,在切点前后切入一个CacheInterceptor来实现缓存的逻辑。
@EnableCaching是开启SpringCache的一个总开关,开启时候我们的缓存相关注解才会生效,所以我们@EnableCaching开始作为入口进行分析,
(资料图片仅供参考)
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Import(CachingConfigurationSelector.class) // 这里有一个Import,导入了一个Selector类public @interface EnableCaching { // 是否创建cglib代理,默认为false, 也就是使用jdk动态代理 boolean proxyTargetClass() default false; // 增强模式 默认使用JDK动态代理,引入cglib可以使用ASPECTJ AdviceMode mode() default AdviceMode.PROXY; // 排序字段 int order() default Ordered.LOWEST_PRECEDENCE;}
public class CachingConfigurationSelector extends AdviceModeImportSelector { // ...此处省略一万行代码 // CachingConfigurationSelector继承了AdviceModeImportSelector, 而AdviceModeImportSelector又实现了ImportSelector // 所以我们实现类selectImports,用于返回要导入的配置类列表 @Override public String[] selectImports(AdviceMode adviceMode) { // 如果是jdk动态代理,走getProxyImports逻辑。如果是cglib动态代理,走getAspectJImports逻辑 switch (adviceMode) { case PROXY: return getProxyImports(); case ASPECTJ: return getAspectJImports(); default: return null; } } // 获取要进行自动配置的配置类 private String[] getProxyImports() { List result = new ArrayList<>(3); // 这里添加了两个类,AutoProxyRegistrar(自动代理注册器),ProxyCachingConfiguration(代理缓存配置类) // AutoProxyRegistrar点进去可以发现,里面其实就是提供了registerBeanDefinitions方法用于注册BeanDefinition result.add(AutoProxyRegistrar.class.getName()); // ProxyCachingConfiguration点进去发现,配置类缓存相关的一些Bean(就是SpringCache的一些核心Bean) result.add(ProxyCachingConfiguration.class.getName()); if (jsr107Present && jcacheImplPresent) { result.add(PROXY_JCACHE_CONFIGURATION_CLASS); } return StringUtils.toStringArray(result); } // ...此处省略一万行代码}
CachingConfigurationSelector继承了AdviceModeImportSelector, 而AdviceModeImportSelector又实现了ImportSelector,所以我们实现了selectImports方法,用于返回要导入的配置类列表.
selectImports会去判断,如果是jdk动态代理,走getProxyImports逻辑。如果是cglib动态代理,走getAspectJImports逻辑。
我们直接关注JDK动态代理的方法getProxyImports。这里面添加了两个类AutoProxyRegistrar和ProxyCachingConfiguration。
AutoProxyRegistrar点进去可以发现,里面其实就是提供了registerBeanDefinitions方法用于注册BeanDefinition。
ProxyCachingConfiguration点进去发现,配置类缓存相关的一些Bean(就是SpringCache的一些核心Bean),所以我们会重点关注ProxyCachingConfiguration并着重分析。
@Configuration(proxyBeanMethods = false)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public class ProxyCachingConfiguration extends AbstractCachingConfiguration { // BeanFactoryCacheOperationSourceAdvisor是对CacheOperationSource进行增强,其实就是添加一个拦截器,用于获取相关缓存的注解信息 // 所以有些逻辑会在CacheInterceptor里 @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); if (this.enableCaching != null) { advisor.setOrder(this.enableCaching.getNumber("order")); } return advisor; } // 定义一个CacheOperationSource,主要用于获取类或者方法上的注解。 @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public CacheOperationSource cacheOperationSource() { return new AnnotationCacheOperationSource(); } // 定义了一个拦截器,该拦截器用于用于拦截缓存相关注解,做AOP操作。比如先查询缓存,查询到直接返回,查询不到就执行方法体,将结果写入缓存。 @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) { CacheInterceptor interceptor = new CacheInterceptor(); // 缓存拦截器在这里注入了cacheManager(缓存管理器) interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager); interceptor.setCacheOperationSource(cacheOperationSource); return interceptor; }}
来分析一下BeanFactoryCacheOperationSourceAdvisor
public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { @Nullable private CacheOperationSource cacheOperationSource; // 定义我们自己的切点,缓存操作切点 private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() { // 该切点存在一个方法,获取CacheOperationSource(获取切点的那些注解操作)。 @Override @Nullable protected CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } }; // 使用该方法设置CacheOperationSource,在上一层有设置advisor.setCacheOperationSource(cacheOperationSource); // 把这个数据塞入BeanFactoryCacheOperationSourceAdvisor, 以便于在自定义的切点类CacheOperationSourcePointcut中可以获取 public void setCacheOperationSource(CacheOperationSource cacheOperationSource) { this.cacheOperationSource = cacheOperationSource; } // 设置ClassFilter到CacheOperationSourcePointcut public void setClassFilter(ClassFilter classFilter) { this.pointcut.setClassFilter(classFilter); } // 重写getPointcut。也就是获取切点的方法,因为需要对切点进行增强 @Override public Pointcut getPointcut() { return this.pointcut; }}
BeanFactoryCacheOperationSourceAdvisor继承了AbstractBeanFactoryPointcutAdvisor,重写了Pointcut getPointcut()方法。
使用自定义的切点类CacheOperationSourcePointcut来作为切面的切点。而里面需要用到CacheOperationSource和ClassFilter。在BeanFactoryCacheOperationSourceAdvisor实例化时就已经设置。
而上面又执行了advisor.setAdvice(cacheInterceptor); 其实就是对这个切点添加了一个缓存拦截器,所以核心逻辑就在拦截器里面。
先再来看一下AnnotationCacheOperationSource
public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable { private final boolean publicMethodsOnly; // 缓存注解解析集合 private final Set annotationParsers; public AnnotationCacheOperationSource() { this(true); } public AnnotationCacheOperationSource(boolean publicMethodsOnly) { this.publicMethodsOnly = publicMethodsOnly; // 重点:解析集合从SpringCacheAnnotationParser中获取,这个解析类就是解析注解的核心 this.annotationParsers = Collections.singleton(new SpringCacheAnnotationParser()); } // ...此处省略一万行代码 // 判断是否时候选类 @Override public boolean isCandidateClass(Class> targetClass) { for (CacheAnnotationParser parser : this.annotationParsers) { if (parser.isCandidateClass(targetClass)) { return true; } } return false; } // 重点:查找类级别的CacheOperation列表,就是看标注在类上的@Cacheable,@CacheEvict的集合 @Override @Nullable protected Collection findCacheOperations(Class> clazz) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz)); } // 重点:查找方法级别的CacheOperation列表,就是看标注在方法上的@Cacheable,@CacheEvict的集合 @Override @Nullable protected Collection findCacheOperations(Method method) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(method)); } // ...此处省略一万行代码}
接着看一下SpringCacheAnnotationParser
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable { private static final Set> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8); // 初始化缓存操作的注解集合 static { CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class); CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class); CACHE_OPERATION_ANNOTATIONS.add(CachePut.class); CACHE_OPERATION_ANNOTATIONS.add(Caching.class); } // 解析类级别的注解,封装为CacheOperation集合 @Override @Nullable public Collection parseCacheAnnotations(Class> type) { DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type); return parseCacheAnnotations(defaultConfig, type); } // 解析方法级别的注解,封装为CacheOperation集合 @Override @Nullable public Collection parseCacheAnnotations(Method method) { DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass()); return parseCacheAnnotations(defaultConfig, method); } // 解析注解 @Nullable private Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { Collection ops = parseCacheAnnotations(cachingConfig, ae, false); if (ops != null && ops.size() > 1) { // More than one operation found -> local declarations override interface-declared ones... Collection localOps = parseCacheAnnotations(cachingConfig, ae, true); if (localOps != null) { return localOps; } } return ops; } // 具体解析注解的方法,包含了Cacheable,CacheEvict,CachePut,Caching等 @Nullable private Collection parseCacheAnnotations( DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) { Collection extends Annotation> anns = (localOnly ? AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) : AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS)); if (anns.isEmpty()) { return null; } final Collection ops = new ArrayList<>(1); anns.stream().filter(ann -> ann instanceof Cacheable).forEach( ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann))); anns.stream().filter(ann -> ann instanceof CacheEvict).forEach( ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann))); anns.stream().filter(ann -> ann instanceof CachePut).forEach( ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann))); anns.stream().filter(ann -> ann instanceof Caching).forEach( ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops)); return ops; } // ...此处省略了一万行代码,基本这个类都是解析这些注解并封装为CacheOperation集合}
所以,SpringCacheAnnotationParser的作用就是将这些注解解析出来,并且封装为Collection
ProxyCachingConfiguration总的来说就是声明了一个SpringCacheAnnotationParser和一个CacheInterceptor。在使用自定义的切点类,在切点前后切入一个CacheInterceptor来实现缓存的逻辑。
所以我们就找到的缓存的核心类CacheInterceptor,并且在构造拦截器时,传入了cacheManager作为缓存管理。
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 { // 调用父类的execute方法,实现缓存的逻辑 return execute(aopAllianceInvoker, target, method, invocation.getArguments()); } catch (CacheOperationInvoker.ThrowableWrapper th) { throw th.getOriginal(); } }}
可以看到,这个类很简单,就是拿到原方法的invoke,然后通过父类CacheAspectSupport的execute方法实现缓存逻辑。
关注CacheAspectSupport的execute方法
public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { protected final Log logger = LogFactory.getLog(getClass()); private final Map metadataCache = new ConcurrentHashMap<>(1024); private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(); @Nullable private CacheOperationSource cacheOperationSource; private SingletonSupplier keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new); @Nullable private SingletonSupplier cacheResolver; @Nullable private BeanFactory beanFactory; private boolean initialized = false; @Nullable protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { // 如果bean已经被初始化了,则调用相应的缓存增强 if (this.initialized) { Class> targetClass = getTargetClass(target); CacheOperationSource cacheOperationSource = getCacheOperationSource(); if (cacheOperationSource != null) { // 通过CacheOperationSource,获取所有的CacheOperation列表(就是那一堆标有缓存注解的类和方法的集合) Collection operations = cacheOperationSource.getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { // 调用重载的execute方法 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, handleSynchronizedGet(invoker, key, cache)); } 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); } } // 如果存在@CacheEvict注解、并且标记为在调用前执行,调用processCacheEvicts进行缓存清除操作 processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT); // 如果存在Cacheable注解、调用findCachedItem查询缓存 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); // 如果没有命中缓存,则调用cachePutRequests,存储在List中,后续执行原始方法后会写入缓存 List cachePutRequests = new ArrayList<>(); if (cacheHit == null) { collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests); } Object cacheValue; Object returnValue; // 如果缓存命中且没有@CachePut注解,使用缓存的值作为返回值 if (cacheHit != null && !hasCachePut(contexts)) { // If there are no put requests, just use the cache hit cacheValue = cacheHit.get(); returnValue = wrapCacheValue(method, cacheValue); } // 缓存没有命中或者有@CachePut注解 else { // 调用原始方法作为返回值 returnValue = invokeOperation(invoker); // 将原始方法的返回值作为缓存值 cacheValue = unwrapReturnValue(returnValue); } // 如果有@CachePut注解,则新增到cachePutRequests collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests); // 缓存未命中或者存在@CachePut注解,调用CachePutRequest的apply方法将数据写入缓存 for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(cacheValue); } // 如果有@CacheEvict注解,并且标记为在调用后执行,则还需要执行清除缓存操作 processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); return returnValue; } // 此处省略一万行代码}
总结来说,
如果存在@CacheEvict注解,并且标记在方法执行前执行,就执行清除缓存相关操作。使用findCachedItem获取缓存,缓存没有命中,加入collectPutRequests,后续进行写入缓存操作。如果命中缓存并且没有@CachePut注解,获取命中的值作为方法的返回值如果没有命中,或者包含了@CachePut注解,加入collectPutRequests,后续进行写入缓存操作。遍历cachePutRequests,将需要写入缓存的数据写入缓存如果存在@CacheEvict注解,并且标记在方法执行后执行,就执行清除缓存相关操作。还没完呢,因为我们定义的CacheManager怎么没有用到呢?我们继续跟踪下去,以get缓存方法为例子分析。
关注findCachedItem获取缓存方法
@Nullableprivate Cache.ValueWrapper findCachedItem(Collection contexts) { // 遍历上下文列表 Object result = CacheOperationExpressionEvaluator.NO_RESULT; for (CacheOperationContext context : contexts) { if (isConditionPassing(context, result)) { Object key = generateKey(context, result); // 根据生成的key获取缓存值 Cache.ValueWrapper cached = findInCaches(context, key); if (cached != null) { return cached; } else { if (logger.isTraceEnabled()) { logger.trace("No cache entry for key "" + key + "" in cache(s) " + context.getCacheNames()); } } } } return null;}
关注findInCaches获取缓存方法
@Nullableprivate Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) { // 遍历缓存集合(getCaches),使用缓存的key去和获取缓存 for (Cache cache : context.getCaches()) { // 最终是使用Cache接口的get方法去获取缓存的 Cache.ValueWrapper wrapper = doGet(cache, key); if (wrapper != null) { if (logger.isTraceEnabled()) { logger.trace("Cache entry for key "" + key + "" found in cache "" + cache.getName() + """); } return wrapper; } } return null;}
关注doGet获取缓存方法
@Nullableprotected Cache.ValueWrapper doGet(Cache cache, Object key) { try { return cache.get(key); } catch (RuntimeException ex) { getErrorHandler().handleCacheGetError(ex, cache, key); return null; // If the exception is handled, return a cache miss }}
我们发现,最终是通过Cache接口的get方法去获取缓存的,那么我们只要知道Cache集合对象是在哪里传入进来的就清楚了整个逻辑。
重新回到execute方法
@Nullableprotected 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 operations = cacheOperationSource.getCacheOperations(method, targetClass); if (!CollectionUtils.isEmpty(operations)) { // 这里创建了一个CacheOperationContexts,我们有理由猜测CacheOperationContext.getCaches方法就是在这里面 return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass)); } } } return invoker.invoke();}
跟踪CacheOperationContexts
private class CacheOperationContexts { // 就是一个CacheOperationContext的集合,key是CacheOperation或者其子类 private final MultiValueMap, CacheOperationContext> contexts; // 是否开启了sync=true属性 private final boolean sync; public CacheOperationContexts(Collection extends CacheOperation> operations, Method method, Object[] args, Object target, Class> targetClass) { // 根据CacheOperation集合,方法,参数创建了一个CacheOperationContext集合 this.contexts = new LinkedMultiValueMap<>(operations.size()); for (CacheOperation op : operations) { // 重点:getOperationContext是具体创建CacheOperationContext的方法 this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass)); } // 获取sync属性并赋值给this.sync this.sync = determineSyncFlag(method); } public Collection get(Class extends CacheOperation> operationClass) { Collection result = this.contexts.get(operationClass); return (result != null ? result : Collections.emptyList()); } public boolean isSynchronized() { return this.sync; } // ...此处省略了一万行代码}
关注getOperationContext创建CacheOperationContext
protected CacheOperationContext getOperationContext( CacheOperation operation, Method method, Object[] args, Object target, Class> targetClass) { CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass); // 其实就是实例化一个CacheOperationContext return new CacheOperationContext(metadata, args, target);}
其实就是拿到CacheOperationMetadata(CacheOperation的元数据信息),然后传给CacheOperationContext进行实例化CacheOperationContext。
关注CacheOperationContext的构造方法
上面实例化了CacheOperationContext,所以其构造方法内一定做了写什么事情。比如初始化操作。
// 缓存的集合private final Collection extends Cache> caches;public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) { this.metadata = metadata; this.args = extractArgs(metadata.method, args); this.target = target; // 初始化了缓存名称列表和缓存集合 this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver); this.cacheNames = createCacheNames(this.caches);}
关注getCaches(获取缓存集合)
protected Collection extends Cache> getCaches( CacheOperationInvocationContext context, CacheResolver cacheResolver) { // 这里可以知道是通过CacheResolver来获取的缓存集合 Collection extends Cache> caches = cacheResolver.resolveCaches(context); if (caches.isEmpty()) { throw new IllegalStateException("No cache could be resolved for "" + context.getOperation() + "" using resolver "" + cacheResolver + "". At least one cache should be provided per cache operation."); } return caches;}
关注CacheResolver以及实现类
@FunctionalInterfacepublic interface CacheResolver {// 根据CacheOperationInvocationContext获取缓存集合Collection extends Cache> resolveCaches(CacheOperationInvocationContext> context);}
CacheResolver的抽象实现类AbstractCacheResolver
public abstract class AbstractCacheResolver implements CacheResolver, InitializingBean { // 这里就有CacheManager(缓存管理器)@Nullableprivate CacheManager cacheManager;protected AbstractCacheResolver() {}// 构造注入protected AbstractCacheResolver(CacheManager cacheManager) {this.cacheManager = cacheManager;}// set注入public void setCacheManager(CacheManager cacheManager) {this.cacheManager = cacheManager;} // 获取CacheManagerpublic CacheManager getCacheManager() {Assert.state(this.cacheManager != null, "No CacheManager set");return this.cacheManager;}@Overridepublic void afterPropertiesSet() {Assert.notNull(this.cacheManager, "CacheManager is required");}// 获取缓存集合@Overridepublic Collection extends Cache> resolveCaches(CacheOperationInvocationContext> context) { // 先获取缓存名称Collection cacheNames = getCacheNames(context);if (cacheNames == null) {return Collections.emptyList();}Collection result = new ArrayList<>(cacheNames.size());// 遍历名称,通过CacheManager获取缓存,加入缓存集合for (String cacheName : cacheNames) {Cache cache = getCacheManager().getCache(cacheName);if (cache == null) {throw new IllegalArgumentException("Cannot find cache named "" +cacheName + "" for " + context.getOperation());}result.add(cache);}return result;}// 获取缓存名称集合@Nullableprotected abstract Collection getCacheNames(CacheOperationInvocationContext> context);}
而我们的CacheManager默认使用SimpleCacheManager,我们注入了CustomRedisCacheManager, 所以会调用CustomRedisCacheManager的getCache方法获取缓存。
而getCache方法在父类AbstractCacheManager已经实现了。
// SpringCache最底层的数据结构就是以一个ConcurrentMapprivate final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16);@Override@Nullablepublic Cache getCache(String name) { // 先从cacheMap中获取Cache, 获取到了直接返回 Cache cache = this.cacheMap.get(name); if (cache != null) { return cache; } // 获取不到,使用双重检测所写入数据到cacheMap Cache missingCache = getMissingCache(name); if (missingCache != null) { // Fully synchronize now for missing cache registration synchronized (this.cacheMap) { cache = this.cacheMap.get(name); if (cache == null) { cache = decorateCache(missingCache); this.cacheMap.put(name, cache); updateCacheNames(name); } } } return cache;}
到了这里,SpringCache的流程我们就真正的清楚了。
所以,SpringCache的源码分析就到此为止了。
标签: