前言
https://docs.spring.io/spring-framework/docs/5.3.3/reference/html/integration.html#cache-annotations
https://docs.spring.io/spring-data/redis/docs/current/reference/html
涉及知识点
spring cache
@EnableCaching
源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
public enum AdviceMode {
PROXY,
ASPECTJ
}
注解的主要作用是引入一个选择器 CachingConfigurationSelector ,选择器会根据注解的 mode 参数使用不同的方法,默认为 PROXY ,执行 getProxyImports 方法。
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
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);
}
private String[] getAspectJImports() {
List<String> result = new ArrayList<>(2);
result.add(CACHE_ASPECT_CONFIGURATION_CLASS_NAME);
if (jsr107Present && jcacheImplPresent) {
result.add(JCACHE_ASPECT_CONFIGURATION_CLASS_NAME);
}
return StringUtils.toStringArray(result);
}
getProxyImports 方法导入的三个类名如图所示
@Cacheable
源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
//value 和 cacheNames 互为别名
@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;
}
案例1 缓存list
@Component
public class ArticleService {
@Cacheable(cacheNames = {"cache1"})
public List<String> list() {
System.out.println("获取文章列表!");
return Arrays.asList("spring", "mysql", "java高并发", "maven");
}
}
@EnableCaching
@ComponentScan
@Configuration
public class Client {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("cache1");
return cacheManager;
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
System.out.println(articleService.list());
System.out.println(articleService.list());
}
}
获取文章列表!
[spring, mysql, java高并发, maven]
[spring, mysql, java高并发, maven]
key属性:自定义key
案例2 自定义key
@Cacheable(cacheNames = {"cache1"}, key = "#root.target.class.name+'-'+#page+'-'+#pageSize")
public String getPage(int page, int pageSize) {
String msg = String.format("page-%s-pageSize-%s", page, pageSize);
System.out.println("从db中获取数据:" + msg);
return msg;
}
@EnableCaching
@ComponentScan
@Configuration
public class Client2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client2.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
//page=1,pageSize=10调用2次
System.out.println(articleService.getPage(1, 10));
System.out.println(articleService.getPage(1, 10));
//page=2,pageSize=10调用2次
System.out.println(articleService.getPage(2, 10));
System.out.println(articleService.getPage(2, 10));
{
System.out.println("下面打印出cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
}
}
}
从db中获取数据:page-1-pageSize-10
page-1-pageSize-10
page-1-pageSize-10
从db中获取数据:page-2-pageSize-10
page-2-pageSize-10
page-2-pageSize-10
下面打印出cache1缓存中的key列表
com.example.lurenjia.spring.c40.cacheable.d1.ArticleService-1-10
com.example.lurenjia.spring.c40.cacheable.d1.ArticleService-2-10
condition属性:控制缓存的使用条件
案例3 condition
@Cacheable(cacheNames = "cache1", key = "'getById'+#id", condition = "#cache")
public String getById(Long id, boolean cache) {
System.out.println("获取数据!");
return "spring缓存:" + UUID.randomUUID().toString();
}
@EnableCaching
@ComponentScan
@Configuration
public class Client3 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client3.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
System.out.println(articleService.getById(1L, true));
System.out.println(articleService.getById(1L, true));
System.out.println(articleService.getById(1L, false));
System.out.println(articleService.getById(1L, true));
}
}
获取数据!
spring缓存:0320318f-7cff-4efb-91ac-e7ed2da8eadb
spring缓存:0320318f-7cff-4efb-91ac-e7ed2da8eadb
获取数据!
spring缓存:04ffafe0-fc36-43a3-b400-2243dca2c545
spring缓存:0320318f-7cff-4efb-91ac-e7ed2da8eadb
unless属性:控制是否需要将结果丢到缓存中
案例4 unless 当返回结果为null的时候,不要将结果进行缓存
java.util.Map<Long, String> articleMap = new HashMap<>();
@Cacheable(cacheNames = "cache1", key = "'findById'+#id", unless = "#result==null")
public String findById(Long id) {
this.articleMap.put(1L, "spring系列");
System.out.println("----获取文章:" + id);
return articleMap.get(id);
}
@EnableCaching
@ComponentScan
@Configuration
public class Client4 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client4.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
System.out.println(articleService.findById(1L));
System.out.println(articleService.findById(1L));
System.out.println(articleService.findById(3L));
System.out.println(articleService.findById(3L));
{
System.out.println("下面打印出缓cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
}
}
}
----获取文章:1
spring系列
spring系列
----获取文章:3
null
----获取文章:3
null
下面打印出缓cache1缓存中的key列表
findById1
condition和unless对比
@CachePut:将结果放入缓存
源码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CachePut {
@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 "";
}
案例1
@CachePut(cacheNames = "cache1", key = "'findById'+#id")
public String add(Long id, String content) {
System.out.println("新增文章:" + id);
this.articleMap.put(id, content);
return content;
}
@EnableCaching
@ComponentScan
@Configuration
public class Client6 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client6.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
//新增3个文章,由于add方法上面有@CachePut注解,所以新增之后会自动丢到缓存中
articleService.add(1L, "java高并发系列");
articleService.add(2L, "Maven高手系列");
articleService.add(3L, "MySQL高手系列");
//然后调用findById获取,看看是否会走缓存
System.out.println("调用findById方法,会尝试从缓存中获取");
System.out.println(articleService.findById(1L));
System.out.println(articleService.findById(2L));
System.out.println(articleService.findById(3L));
{
System.out.println("下面打印出cache1缓存中的key列表");
ConcurrentMapCacheManager cacheManager = context.getBean(ConcurrentMapCacheManager.class);
ConcurrentMapCache cache1 = (ConcurrentMapCache) cacheManager.getCache("cache1");
cache1.getNativeCache().keySet().stream().forEach(System.out::println);
}
}
}
@CacheEvict:缓存清理
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
@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 "";
boolean allEntries() default false;
boolean beforeInvocation() default false;
}
修饰范围:类型、方法,修饰类子类继承,value和cacheNames互为别名。
案例1
@CacheEvict(cacheNames = "cache1", key = "'findById'+#id") //@1
public void delete(Long id) {
System.out.println("删除文章:" + id);
this.articleMap.remove(id);
}
@EnableCaching
@ComponentScan
@Configuration
public class Client7 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Client7.class);
context.refresh();
ArticleService articleService = context.getBean(ArticleService.class);
//第1次调用findById,缓存中没有,则调用方法,将结果丢到缓存中
System.out.println(articleService.findById(1L));
//第2次调用findById,缓存中存在,直接从缓存中获取
System.out.println(articleService.findById(1L));
//执行删除操作,delete方法上面有@CacheEvict方法,会清除缓存
articleService.delete(1L);
//再次调用findById方法,发现缓存中没有了,则会调用目标方法
System.out.println(articleService.findById(1L));
}
}
----获取文章:1
spring系列
spring系列
删除文章:1
----获取文章:1
spring系列
@Caching:缓存注解组
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
修饰范围:类型、方法,修饰类子类继承。
Cacheable、CachePut、CacheEvict不支持重复使用,所以定义该注解存放。
@CacheConfig
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
修饰范围:类型,用于统一设置一些属性。
Redis整合Spring Cache
yml添加注解
spring:
cache:
type: redis
测试方法
@GetMapping("cache1")
@Cacheable(cacheNames = "cache1")
public List<String> list() {
System.out.println("---模拟从db中获取数据---");
return Arrays.asList("java高并发", "springboot", "springcloud");
}
效果
不足
time-to-live: 3600000
Redisson整合Spring Cache
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.7</version>
</dependency>
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
if (org.springframework.util.ObjectUtils.isEmpty(password)) {
config.useSingleServer().setAddress("redis://" + host + ":" + port);
} else {
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
}
config.setCodec(new org.redisson.codec.JsonJacksonCodec());
return Redisson.create(config);
}
@ConditionalOnBean(value = RedissonClient.class)
@Bean
public RedissonSpringCacheManager manager(@Autowired RedissonClient client) {
RedissonSpringCacheManager manager = new RedissonSpringCacheManager(client);
return new RedissonSpringCacheManager(client);
}
}
@GetMapping("cache1")
@Cacheable(cacheNames = "cache1", key = "#root.targetClass.name+'-'+#root.method.name")
public List<String> list() {
System.out.println("---模拟从db中获取数据---");
return Arrays.asList("java高并发", "springboot", "springcloud");
}
编码问题 codec
if (oldConf.getCodec() == null) {
// use it by default
oldConf.setCodec(new MarshallingCodec());
}
默认情况下会使用 MarshallingCodec
在配置文件中修改为jackson
config.setCodec(new org.redisson.codec.JsonJacksonCodec());
原理
CacheAutoConfiguration
源码如下,重点是导入了 CacheConfigurationImportSelector.class这一个类
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {}
CacheConfigurationImportSelector
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
通过这个选择器来获取导入内容。 imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
CacheConfigurations
final class CacheConfigurations {
private static final Map<CacheType, Class<?>> MAPPINGS;
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);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
private CacheConfigurations() {
}
//如果在MAPPINGS中找到了,返回class的名字
static String getConfigurationClass(CacheType cacheType) {
Class<?> configurationClass = MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
return configurationClass.getName();
}
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);
}
}
CacheConfigurations会帮我们导入 mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
RedisCacheConfiguration
最重要的就是 RedisCacheConfiguration 中的这个bean,RedisCacheManager
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {}
}
这个类里面有一个比较重要的bean RedisCacheManager ,下面我们重点看一下这一个类。
RedisCacheManager
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
//
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
if (cacheProperties.getRedis().isEnableStatistics()) {
builder.enableStatistics();
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}
在这个bean上定义的参数首先会去容器中查找。
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
CacheProperties cacheProperties,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
//如果没有默认创建一个新的redisCacheConfiguration
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}
createConfiguration这一个类就是创建配置的核心类
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
当没有 org.springframework.data.redis.cache.RedisCacheConfiguration 这个bean的时候会使用这个方法默认创建一个新的配置类。
当我们
ObjectProvider的用法
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration