每個開發者勢必都會用到一些Cache暫存工具,但依據小編在業界與各國開發者經驗交手而言,大部分都是採用Guava Cache這項套件,甚少人知道Spring框架有內部提供Cache註解模式機制,小編今天就帶領大家來一窺Spring框架中三項Cache註解可緩存(@Cacheable)、緩存放置(@CachePut)及緩存清除(@CacheEvict),其註解模式支援相當多種判斷依據,可說是相當不錯,不僅可透過各種模型內的宣告欄位當作索引,亦可透過配置的引數(argument)的指數位置當做索引等等,可是支援相當多元化的快取性註解支援,相關原理我們在下面做進一步介紹。
快取為一種一數據暫存在記憶體中,他的操作原理就是置放、刪除兩種行為,在Spring Cache操作原理中,會分析是否有重複的索引(Key)並檢查其值(Value),若有會將其值(Value)刪除掉,並在將其新值逐一至放進此Cache,所有的索引值配對(Key-Value)都會存放在一個ConcurrentMap的配置池中,其鎖引鍵產生原理為來源類別、來源方法及註解上鎖配置的索引關鍵詞三項所組成,當組裝完成後,會透過Spring框架SpelExpression核心元件中的SpelNodeImpl物件,並針對EvaluationContext進行包裝後取得關鍵索引值物件,其EvaluationContext以包裝BeanFactory,協助內部個元件快速獲取相關物件值,追朔生成原理可看出相當複雜,當開發者進行取得值行為時,皆會產生一筆執行緒,並去行執行Cache.get()行為,相關生成與範例請參照下方程式碼。
關鍵詞物件產生核心程式碼片段
@Override
@Nullable
public Object getValue(EvaluationContext context) throws EvaluationException {
Assert.notNull(context, "EvaluationContext is required");
CompiledExpression compiledAst = this.compiledAst;
if (compiledAst != null) {
try {
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
}
}
}
ExpressionState expressionState = new ExpressionState(context, this.configuration);
Object result = this.ast.getValue(expressionState);
checkCompile(expressionState);
return result;
}
範例一、同步儲存快取範例
@Cacheable(value = "cache1", sync = true)
@Override
public List<User> listUserObject() {
System.out.println("list user Object!");
return this.userList;
}
@CacheEvict(value="cache1", allEntries=true, beforeInvocation=true)
@Override
public boolean clearUserObject() {
System.out.println("Clear cache finish");
this.userList.clear();
return true;
}
@Override
public User createUserObject(User user) {
System.out.println("create user Object!");
this.userList.add(user);
return user;
}
範例一結果、由下可得知在建立新的使用者物件,雖未在程式碼中執行listUserObject(),但有觸發到該方法內關聯的物件,必定會執行到Spring核心中再次觸發到listUserObject()方法,由結果中可看出在建立中及清空都有呼叫相關方法。
========== Before Test ==========
create user Object!
list user Object!
[{"id":10,"username":"john","password":"123456","email":"john@test.com.tw","role":"admin","remark":"come from penghu."},{"id":20,"username":"weisting","password":"654321","email":"test@test.com.tw","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"},{"id":0,"username":"abc0","password":"123456","email":"abc@test.com.hk","role":"customer service.","remark":"remark"}]
.....
create user Object!
.....
.....
Clear cache finish
list user Object!
[]
[]
[]
========== Finish Test ==========
範例二、透過方法引數名稱當快取索引
@Cacheable(value="user", key="#name")
@Override
public User getUserObjectByUserName(String name) {
System.out.println("get user Object by user " + name + "!");
return this.userList.stream().filter(user -> {
return user.getUsername().equalsIgnoreCase(name);
}).findAny().orElse(null);
}
範例二結果:僅有第一次觸發方法進行分析暫存,後續相關結果皆會採用快取內的直,由測試後可得知
========== Before Test ==========
get user Object by user weisting!
{"id":20,"username":"weisting","password":"654321","email":"test@test.com.tw","role":"sales","remark":"come from taoyuan"}
get user Object by user show!
{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"}
......
......
......
{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"}
{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"}
{"id":20,"username":"weisting","password":"654321","email":"test@test.com.tw","role":"sales","remark":"come from taoyuan"}
========== Finish Test ==========
範例三、透過方法引數模型中的宣告欄位(Field)當快取索引
@Cacheable(value="userCache", key="#user.email")
@Override
public User verifyUserEmail(User user) {
System.out.println("Verify User Email success.");
if (user.getEmail().contains("@"))
return user;
user.setEmail("N/A");
return user;
}
@Test
public void testCacheByObjectField() {
Random random = new Random();
for ( int i = 0 ; i < 20 ; i++ ) {
int rand = random.nextInt(500)*100;
User user = new User();
if (i % 5 == 0) {
user.setId(rand).setUsername("cache")
.setPassword("3383838")
.setRole("sale")
.setRemark("cache is get")
.setEmail("test"+ i+(rand % 3 == 0 ? '@' : "") + "abbbb.com");
} else {
user.setId(rand).setUsername("cache")
.setPassword("3383838")
.setRole("sale")
.setRemark("cache is get")
.setEmail("test"+ (rand % 3 == 0 ? '@' : "") + "abbbb.com");
}
System.out.println(new Gson().toJson(userService.verifyUserEmail(user)));
}
}
範例三結果:雖用email當索引值,但依舊內部模型值有變動,依舊會進行更新相關值。
========== Before Test ==========
Verify User Email success.
{"id":8100,"username":"cache","password":"3383838","email":"test0@abbbb.com","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":30300,"username":"cache","password":"3383838","email":"test@abbbb.com","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":30300,"username":"cache","password":"3383838","email":"test@abbbb.com","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":42000,"username":"cache","password":"3383838","email":"test5@abbbb.com","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":30300,"username":"cache","password":"3383838","email":"test@abbbb.com","role":"sale","remark":"cache is get"}
{"id":30300,"username":"cache","password":"3383838","email":"test@abbbb.com","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":26900,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":25800,"username":"cache","password":"3383838","email":"test15@abbbb.com","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
{"id":36500,"username":"cache","password":"3383838","email":"N/A","role":"sale","remark":"cache is get"}
========== Finish Test ==========
範例四、判斷式條件快取,並以模型內欄位當索引
@Cacheable(value={"userCacheId"}, key="#user.id", condition="#user.id%2==0")
@Override
public User logUserById(User user) {
System.out.println("log user by username: " + user.getId());
logList.add(user);
return user;
}
@Test
public void testLogCacheByLogId() {
for (int i = 0 ; i < 20 ; i++) {
User user = new User();
user.setId(i%5).setUsername("cache")
.setPassword("3383838")
.setRole("sale")
.setRemark("cache is get")
.setEmail("test"+ (i%5)+ "@abbbb.com");
System.out.println(new Gson().toJson(userService.logUserById(user)));
}
}
範例四結果,可以看到執行到第一段偶數ID後,後續所有奇數筆數資料會跳過快取,並直接運行方法內在獲取相關結果。
========== Before Test ==========
log user by username: 0
{"id":0,"username":"cache","password":"3383838","email":"test0@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"test1@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 2
{"id":2,"username":"cache","password":"3383838","email":"test2@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"test3@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 4
{"id":4,"username":"cache","password":"3383838","email":"test4@abbbb.com","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"test0@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"test1@abbbb.com","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"test2@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"test3@abbbb.com","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"test4@abbbb.com","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"test0@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"test1@abbbb.com","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"test2@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"test3@abbbb.com","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"test4@abbbb.com","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"test0@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"test1@abbbb.com","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"test2@abbbb.com","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"test3@abbbb.com","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"test4@abbbb.com","role":"sale","remark":"cache is get"}
========== Finish Test ==========
範例五、快取置放方法
@CachePut("updateCache")
@Override
public List<User> updateUserObject(User user) {
System.out.println("update user Object!");
this.userList = this.userList.stream().map(userObj -> {
if (String.valueOf(userObj.getId()).equalsIgnoreCase(String.valueOf(user.getId()))) {
userObj.setUsername(user.getUsername());
userObj.setEmail(user.getEmail());
userObj.setPassword(user.getPassword());
userObj.setRole(user.getRole());
userObj.setRemark(user.getRemark());
}
return userObj;
}).collect(Collectors.toList());
return this.userList;
}
@Test
public void testUpdateUserObject() {
User user = new User()
.setId(10)
.setUsername("john2")
.setRole("admin2")
.setPassword("654321")
.setEmail("john2@test.com.tw")
.setRemark("come from Taipei.");
System.out.println(new Gson().toJson(userService.listUserObject()));
userService.updateUserObject(user);
System.out.println(new Gson().toJson(userService.listUserObject()));
}
範例五結果、可以看到結果,id為10這筆資料我們進行更新,會直接進行更新快取內的值,不會在觸發到listUserObject()方法,可由下列結果得知。
========== Before Test ==========
list user Object!
[{"id":10,"username":"john","password":"123456","email":"john@test.com.tw","role":"admin","remark":"come from penghu."},{"id":20,"username":"weisting","password":"654321","email":"test@test.com.tw","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"}]
update user Object!
[{"id":10,"username":"john2","password":"654321","email":"john2@test.com.tw","role":"admin2","remark":"come from Taipei."},{"id":20,"username":"weisting","password":"654321","email":"test@test.com.tw","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"ssss@test.com.tw","role":"customer","remark":"come from taipei"}]
========== Finish Test ==========
由以上範例可以詳細地看出@Cacheable、@CachePut及@CacheEvict三種方法運用的方式,給各位開發者做一個參考。
圖一、Spring Cache運作存取架構圖
此項Spring Cache為2015年提倡出來的註解行快取技術,能夠減少開發者元件觸發(invoke)方法的頻率,透過以上架構圖可得知,所有快取的攔截模組皆透過CacheInterceptor元件,其元件會透過支援角色CacheAspectSupport進行處理,會經過七道流程處理,第一道同步調曲及特殊處理,先判斷是否有採用條件式(Condition)判斷,並在進行產生索引值,取得快取值及回傳值,第二道進行先行刪除部分舊快取資料,第三道檢查是否有符合條件的緩存項目,第四項尋找沒有存放在可緩存(@Cacheable)的項目,第五道搜集所有確定的快取置放池(@CachePuts),第六道處理所有相關漏掉存放的快取及將要存放的快取資料,最後第七道處理相關不需要的快取將其刪除,各位開發者有興趣可從原始碼得知,以上敘述提供給各位做參考。
Run test task
gradle test
Run open result html
open ./build/reports/tests/test/index.html
Cache test report
Mind-blowing cache test case information & detail
Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用