PS: spring-data-redis在springboot2.0版本时有一次较大的逻辑功能改动,当前文章基于springboot2.1.13版本
Spring缓存支持
Spring定义了org.springframework.cache.CacheManager 和 org.springframework.cache.Cache 接口来统一不同缓存技术。
其中CacheManager是Spring提供的各种缓存技术抽象接口,内部使用Cache接口进行缓存的增删改查操作,我们一般不会直接和Cache打交道。
针对不同的缓存技术,Spring有不同的CacheManager实现类,定义如下表:
CacheManager | 描述 |
SimpleCacheManager | 使用简单的Collection存储缓存数据,用来做测试用 |
ConcurrentMapCacheManager | 使用ConcurrentMap存储缓存数据 |
EhCacheCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Google Guava的GuavaCache作为缓存技术 |
JCacheCacheManager | 使用JCache(JSR-107)标准的实现作为缓存技术,比如Apache Commons JCS |
RedisCacheManager | 使用Redis作为缓存技术 |
声明式缓存注解
Spring提供5个注解来声明缓存规则,如下表所示:
注解 | 说明 |
@Cacheable | 方法执行前先看缓存中是否有数据,如果有直接返回。如果没有就调用方法,并将方法返回值放入缓存 |
@CachePut | 无论怎样都会执行方法,并将方法返回值放入缓存 |
@CacheEvict | 将数据从缓存中删除 |
@Caching | 可通过此注解组合多个注解策略在一个方法上面 |
@CacheConfig | 在类级别共享一些常见的缓存相关设置。 |
@Cacheable 、@CachePut 、@CacheEvict都有value属性,指定要使用的缓存名称,而key属性指定缓存中存储的键。
@Cacheable
这个注解含义是方法结果会被放入缓存,并且一旦缓存后,下一次调用此方法,会通过key去查找缓存是否存在,如果存在就直接取缓存值,不再执行方法。
参数 | 解释 |
cacheNames | 缓存名称 |
value | 缓存名称的别名 |
condition | Spring SpEL 表达式,判断条件用来确定是否缓存 |
key | SpEL 表达式,用来动态计算key |
keyGenerator | Bean 名字,用来自定义key生成算法,跟key不能同时用 |
unless | SpEL 表达式,用来否决缓存,作用跟condition相反 |
sync | 多线程同时访问时候进行同步 |
在计算key、condition或者unless的值得时候,可以使用到以下的特有的SpEL表达式
表达式 | 解释 |
#result | 表示方法的返回结果 |
#root.method | 当前方法 |
#root.target | 目标对象 |
#root.caches | 被影响到的缓存列表 |
#root.methodName | 方法名称简称 |
#root.targetClass | 目标类 |
#root.args[x] | 方法的第x个参数 |
#参数名称 | 任何方法参数的名称。如果名称不可用(可能是由于没有调试信息),则参数名称也可以在root.args[x]x代表参数索引 |
@CachePut
无论怎样都会执行方法,并将方法返回值放入缓存
@CacheEvict
该注解在执行完方法后会触发一次缓存evict操作,参数除了@Cacheable里的外,还有个特殊的allEntries, 表示将清空缓存中所有的值。(尽量不要使用allEntries)
查看源码:org.springframework.cache.interceptor.CacheAspectSupport
private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) { //验证是否为删除缓存中的全部值
logInvalidating(context, operation, null);
//doClear中删除数据使用的为keys命令,会阻塞redis
doClear(cache);
}
else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
doEvict(cache, key);
}
}
}
快速入门
1、maven引入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、yam中加入redis的配置
3、初始化RedisConfig
FastJsonHttpMessageConverter还需要引入fastjson的依赖。
@Configuration
@EnableCaching //开启redis配置
public class RedisConfig extends CachingConfigurerSupport {
public RedisConfig() {
super();
}
@Bean
public <T> RedisTemplate<String, T> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, T> template = new RedisTemplate();
template.setConnectionFactory(connectionFactory);
//使用fastJson作为默认的序列化方式
GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
template.setDefaultSerializer(genericFastJsonRedisSerializer);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setValueSerializer(genericFastJsonRedisSerializer);
template.setKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(genericFastJsonRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//初始化一个不加锁验证的RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
//声明一个需要加锁验证的CacheWriter,采用当前声明将会验证执行过程中是否加锁及锁等待
// RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration defaultCacheConfig = this.getRedisCacheConfigurationWithTtl(10);
//初始化RedisCacheManager
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, this.getRedisCacheConfigurationMap());
return cacheManager;
}
/**
* 设置redis缓存策略
*
* @return
*/
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
//添加自己定义的
redisCacheConfigurationMap.put(RedisKeys.WEPLAY_CONSTELLATION_CONFIG, this.getRedisCacheConfigurationWithTtl(1800L));
return redisCacheConfigurationMap;
}
/**
* 设置redis缓存配置
*
* @param seconds 单位:秒
* @return
*/
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Long seconds) {
//设置CacheManager的值序列化方式为json序列化
RedisSerializer<Object> jsonSerializer = new GenericFastJsonRedisSerializer();
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer);
//设置过期时间
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(pair).entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
// 1、需要先定义一个converter 转换器
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
// 2、添加fastJson 的配置信息,比如:是否要格式化返回的json数据
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteEnumUsingToString, SerializerFeature.WriteNullListAsEmpty);
// 中文乱码解决方案
List<MediaType> mediaTypes = new ArrayList<>();
//设定json格式且编码为UTF-8
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(mediaTypes);
// 3、在convert 中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
// 4、将convert 添加到converters当中
HttpMessageConverter<?> converter = fastConverter;
return new HttpMessageConverters(converter);
}
@Override
public KeyGenerator keyGenerator() {
KeyGenerator keyGenerator = new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
StringBuilder sb = new StringBuilder();
sb.append(o.getClass().getName());
sb.append(method.getName());
for (Object obj : objects) {
sb.append(Optional.ofNullable(obj).map(p -> p.toString()).orElse("-"));
}
return sb.toString();
}
};
return keyGenerator;
}
@Override
public CacheResolver cacheResolver() {
return super.cacheResolver();
}
@Override
public CacheErrorHandler errorHandler() {
return super.errorHandler();
}
/**
* 生成key
*
* @param args
* @return
*/
public static String genKey(Object... args) {
return Optional.ofNullable(args).map(ag -> {
StringBuilder sb = new StringBuilder();
int len = ag.length;
for (int i = 0; i < len; ++i) {
sb.append(Optional.ofNullable(ag[i]).map(Object::toString).map(s -> s.concat("-")).orElse("[]"));
}
return sb.toString();
}).orElse("[]");
}
}
过期时间,序列化方式由此类决定 RedisCacheConfiguration,可以覆盖此类达到自定义配置。默认配置为RedisCacheConfiguration.defaultCacheConfig() ,它配置为永不过期,key 为 String 序列化,并加上了一个前缀做为命名空间,value 为 Jdk 序列化,所以你要存储的类必须要实现 java.io.Serializable
存储的 key 值的生成由 KeyGenerator 决定,可以在各缓存注解上进行配置,默认使用的是 SimpleKeyGenerator
缓存穿透问题:
使用@Cacheable在并发访问时,会多次访问数据库。在spring4.3版本后增加了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。
源码
从 @EnableCaching 开始,可以看到导入了一个选择导入配置的配置类,默认使用 PROXY 模式
包路径:org.springframework.cache.annotation.CachingConfigurationSelector
public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching>
导入配置
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
进入getAspectJImports()方法查看代码
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是核心
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
//BeanFactoryCacheOperationSourceAdvisor 是 CacheOperationSource 的一个增强器
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource());
advisor.setAdvice(cacheInterceptor());
if (this.enableCaching != null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
//CacheOperationSource 主要提供查找方法上缓存注解的方法 findCacheOperations
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
return new AnnotationCacheOperationSource();
}
//CacheInterceptor 它是一个 MethodInterceptor 在调用缓存方法时,会执行它的 invoke 方法
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor() {
CacheInterceptor interceptor = new CacheInterceptor();
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);
}
};
try {
//aopAllianceInvoker 是一个函数式接口,会执行你的真实方法
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
}
进入 execute 方法,可以看到这一层只是获取到所有的缓存操作集合,@CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 然后把其配置和当前执行上下文进行绑定成了 CacheOperationContexts
@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) {
//获取到所有的缓存操作集合,@CacheConfig,@Cacheable,@CachePut,@CacheEvict,@Caching 然后把其配置和当前执行上下文进行绑定成了 CacheOperationContexts
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();
}
再进入 execute 方法,这里就是整合的核心处理逻辑了
@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
if (contexts.isSynchronized()) {
//如果使用的有sync参数将会单独进行处理
}
// Process any early evictions
//先做缓存清理工作
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// 查询缓存中内容
// Check if we have a cached item matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
//如果缓存没有命中,收集 put 请求,后面会统一把需要放入缓存中的统一应用
// Collect puts from any @Cacheable miss, if no cached item is found
List<CachePutRequest> cachePutRequests = new LinkedList<>();
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);
}
else {
//缓存没有命中,执行真实方法
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
//把前面收集到的所有 putRequest 数据放入缓存
// 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;
}
无sync逻辑
将数据放入缓存cachePutRequest.apply(cacheValue) 的方法代码
public void apply(@Nullable Object result) {
if (this.context.canPutToCache(result)) {
for (Cache cache : this.context.getCaches()) {
doPut(cache, this.key, result);
}
}
}
doPut方法内部调用到了Cache.put()方法,其中Cache类为接口类,其最终实现在RedisCache中
@Override
public void put(Object key, @Nullable Object value) {
//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());
}
接着看下CacheWriter的put方法, 调用的是org.springframework.data.redis.cache.DefaultRedisCacheWriter方法内部的put方法
@Override
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
Assert.notNull(value, "Value must not be null!");
//函数式接口,设置缓存
execute(name, connection -> {
//如果设置的有超时时间则设置过期时间
if (shouldExpireWithin(ttl)) {
connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
} else {
//无过期时间则直接设置
connection.set(key, value);
}
return "OK";
});
}
综上来看,直接使用@Cacheable存在缓存击穿的问题,在查询设置缓存的时候,全程未加锁。所以会多次执行数据库查询和设置缓存。为解决上述问题spring-context4.3版本引入sync参数,当设置sync参数时会执行excute中的sync部分代码块。
sync逻辑
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
//是否满足条件
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
//将KEY有spel表达式解析出值
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
//返回数据 函数式接口格式化value信息
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);
}
}
从上面的代码来看并没有实现同步相关的代码,实际上加锁信息在cache.get()的实现中
@Override
@SuppressWarnings("unchecked")
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = get(key);
if (result != null) {
return (T) result.get();
}
T value = valueFromLoader(key, valueLoader);
put(key, value);
return value;
}
从上面的代码可以看到这个get方法使用了synchronized关键字,使其变为线程阻塞的获取,所以增加了sync参数后只会获取一次
虽然增加了sync参数,但是由于使用的synchronized关键字只是当前节点的机器变为阻塞获取,如果是分布式系统仍然会存在多次请求进入查询的情况,但是相对于缓存击穿的场景还是优化了很多
@CacheEvit逻辑
在CacheAspectSupport.execute执行完成后将验证是否为@CacheEvit注解
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
其内部代码为
private void processCacheEvicts(
Collection<CacheOperationContext> contexts, boolean beforeInvocation, @Nullable Object result) {
for (CacheOperationContext context : contexts) {
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
//执行删除缓存
performCacheEvict(context, operation, result);
}
}
}
@CacheEvit注解中allEntries默认值为false,执行器根据allEntries的值设置执行不同的处理方法
private void performCacheEvict(
CacheOperationContext context, CacheEvictOperation operation, @Nullable Object result) {
Object key = null;
for (Cache cache : context.getCaches()) {
//验证allEntries参数是否为true
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
//执行的为keys命令检查符合条件的数据,最后再执行del命令
doClear(cache);
}
else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
//最终执行为redis的del命令
doEvict(cache, key);
}
}
}
其中doClear方法主要调用的为DefaultRedisCacheWriter.clean方法
@Override
public void clean(String name, byte[] pattern) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(pattern, "Pattern must not be null!");
execute(name, connection -> {
boolean wasLocked = false;
try {
//CacheManager声明是否为lock,如果为lock则返回true
if (isLockingCacheWriter()) {
//加锁,key的名称为cacheName+"~lock",设置方式的setNx
doLock(name, connection);
wasLocked = true;
}
//使用keys命令找出符合条件的数据
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
.toArray(new byte[0][]);
if (keys.length > 0) {
//执行删除
connection.del(keys);
}
} finally {
//解锁
if (wasLocked && isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
return "OK";
});
}
看了解锁的代码可以发现在使用allEntries参数时还是存在很大的问题,在dolock方法中加锁是没有超时时间的,如果在加完锁之后服务宕机,重启后重新进入这个逻辑将会无限sleep获取锁。对于allEntries还是尽量少使用。
execute中执行锁状态验证的代码
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
if (!isLockingCacheWriter()) {
return;
}
try {
while (doCheckLock(name, connection)) {
Thread.sleep(sleepTime.toMillis());
}
} catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other participants to react.
Thread.currentThread().interrupt();
throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
ex);
}
}
CacheAspectSupport
看完了执行流程,现在看一下CacheInterceptor 的超类 CacheAspectSupport ,因为可以不设置 cacheManager 就可以使用,查看默认的 cacheManager是在哪设置的
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
// ....
}
BeanFactoryAware 用来获取 BeanFactory
InitializingBean 用来管理 Bean 的生命周期,可以在 afterPropertiesSet后添加逻辑
SmartInitializingSingleton 实现该接口后,当所有单例 bean 都初始化完成以后, 容器会回调该接口的方法 afterSingletonsInstantiated
在 afterSingletonsInstantiated 中,果然进行了 cacheManager 的设置,从 IOC 容器中拿了一个 cacheManger
setCacheManager(this.beanFactory.getBean(CacheManager.class));
那这个 CacheManager 是谁呢 ,可以从RedisCacheConfiguration类知道答案 ,在这里面配置了一个 RedisCacheManager
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return this.customizerInvoker.customize(builder.build());
}
从 determineConfiguration() 方法中可以知道 cacheManager 的默认配置
最后看一下,它的切点是如何定义的,即何时会调用 CacheInterceptor 的 invoke 方法。
切点的配置是在 BeanFactoryCacheOperationSourceAdvisor 类中,返回一个这样的切点 CacheOperationSourcePointcut ,覆写 MethodMatcher 中的 matchs ,如果方法上存在注解 ,则认为可以切入。