[Day - 16] - Spring 快取上手一天就搞定

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":"[email protected]","role":"admin","remark":"come from penghu."},{"id":20,"username":"weisting","password":"654321","email":"[email protected]","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"[email protected]","role":"customer","remark":"come from taipei"},{"id":0,"username":"abc0","password":"123456","email":"[email protected]","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":"[email protected]","role":"sales","remark":"come from taoyuan"}
get user Object by user show!
{"id":30,"username":"show","password":"987765","email":"[email protected]","role":"customer","remark":"come from taipei"}
......
......
......
{"id":30,"username":"show","password":"987765","email":"[email protected]","role":"customer","remark":"come from taipei"}
{"id":30,"username":"show","password":"987765","email":"[email protected]","role":"customer","remark":"come from taipei"}
{"id":20,"username":"weisting","password":"654321","email":"[email protected]","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":"[email protected]","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":30300,"username":"cache","password":"3383838","email":"[email protected]","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":"[email protected]","role":"sale","remark":"cache is get"}
Verify User Email success.
{"id":42000,"username":"cache","password":"3383838","email":"[email protected]","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":"[email protected]","role":"sale","remark":"cache is get"}
{"id":30300,"username":"cache","password":"3383838","email":"[email protected]","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":"[email protected]","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":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 2
{"id":2,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 4
{"id":4,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":0,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 1
{"id":1,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":2,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
log user by username: 3
{"id":3,"username":"cache","password":"3383838","email":"[email protected]","role":"sale","remark":"cache is get"}
{"id":4,"username":"cache","password":"3383838","email":"[email protected]","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("[email protected]")
                .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":"[email protected]","role":"admin","remark":"come from penghu."},{"id":20,"username":"weisting","password":"654321","email":"[email protected]","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"[email protected]","role":"customer","remark":"come from taipei"}]
update user Object!
[{"id":10,"username":"john2","password":"654321","email":"[email protected]","role":"admin2","remark":"come from Taipei."},{"id":20,"username":"weisting","password":"654321","email":"[email protected]","role":"sales","remark":"come from taoyuan"},{"id":30,"username":"show","password":"987765","email":"[email protected]","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


<<:  企划实现(16)

>>:  爬虫怎麽爬 从零开始的爬虫自学 DAY17 python爬虫所需套件

sed - 4 Write commands

前篇回顾 sed - 简介 读取编辑文字档的好工具 sed - 2 Pattern sed - 3 ...

[Day23] 第二十三章 - 学会laravel的query方法来filter资料(Query Builder)

前言 前几天我们完成惹user跟skill的ajax的前端界接服务 并且有成功更新资料进资料库 今天...

18. PHPer x API document x Swagger API

想当一个 Good PHPer,不但要写程序、写注解还要写 API 文件,想到要维护三个地方工程师就...

网页颜色-30天学会HTML+CSS,制作精美网站

好的网站除了内容传达之外,颜色是进入网站的第一印象,可以针对文字大小、框线、背景色...等做变化,是...

【网页设计 入门 】如何使用 Bootstrap 与 Github Pages 制作 个人网站 ?

简单架设 x 不失质感 目录 源起 : 开发者网站 开发工具 : Adobe Brackets 基础...