Spring Cache缓存注解

本篇文章代码示例在Spring Cache简略实现上的代码示例加以批改。

只有应用public定义的办法才能够被缓存,而private办法、protected 办法或者应用default 修饰符的办法都不能被缓存。 当在一个类上应用注解时,该类中每个公共办法的返回值都将被缓存到指定的缓存项中或者从中移除。

@Cacheable

@Cacheable注解属性一览:

属性名

作用与形容

cacheNames/value

指定缓存的名字,缓存应用CacheManager治理多个缓存Cache,这些Cache就是依据该属性进行辨别。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有本人惟一的名字。

key

缓存数据时的key的值,默认是应用办法所有入参的值,能够应用SpEL表达式示意key的值。

keyGenerator

缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。

cacheManager

指定缓存管理器(例如ConcurrentHashMap、Redis等)。

cacheResolver

和cacheManager作用一样,应用时二选一。

condition

指定缓存的条件(对参数判断,满足什么条件时才缓存),可用SpEL表达式,例如:办法入参为对象user则表达式能够写为condition = "#user.age>18",示意当入参对象user的属性age大于18才进行缓存。

unless

否定缓存的条件(对后果判断,满足什么条件时不缓存),即满足unless指定的条件时,对调用办法获取的后果不进行缓存,例如:unless = "result==null",示意如果后果为null时不缓存。

sync

是否应用异步模式进行缓存,默认false。

@Cacheable指定了被注解办法的返回值是可被缓存的。其工作原理是Spring首先在缓存中查找数据,如果没有则执行办法并缓存后果,而后返回数据。

缓存名是必须提供的,能够应用引号、Value或者cacheNames属性来定义名称。上面的定义展现了users缓存的申明及其注解的应用:

@Cacheable("users")//Spring 3.x@Cacheable(value = "users")//Spring 从4.0开始新增了value别名cacheNames比value更达意,举荐应用@Cacheable(cacheNames = "users")

键生成器

缓存的实质就是键/值对汇合。在默认状况下,缓存形象应用(办法签名及参数值)作为一个键值,并将该键与办法调用的后果组成键/值对。 如果在Cache注解上没有指定key,
则Spring会应用KeyGenerator来生成一个key。

package org.springframework.cache.interceptor;import java.lang.reflect.Method;@FunctionalInterfacepublic interface KeyGenerator {    Object generate(Object var1, Method var2, Object... var3);
}

Sping默认提供了SimpleKeyGenerator生成器。Spring 3.x之后废除了3.x 的DefaultKey
Generator而用SimpleKeyGenerator取代,起因是DefaultKeyGenerator在有多个入参时只是简略地把所有入参放在一起应用hashCode()办法生成key值,这样很容易造成key抵触。SimpleKeyGenerator应用一个复合键SimpleKey来解决这个问题。通过其源码可得悉Spring生成key的规定。

/**
 * SimpleKeyGenerator源码的类门路参见{@link org.springframework.cache.interceptor.SimpleKeyGenerator}
 */

从SimpleKeyGenerator的源码中能够发现其生成规定如下(附SimpleKey源码):

  • 如果办法没有入参,则应用SimpleKey.EMPTY作为key(key = new SimpleKey())。
  • 如果只有一个入参,则应用该入参作为key(key = 入参的值)。
  • 如果有多个入参,则返回蕴含所有入参的一个SimpleKey(key = new SimpleKey(params))。

package org.springframework.cache.interceptor;import java.io.Serializable;import java.util.Arrays;import org.springframework.util.Assert;import org.springframework.util.StringUtils;public class SimpleKey implements Serializable {    public static final SimpleKey EMPTY = new SimpleKey(new Object[0]);    private final Object[] params;    private final int hashCode;    public SimpleKey(Object... elements) {
        Assert.notNull(elements, "Elements must not be null");        this.params = new Object[elements.length];
        System.arraycopy(elements, 0, this.params, 0, elements.length);        this.hashCode = Arrays.deepHashCode(this.params);
    }    public boolean equals(Object other) {        return this == other || other instanceof SimpleKey && Arrays.deepEquals(this.params, ((SimpleKey)other).params);
    }    public final int hashCode() {        return this.hashCode;
    }    public String toString() {        return this.getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
    }
}

