spring在3.1版本中增加了缓存的支持。但是不进行Cache的缓存策略的维护,只是提供了一个Wrapper,提供一套对外一致的API。
spring主要提供了6个注解来对缓存进行操作。
注解 | 作用 |
---|---|
@EnableCaching |
是否开启缓存功能 |
@CacheConfig |
缓存的配置 |
@Cacheable |
声明一个方法的调用结果可以被缓存,先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中 |
@CacheEvict |
调用方法时会从缓存中移除相应的数据: |
@CachePut |
调用方法时会自动把相应的数据放入缓存: |
@Caching |
@Cacheable ,@CacheEvict ,@CachePut 三个注解集合 |
@EnableCaching
如何开启缓存的功能@Import
注解派生出来的@EnableCaching
看@EnableCaching
注解上面有一个@Import
注解,并且指定的类是CachingConfigurationSelector
类。对于@Import
注解熟悉的就会知道他的作用。这里不对这个注解进行讲解,可以参考前面的一篇博文spring源码解析------@Import注解解析。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
/**
* 指示是否创建基于子类(CGLIB)的代理,而不是标准的基于Java接口的代理。
*/
boolean proxyTargetClass() default false;
/**
* 代理模式
*/
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
CachingConfigurationSelector
跟AdviceModeImportSelector
注入bean到容器 从上面分析,这里直接进入到CachingConfigurationSelector
类中。
public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
......
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
//默认的是PROXY类型的,返回需要支持这个类型需要注入的bean
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
private String[] getProxyImports() {
List<String> result = new ArrayList<>(3);
//启动aop的注解
result.add(AutoProxyRegistrar.class.getName());
//代理配置类,这里会注入,
// BeanFactoryCacheOperationSourceAdvisor类
// CacheInterceptor类
result.add(ProxyCachingConfiguration.class.getName());
if (jsr107Present && jcacheImplPresent) {
//注入BeanFactoryJCacheOperationSourceAdvisor
//JCacheInterceptor类
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return StringUtils.toStringArray(result);
}
......
}
这里发现没有应该实现的ImportSelector
接口的selectImports
方法。这个方法的实现在其父类AdviceModeImportSelector
中。进入到AdviceModeImportSelector
的selectImports
方法中。
public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
//获取AdviceModeImportSelector中的注解泛型的Class对象
Class<?> annType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
Assert.state(annType != null, "Unresolvable type argument for AdviceModeImportSelector");
//从传入的importingClassMetadata对象中获取对应的Class类型的注解的内部属性
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
if (attributes == null) {
throw new IllegalArgumentException(String.format(
"@%s is not present on importing class '%s' as expected",
annType.getSimpleName(), importingClassMetadata.getClassName()));
}
//获取属性中的代理类型属性“mode”的值
AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName());
//根据代理类型获取需要注入的bean
String[] imports = selectImports(adviceMode);
if (imports == null) {
throw new IllegalArgumentException("Unknown AdviceMode: " + adviceMode);
}
return imports;
}
结合上面的两段代码进行说明顺序:
AdviceModeImportSelector
的selectImports
方法中。这个类会获取指定的泛型注解中的属性,然后调用由子类来实现的selectImports
方法,选择需要注入容器的bean。CachingConfigurationSelector
中实现了selectImports
。这个方法中会根据@EnableCaching
注解中的mode
属性来选择注入不同的bean。这里默认情况下mode
属性是PROXY
类型的,所以进入到getProxyImports
方法中。getProxyImports
方法中。会向容器中注入两个bean,AutoProxyRegistrar
,ProxyCachingConfiguration
。另外一个ProxyJCacheConfiguration
类型的bean在是当前类的加载器是javax.cache.Cache
跟org.springframework.cache.jcache.config.ProxyJCacheConfiguration
父类的时候会被注册。这里说明一下步骤3注册的前两个bean 的作用:
bean | 作用 |
---|---|
AutoProxyRegistrar |
缓存的值的保存跟获取,会用到aop,这个类是启动aop。是@EnableAspectJAutoProxy 注解实现类 |
ProxyCachingConfiguration |
缓存的配置信息类 |
ProxyCachingConfiguration
缓存注解拦截相关的配置 ProxyCachingConfiguration
会在CachingConfigurationSelector
中被加入到容器中,这个类主要包含了缓存的一些配置。
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
//缓存操作的Advisor,间接继承了AbstractPointcutAdvisor,能返回指定的切点
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
//设置缓存操作源,也就是切点源,AOP的pointcut属性
advisor.setCacheOperationSource(cacheOperationSource());
//设置advice,拦截器
advisor.setAdvice(cacheInterceptor());
if (this.enableCaching != null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
//返回注解操作缓存的操作源,AnnotationCacheOperationSource的父类AbstractFallbackCacheOperationSource里面包含了对注解相关的解析
return new AnnotationCacheOperationSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor() {
//创建对应的拦截器,aop时候拦截的时候用到
CacheInterceptor interceptor = new CacheInterceptor();
//设置对应的配置
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
//为缓存的切面这只操作源
interceptor.setCacheOperationSource(cacheOperationSource());
return interceptor;
}
}
可以看到这里面主要配置的东西就是跟AOP相关的信息,主要就是切点源以及拦截器。关于AOP相关的可以看看前面的文章Spring的AOP的解析——AOP的自定义组件,对于AOP的讲解还是比较详细的。
上面的配置中除了表面看到的AOP相关的配置,这里还需要注意一点就是,在CacheInterceptor
在创建之后调用了一个configure
方法,而这个方法里面传入了四个参数,分别是errorHandler
,keyGenerator
,cacheResolver
以及cacheManager
。这里说明一下四个参数的作用,然后一一进行分析。
参数对应的对象 | 作用 |
---|---|
errorHandler |
在对应的缓存操作出错的时候进行处理,可以制定错误类型 |
keyGenerator |
缓存键生成器。用于根据给定的方法*(用作上下文)及其参数创建键 |
cacheResolver |
确定缓存信息的缓存对象类型 |
cacheManager |
缓存管理器,用于获取缓存的 |
public void configure(
@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,
@Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
//设置errorHandler,如果没有指定errorHandler则使用SimpleCacheErrorHandler作为默认的错误处理器
this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
//设置keyGenerator,如果没有指定keyGenerator则使用默认的SimpleKeyGenerator作为默认的缓存key生成器
this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
//设置cacheResolver,如果没有指定cacheResolver则根据cacheManager来生成SimpleCacheResolver
this.cacheResolver = new SingletonSupplier<>(cacheResolver,
() -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
}
CacheErrorHandler
缓存操作错误的处理器 CacheErrorHandler
是用处理在操作缓存时抛出的指定RuntimeException
类型异常的一个接口类,其中定义了四个四个方法分别对应对缓存的四种操作的时候可能出错的操作。
方法名 | 操作 |
---|---|
handleCacheGetError |
处理在获取缓存操作时抛出的指定异常 |
handleCachePutError |
处理在更新缓存操作时抛出的指定异常 |
handleCacheEvictError |
处理在清除指定key缓存操作时抛出的指定异常 |
handleCacheClearError |
处理在清楚指定Cache缓存操作时抛出的指定异常 |
从上面的configure
方法片段中可以看出这个类默认实现是SimpleCacheErrorHandler
,在这个实现类中,对于错误的处理逻辑很简单就是简单的抛出我们指定的异常。
public class SimpleCacheErrorHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
throw exception;
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, @Nullable Object value) {
throw exception;
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
throw exception;
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
throw exception;
}
}
KeyGenerator
缓存的key生成器 跟类名的意思一样,KeyGenerator
的作用就是生成缓存的key用的。这个接口只有一个方法generate
方法用来生成一个缓存的key。在上面的configure
方法中设置了在没有指定的情况下使用的是SimpleKeyGenerator
类。看看这个类里面的方法。
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
//生成key对象
return generateKey(params);
}
/**
* Generate a key based on the specified parameters.
*/
public static Object generateKey(Object... params) {
//如果生成key的时候的参数为空的,那就直接返回一个空的SimpleKey对象
if (params.length == 0) {
return SimpleKey.EMPTY;
}
//如果参数只有一个并且不是数组类型的,则直接返回原对象
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
//如果参数是多个,则将这些参数保存到一个Object类型的数组中,然后保存到SimpleKey对象中,并返回SimpleKey对象
return new SimpleKey(params);
}
}
可以看到当key参数是多个或者为空的时候,都是用的SimpleKey
对象作为key的。如果只有一个参数,那么就用这个参数作为key。
CacheResolver
确定拦截方法调用的缓存 这个接口中也是只有一个方法resolveCaches
,这个方法的作用就是从缓存操作的上下文获取对应的Cache
对象,在后续的对缓存的保存,删除方法拦截的时候获取操作对象用。
@FunctionalInterface
public interface CacheResolver {
Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}
这个类的子类AbstractCacheResolver
实现了InitializingBean
接口的afterPropertiesSet
方法。在这个方法里面会检查CacheManager
是不是null。说明CacheManager
是必须的,这个类后面会进行讲解。
在初始化配置的时候,如果没有指定CacheResolver
的实现,那么就会使用的默认的SimpleCacheResolver
类。SimpleCacheResolver
类只实现了一个获取根据上下文获取缓存名的方法getCacheNames
。而CacheResolver
接口中resolveCaches
是AbstractCacheResolver
实现的
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
//调用需要子类实现的getCacheNames获取缓存的名称
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
}
Collection<Cache> result = new ArrayList<>(cacheNames.size());
//依次从CacheManager中根据缓存的cacheName获取Cache对象,并保存到要返回的集合中
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;
}
CacheManager
缓存对象的管理器 CacheManager
用来管理Cache
对象的接口类,主要提供了两个方法分别获取缓存的名跟缓存对象。
public interface CacheManager {
//根据缓存的名称获取指定的Cache对象
@Nullable
Cache getCache(String name);
Collection<String> getCacheNames();
}
这个对象是必须的有实例对象的,在AbstractCacheResolver
中会检查这个对象是不是为null。配置的时候CacheManager
默认的是SimpleCacheManager
。spring中还增加了对ehcache以及JSR-107的Jcache的支持,以及一个可以配置多个CacheManager
的CompositeCacheManager
,只不过需要配置。这里列举一下这些持续需要配置的类
缓存类型 | 对应的配置类 |
---|---|
EhCache | EhCacheCacheManager |
Jcache | JCacheCacheManager |
复合 | CompositeCacheManager |
CacheInterceptor
实现对缓存的设置获取以及删除 CacheInterceptor
实现类MethodInterceptor
可以拦截前面在配置的时候设定的切点,这里再次把前面的配置展示说明一下,因为这里比较难以理解。
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
//缓存操作的Advisor,间接继承了AbstractPointcutAdvisor,能返回指定的切点
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
//设置缓存操作源,也就是切点源,AOP的pointcut属性
advisor.setCacheOperationSource(cacheOperationSource());
//设置advice,拦截器
advisor.setAdvice(cacheInterceptor());
......
}
}
setCacheOperationSource
方法 这里创建的BeanFactoryCacheOperationSourceAdvisor
对象间接实现类PointcutAdvisor
接口的getPointcut
方法,这个方法在AOP确定advisor
能否在代理目标类上适用的时候会调用具体在AopUtils
里面,而setCacheOperationSource
方法间接设置了pointcut
属性。这里截取部分代码,具体的还是可以去看前面的aop相关的文章spring源码解析------@Import注解解析。
public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
//如果是IntroductionAdvisor类型的,则直接获取到ClassFilter之后然后进行匹配返回结果
if (advisor instanceof IntroductionAdvisor) {
return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
}
//如果是PointcutAdvisor类型的
else if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pca = (PointcutAdvisor) advisor;
//如果是PointcutAdvisor类型的需要获取到对应的Pointcut,然后分别对Class跟method进行校验看是否合适
return canApply(pca.getPointcut(), targetClass, hasIntroductions);
}
else {
// It doesn't have a pointcut so we assume it applies.
return true;
}
}
setAdvice
方法 后面还有一个setAdvice
方法。这个方法就是将CacheInterceptor
加入到spring的候选的Advice
对象中。这里设置了之后,后面就能对缓存的操作方法进行拦截。
pointcut
对应的CacheOperationSourcePointcut
类 前面说到了AopUtil中会获取设置的Pointcut
后会先获取其中的ClassFilter
并调用matches
方法检查当前需要被代理的类是否合适PointcutAdvisor
,然后获取其中的MethodMatcher
的matches
方法检查对应的方法是否合适PointcutAdvisor
。
而配置中CacheOperationSourcePointcut
就是对应的Pointcut
,这里看一下这个类。
abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
protected CacheOperationSourcePointcut() {
//设置Pointcut用的ClassFilter
setClassFilter(new CacheOperationSourceClassFilter());
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
//获取设置的CacheOperationSource,在配置的时候设置的是AnnotationCacheOperationSource类
CacheOperationSource cas = getCacheOperationSource();
//检查是否存在对应的目标类的方法上面是否存在,缓存操作
return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
}
......
private class CacheOperationSourceClassFilter implements ClassFilter {
@Override
public boolean matches(Class<?> clazz) {
if (CacheManager.class.isAssignableFrom(clazz)) {
return false;
}
//获取设置的CacheOperationSource,在配置的时候设置的是AnnotationCacheOperationSource类
CacheOperationSource cas = getCacheOperationSource();
//检查目标类是不是候选类,这个isCandidateClass最后调用的是,SpringCacheAnnotationParser的isCandidateClass方法
return (cas == null || cas.isCandidateClass(clazz));
}
}
}
可以看到对应的匹配的方法都在这个类里面。这些都决定是否进行拦截。
&esmp;现在正式进入到CacheInterceptor
中进行分析。
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);
}
};
try {
//将方法执行的结果传入父类方法中进行缓存的操作
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
}
经过前面的分析已经知道,能够进入到这里的方法都是贴有那些缓存操作@Cacheable
,@CacheEvict
等注解的方法。在这里主要就是调用那些方法,获取到方法的结果然后,进入到父类CacheAspectSupport
中进一步处理。
CacheAspectSupport
操作缓存 现在直接进入到被CacheInterceptor
调用的execute
方法
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
//检查切面是否已经准备好了
if (this.initialized) {
//获取目标类
Class<?> targetClass = getTargetClass(target);
//获取cacheOperationSource默认的是AnnotationCacheOperationSource,父类是AbstractFallbackCacheOperationSource
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
//获取目标类的目标方法上的缓存相关的注解
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
/**
* invoker为贴有注解方法的调用结果
* method 为贴有注解的方法
*/
return execute(invoker, method,
//根据注解上的相关的信息生成CacheOperationContexts,指定 CacheResolver跟 KeyGenerator
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invoker.invoke();
}
上面的这个方法的作用主要就是获取当前贴有注解的方法上的所有的缓存操作的注解包括@Cacheable
,@CacheEvict
,@CachePut
,@Caching
这些注解,生成一个缓存操作的上下文,在生成的上下文中会包含生成的Cache
对象,这里不对生成上下文的过程进行讲解。生成上下文信息之后,然后调用另外的一个execute
方法进行处理。下面的execute
才是创建Cache
对象的位置。
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)) {
//生成key
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
//从缓存操作上下文中获取cache
Cache cache = context.getCaches().iterator().next();
try {
//调用invoke方法,然后把调用的结果保存起来
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
//如果不需要缓存,则直接调用方法
return invokeOperation(invoker);
}
}
// Process any early evictions
//执行@CacheEvict注解的处理逻辑类CacheEvictOperation,如果CacheEvictOperation=true
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached item matching the conditions
//执行@Cacheable注解的处理逻辑类CacheableOperation,获取对应的缓存数据
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注解收集不到chache(condition 通过,且key对应的数据不在缓存)
if (cacheHit == null) {
//如果缓存不存在,但是存在@CachePut操作,则将缓存的值放到cachePutRequests中
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
//如果CachePutRequest不是空则说明存在缓存,并且没有CachePut操作,则直接从缓存获取。然后将缓存值包装未返回值
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
//todo 如果没有缓存或者说存在CachePut操作,这时候调用方法获取方法的返回值,然后将返回值包装为缓存值
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
//将缓存的值放到cachePutRequests中
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
//将cachePutRequests中的值一次保存到缓存中
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
//执行@CacheEvict,方法调用之后
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
上面的这些操作的步骤进行讲解一下:
@CacheEvict
(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有@Cacheable
(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)@CachePut
操作,那么将查找@Cacheable
的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)@CachePut
操作(如果condition 通过),那么放入cachePutRequests@CacheEvict
(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有 这里需要注意2/3/4步:
如果有@CachePut
操作,即使有@Cacheable
也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut
和@Cacheable
;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景);不过个人感觉还是不要考虑这个好,让用户来决定如何使用,否则一会介绍的场景不能满足。
到这里@EnableCaching
,@Cacheable
,@CacheEvict
,@CachePut
的实现原理就完毕了。