JSR107
JSR是Java-Specification-Requests的缩写,意思是Java规范提案。
JSR107中Java Caching定义了5个核心接口,分别是:CachingProvider,CacheManager、Cache、Entry、Expiry;
接口名称 | 接口作用 |
CachingProvider | 定义了创建、配置、获取、管理、控制多个CacheManager,一个应用可以在运行期访问多个CachingProvider; |
CacheManager | 定义了创建、配置、获取、管理、控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中,一个CacheManager仅仅被一个CachingProvider所拥有; |
Cache | 一个类似于Map的数据结构并临时存储以Key作为索引的值,一个Cache仅仅被一个CacheManager所拥有; |
Entry | 一个存储在Cache中的key-value对 |
Expiry | 每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个时间,条目为过期状态,一旦过期,条目将不可访问、更新、删除;有效期可以通过ExpiryPolicy设置; |
JSR107其实是一套接口,是一种规范,需要市场上其他的缓存框架去遵守,去实现该接口,但实际情况是,JSR107规范使用的不多;
<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> </dependency> |
Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存框架,并支持使用JSR107注解来简化开发;
①Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
②Cache接口下Spring提供了各种XXCache的实现,比如常见的RedisCache、EhCacheCache等;
③每次调用需要缓存功能的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过,如果被调用过则直接从缓存中获取方法调用后的结果,如果没有则调用方法并缓存结果后再返回给用户,下次调用就直接从缓存中获取;
④使用Spring缓存对象时需要注意2点:确定方法需要被缓存以及对应的缓存策略、从缓存中读取之前缓存的数据;
这里有几个常见的概念与注解:
Cache | 缓存接口,定义缓存操作,其常见实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,负责管理各种缓存Cache组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key的生成策略 |
serialize | 缓存数据时value序列化策略 |
Spring基础缓存注解
验证项目,还是使用之前已经写好的druid + jpa项目,且druid可以从侧面验证缓存效果,下面是基础验证逻辑:
第一步:加入chache的<dependency/>依赖;
<dependency> |
第二步:@EnableCaching开启基于注解的缓存;注意这里要在@SpringBootApplication上添加:
@MapperScan(value = "com.springboot.web.mapper") @SpringBootApplication @EnableCaching public class SpringBootMainApplication { public static void main(String[] args) { SpringApplication.run(SpringBootMainApplication.class, args); } } |
第三步:标注缓存注解;
注解名称 | 作用 | 参数 |
@Cacheable | 将方法的运行结果进行缓存,以后遇到相同的数据,则直接从缓存中获取,不用调用方法; | cacheNames/value:指定缓存组件的名字; key:缓存数据使用的key,可以用它来指定;默认是使用方法参数的值;可以使用SpEL表达式来表达; keyGenerator:key的生成器,通过该属性来指定key的生成器的组件id;该属性与key只能二选一; cacheManager:指定缓存管理器;或者通过cacheResolver来指定缓存缓存解析器;该属性与cacheResolver也是二选一; condition:指定符合条件的情况下才进行缓存;也可以使用SpEL来表达; unless:否定缓存,当unless指定的条件为true,则方法返回值不会被缓存;可以获取到结果进行判断; sync:是否使用异步模式; |
@CachePut | 既调用方法,同时又更新缓存数据; 主要用于修改了数据库的某个数据,同时更新缓存,这样即调用了方法,也同时更新了缓存数据; | 跟@Cacheable一致; 注意事项:该cache的key要与其他cache中的key保持一致,否则其他cache不会更新缓存; |
@CacheEvict | 缓存清除; 与@CachePut类似,@CachePut针对的是update业务场景,而@CacheEvict针对的是delete业务场景; | allEntries:是否清除全部缓存,默认是false,即不清除全部缓存; beforeInvocation:是否先执行清理缓存,后执行方法;默认是false即先执行方法,后清理缓存,这样当方法抛出异常后,缓存就不会被清理; |
@Caching | @Cacheable + @CachePut + @CacheEvict组合 | 就是把3个注解拼接到一起,规则跟具体某个注解保持一致; |
@CacheConfig | 该注解用于注解类,其他注解用于注解方法; 该注解可以把其他注解的公共属性抽取出来,配置在@CacheConfig中; |
|
注意事项:
①@CachePut注解的方法必须被执行,而@Cacheable注解的方法则不一定被执行;
②@CacheEvict/@CachePut/@Cacheable注意缓存时的key要保持一致,否则就会当成2个独立的缓存进行存储;
注解常用的SpEL基础元素
名字 | 位置 | 描述 | 示例 |
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 | 当前方法调用使用的缓存列表,即配置的cacheNames/value属性的数量 | #root.caches[0].name |
argumentname | evaluation context | 方法参数的名字,可以直接用#参数名、#p0、#a0的形式来表示,0代表参数的索引; | #id/#a0/#p0 |
result | evaluation context | 方法执行后的返回值,指的是满足unless、condition、beforeInvocation=false后的返回值; | #result |
注解基础代码
@GetMapping("/userinfo/{id}") // @Cacheable(cacheNames = {"userinfo"},key = "#root.args[0]") @Cacheable(cacheNames = {"userinfo"},keyGenerator = "testKeyGenerator") public Userinfo getUserInfoById( @PathVariable Integer id){ System.out.println("-------------------->"); Optional<Userinfo> userinfo = userInfoRepository.findById(id); return userinfo.get(); } -------------------------------------------------------------------------------------------- @Configuration public class MyKeyGenerator {
@Bean("testKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator() { @Override public Object generate(Object o, Method method, Object... objects) { return method.getName() + "[" + Arrays.asList(objects).toString() + "]"; } }; } } |
效果:
注解基础源码
打开CacheAutoConfiguration,先看一下类声明:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(CacheManager.class) @ConditionalOnBean(CacheAspectSupport.class) @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver") @EnableConfigurationProperties(CacheProperties.class) @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class }) @Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class }) public class CacheAutoConfiguration { ............. } |
属性以spring.cache开头;
继续跟踪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; } } |
通过debug方式,可以看到该方法返回如下数组:
这里找一个最简单的cache,即SimpleCacheConfiguration,以这个为例来大致浏览下:
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(CacheManager.class) @Conditional(CacheCondition.class) class SimpleCacheConfiguration { @Bean ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers) { ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); List<String> cacheNames = cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { cacheManager.setCacheNames(cacheNames); } return cacheManagerCustomizers.customize(cacheManager); } } |
这里关键的是ConcurrentMapCacheManager,继续观察下这个类的类声明:
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware { } |
接口CacheManager只有2个方法:getCache、getCacheNames,那我们重点关注下getCache方法:
@Nullable public Cache getCache(String name) { Cache cache = (Cache)this.cacheMap.get(name); if (cache == null && this.dynamic) { synchronized(this.cacheMap) { cache = (Cache)this.cacheMap.get(name); if (cache == null) { cache = this.createConcurrentMapCache(name); this.cacheMap.put(name, cache); } } } return cache; } |
这里看一下变量cacheMap是ConcurrentMap<String, Cache>类型,getCache方法还是用的synchronized来进行同步;
Web应用上的缓存,肯定涉及到多线程的问题,到时候看看其他的CacheConfiguration是怎么处理多线程问题的;
如何启用Redis来作为缓存
第一步:添加Redis对应的<dependency/>;
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> |
第二步:根据个人需要,来使用RedisTemplate或者StringRedisTemplate;
@Autowired RedisTemplate redisTemplate; //k-v都是object @Autowired StringRedisTemplate stringRedisTemplate; // k-v都是字符串 |
第三步:根据数据结构,去获取数据,例如:
stringRedisTemplate.opsFor数据结构().该数据结构对应的命令; 例如: stringRedisTemplate.opsForValue().append(“msg”,”hello word”); String msg = stringRedisTemplate.opsForValue().get(“msg”); |
RedisAutoConfiguration源码分析
先查看一下该类的源码:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } } |
这里有2个@Bean注解,说明要往IOC容器中注册2个Bean组件,这也从侧面证明了第2步中对应的RedisTemplate/StringRedisTemplate;
Redis属性配置都是以spring.redis来开头的;
这里重点关注下RedisTemplate类的几个变量:
private boolean enableTransactionSupport = false; private boolean exposeConnection = false; private boolean initialized = false; private boolean enableDefaultSerializer = true; @Nullable private RedisSerializer<?> defaultSerializer; @Nullable private ClassLoader classLoader; @Nullable private RedisSerializer keySerializer = null; @Nullable private RedisSerializer valueSerializer = null; @Nullable private RedisSerializer hashKeySerializer = null; @Nullable private RedisSerializer hashValueSerializer = null; private RedisSerializer<String> stringSerializer = RedisSerializer.string(); @Nullable private ScriptExecutor<K> scriptExecutor; private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this); private final ListOperations<K, V> listOps = new DefaultListOperations(this); private final SetOperations<K, V> setOps = new DefaultSetOperations(this); private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this, new ObjectHashMapper()); private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this); private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this); private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this); private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this); |
这里有几个RedisSerializer类型的变量,而RedisSerializer是一个接口,下面是Spring Boot提供的接口实现类:
为什么会有RedisSerializer类型的key/value呢,这是因为默认情况下,keySerializer/valueSerializer/hashKeySerializer/hashValueSerializer都取自defaultSerializer,而defaultSerializer又是JdkSerializationRedisSerializer类型,即一般情况下,放在Redis的value会采用Serializer方式来存储到Redis中;
可以看看deserializeMixedResults方法源代码,当value取特定值的时候,会要求value必须实现Serializer接口;
如果我想把Object按照json方式存储到Redis,那么有2种方式:
①自己通过第三方jar包,把object转化成json串;
②自己编写一个@Configuration类,里面生成一个RedisTemplate/StringRedisTemplate的Bean,通过setValueSerializer方法,把Spring Boot提供的Jackson2JsonRedisSerializer传入即可;
(valueSerializer是通过@Nullable来注解的,因此无法通过属性配置文件来配置)
RedisCacheManager
每一种cache都对应一个CacheManager,这里是RedisCacheManager,通过自定义CacheManager,然后注解中通过cacheManager属性来配置自定义CacheManager即可;