iT邦幫忙

2021 iThome 鐵人賽

DAY 16
1

Abstract

每個開發者勢必都會用到一些Cache暫存工具,但依據小編在業界與各國開發者經驗交手而言,大部分都是採用Guava Cache這項套件,甚少人知道Spring框架有內部提供Cache註解模式機制,小編今天就帶領大家來一窺Spring框架中三項Cache註解可緩存(@Cacheable)、緩存放置(@CachePut)及緩存清除(@CacheEvict),其註解模式支援相當多種判斷依據,可說是相當不錯,不僅可透過各種模型內的宣告欄位當作索引,亦可透過配置的引數(argument)的指數位置當做索引等等,可是支援相當多元化的快取性註解支援,相關原理我們在下面做進一步介紹。

Principle Introduction

快取為一種一數據暫存在記憶體中,他的操作原理就是置放、刪除兩種行為,在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三種方法運用的方式,給各位開發者做一個參考。

Structure

圖一、Spring Cache運作存取架構圖
image
此項Spring Cache為2015年提倡出來的註解行快取技術,能夠減少開發者元件觸發(invoke)方法的頻率,透過以上架構圖可得知,所有快取的攔截模組皆透過CacheInterceptor元件,其元件會透過支援角色CacheAspectSupport進行處理,會經過七道流程處理,第一道同步調曲及特殊處理,先判斷是否有採用條件式(Condition)判斷,並在進行產生索引值,取得快取值及回傳值,第二道進行先行刪除部分舊快取資料,第三道檢查是否有符合條件的緩存項目,第四項尋找沒有存放在可緩存(@Cacheable)的項目,第五道搜集所有確定的快取置放池(@CachePuts),第六道處理所有相關漏掉存放的快取及將要存放的快取資料,最後第七道處理相關不需要的快取將其刪除,各位開發者有興趣可從原始碼得知,以上敘述提供給各位做參考。

Follow up

Run test task

gradle test

Run open result html

open ./build/reports/tests/test/index.html

Test Report

Cache test report
image

Mind-blowing cache test case information & detail
image

Sample Source

spring-sample-cache

Reference Url

Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用

Spring使用Cache、整合Ehcache

Guava Cache


上一篇
[Day-15] - Spring 標示說明性註解運用與設計
下一篇
[Day - 17 ] - Spring 導入選擇器原理與開發
系列文
Wow ! There is no doubt about Learn Spring framework in a month.30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言