如需自定义键生成策略,能够通过实现org.springframework.cache.interceptor.KeyGenerator接口来定义本人理论须要的键生成器。示例如下,自定义了一个MyKeyGenerator类并且实现(implements)了KeyGenerator以实现自定义的键值生成器:

package com.example.cache.springcache;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.cache.interceptor.KeyGenerator;import org.springframework.cache.interceptor.SimpleKey;import java.lang.reflect.Method;/**
 * @author: 博客「成猿手册」
 * @description: 为不便演示,这里自定义的键生成器只是在SimpleKeyGenerator根底上加了一些logger打印以区别自定义的Spring默认的键值生成器;
 */public class MyKeyGenerator implements KeyGenerator {    private static final Logger logger =  LoggerFactory.getLogger(MyKeyGenerator.class);    @Override
    public Object generate(Object o, Method method, Object... objects) {
        logger.info("执行自定义键生成器");        return generateKey(objects);
    }    public static Object generateKey(Object... params) {        if (params.length == 0) {
            logger.debug("本次缓存键名称:{}", SimpleKey.EMPTY);            return SimpleKey.EMPTY;
        } else {            if (params.length == 1) {
                Object param = params[0];                if (param != null && !param.getClass().isArray()) {
                    logger.debug("本次缓存键名称:{}", params);                    return param;
                }
            }
            SimpleKey simpleKey = new SimpleKey(params);
            logger.debug("本次缓存键名称:{}", simpleKey.toString());            return simpleKey;
        }
    }
}

同时在Spring配置文件中配置:

<!-- 配置键生成器Bean --><bean id = "myKeyGenerator" class="com.example.cache.springcache.MyKeyGenerator" />

应用示例如下:

@Cacheable(cacheNames = "userId",keyGenerator = "myKeyGenerator")public User getUserById(String userId)

执行的打印后果如下:

first query...14:50:29.901 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器14:50:29.902 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test00114:50:29.904 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器14:50:29.904 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test001
query user by userId=test001
querying id from DB...test001
result object: com.example.cache.customize.entity.User@1a6c1270
second query...14:50:29.927 [main] INFO com.example.cache.springcache.MyKeyGenerator - 执行自定义键生成器14:50:29.927 [main] DEBUG com.example.cache.springcache.MyKeyGenerator - 本次键名称:test001
result object: com.example.cache.customize.entity.User@1a6c1270

@CachePut

@CachePut注解属性与@Cacheable注解属性相比少了sync属性。其余用法基本相同:

属性名

作用与形容

cacheNames/value

指定缓存的名字,缓存应用CacheManager治理多个缓存Cache,这些Cache就是依据该属性进行辨别。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有本人惟一的名字。

key

缓存数据时的key的值,默认是应用办法所有入参的值,能够应用SpEL表达式示意key的值。

keyGenerator

缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。

cacheManager

指定缓存管理器(例如ConcurrentHashMap、Redis等)。

cacheResolver

和cacheManager作用一样,应用时二选一。

condition

指定缓存的条件(对参数判断,满足什么条件时才缓存),可用SpEL表达式,例如:办法入参为对象user则表达式能够写为condition = "#user.age>18",示意当入参对象user的属性age大于18才进行缓存。

unless

否定缓存的条件(对后果判断,满足什么条件时不缓存),即满足unless指定的条件时,对调用办法获取的后果不进行缓存,例如:unless = "result==null",示意如果后果为null时不缓存。

如果一个办法应用了@Cacheable注解,当反复(n>1)调用该办法时,因为缓存机制,并未再次执行办法体,其后果间接从缓存中找到并返回,即获取还的是第一次办法执行后放进缓存中的后果。

但理论业务并不总是如此,有些状况下要求办法肯定会被调用,例如数据库数据的更新,系统日志的记录,确保缓存对象属性的实时性等等。

@CachePut注解就确保办法调用即执行,执行后更新缓存。

示例代码清单:

