天天看點

Spring Cache緩存注解

Spring Cache緩存相關注解的總結與記錄

目錄

  • Spring Cache緩存注解
    • @Cacheable
      • 鍵生成器
    • @CachePut
    • @CacheEvict
    • @Caching
    • @CacheConfig

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;

@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

屬性。其他用法基本相同:

屬性名 作用與描述
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...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

           

結果表明,執行了兩次模拟調用資料庫的方法。需要注意的是,在這個簡單示例中,兩次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(方法中不再需要重複指定,而是預設使用類級别的定義。