Spring缓存注解

一、启用Spring缓存注解

1.引入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

2.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容器中的。如果没有注入,那么在该类上或该类中的缓存注解是不会生效的。

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)指定了多个命名空间;

  1. 当进行缓存存储时,会在这些命名空间下都存一份key-value。
  2. 当进行缓存读取时,会按照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);
    }
}

参考文章

相关文章

这篇文章主要介绍了spring的事务传播属性REQUIRED_NESTED的原...
今天小编给大家分享的是一文解析spring中事务的传播机制,相...
这篇文章主要介绍了SpringCloudAlibaba和SpringCloud有什么区...
本篇文章和大家了解一下SpringCloud整合XXL-Job的几个步骤。...
本篇文章和大家了解一下Spring延迟初始化会遇到什么问题。有...
这篇文章主要介绍了怎么使用Spring提供的不同缓存注解实现缓...