Spring Cache快取註解

成猿手冊發表於2020-07-28

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()之前獲取的物件。

應該在實際操作中將getFromDBupdateData調整為更新資料庫的具體方法,再通過加與不加@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(方法中不再需要重複指定,而是預設使用類級別的定義。

相關文章