Spring Cache緩存註解

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(方法中不再需要重複指定,而是默認使用類級別的定義。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章