詳解Spring數(shù)據(jù)緩存注解@Cacheable、@CachePut、@CacheEvict
前言
如果想讓應(yīng)用程序避免一遍遍地為同一個問題推導(dǎo)、計算或查詢答案的話,緩存是一種很棒的方式。當(dāng)以一組參數(shù)第一次調(diào)用某個方法時,返回值會被保存在緩存中,如果這個方法再次以相同的參數(shù)進(jìn)行調(diào)用時,這個返回值會從緩存中查詢獲取。在很多場景中,從緩存查找值會比其他的方式(比如,執(zhí)行數(shù)據(jù)庫查詢)成本更低。因此,緩存會對應(yīng)用程序的性能帶來正面的影響。
通過XML啟用注解驅(qū)動的緩存
使用XML的方式配置時,需要使用Spring cache命名空間中的<cache:annotation-driven>元素來啟用注解驅(qū)動的緩存。從本質(zhì)上來講,其工作方式它是會創(chuàng)建一個切面(aspect)并觸發(fā)Spring緩存注解的切點(pointcut)。根據(jù)所使用的注解以及緩存的狀態(tài),這個切面會從緩存中獲取數(shù)據(jù),將數(shù)據(jù)添加到緩存之中或者從緩存中移除某個值。
<cache:annotation-driven>
緩存管理器
緩存管理器是Spring緩存抽象的核心,它能夠與多個流行的緩存實現(xiàn)進(jìn)行集成。Spring常見管理器如下表所示:
對于上表中ConcurrentMapCacheManager,這是一個簡單的緩存管理器使用java.util.concurrent.ConcurrentHashMap作為其緩存存儲。它非常簡單,因此對于開發(fā)、測試或基礎(chǔ)的應(yīng)用來講,這是一個很不錯的選擇。但它的緩存存儲是基于內(nèi)存的,所以它的生命周期是與應(yīng)用關(guān)聯(lián)的,對于生產(chǎn)級別的大型企業(yè)級應(yīng)用程序,這可能并不是理想的選擇。
基于SimpleCacheManager的XML配置示例1
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <cache:annotation-driven/> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="users"/> </set> </property> </bean> </beans
為方法添加注解以支持緩存
如前文所述,Spring的緩存抽象在很大程度上是圍繞切面構(gòu)建的。在Spring中啟用緩存時,會創(chuàng)建一個切面,它觸發(fā)一個或更多的Spring的緩存注解。下表列出了Spring所提供的緩存注解。
填充緩存
可以看到,@Cacheable和@CachePut注解都可以填充緩存,但是它們的工作方式略有差異。
@Cacheable首先在緩存中查找條目,如果找到了匹配的條目,那么就不會對方法進(jìn)行調(diào)用了。如果沒有找到匹配的條目,方法會被調(diào)用并且返回值要放到緩存之中。而@CachePut并不會在緩存中檢查匹配的值,目標(biāo)方法總是會被調(diào)用,并將返回值添加到緩存之中。@Cacheable和@CachePut有一些屬性是共有的,如下表所示:
在最簡單的情況下,在@Cacheable和@CachePut的這些屬性中,只需使用value屬性指定一個或多個緩存即可。例如,考慮UserDao的findById(Integer id)方法。在初始保存之后,User數(shù)據(jù)表就不會再發(fā)生變化了。如果有的用戶會被頻繁請求,反復(fù)地在數(shù)據(jù)庫中進(jìn)行獲取是對時間和資源的浪費。通過在findById(Integer id)方法上添加@Cacheable注解,如下面的程序清單所示,能夠確保將User對象保存在緩存users中,從而避免對數(shù)據(jù)庫的不必要訪問。
@Cacheable(value="users") public User findUserById(int id) { String sql="select * from t_user where id=?"; return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id); } class UserRowMapper implements RowMapper<User> { //rs為返回結(jié)果集,以每行為單位封裝著 @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setID(rs.getInt("id")); user.setUserName(rs.getString("name")); user.setUserPwd(rs.getString("pwd")); return user; } }
當(dāng)findUserById(int id)被調(diào)用時,緩存切面會攔截調(diào)用并在緩存中查找之前以名users存儲的返回值。緩存的key是傳遞到findUserById(int id)方法中的id參數(shù)。如果按照這個key能夠找到值的話,就會返回找到的值,方法不會再被調(diào)用。如果沒有找到值的話,那么就會調(diào)用這個方法,并將返回值放到緩存之中,為下一次調(diào)用findUserById(int id)方法做好準(zhǔn)備。
當(dāng)@Cacheable為接口方法添加注解后,所有實現(xiàn)類都會應(yīng)用相同的緩存規(guī)則。
當(dāng)一個全新的User對象通過addUser(User user)方法保存之后,很可能馬上就會請求這條記錄。所以,當(dāng)save()方法調(diào)用后,立即將user塞到緩存之中是很有意義的,這樣當(dāng)其他人通過findUserById(int id)對其進(jìn)行查找時,它就已經(jīng)準(zhǔn)備就緒了。為了實現(xiàn)這一點,可以在addUser(User user)方法上添加@CachePut注解,如下所示:
@CachePut(value="users") public void addUser(User user) { String sql = "insert into t_user values(?,?,?)"; jdbcTemplate.update(sql, null,user.getUserName(),user.getUserPwd()); return user; }
當(dāng)addUser(User user)方法被調(diào)用時,它首先會做所有必要的事情來保存user對象,然后返回的user會被放到users緩存中。在這里只有一個問題:緩存的key。如前文所述,默認(rèn)的緩存key要基于方法的參數(shù)來確定。因為addUser(User user)方法的唯一參數(shù)就是user,所以它會用作緩存的key。將users放在緩存中,而它的緩存key恰好是同一個user,這是不是有一點詭異呢?顯然,在這個場景中,默認(rèn)的緩存key并不是我們想要的。我們需要的緩存key是新保存user的ID,而不是user本身。所以,在這里需要指定一個key而不是使用默認(rèn)的key。讓我們看一下怎樣自定義緩存key。
自定義緩存key
@Cacheable和@CachePut都有一個名為key屬性,這個屬性能夠替換默認(rèn)的key,它是通過一個SpEL表達(dá)式計算得到的。任意的SpEL表達(dá)式都是可行的,但是更常見的場景是所定義的表達(dá)式與存儲在緩存中的值有關(guān),據(jù)此計算得到key。
具體到我們這個場景,我們需要將key設(shè)置為所保存user的ID。以參數(shù)形式傳遞給addUser(User user)的user還沒有保存,因此并沒有ID。我們只能通過addUser(User user)返回的user得到id屬性。
幸好,在為緩存編寫SpEL表達(dá)式的時候,Spring暴露了一些很有用的元數(shù)據(jù)。下表列出了SpEL中可用的緩存元數(shù)據(jù)。
對于addUser(User user)方法來說,我們需要的鍵是所返回user對象的id屬性。表達(dá)式#result能夠得到返回的user。借助這個對象,我們可以通過將key屬性設(shè)置為#result.id來引用id屬性:
@CachePut(value="users",key="#result.id") public void addUser(User user) { String sql = "insert into t_user values(?,?,?)"; jdbcTemplate.update(sql, null,user.getUserName(),user.getUserPwd()); return user; }
按照這種方式配置@CachePut,緩存不會去干涉addUser(User user)方法的執(zhí)行,但是返回的user將會保存在緩存中,并且緩存的key與user的id屬性相同。
條件化緩存
通過為方法添加Spring的緩存注解,Spring就會圍繞著這個方法創(chuàng)建一個緩存切面。但是,在有些場景下我們可能希望將緩存功能關(guān)閉。
@Cacheable和@CachePut提供了兩個屬性用以實現(xiàn)條件化緩存:unless和condition,這兩個屬性都接受一個SpEL表達(dá)式。如果unless屬性的SpEL表達(dá)式計算結(jié)果為true,那么緩存方法返回的數(shù)據(jù)就不會放到緩存中。與之類似,如果condition屬性的SpEL表達(dá)式計算結(jié)果為false,那么對于這個方法緩存就會被禁用掉。
表面上來看,unless和condition屬性做的是相同的事情。但是,這里有一點細(xì)微的差別。unless屬性只能阻止將對象放進(jìn)緩存,但是在這個方法調(diào)用的時候,依然會去緩存中進(jìn)行查找,如果找到了匹配的值,就會返回找到的值。與之不同,如果condition的表達(dá)式計算結(jié)果為false,那么在這個方法調(diào)用的過程中,緩存是被禁用的。就是說,不會去緩存進(jìn)行查找,同時返回值也不會放進(jìn)緩存中。
移除緩存條目
@CacheEvict并不會往緩存中添加任何東西。相反,如果帶有@CacheEvict注解的方法被調(diào)用的話,那么會有一個或更多的條目會在緩存中移除。
那么在什么場景下需要從緩存中移除內(nèi)容呢?當(dāng)緩存值不再合法時,我們應(yīng)該確保將其從緩存中移除,這樣的話,后續(xù)的緩存命中就不會返回舊的或者已經(jīng)不存在的值,其中一個這樣的場景就是數(shù)據(jù)被刪除掉了。這樣的話,UserDao的deleteUser(int id)方法就是使用@CacheEvict的絕佳選擇:
@CacheEvict(value="users") public void deleteUser(int id) { String sql = "delete from t_user where id = ?"; jdbcTemplate.update(sql, id); }
注意:與@Cacheable和@CachePut不同,@CacheEvict能夠應(yīng)用在返回值為void的方法上,而@Cacheable和@CachePut需要非void的返回值,它將會作為放在緩存中的條目。因為@CacheEvict只是將條目從緩存中移除,因此它可以放在任意的方法上,甚至void方法。
從上述代碼可以看到,當(dāng)deleteUser()調(diào)用時,會從緩存中刪除一個條目。被刪除條目的key與傳遞進(jìn)來的id參數(shù)的值相等。
@CacheEvict有多個屬性,如下表所示,這些屬性會影響到該注解的行為,使其不同于默認(rèn)的做法??梢钥吹?,@CacheEvict的一些屬性與@Cacheable和@CachePut是相同的,另外還有幾個新的屬性。與@Cacheable和@CachePut不同,@CacheEvict并沒有提供unless屬性。
使用XML聲明緩存
Spring的cache命名空間提供了使用XML聲明緩存規(guī)則的方法,可以作為面向注解緩存的替代方案。因為緩存是一種面向切面的行為,所以cache命名空間會與Spring的aop命名空間結(jié)合起來使用,用來聲明緩存所應(yīng)用的切點在哪里。
要開始配置XML聲明的緩存,首先需要創(chuàng)建Spring配置文件,這個文件中要包含cache和aop命名空間。cache命名空間定義了在Spring XML配置文件中聲明緩存的配置元素。如下表所示:
<!--將緩存通知綁定到切點上--> <aop:config> <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* com.boss.spring.learning.dao.*(..))"/> </aop:config> <cache:advice id="cacheAdvice"> <cache:caching> <cache:cacheable cache="users" method="addUser"></cache:cacheable> <cache:cache-put cache="users" method="addUser" key="#result.id"></cache:cache-put> <cache:cache-evict cache="users" method="deleteUser"></cache:cache-evict> </cache:caching> </cache:advice> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> <property name="caches"> <set> <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="users"/> </set> </property> </bean>
到此這篇關(guān)于詳解Spring數(shù)據(jù)緩存注解@Cacheable、@CachePut、@CacheEvict的文章就介紹到這了,更多相關(guān)Spring數(shù)據(jù)緩存注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
為什么ConcurrentHashMap的key value不能為null,map可以?
這篇文章主要介紹了為什么ConcurrentHashMap的key value不能為null,map可以呢?具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01Java鏈表中元素刪除的實現(xiàn)方法詳解【只刪除一個元素情況】
這篇文章主要介紹了Java鏈表中元素刪除的實現(xiàn)方法,結(jié)合實例形式分析了java只刪除鏈表中一個元素的相關(guān)操作原理、實現(xiàn)方法與注意事項,需要的朋友可以參考下2020-03-03SpringBoot 下在 yml 中的 logging 日志配置方法
logging 配置主要用于控制應(yīng)用程序的日志輸出行為,可以通過配置定制日志的格式、級別、輸出位置等,這篇文章主要介紹了SpringBoot 下在 yml 中的 logging 日志配置,需要的朋友可以參考下2024-06-06SpringController返回值和異常自動包裝的問題小結(jié)
今天遇到一個需求,在不改動原系統(tǒng)代碼的情況下,將Controller的返回值和異常包裝到一個統(tǒng)一的返回對象中去,下面通過本文給大家介紹SpringController返回值和異常自動包裝的問題,需要的朋友可以參考下2024-03-03