接着上一篇讲了 Spring Cache 如何被 Spring Aop 代理加载对应的代码,以及何如注入相关界面逻辑。
本篇我们围绕两个要点展开:
Spring Cache 相关的注解有 5 个:
@Cacheable 是我们最常使用的注解:
Copy
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
}
cacheNames 和 value 这两个属性任意使用一个都可以,它们的作用可以理解为 key 的前缀。
Copy@Cacheable(value = "user:cache")
public User findById(String id) {
User user = this.getById(id);
if (user != null){
System.out.println("user.name = " + user.getName());
}
return user;
}
key 和 keyGenerator 是互斥的一对。当指定了 key 的时候就会使用你指定的 key + 参数 作为缓存 key。否则则使用默认 keyGenerator(SimpleKeyGenerator)或者你自定义的 Generator 来生成 key。
默认的 SimpleKeyGenerator 通过源码我们能看到它的生成规则:
Copypublic static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
new SimpleKey(params)
。Spring 官方推荐使用显式指定 key 的方式来生成 key。当然你也可以通过自定义 KeyGenerator 来实现自己制定规则的 key 生成方式,只需要实现 KeyGenerator 接口即可。
注意 key 属性为 spEL 表达式,如果要写字符串需要将该字符串用单引号括起来。比如我们有如下配置:
Copy@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name")
public String getName(String name) {
return "hello:" + name;
}
假设 name = xiaoming
,那么缓存的 key = userInfo::p_xiaoming
。
condition 参数的作用是限定存储条件:
Copy@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name",condition = "#sex == 1")
public String getName(String name, int sex) {
return "hello:" + name;
}
上例限制条件为 sex == 1
的时候才写入缓存,否则不走缓存。
unless 参数跟 condition 参数相反,作用是当不满足某个条件的时候才写入缓存。
sync 字段上一篇说过,多线程情况下并发更新的情况是否只需要一个线程更新即可。
还有个属性 cacheManager 比较大头放在后面单独说,从命名上能看出它是 cache 的管理者,即指定当前 Cache 使用何种 Cache 配置,比如是 Redis 还是 local Cache 等等。这也是我们这一篇要讨论的重点。
CacheConfig 注解包含以下配置:
Copy@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
如果你在一个类中使用多个 Cache 注解,并且这些 Cache 注解有公共的基础操作,比如:使用相同的 Cache key 生成规则,使用相同的 Cache Name 前缀等等,那么你就可以定义一个 CacheConfig 来统一单独管理这些 Cache 操作。
Copy@CacheConfig(cacheNames = "user")
public class UserService {
@Cacheable(key = "#userInfoDTO.uid")
public GirgirUser.UserInfo getUser(UserInfoDTO userInfoDTO) {
return xxx;
}
@Cacheable(key = "'base_' + #userInfoDTO.uid")
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
}
上面示例中的 两个 Cache Key 都会有一个公共前缀 ”user“。需要注意的是:CacheConfig 注解的优先级高于同类当中别的注解,如果你在 CacheConfig 中配置了 cacheNames,方法中也配置了,那么 CacheConfig 中的 cacheNames 会覆盖掉方法上的配置。
@Caching 注解适用于复杂缓存操作的场景,当你有多个缓存操作的需求,比如下例:你需要先删除就缓存,再插入新数据到缓存:
Copy@Caching(evict = @CacheEvict(key = "'base' + #userInfoDTO.uid"),
put = @CachePut(key = "'base' + #userInfoDTO.uid"))
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
那么你可以使用 @Caching 注解来操作多个缓存。
注解的使用就说到这里,其余几个注解的配置基本同 @Cacheable 差不多,剩下的大家可以自己学习。接下来我们要说的重点来了:待缓存的数据到底是如何被存储起来的。Spring Cache 如何知道当前要使用的数据源。
Spring EL 对 Cache 的支持
Name | Location | Description | Example |
---|---|---|---|
methodName | Root object | 被调用的方法的名称 | #root.methodName |
method | Root object | 被调用的方法 | #root.method.name |
target | Root object | 当前调用方法的对象 | #root.target |
targetClass | Root object | 当前调用方法的类 | #root.targetClass |
args | Root object | 当前方法的参数 | #root.args[0] |
caches | Root object | 当前方法的缓存集合 | #root.caches[0].name |
Argument name | Evaluation context | 当前方法的参数名称 | #iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias). |
result | Evaluation context | 方法返回的结果(要缓存的值)。只有在 unless 、@CachePut(用于计算键)或@CacheEvict(beforeInvocation=false) 中才可用.对于支持的包装器(例如Optional),#result 引用的是实际对象,而不是包装器 | #result |
Spring 在 application.yml 中提供配置文件支持,通过配置 spring.cache.type
标签来指定当前要使用的存储方案,目前支持的有:
Copypublic enum CacheType {
GENERIC,
JCACHE,
EHCACHE,
HAZELCAST,
INFINISPAN,
COUCHBASE,
REDIS,
CAFFEINE,
SIMPLE,
NONE;
private CacheType() {
}
}
使用的时候需要引入相关存储对应的 jar 包以及相关的配置。
Java Caching 定义了 5 个核心接口,分别是 CachingProvider, CacheManager, Cache, Entry和 Expiry
Spring 定义了org.springframework.cache.CacheManager
和org.springframework.cache.Cache
接口来统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术抽象接口,Cache 接口包含了缓存的各种操作。
针对不同的缓存方案需要提供不同的 CacheManager,Spring提供的实现类包括:
CacheManager 的加载来自于 spring.factories
文件中的配置:org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
,即在 Spring启动的时候加载:
Copy@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
......
}
那不同的存储实现是如何加载各自的 CacheManger 的呢?我们就拿 Redis 来说,在配置类:
Copy@Configuration
@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(this.determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}
......
}
Redis 的配置类启动的时候先检查 CacheManager 是否有加载成功,有的话则去执行各种配置相关操作。上面代码截出来了初始化 RedisCacheManager 的步骤。RedisCacheManager 实现了 CacheManager 接口。
当使用 RedisCacheManager 进行存储的时候,通过被包装的 Cache 对象来使用相关的存储操作,我们看一下 RedisCache 对应的操作:
Copypublic class RedisCache extends AbstractValueAdaptingCache {
......
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = this.get(key);
if (result != null) {
return result.get();
} else {
T value = valueFromLoader(key, valueLoader);
this.put(key, value);
return value;
}
}
public void put(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.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.", this.name));
} else {
this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
}
}
......
}
可以看到 Redis 的存储使用的是普通的 KV 结构,value 的序列化方式是 yml 文件中的配置。另外很重要的一点是 ttl 的配置,这里能看到也是获取配置文件的属性。所以当你想给每个 key 单独设置过期时间的话就不能使用默认的 Redis 配置。而是需要自己实现 CacheManager。# Spring Cache 带你飞(二)
接着上一篇讲了 Spring Cache 如何被 Spring Aop 代理加载对应的代码,以及何如注入相关界面逻辑。
本篇我们围绕两个要点展开:
Spring Cache 相关的注解有 5 个:
@Cacheable 是我们最常使用的注解:
Copy
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
}
cacheNames 和 value 这两个属性任意使用一个都可以,它们的作用可以理解为 key 的前缀。
Copy@Cacheable(value = "user:cache")
public User findById(String id) {
User user = this.getById(id);
if (user != null){
System.out.println("user.name = " + user.getName());
}
return user;
}
key 和 keyGenerator 是互斥的一对。当指定了 key 的时候就会使用你指定的 key + 参数 作为缓存 key。否则则使用默认 keyGenerator(SimpleKeyGenerator)或者你自定义的 Generator 来生成 key。
默认的 SimpleKeyGenerator 通过源码我们能看到它的生成规则:
Copypublic static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
new SimpleKey(params)
。Spring 官方推荐使用显式指定 key 的方式来生成 key。当然你也可以通过自定义 KeyGenerator 来实现自己制定规则的 key 生成方式,只需要实现 KeyGenerator 接口即可。
注意 key 属性为 spEL 表达式,如果要写字符串需要将该字符串用单引号括起来。比如我们有如下配置:
Copy@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name")
public String getName(String name) {
return "hello:" + name;
}
假设 name = xiaoming
,那么缓存的 key = userInfo::p_xiaoming
。
condition 参数的作用是限定存储条件:
Copy@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name",condition = "#sex == 1")
public String getName(String name, int sex) {
return "hello:" + name;
}
上例限制条件为 sex == 1
的时候才写入缓存,否则不走缓存。
unless 参数跟 condition 参数相反,作用是当不满足某个条件的时候才写入缓存。
sync 字段上一篇说过,多线程情况下并发更新的情况是否只需要一个线程更新即可。
还有个属性 cacheManager 比较大头放在后面单独说,从命名上能看出它是 cache 的管理者,即指定当前 Cache 使用何种 Cache 配置,比如是 Redis 还是 local Cache 等等。这也是我们这一篇要讨论的重点。
CacheConfig 注解包含以下配置:
Copy@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
如果你在一个类中使用多个 Cache 注解,并且这些 Cache 注解有公共的基础操作,比如:使用相同的 Cache key 生成规则,使用相同的 Cache Name 前缀等等,那么你就可以定义一个 CacheConfig 来统一单独管理这些 Cache 操作。
Copy@CacheConfig(cacheNames = "user")
public class UserService {
@Cacheable(key = "#userInfoDTO.uid")
public GirgirUser.UserInfo getUser(UserInfoDTO userInfoDTO) {
return xxx;
}
@Cacheable(key = "'base_' + #userInfoDTO.uid")
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
}
上面示例中的 两个 Cache Key 都会有一个公共前缀 ”user“。需要注意的是:CacheConfig 注解的优先级高于同类当中别的注解,如果你在 CacheConfig 中配置了 cacheNames,方法中也配置了,那么 CacheConfig 中的 cacheNames 会覆盖掉方法上的配置。
@Caching 注解适用于复杂缓存操作的场景,当你有多个缓存操作的需求,比如下例:你需要先删除就缓存,再插入新数据到缓存:
Copy@Caching(evict = @CacheEvict(key = "'base' + #userInfoDTO.uid"),
put = @CachePut(key = "'base' + #userInfoDTO.uid"))
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
那么你可以使用 @Caching 注解来操作多个缓存。
注解的使用就说到这里,其余几个注解的配置基本同 @Cacheable 差不多,剩下的大家可以自己学习。接下来我们要说的重点来了:待缓存的数据到底是如何被存储起来的。Spring Cache 如何知道当前要使用的数据源。
Spring EL 对 Cache 的支持
Name | Location | Description | Example |
---|---|---|---|
methodName | Root object | 被调用的方法的名称 | #root.methodName |
method | Root object | 被调用的方法 | #root.method.name |
target | Root object | 当前调用方法的对象 | #root.target |
targetClass | Root object | 当前调用方法的类 | #root.targetClass |
args | Root object | 当前方法的参数 | #root.args[0] |
caches | Root object | 当前方法的缓存集合 | #root.caches[0].name |
Argument name | Evaluation context | 当前方法的参数名称 | #iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias). |
result | Evaluation context | 方法返回的结果(要缓存的值)。只有在 unless 、@CachePut(用于计算键)或@CacheEvict(beforeInvocation=false) 中才可用.对于支持的包装器(例如Optional),#result 引用的是实际对象,而不是包装器 | #result |
Spring 在 application.yml 中提供配置文件支持,通过配置 spring.cache.type
标签来指定当前要使用的存储方案,目前支持的有:
Copypublic enum CacheType {
GENERIC,
JCACHE,
EHCACHE,
HAZELCAST,
INFINISPAN,
COUCHBASE,
REDIS,
CAFFEINE,
SIMPLE,
NONE;
private CacheType() {
}
}
使用的时候需要引入相关存储对应的 jar 包以及相关的配置。
Java Caching 定义了 5 个核心接口,分别是 CachingProvider, CacheManager, Cache, Entry和 Expiry
Spring 定义了org.springframework.cache.CacheManager
和org.springframework.cache.Cache
接口来统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术抽象接口,Cache 接口包含了缓存的各种操作。
针对不同的缓存方案需要提供不同的 CacheManager,Spring提供的实现类包括:
CacheManager 的加载来自于 spring.factories
文件中的配置:org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
,即在 Spring启动的时候加载:
Copy@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
......
}
那不同的存储实现是如何加载各自的 CacheManger 的呢?我们就拿 Redis 来说,在配置类:
Copy@Configuration
@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(this.determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}
......
}
Redis 的配置类启动的时候先检查 CacheManager 是否有加载成功,有的话则去执行各种配置相关操作。上面代码截出来了初始化 RedisCacheManager 的步骤。RedisCacheManager 实现了 CacheManager 接口。
当使用 RedisCacheManager 进行存储的时候,通过被包装的 Cache 对象来使用相关的存储操作,我们看一下 RedisCache 对应的操作:
Copypublic class RedisCache extends AbstractValueAdaptingCache {
......
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = this.get(key);
if (result != null) {
return result.get();
} else {
T value = valueFromLoader(key, valueLoader);
this.put(key, value);
return value;
}
}
public void put(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.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.", this.name));
} else {
this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
}
}
......
}
可以看到 Redis 的存储使用的是普通的 KV 结构,value 的序列化方式是 yml 文件中的配置。另外很重要的一点是 ttl 的配置,这里能看到也是获取配置文件的属性。所以当你想给每个 key 单独设置过期时间的话就不能使用默认的 Redis 配置。而是需要自己实现 CacheManager。