一、从Java缓存规范(JSR107)说起
- 在JSR107中,定义了5个核心的接口,分别是CachingProvider, CacheManager, Cache, Entry和Expiry。具体解释如下:
- CachingProvider:CacheingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
- CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache。这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CacheingProvider所拥有。
- Cache:Cache是一个类似Map数据结构并临时存储以Key为索引的值,一个Cache仅被一个CacheManger所拥有。
- Entry:存储在Cache中的一个key-value对
- Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目不能访问、更新、删除。缓存有效期可以通过ExpiryPolicy设置。
- 5个核心接口关系图
二、Spring的缓存抽象
- 在Spring3.1之后,Spring定义了
org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术。并支持使用JCache(JSR107)注解简化我们的开发。- Cache:缓存接口,定义统一的缓存操作。方便对不同的缓存组件进行整合。只需要实现该接口即可。实现类有:
RedisCache
、ConcurrentMapCache
等。 - CacheManager:缓存管理器。管理各种缓存(Cache)组件。一个缓存管理器可以管理多个Cache组件
- Cache:缓存接口,定义统一的缓存操作。方便对不同的缓存组件进行整合。只需要实现该接口即可。实现类有:
- 关系图
- 几个重要概念以及缓存注解解释
- Cache:这个上面也说过了,是Spring定义的缓存接口,用于统一管理各种缓存组件,每个缓存组件都有一个唯一的名字。
- CacheManager:这个也在上面说了,是Spring定义的缓存管理器,管理Cache的。一个缓存管理器可以管理多个Cache。
- @Cacheable:配置在方法定义上,作用就是根据方法的请求参数对其结果进行缓存。可以看成类似<方法参数,结果>结构的Map
- @CacheEvict:清空缓存
- @CachePut:保证方法被调用,但是又希望结果被缓存。
- @EnableCaching:开启基于注解的缓存
- keyGenerator:缓存数据时Key生成的策略
- serialize:缓存数据时value序列化策略。
具体操作将在代码中给出
- @Cacheable注解主要参数解释
- value/cacheNames:指定缓存的名称,在Spring配置文件中定义。必须指定至少一个。例如:
@Cacheable(value="myCache")或value={"cache1","cache2"}
- key:缓存的key,可以为空,为空时,则按照方法的所有参数进行组合。若指定,则要按照SpELl表达式编写。
- 常用的SpEl表达式
- 获取当前被调用的方法名:#root.methodName
- 当前被调用的方法:#root.method.name
- 当前被调用的目标对象:#root.target
- 当前被调用的目标对象类:#root.targetClass
- 当前被调用的方法的参数列表:#root.args[0]/#参数名/#p0/#a0 (其中0代表参数的索引,从0开始)
- 当前方法调用使用的缓存对象:#root.caches[0].name(因为可以指定多个缓存,所以是数组形式)
- 当前方法执行后的返回值(仅对当前方法执行之后的判断有效):#result
- 常用的SpEl表达式
- keyGenerator:key的生成器,可以自己指定key的生成器的组件id。keyGenerator和key二选一使用
- cacheManager:指定缓存管理器,或者使用cacheResolver指定获取解析器。
- condition:指定符合条件的情况下才缓存,只有返回true才会进行缓存。也是使用SpEL表达式
- unless:是否缓存,当unless指定的条件为true,方法的返回值就不会被缓存,可以取到结果进行判断
- sync:是否使用异步模式
- value/cacheNames:指定缓存的名称,在Spring配置文件中定义。必须指定至少一个。例如:
三、Spring缓存抽象的使用
- 新建一个SpringBoot项目,勾选core里面的cache组件,然后再勾选web组件。为了简单起见,我们就不连接数据库了。就用HashMap造一些数据。
- 以下为Dao的代码
1 |
|
- 以下为Service的代码,这里暂时演示
@Cacheable
的用法
1 |
|
- 以下为Controller的代码
1 |
|
- 以下为启动类代码
1 |
|
- 结果
- 第一次访问
http://localhost:8080/user/getUser/1
得到的结果如下:- 网页效果
- 控制台效果
- 网页效果
- 第二次访问/刷新网页结果如下:
- 网页效果
- 控制台效果
- 网页效果
- 第一次访问
- 结论:
- 如果需要使用Spring的缓存抽象。则需要如下几步,就可以简单使用了
- 在Maven中引入Cache组件
- 在启动类上声明@EnableCaching注解,表示开启基于注解的缓存
- 在需要缓存数据的地方使用对应的注解即可
- 如果需要使用Spring的缓存抽象。则需要如下几步,就可以简单使用了
四、Spring缓存抽象原理及使用@Cacheable缓存基本流程分析
- Spring缓存抽象原理分析
- 对SpringBoot有一点了解的同学就应该都知道,SpringBoot中的每一个组件,都会有一个或多个对应的XXXAutoConfiguration.在项目启动时自动配置。Cache也不例外。这次我们就来看看
CacheAutoConfiguration
。声明代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
.class) //只有引入了CacheManager才会生效 (CacheManager
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
.class) (CacheProperties
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
//导入CacheConfigurationImportSelector类,并执行
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
....
} - 从上面的源码看,他会导入一个
CacheConfigurationImportSelector
类,并且执行它,那么我们就看看在这里执行了什么吧。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static class CacheConfigurationImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
//CacheType是枚举类型。取值如下:
//GENERIC、JCACHE(JSR107)、EHCACHE、HAZELCAST、INFINISPAN、COUCHBASE、REDIS、CAFFEINE、SIMPLE、NONE
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
//循环获取CacheType里面的值
for (int i = 0; i < types.length; i++) {
//通过type获取Configuration的类名,赋给imports
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
} - 在
CacheConfigurationImportSelector
中会调用CacheConfigurations.getConfigurationClass()
方法获取Configuration的类名。来一起看下吧。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
final class CacheConfigurations {
private static final Map<CacheType, Class<?>> MAPPINGS;
//这里面就是各种各样的Configuration类
static {
Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); //redis的Configuration
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
private CacheConfigurations() {
}
//通过type来获取configuration类的类名
public static String getConfigurationClass(CacheType cacheType) {
//从上面map中获取Configuration的类
Class<?> configurationClass = MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
//返回类的全类名
return configurationClass.getName();
}
//根据全类名获取类型
public static CacheType getType(String configurationClassName) {
for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
if (entry.getValue().getName().equals(configurationClassName)) {
return entry.getKey();
}
}
throw new IllegalStateException(
"Unknown configuration class " + configurationClassName);
}
} - 既然他引入了这么多个Configuration。那么默认是用的哪个呢?这里默认的使用的是**
SimpleCacheConfiguration
** - 那我们就去
SimpleCacheConfiguration
分析一波吧1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.class) (CacheManager
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
//缓存配置相关文件
private final CacheProperties cacheProperties;
//缓存管理器定制器
private final CacheManagerCustomizers customizerInvoker;
//在创建对象的时候注入配置文件和定制器
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
//往容器中加入一个ConcurrentMapCacheManager的实例
//这里可以看出,SimpleCacheConfiguration使用的是ConcurrentMapCacheManager
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
//从配置文件中读取缓存组件的名字。也就是说我们可以在配置文件中配置缓存的名字
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
//指定定制器并返回已经执行完定制器的cacheManager
return this.customizerInvoker.customize(cacheManager);
}
} - 到此,Cache的自动配置算是分析完了。总结下下
- 在项目启动的时候,执行
CacheAutoConfiguration
类,导入CacheConfigurationImportSelector
选择器 - 在
CacheConfigurationImportSelector
中通过CacheType获取对应类型的CacheConfiguration类的全类名 - 默认使用的是
SimpleCacheConfiguration
- 在
SimpleCacheConfiguration
中往容器加入了一个ConcurrentMapCacheManager
。也就是说,我们默认使用的CacheManager是ConcurrentMapCacheManager。
- 在项目启动的时候,执行
- 对SpringBoot有一点了解的同学就应该都知道,SpringBoot中的每一个组件,都会有一个或多个对应的XXXAutoConfiguration.在项目启动时自动配置。Cache也不例外。这次我们就来看看
- @Cacheable基本流程分析
-
上面说到我们默认使用的是
ConcurrentMapCacheManager
.那么我们在使用@Cacheable
的时候,他获取的缓存对象是什么呢?下面就是他获取cache的方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public Cache getCache(String name) {
//首先根据缓存的名字,也就是@Cacheable的value值去缓存map中获取缓存
Cache cache = this.cacheMap.get(name);
//如果获取不到对应的cache,就创建个
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
//创建个缓存,并加入到cacheMap中
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
} -
如果获取不到,就调用
createConcurrentMapCache(name)
创建缓存。代码如下:1
2
3
4
5
6
7protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
//可以看出来,他是创建个ConcurrentMapCache。也就是说默认使用的Cache是ConcurrentHashMap
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256),
isAllowNullValues(), actualSerialization);
} -
Cache拿到了,默认为
ConcurrentHashMap
,那么他是怎么往缓存中存取数据呢?- 存
1
2
3
4public void put(Object key, @Nullable Object value) {
//使用toStoreValue()序列化值,放到ConcurrentHashMap中
this.store.put(key, toStoreValue(value));
} - 取
1
2
3
4protected Object lookup(Object key) {
//直接通过key从缓存中去
return this.store.get(key);
}
- 存
-
上面只是存取调用的方法,那么它的流程是怎样呢?我们在上面两个地方都下断点。访问一次。
- 断点首先断在lookup()这个方法上。即说明,在执行具体方法前会先执行lookup()查看缓存中是否有值,如果有就会返回。如果没有就会去执行方法。随后,我们在堆栈中发现是通过
CacheAspectSupport.findCachedItem()
方法调用过来的。发现key是在该方法中生成的。它的源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
//在这里生成key
Object key = generateKey(context, result);
//然后通过生成的key,去Cache中找值
Cache.ValueWrapper cached = findInCaches(context, key);
//如果找到了,就直接返回
if (cached != null) {
return cached;
}
//找不到就返回null
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
- 断点首先断在lookup()这个方法上。即说明,在执行具体方法前会先执行lookup()查看缓存中是否有值,如果有就会返回。如果没有就会去执行方法。随后,我们在堆栈中发现是通过
-
于是乎,我们得去看看这个key是怎么生成的。默认采用的策略是什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private Object generateKey(CacheOperationContext context, @Nullable Object result) {
//使用context.generateKey进行key的生成。
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
"using named params on classes without debug info?) " + context.metadata.operation);
}
if (logger.isTraceEnabled()) {
logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
}
return key;
}
//调用该方法生成key
protected Object generateKey(@Nullable Object result) {
//判断是否在@Cacheable注解里指定了key的值或生成策略
if (StringUtils.hasText(this.metadata.operation.getKey())) {
EvaluationContext evaluationContext = createEvaluationContext(result);
//如果有,则使用evaluator.key()按照指定的key生成
return evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext);
}
//如果在@Cacheable中不指定key的值。则执行下面的方法,生成key。通过断点调试,发现这里的keyGenerator为SimpleKeyGenerator
//参数分别为:标注注解方法的类、标注注解方法名、标注注解方法的参数
return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
} -
通过断点调试发现,他默认使用的keyGenerator是
SimpleKeyGenerator
。下面我们看看SimpleKeyGenerator
是怎么生成key的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31//调用该方法进行key的生成
public Object generate(Object target, Method method, Object... params) {
//把标注注解方法的参数传递过去
return generateKey(params);
}
public static Object generateKey(Object... params) {
//如果没有参数,则返回一个空的Simplekey对象
if (params.length == 0) {
return SimpleKey.EMPTY;
}
//如果参数只有1个
if (params.length == 1) {
//去参数值
Object param = params[0];
//如果参数值不为空,并且不是数组就把参数值作为key返回
if (param != null && !param.getClass().isArray()) {
return param;
}
}
//如果方法参数不止一个就创建个SimpleKey对象
return new SimpleKey(params);
}
public SimpleKey(Object... elements) {
Assert.notNull(elements, "Elements must not be null");
this.params = new Object[elements.length];//创建个同等大小的数组
System.arraycopy(elements, 0, this.params, 0, elements.length);//拷贝值
//对参数进行散列
this.hashCode = Arrays.deepHashCode(this.params);
} -
key的生成策略已经分析完了,下面总结下:
- 默认使用的生成策略为
SimpleKeyGenerator
(指在使用注解的时候未声明key的值或生成策略) - 如果在使用注解时声明了key的值或者生成策略。那么就使用声明的值或策略生成key
- 默认情况下,使用方法的参数作为key,根据方法的参数不同,又分为这几种情况
- 参数为空
- 返回new SimpleKey()对象作为key
- 参数只有一个
- 返回该参数的值作为key
- 参数有多个
- 返回new SimpleKey(params)对象作为key
- 参数为空
- 默认使用的生成策略为
-
总结下流程吧
- 在方法调用前,先去通过定义的缓存的name去
CacheManager<ConcurrentCacheManager>
中查询Cache组件,如果没有就创建。并放入CacheManager
的cacheMap中。 - 拿到cache组件后,首先根据生成的key调用Cache组件的
lookup()
查看缓存中是否有对应的值,如果有就直接返回 - 如果查不到值,就调用方法。并再方法调用后,调用Cache组件的
put()
把key和value存放到Cache中
- 在方法调用前,先去通过定义的缓存的name去
-
五、结语
首先,这篇分析篇幅算比较长。希望可以耐心的看完。下面把所有的流程的总结作为结尾吧~
- 在项目跑起来后,会自动执行
CacheAutoConfigration
自动配置类。 - 在该自动配置类中,会导入
CacheConfigurationImportSelector
,在该类中会根据Cache的类型获取对应的Configuration
- 在我们没有引入其他的Cache组件下,默认使用的是
SimpleCacheConfiguration
- 而在
SimpleCacheConfiguration
中,会为我们创建一个ConcurrentCacheManager
缓存管理器。 - 如果方法标注了
@Cacheable
,则在目标方法执行前,会先通过我们定义的缓存名称,去ConcurrentCacheManager
缓存管理器中寻找Cache组件。如果没有找到就帮我们创建个,并且加入到一个cacheMap中。 - 到此,我们已经拿到了Cache组件了。然后就会通过key的生成策略,去生成一个key。然后通过该key调用Cache的lookup()方法获取对应的value。如果value存在,则直接返回,不执行目标方法。
- 如果没有该value,则会执行目标方法,在执行目标方法后,会调用Cache的put()方法,把key和value保存到Cache中。
- 再次调用时,如果有值,就不会执行方法了。达到缓存的目的。
谢谢大家观看~下面将继续带来Spring缓存抽象其他的注解使用及原理分析。敬请期待~~
评论