package com.example.cache.springcache;import com.example.cache.customize.entity.User;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */@Service(value = "userServiceBean2")public class UserService2 {    /**
     * 申明缓存名称为userCache
     * 缓存键值key未指定默认为userNumber+userName组合字符串
     *
     * @param userId 用户Id
     * @return 返回用户对象
     */
    @Cacheable(cacheNames = "userCache")    public User getUserByUserId(String userId) {        // 办法外部实现不思考缓存逻辑,间接实现业务
        return getFromDB(userId);
    }    /**
     * 注解@CachePut:确保办法体内办法肯定执行,执行完之后更新缓存;
     * 应用与 {@link com.example.cache.springcache.UserService2#getUserByUserId(String)}办法
     * 雷同的缓存userCache和key(缓存键值应用spEl表达式指定为userId字符串)以实现对该缓存更新;
     *
     * @param user 用户参数
     * @return 返回用户对象
     */
    @CachePut(cacheNames = "userCache", key = "(#user.userId)")    public User updateUser(User user) {        return updateData(user);
    }    private User updateData(User user) {
        System.out.println("real updating db..." + user.getUserId());        return user;
    }    private User getFromDB(String userId) {
        System.out.println("querying id from db..." + userId);        return new User(userId);
    }
}

测试代码清单:

package com.example.cache.springcache;import com.example.cache.customize.entity.User;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */public class UserMain2 {    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService2 userService2 = (UserService2) context.getBean("userServiceBean2");        //第一次查问,缓存中没有,从数据库查问
        System.out.println("first query...");
        User user1 = userService2.getUserByUserId("user001");
        System.out.println("result object: " + user1);

        user1.setAge(20);
        userService2.updateUser(user1);        //调用即执行,而后更新缓存
        user1.setAge(21);
        userService2.updateUser(user1);

        System.out.println("second query...");
        User user2 = userService2.getUserByUserId("user001");
        System.out.println("result object: " + user2);
        System.out.println("result age: " + user2.getAge());
    }
}

测试打印后果如下:

first query...querying id from db...user001result object: com.example.cache.customize.entity.User@6d1ef78dreal updating db...user001
real updating db...user001
second query...
result object: com.example.cache.customize.entity.User@6d1ef78d
result age: 21

结果表明,执行了两次模仿调用数据库的办法。须要留神的是,在这个简略示例中,两次setAge()办法并不可能证实的确更新了缓存:把updateData()办法去掉也能够失去最终的用户年龄后果,因为set操作的依然是getUserByName()之前获取的对象。

应该在实际操作中将getFromDB和updateData调整为更新数据库的具体方法,再通过加与不加@CachePut来比照最初的后果判断是否更新缓存。

@CacheEvict

@CacheEvict注解属性一览:

属性名

作用与形容

cacheNames/value

指定缓存的名字,缓存应用CacheManager治理多个缓存Cache,这些Cache就是依据该属性进行辨别。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有本人惟一的名字。

key

缓存数据时的key的值,默认是应用办法所有入参的值,能够应用SpEL表达式示意key的值。

keyGenerator

缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。

cacheManager

指定缓存管理器(例如ConcurrentHashMap、Redis等)。

cacheResolver

和cacheManager作用一样,应用时二选一。

condition

指定删除缓存的条件(对参数判断,满足什么条件时才删除缓存),可用SpEL表达式,例如:入参为字符userId的办法删除缓存条件设定为当入参不是user001就删除缓存,则表达式能够写为condition = "!('user001').equals(#userId)"。

allEntries

allEntries是布尔类型的,用来示意是否须要革除缓存中的所有元素。默认值为false,示意不须要。当指定allEntries为true时,Spring Cache将疏忽指定的key,革除缓存中的所有内容。

beforeInvocation

革除操作默认是在对应办法执行胜利后触发的(beforeInvocation = false),即办法如果因为抛出异样而未能胜利返回时则不会触发革除操作。应用beforeInvocation属性能够扭转触发革除操作的工夫。当指定该属性值为true时,Spring会在调用该办法之前革除缓存中的指定元素。

