Spring Cache缓存注解

当在一个类上使用注解时,该类中每个公共方法的返回值都将被缓存到指定的缓存项中或者从中移除。

@Cacheable

@Cacheable注解属性一览:

@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;

@FunctionalInterface
public 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 - 本次键名称:test001
14: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属性。其他用法基本相同:

如果一个方法使用了@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...user001
result object: com.example.cache.customize.entity.User@6d1ef78d
real updating db...user001
real updating db...user001
second query...
result object: com.example.cache.customize.entity.User@6d1ef78d
result age: 21

结果表明,执行了两次模拟调用数据库的方法。需要注意的是,:把updateData()方法去掉也可以得到最终的用户年龄结果,因为set操作的仍然是getUserByName()之前获取的对象。

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

@CacheEvict

@CacheEvict注解属性一览:

@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注解属性一览:

总结来说,@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注解属性一览:

前面我们所介绍的注解都是基于方法的,如果在同一个类中需要缓存的方法注解属性都相似,则需要重复增加。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(方法中不再需要重复指定,而是默认使用类级别的定义。

07-28 20:12