Spring缓存注解
一、启用Spring缓存注解
- 引入
spring-boot-starter-data-redis
依赖,配置redis
的连接属性
spring:
redis:
password:
host: localhost
port: 6379
cache:
redis:
## Entry expiration in milliseconds. By default the entries never expire.
time-to-live: 1d
#写入redis时是否使用键前缀。
use-key-prefix: true
- SpringCache配置
@EnableCaching
:开启缓存注解,在项目启动类或某个配置类上使用此注解后,则表示允许使用注解的方式进行缓存操作
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)
* 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值
* @return
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.create(redisConnectionFactory);
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
// 创建一个模板类
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
// 将刚才的redis连接工厂设置到模板类中
template.setConnectionFactory(factory);
// 设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化器
// 使用Jackson 2,将对象序列化为JSON
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(Object.class);
// json转对象类,不设置默认的会将json转成hashmap
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
二、使用缓存注解
在类上或类中的方法上使用缓存注解。
注:这个【类】指的是注入了Spring容器中的。如果没有注入,那么在该类上或该类中的缓存注解是不会生效的。
项目中使用了@Cacheable注解,发现没生效,原来是static关键字的原因,导致@Cacheable注解动态代理失败,所以无效。还有其他原因,比如在同一个类下A方法内调用B方法,B方法使用@Cacheable注解,调用A方法,也会不生效。
2.1 Cacheable
可用于类或方法上;在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值。不再执行目标方法;无则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存,如:
@Component
public class CacheableDemo {
@Cacheable(cacheManger = "cacheManage",
value ="cacheServiceImpl:test",
key = "#str.hashCode() + '*****' + #user.name")
public String testCacheable(String str, User user) {
return str;
}
}
2.2 CachePut
可用于类或方法上;在执行完目标方法后,并将方法的返回值作为value,并以键值对的形式存入缓存中,
@Component
public class CachePutDemo {
@CachePut(cacheNames = "cache-name-two", key = "#p0", unless = "#result < 5000")
public Integer testCachePut(Integer i) {
return i;
}
}
2.3 CacheEvict
可用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话),
@Component
public class CacheEvictDemo {
@CacheEvict(cacheNames = "cache-name-three", key = "#p0", beforeInvocation = true)
public String fa(String str) {
return str;
}
}
2.4 Caching
@Caching:此注解即可作为@Cacheable、@CacheEvict、@CachePut三种注解中的的任何一种或几种来使用
@Component
public class CachingDemo {
@Caching(cacheable = {@Cacheable(cacheNames = "cache-name-one", key = "#a0")},
put = {@CachePut(cacheNames = "cache-name-two", key = "#a0 + 1")},
evict = { @CacheEvict(cacheNames = "cache-name-three", key = "#a0")})
public Integer fa(Integer i) {
return i;
}
}
2.5 CacheConfig
@Cacheable、@CacheEvict、@CachePut这三个注解的cacheNames属性是必填项(或value属性是必填项,因为value属性是cacheNames的别名属性);如果上述三种注解都用的是同一个cacheNames的话,那么在每此都写cacheNames的话,就会显得麻烦。如将@CacheConfig注解就是来配置一些公共属性(如:cacheNames、keyGenerator等)的值的
@Component
@CacheConfig(cacheNames = {"cache-name-nb"})
public class CacheConfigDemo {
@Cacheable(key = "#str")
public String testOne(String str) {
return str;
}
@CacheEvict( key = "#str")
public String testTwo(String str) {
return str;
}
@CachePut(key = "#str")
public String testThree(String str) {
return str;
}
}
使用@CacheConfig声明类下的缓存注解的value默认是"users",让代码更简洁、优雅,效果与上面一样。
三、缓存注解的常用属性
3.1 cacheNames & value
@Cacheable提供两个参数来指定缓存名:value、cacheNames,二者选其一即可。
3.2 key
key的来源可分为三类,分别是:默认的、keyGenerator生成的、主动指定的。
3.2.1 默认key
@Component
public class keyDemo {
/**
* 方法无参时,默认的key为SimpleKey[]
*
* 注:前提条件是 不指定key属性,也无keyGenerator
*/
@Cacheable(cacheNames = "TestKeySpace")
public String methodOne() {
return "methodOne";
}
/**
* 方法只有一个参数时,默认的key为传入的参数的toString结果
* 如:调用此方法时,传入的传入的参数为 字符串paramA,那么key就为paramA
*
* 注:前提条件是不指定key属性,也无keyGenerator
*/
@Cacheable(cacheNames = "TestKeySpace")
public String methodTwo(String str) {
return "methodTwo";
}
/**
* 方法只有一个参数时,默认的key为传入的参数的toString结果
* 如:调用此方法时,传入的传如参数为User对象,那么就为以User对象的toString结果作为key
*
* 注:前提条件是不指定key属性,也无keyGenerator
*/
@Cacheable(cacheNames = "TestKeySpace")
public String methodThree(User user) {
return "methodThree";
}
/**
* 方法有多个参数时,默认的key为SimpleKey[${参数的toString结果},${参数的toString结果}...]
*
* 如:调用此方法时传入的参数的toString结果跑分别是:
* paramA
* 1
* User(id=null, name=张三, age=18, gender=null, motto=蚂蚁牙黑!)
* 那么默认的key就为:
* SimpleKey [paramA,1,User(id=null, name=张三, age=18, gender=null, motto=蚂蚁牙黑!)]
*
* 注:前提条件是不指定key属性,也无keyGenerator
*/
@Cacheable(cacheNames = "TestKeySpace")
public String methodFour(String str, Integer i, User user) {
return "methodFour";
}
}
3.2.2 keyGenerator生成key
编写配置类、定制化key生成器
//定制化CachingConfigurer
@Configuration
public class MyCachingConfigurer extends CachingConfigurerSupport {
/**
* 定制化key生成器
*
* 设置 全限定类名 + 方法名 + 参数名 共同组成 key
*
* @return key生成器
* @date 2019/4/12 14:09
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (Object target, Method method, Object... params) -> {
StringBuilder sb = new StringBuilder(16);
sb.append(target.getClass().getName());
sb.append("_");
sb.append(method.getName());
sb.append("_");
for (int i = 0; i < params.length; i++) {
sb.append(params[i]);
if (i < params.length - 1) {
sb.append(",");
}
}
return sb.toString();
};
}
}
此时,若使用缓存注解时不指定key属性,那么就会默认采用Key生成器生成的注解
@Component
public class keyDemo {
/**
* 若注入有KeyGenerator,当不主动设置注解的key属性时,会默认采用KeyGenerator生成的key
*
* 注:前提条件是 设置(注入)有keyGenerator,但不主动指定key属性
* 提示:本人在com.config包下注入了keyGenerator
*
* 如:本人单元测试调用此方法时,传入的参数为字符串“paramA”,如果没注入keyGenerator的话,
* key应该是【paramA】,但是本人注入了KeyGenerator,所以这里key
* 是【com.demo.KeyDemo_methodFive_paramA】
*/
@Cacheable(cacheNames = "TestKeySpace")
public String methodFive(String str) {
return "methodFive";
}
}
3.2.3 主动指定key
@Component
public class keyDemo {
/**
* 说明一: 若主动设置了key属性,那么以主动设置的key属性值为准(无论是否注入有KeyGenerator)
* 说明二: 如果key为常量的话,需要再使用单引号''引起来
*/
@Cacheable(cacheNames = "TestKeySpace", key = "'i_am_key'")
public String methodSix() {
return "methodSix";
}
/**
* 说明一: 若主动设置了key属性,那么以主动设置的key属性值为准(无论是否注入有KeyGenerator)
*
* 说明二: 我们也可以使用Spring Expression Language(SpEL)动态设置key的属性值,
* 通过【#形参名】或【#p参数索引】来动态获取传入的参数
*
* 如: 这里的 key = "#str" 等价于 key = "#p0" 等价于 key = "#a0"
* 辅助理解:p即params,a即args
*/
@Cacheable(cacheNames = "TestKeySpace", key = "#p0")
public String methodSeven(String str) {
return "methodSeven";
}
/**
* 说明一: 若主动设置了key属性,那么以主动设置的key属性值为准(无论是否注入有KeyGenerator)
*
* 说明二: 我们也可以使用Spring Expression Language (SpEL)动态设置key的属性值,
* 通过【#形参名】或【#p参数索引】来动态获取传入的参数,
* 并通过打点的方式对获得的参数进行方法或属性调用
*
*/
@Cacheable(cacheNames = "TestKeySpace", key = "#str.hashCode() + '*****' + #p1.name")
public String methodEight(String str, User user) {
return "methodEight";
}
/**
* 说明一: 若主动设置了key属性,那么以主动设置的key属性值为准(无论是否注入有KeyGenerator)
*
* 说明二: 除了使用Spring Expression Language (SpEL)动动态获取传入的参数外,
* 我们还可以通过SePL获取Spring为我们提供的隐藏的根对象root
*
* 注:#root获取到的其实是CacheExpressionRootObject类的实例,在通过#root打点调用的方式,
* 可进一步获取到当前环境的一些相关值;
*
* 如:这里获取到的key为:
* TestKeySpace::com.demo.JustForTest@4f169009--class com.demo.JustForTest--public java.lang.String com.demo.JustForTest.methodNine()--methodNine
*
* 再如:#root.args[0]等价于 #p0
*/
@Cacheable(cacheNames = "TestKeySpace",
key = "#root.target + '--' + #root.targetClass + '--' + #root.method + '--' + #root.methodName")
public String methodNine() {
return "methodNine";
}
}
3.3 condition
在激活注解功能前,进行condition
验证,如果condition
结果为true
,则表明验证通过,缓存注解生效;否则缓存注解不生效。
condition
作用时机在:缓存注解检查缓存中是否有对应的key-value
之前。
注:缓存注解检查缓存中是否有对应的
key-value
在运行目标方法之前,所以condition
作用时机也在运行目标方法之前。
@Component
public class ConditionAndCacheNamesDemo {
@Cacheable(cacheNames = "TestConditionSpace", key = "#p0")
public String methodOne(String keyStr) {
return "XYZ";
}
/**
* condition作用时机在: 缓存注解检查缓存中是否有对应的key-value之前
* 注:缓存注解检查缓存中是否有对应的key-value在运行目标方法之前,
* 所以condition作用时机也在运行目标方法之前
*
* 【(若condition结果为真) && (指定cacheNames下存在对应key的缓存)】为真,那么就会从缓存中获取数据;
* 否则就会执行方法,并将返回值作为key-value关系中的value,存入缓存
*/
@Cacheable(cacheNames = "TestConditionSpace", key ="#str", condition = "#str.startsWith('abc')")
public String methodTwo(String str) {
System.out.println("说明【(若condition结果为真) && (指定cacheNames下存在对应key的缓存)】的结果为false!");
return "methodTwo" + new Random().nextInt(10000);
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConditionTests {
@Autowired
ConditionAndCacheNamesDemo demo;
/**
* methodOne无condition条件
* methodTwo需要验证condition条件
*
*/
@Test
public void testVoidCacheOne() {
// -------------------------> 测试condition不成立的情况
// 向缓存中 存入 key为 'xyz',(返回)值为'XYZ'的键值对缓存
demo.methodOne("xyz");
// 尝试从缓存中读取key为'xyz'的缓存
String str1 = demo.methodTwo("xyz");
// 输出结果为“methodTwo3448”
// 说明没从缓存中进行读取,这是因为键'xyz'不满足condition属性条件
System.out.println(str1);
System.out.println("*************分割线*************");
// -------------------------> 测试condition成立的情况
// 向缓存中存入key为'abcdefg',(返回)值为'XYZ'的键值对缓存
demo.methodOne("abcdefg");
// 尝试从缓存中读取key为'abcdefg'的缓存
String str2 = demo.methodTwo("abcdefg");
// 输出结果为“XYZ”
// 说明从缓存中进行数据读取了,这是因为:【(若condition结果为真) && (指定cacheNames下存在对应key的缓存)】结果为true
System.out.println(str2);
}
}
3.4 cacheNames
通过cacheNames
对数据进行隔离,不同cacheName
下可以有相同的key
。也可称呼cacheName
为命名空间。实际上(以spring-cache
为例),可以通过设置RedisCacheConfiguration#usePrefix
的true
或false
来控制是否使用前缀。
如果否,那么最终的redis
键就是key
值;如果是,那么就会根据cacheName
生成一个前缀,然后再追加上key
作为最终的redis键.cacheName
还有其它重要的功能:cacheName
(就像其名称【命名空间】所说)实现了数据分区的功能,一些操作可以直接按照命名空间批量进行。如:spring框架中的Cache实际对应的就是一个【命名空间】,spring会先去找到数据所在的命名空间(即:先找到对应的Cache),再由Cache结合key,最终定位到数据。
下面验证的是:当同时指定多个cacheName
时,从哪一个cacheName
取数据。
这里先给出结论:
若属性cacheNames
(或属性value
)指定了多个命名空间;
- 当进行缓存存储时,会在这些命名空间下都存一份
key-value
。 - 当进行缓存读取时,会按照
cacheNames
值里命名空间的顺序,挨个挨个从命名空间中查找对应的key
,如果在某个命名空间中查找打了对应的缓存,就不会再查找排在后面的命名空间,也不会再执行对应方法,直接返回缓存中的value
值。
实验示例:
@Component
public class ConditionAndCacheNamesDemo {
/**
* 说明:本人将cacheName称呼为命名空间
*
* 注:若属性cacheNames(或属性value)指定了多个命名空间;
*
* 1.当进行缓存存储时,会在这些命名空间下都存一份key-value;
*
* 2.当进行缓存读取时,会按照cacheNames值里命名空间的顺序,挨个挨个从命名
* 空间中查找对应的key,如果在某个命名空间中查找打了对应的缓存,就不会再
* 查找排在后面的命名空间,也不会再执行对应方法,直接返回缓存中的value值
*
*/
@Cacheable(cacheNames = "TestConditionSpaceA", key = "'abcd'")
public String methodA() {
return "value-A";
}
@Cacheable(cacheNames = "TestConditionSpaceB", key = "'abcd'")
public String methodB() {
return "value-B";
}
@Cacheable(cacheNames = {"TestConditionSpaceB", "TestConditionSpaceA"}, key = "'abcd'")
public String methodC() {
System.out.println("说明(指定cacheNames下存在对应key的缓存)为false!");
return "methodC";
}
}
验证:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConditionTests {
@Autowired
ConditionAndCacheNamesDemo demo;
@Test
public void testVoidCacheABC() {
// 我那个命名空间TestConditionSpaceA中存入key为'abcd',值为'value-A'的数据
demo.methodA();
// 我那个命名空间TestConditionSpaceB中存入key为'abcd',值为'value-B'的数据
demo.methodB();
// methodC方法上的缓存注解如下:
// @Cacheable(cacheNames = {"TestConditionSpaceB", "TestConditionSpaceA"}, key = "'abcd'")
String str = demo.methodC();
// 输出结果为 value-B
System.out.println(str);
}
}
3.5 unless
功能是:是否令注解(在方法执行后的功能)不生效;若unless
的结果为true
,则(方法执行后的功能)不生效;若unless
的结果为false
,则(方法执行后的)功能生效。
注:unless默认为"",即相当于默认为false。
unless的作用时机:目标方法运行后。
注:如果(因为直接从缓存中获取到了数据,而导致)目标方法没有被执行,那么unless字段不生效。
举例说明一:对于@Cacheable
注解,在执行目标方法前,如果从缓存中查询到了数据,那么直接返回缓存中的数据;如果从缓存中没有查询到数据,那么执行目标方法,目标方法执行完毕之后,判断unless
的结果,若unless
的结果为true
,那么不缓存方法的返回值;若unless
的结果为false
,那么缓存方法的返回值。
举例说明二:对于@CachePut
注解,在目标方法执行完毕之后,判断unless
的结果,若unless
的结果为true
,那么不缓存方法的返回值;若unless
的结果为false
,那么缓存方法的返回值。
注:因为unless
的作用时机是在方法运行完毕后,所以我们可以用SpEL表达式#result
来获取方法的返回值。
实验示例:
@Component
public class UnlessDemo {
/**
* unless的作用时机: 目标方法运行后
* 注:如果(因为直接从缓存中获取到了数据,而导致)目标方法没有被执行,那么unless字段不生效
*
* unless的功能: 是否 令注解(在方法执行后的功能)不生效;
* 注:unless的结果为true,则(方法执行后的功能)不生效;
* unless的结果为false,则(方法执行后的)功能生效.
*
* 举例说明一: 对于@Cacheable注解,在执行目标方法前,如果从缓存中查询到了数据,那么直接返回缓存中的数据;
* 如果从 缓存中没有查询到数据,那么执行目标方法,目标方法执行完毕之后,判断unless的结果,
* 若unless的结果为true,那么不缓存方法的返回值;
* 若unless的结果为false,那么缓存方法的返回值。
*
* 举例说明二: 对于@CachePut注解,在目标方法执行完毕之后,判断unless的结果,
* 若unless的结果为true,那么不缓存方法的返回值;
* 若unless的结果为false,那么缓存方法的返回值。
*
* 注:unless默认为"",即相当于默认为false.
*
* 注:因为unless的作用时机是在方法运行完毕后,所以我们可以用SpEL表达式 #result 来获取方法的返回值
*/
@Cacheable(cacheNames = "TestUnlessSpace", key = "#p0", unless = "#result < 5000")
public Integer methodTwo(Integer i) {
System.out.println("执行方法了,说明【指定cacheNames下不存在对应key的缓存】!");
return i;
}
}
验证:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UnlessTests {
@Autowired
UnlessDemo demo;
/**
* methodOne无condition条件
*
* methodTwo需要验证condition条件
*
*/
@Test
public void testVoidCacheOne() {
Integer i = new Random().nextInt(10000);
Integer res = demo.methodTwo(i);
System.out.println(res);
}
}
说明:跑了几次此测试方法,每次随机产生的key(从代码里面可知,已入参参数为key)都是之前缓存里面没有的,也就是说每次都会执行目标方法;发现大于等于5000的随机数都存入缓存汇总了;而小于5000的随机数则没有。
3.5.1 注解的unless和condition
两者都用于对缓存进行过滤,把不需要缓存的排除在外
public String value(Integer i){
return Math.random() > 0.5? String.valueOf():null;
}
上面这个函数,有可能返回integer
的String
,也有可能返回null
。如果不希望返回值为null
时进行缓存,则使用unless="#result == null"
,排除掉返回值为null
的结果。如果我们不希望参数为空的时候进行缓存,则需要使用condition = "#i==null"
,这时函数还没执行,排除掉参数为空的情况。所以两者一个是对结果进行判断,决定是否放入缓存中,一个是对参数进行判断,决定是否放入缓存中。
3.6 allEntries
此属性主要出现在@CacheEvict
注解中,表示是否清除指定命名空间中的所有数据,默认为false
。
3.7 beforeInvocation
此属性主要出现在@CacheEvict
注解中,表示是否在目标方法执行前使此注解生效。默认为false
,即:目标方法执行完毕后此注解生效。
3.8 void
缓存注解使用在返回值为void
方法上的测试:
结论是:缓存注解作用于void
方法上,仍然会向缓存中进行存储,不过键值对中的value
为null
。
实验示例:
/**
* 缓存是以key-value进行缓存的,
* 其中key是按照一定规则生成或我们手动指定的,
* value则是方法的返回值,我们无法进行修改
*
* 那么当方法五返回值时,会怎么样呢?
*
* 注: 结论是: 对返回值为void的方法进行缓存,放入缓存的value值为null
*
*/
@Component
public class VoidDemo {
/**
* 我们先利用@CachePut将缓存放入进去
* 注:@Cacheable当然也能放进去,不过@CachePut语意更加明显一点
*/
@CachePut(cacheNames = "TestVoidSpace", key = "'void-key'")
public void methodOne() {
}
/**
*
*
* 尝试获取{@link VoidDemo methodOne}放入的缓存
*/
@Cacheable(cacheNames = "TestVoidSpace", key = "'void-key'")
public Object methodTwo() {
System.out.println("进方法了,说明TestVoidSpace空间下五key为void-key的缓存!");
return "";
}
}
验证:
@RunWith(SpringRunner.class)
@SpringBootTest
public class VoidTests {
@Autowired
VoidDemo voidDemo;
@Test
public void testVoidCache() {
// 我们试着对返回值为void的方法进行缓存
voidDemo.methodOne();
// 获取该缓存
Object obj = voidDemo.methodTwo();
// 控制台输出结果为: 对返回值为void的方法进行缓存,缓存的value值为null
System.out.println(obj== null ? "对返回值为void的方法进行缓存,缓存的value值为null" : obj);
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器