@CacheEvict注解是@Cachable注解的反向操作,它负责从给定的缓存中移除一个值。大多数缓存框架都提供了缓存数据的有效期,应用该注解能够显式地从缓存中删除生效的缓存数据。该注解通常用于更新或者删除用户的操作。上面的办法定义从数据库中删除-一个用户,而@CacheEvict 注解也实现了雷同的工作,从users缓存中删除了被缓存的用户。

在下面的实例中增加删除办法:

@CacheEvict(cacheNames = "userCache")public void delUserByUserId(String userId) {    //模仿理论业务中的删除数据操作
    System.out.println("deleting user from db..." + userId);
}

测试代码清单:

package com.example.cache.springcache;import com.example.cache.customize.entity.User;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */public class UserMain3 {    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService2 userService2 = (UserService2) context.getBean("userServiceBean2");
        String userId = "user001";        //第一次查问,缓存中没有,执行数据库查问
        System.out.println("first query...");
        User user1 = userService2.getUserByUserId(userId);
        System.out.println("result object: " + user1);        //第二次查问从缓存中查问
        System.out.println("second query...");
        User user2 = userService2.getUserByUserId(userId);
        System.out.println("result object: " + user2);        //先移除缓存再查问,缓存中没有,执行数据库查问
        userService2.delUserByUserId(userId);
        User user3 = userService2.getUserByUserId(userId);
        System.out.println("result object: " + user3);
    }
}

执行的打印后果如下:

first query...
querying id from db...user001
result object: com.example.cache.customize.entity.User@6dee4f1b
second query...
result object: com.example.cache.customize.entity.User@6dee4f1b
deleting user from db...user001
querying id from db...user001
result object: com.example.cache.customize.entity.User@31bcf236

通过打印后果验证了@CacheEvict移除缓存的成果。须要留神的是,在雷同的办法上应用@Caheable和@CacheEvict注解并应用它们指向雷同的缓存没有任何意义,因为这相当于数据被缓存之后又被立刻移除了,所以须要防止在同一办法上同时应用这两个注解。

@Caching

@Caching注解属性一览:

属性名

作用与形容

cacheable

取值为基于@Cacheable注解的数组,定义对办法返回后果进行缓存的多个缓存。

put

取值为基于@CachePut注解的数组,定义执行办法后,对返回方的办法后果进行更新的多个缓存。

evict

取值为基于@CacheEvict注解的数组。定义多个移除缓存。

总结来说,@Caching是一个组注解,能够为一个办法定义提供基于@Cacheable、@CacheEvict或者@CachePut注解的数组。

示例定义了User(用户)、Member(会员)和Visitor(游客)3个实体类,它们彼此之间有一个简略的层次结构:User是一个抽象类,而Member和Visitor类扩大了该类。

User(用户抽象类)代码清单:

package com.example.cache.springcache.entity;/**
 * @author: 博客「成猿手册」
 * @description: 用户抽象类
 */public abstract class User {    private String userId;    private String userName;    public User(String userId, String userName) {        this.userId = userId;        this.userName = userName;
    }    //todo:此处省略get和set办法}

Member(会员类)代码清单:

package com.example.cache.springcache.entity;import java.io.Serializable;/**
 * @author: 博客「成猿手册」
 * @description: 会员类
 */public class Member extends User implements Serializable {    public Member(String userId, String userName) {        super(userId, userName);
    }
}

Visitor(游客类)代码清单:

package com.example.cache.springcache.entity;import java.io.Serializable;/**
 * @author: 博客「成猿手册」
 * @description: 访客类
 */public class Visitor extends User implements Serializable {    private String visitorName;    public Visitor(String userId, String userName) {        super(userId, userName);
    }
}

UserService3类是一个Spring服务Bean,蕴含了getUser()办法。
同时申明了两个@Cacheable注解,并使其指向两个不同的缓存项: members和visitors。而后依据两个@Cacheable注解定义中的条件对办法的参数进行查看,并将对象存储在
members或visitors缓存中。

UserService3代码清单:

package com.example.cache.springcache;import com.example.cache.springcache.entity.Member;import com.example.cache.springcache.entity.User;import com.example.cache.springcache.entity.Visitor;import org.springframework.cache.annotation.Cacheable;import org.springframework.cache.annotation.Caching;import org.springframework.stereotype.Service;import java.util.HashMap;import java.util.Map;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */@Service(value = "userServiceBean3")public class UserService3 {    private Map<String, User> users = new HashMap<>();

    {        //初始化数据,模仿数据库中数据
        users.put("member001", new Member("member001", "会员小张"));
        users.put("visitor001", new Visitor("visitor001", "访客小曹"));
    }    @Caching(cacheable = {            /*
              该condition指定的SpEl表达式用来判断办法传参的类型
              instanceof是Java中的一个二元运算符,用来测试一个对象(援用类型)是否为一个类的实例
             */
            @Cacheable(value = "members", condition = "#user instanceof T(" +                    "com.example.cache.springcache.entity.Member)"),            @Cacheable(value = "visitors", condition = "#user instanceof T(" +                    "com.example.cache.springcache.entity.Visitor)")
    })    public User getUser(User user) {        //模仿数据库查问
        System.out.println("querying id from db..." + user.getUserId());        return users.get(user.getUserId());
    }
}

UserService3类是-一个Spring服务Bean,蕴含了getUser()办法。同时申明了两个@Cacheable注解,并使其指向两个不同的缓存项: members 和visitors。
而后依据两个@Cacheable注解定义中的条件对办法的参数进行查看,并将对象存储在
members或visitors缓存中。

测试代码清单:

package com.example.cache.springcache;import com.example.cache.springcache.entity.Member;import com.example.cache.springcache.entity.User;import com.example.cache.springcache.entity.Visitor;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */public class UserService3Test {    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService3 userService3 = (UserService3) context.getBean("userServiceBean3");

        Member member = new Member("member001", null);        //会员第一次查问,缓存中没有,从数据库中查问
        User member1 = userService3.getUser(member);
        System.out.println("member userName-->" + member1.getUserName());        //会员第二次查问,缓存中有,从缓存中查问
        User member2 = userService3.getUser(member);
        System.out.println("member userName-->" + member2.getUserName());

        Visitor visitor = new Visitor("visitor001", null);        //游客第一次查问,缓存中没有,从数据库中查问
        User visitor1 = userService3.getUser(visitor);
        System.out.println("visitor userName-->" + visitor1.getUserName());        //游客第二次查问,缓存中有,从缓存中查问
        User visitor2 = userService3.getUser(visitor);
        System.out.println("visitor userName-->" + visitor2.getUserName());
    }
}

执行的打印后果如下:

querying id from db...member001
member userName-->会员小张
member userName-->会员小张
querying id from db...visitor001
visitor userName-->访客小曹
visitor userName-->访客小曹

@CacheConfig

@CacheConfig注解属性一览:

属性名

作用与形容

cacheNames/value

指定类级别缓存的名字,缓存应用CacheManager治理多个缓存Cache,这些Cache就是依据该属性进行辨别。对缓存的真正增删改查操作在Cache中定义,每个缓存Cache都有本人惟一的名字。

keyGenerator

类级别缓存的生成策略(键生成器),和key二选一,作用是生成键值key,keyGenerator可自定义。

cacheManager

指定类级别缓存管理器(例如ConcurrentHashMap、Redis等)。

cacheResolver

和cacheManager作用一样,应用时二选一。

后面咱们所介绍的注解都是基于办法的,如果在同一个类中须要缓存的办法注解属性都类似,则须要反复减少。Spring 4.0之后减少了@CacheConfig类级别的注解来解决这个问题。

一个简略的实例如下所示:

package com.example.cache.springcache;import com.example.cache.springcache.entity.User;import org.springframework.cache.annotation.CacheConfig;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;/**
 * @author: 博客「成猿手册」
 * @description: com.example.cache.springcache
 */@CacheConfig(cacheNames = "users",keyGenerator = "myKeyGenerator")public class UserService4 {    @Cacheable
    public User findA(User user){        //todo:执行一些操作
    }        
    @CachePut
    public User findB(User user){        //todo:执行一些操作
    }
}

能够看到,在@CacheConfig注解中定义了类级别的缓存users和自定义键生成器,
那么在findA0和findB(办法中不再须要反复指定,而是默认应用类级别的定义。