简介: 介绍 spring 3.1 激动人心的新特性:注释驱动的缓存,本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 spring 的基于注释的 cache 配置方法,展现了 spring cache 的强大之处,然后介绍了其基本的原理,扩展点和使用场景的限制。通过阅读本文,你可以短时间内掌握 spring 带来的强大缓存技术,在很少的配置下即可给既有代码提供缓存能力。
概述
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
其特点总结如下:
本文将针对上述特点对 Spring cache 进行详细的介绍,主要通过一个简单的例子和原理介绍展开,然后我们将一起看一个比较实际的缓存例子,最后会介绍 spring cache 的使用限制和注意事项。OK,Let ’ s begin!
这里先展示一个完全自定义的缓存实现,即不用任何第三方的组件来实现某种对象的内存缓存。
场景是:对一个账号查询方法做缓存,以账号名称为 key,账号对象为 value,当以相同的账号名称查询账号的时候,直接从缓存中返回结果,否则更新缓存。账号查询服务还支持 reload 缓存(即清空缓存)。
首先定义一个实体类:账号类,具备基本的 id 和 name 属性,且具备 getter 和 setter 方法
清单 1. Account.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package
cacheOfAnno;
public
class
Account {
private
int
id;
private
String name;
public
Account(String name) {
this
.name = name;
}
public
int
getId() {
return
id;
}
public
void
setId(
int
id) {
this
.id = id;
}
public
String getName() {
return
name;
}
public
void
setName(String name) {
this
.name = name;
}
}
|
然后定义一个缓存管理器,这个管理器负责实现缓存逻辑,支持对象的增加、修改和删除,支持值对象的泛型。如下:
清单 2. MyCacheManager.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package
oldcache;
import
java.util.Map;
import
java.util.concurrent.ConcurrentHashMap;
public
class
MyCacheManager
private
Map
new
ConcurrentHashMap
public
T getValue(Object key) {
return
cache.get(key);
}
public
void
addOrUpdateCache(String key,T value) {
cache.put(key, value);
}
public
void
evictCache(String key) {
// 根据 key 来删除缓存中的一条记录
if
(cache.containsKey(key)) {
cache.remove(key);
}
}
public
void
evictCache() {
// 清空缓存中的所有记录
cache.clear();
}
}
|
好,现在我们有了实体类和一个缓存管理器,还需要一个提供账号查询的服务类,此服务类使用缓存管理器来支持账号查询缓存,如下:
清单 3. MyAccountService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package
oldcache;
import
cacheOfAnno.Account;
public
class
MyAccountService {
private
MyCacheManager
public
MyAccountService() {
cacheManager =
new
MyCacheManager
// 构造一个缓存管理器
}
public
Account getAccountByName(String acctName) {
Account result = cacheManager.getValue(acctName);
// 首先查询缓存
if
(result!=
null
) {
System.out.println(
"get from cache..."
+acctName);
return
result;
// 如果在缓存中,则直接返回缓存的结果
}
result = getFromDB(acctName);
// 否则到数据库中查询
if
(result!=
null
) {
// 将数据库查询的结果更新到缓存中
cacheManager.addOrUpdateCache(acctName, result);
}
return
result;
}
public
void
reload() {
cacheManager.evictCache();
}
private
Account getFromDB(String acctName) {
System.out.println(
"real querying db..."
+acctName);
return
new
Account(acctName);
}
}
|
现在我们开始写一个测试类,用于测试刚才的缓存是否有效
清单 4. Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package
oldcache;
public
class
Main {
public
static
void
main(String[] args) {
MyAccountService s =
new
MyAccountService();
// 开始查询账号
s.getAccountByName(
"somebody"
);
// 第一次查询,应该是数据库查询
s.getAccountByName(
"somebody"
);
// 第二次查询,应该直接从缓存返回
s.reload();
// 重置缓存
System.out.println(
"after reload..."
);
s.getAccountByName(
"somebody"
);
// 应该是数据库查询
s.getAccountByName(
"somebody"
);
// 第二次查询,应该直接从缓存返回
}
}
|
按照分析,执行结果应该是:首先从数据库查询,然后直接返回缓存中的结果,重置缓存后,应该先从数据库查询,然后返回缓存中的结果,实际的执行结果如下:
清单 5. 运行结果
1
2
3
4
5
|
real querying db...somebody
//
第一次从数据库加载
get from cache...somebody
//
第二次从缓存加载
after reload...
//
清空缓存
real querying db...somebody
//
又从数据库加载
get from cache...somebody
//
从缓存加载
|
可以看出我们的缓存起效了,但是这种自定义的缓存方案有如下劣势:
如果你的代码中有上述代码的影子,那么你可以考虑按照下面的介绍来优化一下你的代码结构了,也可以说是简化,你会发现,你的代码会变得优雅的多!
Hello World,注释驱动的 Spring Cache
Hello World 的实现目标
本 Hello World 类似于其他任何的 Hello World 程序,从最简单实用的角度展现 spring cache 的魅力,它基于刚才自定义缓存方案的实体类 Account.java,重新定义了 AccountService.java 和测试类 Main.java(注意这个例子不用自己定义缓存管理器,因为 spring 已经提供了缺省实现)
需要的 jar 包
为了实用 spring cache 缓存方案,在工程的 classpath 必须具备下列 jar 包。
图 1. 工程依赖的 jar 包图
注意这里我引入的是最新的 spring 3.2.0.M1 版本 jar 包,其实只要是 spring 3.1 以上,都支持 spring cache。其中 spring-context-*.jar 包含了 cache 需要的类。
定义实体类、服务类和相关配置文件
实体类就是上面自定义缓存方案定义的 Account.java,这里重新定义了服务类,如下:
清单 6. AccountService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package
cacheOfAnno;
import
org.springframework.cache.annotation.CacheEvict;
import
org.springframework.cache.annotation.Cacheable;
public
class
AccountService {
@Cacheable
(value=
"accountCache"
)
// 使用了一个缓存名叫 accountCache
public
Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
System.out.println(
"real query account."
+userName);
return
getFromDB(userName);
}
private
Account getFromDB(String acctName) {
System.out.println(
"real querying db..."
+acctName);
return
new
Account(acctName);
}
}
|
注意,此类的 getAccountByName 方法上有一个注释 annotation,即 @Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。
好,因为加入了 spring,所以我们还需要一个 spring 的配置文件来支持基于注释的缓存
清单 7. Spring-cache-anno.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<
beans
xmlns
=
"http://www.springframework.org/schema/beans"
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
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/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<
cache:annotation-driven
/>
<
bean
id
=
"accountServiceBean"
class
=
"cacheOfAnno.AccountService"
/>
<
bean
id
=
"cacheManager"
class
=
"org.springframework.cache.support.SimpleCacheManager"
>
<
property
name
=
"caches"
>
<
set
>
<
bean
class
=
"org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name
=
"default"
/>
<
bean
class
=
"org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name
=
"accountCache"
/>
set
>
property
>
bean
>
beans
>
|
注意这个 spring 配置文件有一个关键的支持缓存的配置项:
这个配置项缺省使用了一个名字叫 cacheManager 的缓存管理器,这个缓存管理器有一个 spring 的缺省实现,即 org.springframework.cache.support.SimpleCacheManager,这个缓存管理器实现了我们刚刚自定义的缓存管理器的逻辑,它需要配置一个属性 caches,即此缓存管理器管理的缓存集合,除了缺省的名字叫 default 的缓存,我们还自定义了一个名字叫 accountCache 的缓存,使用了缺省的内存存储方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一个内存缓存实现方案。
OK,现在我们具备了测试条件,测试代码如下:
清单 8. Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package
cacheOfAnno;
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
public
class
Main {
public
static
void
main(String[] args) {
ApplicationContext context =
new
ClassPathXmlApplicationContext(
"spring-cache-anno.xml"
);
// 加载 spring 配置文件
AccountService s = (AccountService) context.getBean(
"accountServiceBean"
);
// 第一次查询,应该走数据库
System.out.print(
"first query..."
);
s.getAccountByName(
"somebody"
);
// 第二次查询,应该不查数据库,直接返回缓存的值
System.out.print(
"second query..."
);
s.getAccountByName(
"somebody"
);
System.out.println();
}
}
|
上面的测试代码主要进行了两次查询,第一次应该会查询数据库,第二次应该返回缓存,不再查数据库,我们执行一下,看看结果
清单 9. 执行结果
1
2
3
|
first query...real query account.somebody
// 第一次查询
real querying db...somebody
// 对数据库进行了查询
second query...
// 第二次查询,没有打印数据库查询日志,直接返回了缓存中的结果
|
可以看出我们设置的基于注释的缓存起作用了,而在 AccountService.java 的代码中,我们没有看到任何的缓存逻辑代码,只有一行注释:@Cacheable(value=”accountCache”),就实现了基本的缓存方案,是不是很强大?
如何清空缓存
好,到目前为止,我们的 spring cache 缓存程序已经运行成功了,但是还不完美,因为还缺少一个重要的缓存管理逻辑:清空缓存,当账号数据发生变更,那么必须要清空某个缓存,另外还需要定期的清空所有缓存,以保证缓存数据的可靠性。
为了加入清空缓存的逻辑,我们只要对 AccountService.java 进行修改,从业务逻辑的角度上看,它有两个需要清空缓存的地方
清单 10. AccountService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package
cacheOfAnno;
import
org.springframework.cache.annotation.CacheEvict;
import
org.springframework.cache.annotation.Cacheable;
public
class
AccountService {
@Cacheable
(value=
"accountCache"
)
// 使用了一个缓存名叫 accountCache
public
Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
return
getFromDB(userName);
}
@CacheEvict
(value=
"accountCache"
,key=
"#account.getName()"
)
// 清空 accountCache 缓存
public
void
updateAccount(Account account) {
updateDB(account);
}
@CacheEvict
(value=
"accountCache"
,allEntries=
true
)
// 清空 accountCache 缓存
public
void
reload() {
}
private
Account getFromDB(String acctName) {
System.out.println(
"real querying db..."
+acctName);
return
new
Account(acctName);
}
private
void
updateDB(Account account) {
System.out.println(
"real update db..."
+account.getName());
}
}
|
清单 11. Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package
cacheOfAnno;
import
org.springframework.context.ApplicationContext;
import
org.springframework.context.support.ClassPathXmlApplicationContext;
public
class
Main {
public
static
void
main(String[] args) {
ApplicationContext context =
new
ClassPathXmlApplicationContext(
"spring-cache-anno.xml"
);
// 加载 spring 配置文件
AccountService s = (AccountService) context.getBean(
"accountServiceBean"
);
// 第一次查询,应该走数据库
System.out.print(
"first query..."
);
s.getAccountByName(
"somebody"
);
// 第二次查询,应该不查数据库,直接返回缓存的值
System.out.print(
"second query..."
);
s.getAccountByName(
"somebody"
);
System.out.println();
System.out.println(
"start testing clear cache..."
);
// 更新某个记录的缓存,首先构造两个账号记录,然后记录到缓存中
Account account1 = s.getAccountByName(
"somebody1"
);
Account account2 = s.getAccountByName(
"somebody2"
);
// 开始更新其中一个
account1.setId(
1212
);
s.updateAccount(account1);
s.getAccountByName(
"somebody1"
);
// 因为被更新了,所以会查询数据库
s.getAccountByName(
"somebody2"
);
// 没有更新过,应该走缓存
s.getAccountByName(
"somebody1"
);
// 再次查询,应该走缓存
// 更新所有缓存
s.reload();
s.getAccountByName(
"somebody1"
);
// 应该会查询数据库
s.getAccountByName(
"somebody2"
);
// 应该会查询数据库
s.getAccountByName(
"somebody1"
);
// 应该走缓存
s.getAccountByName(
"somebody2"
);
// 应该走缓存
}
}
|
清单 12. 运行结果
1
2
3
4
5
6
7
8
9
|
first query...real querying db...somebody
second query...
start testing
clear
cache...
real querying db...somebody1
real querying db...somebody2
real update db...somebody1
real querying db...somebody1
real querying db...somebody1
real querying db...somebody2
|
结果和我们期望的一致,所以,我们可以看出,spring cache 清空缓存的方法很简单,就是通过 @CacheEvict 注释来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。注意其中一个 @CacheEvict(value=”accountCache”,key=”#account.getName()”),其中的 Key 是用来指定缓存的 key 的,这里因为我们保存的时候用的是 account 对象的 name 字段,所以这里还需要从参数 account 对象中获取 name 的值来作为 key,前面的 # 号代表这是一个 SpEL 表达式,此表达式可以遍历方法的参数对象,具体语法可以参考 Spring 的相关文档手册。
如何按照条件操作缓存
前面介绍的缓存方法,没有任何条件,即所有对 accountService 对象的 getAccountByName 方法的调用都会起动缓存效果,不管参数是什么值,如果有一个需求,就是只有账号名称的长度小于等于 4 的情况下,才做缓存,大于 4 的不使用缓存,那怎么实现呢?
Spring cache 提供了一个很好的方法,那就是基于 SpEL 表达式的 condition 定义,这个 condition 是 @Cacheable 注释的一个属性,下面我来演示一下
清单 13. AccountService.java(getAccountByName 方法修订,支持条件)
1
2
3
4
5
|
@Cacheable
(value=
"accountCache"
,condition=
"#userName.length() <= 4"
)
// 缓存名叫 accountCache
public
Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
return
getFromDB(userName);
}
|
注意其中的 condition=”#userName.length() <=4”,这里使用了 SpEL 表达式访问了参数 userName 对象的 length() 方法,条件表达式返回一个布尔值,true/false,当条件为 true,则进行缓存操作,否则直接调用方法执行的返回结果。
清单 14. 测试方法
1
2
3
4
|
s.getAccountByName(
"somebody"
);
// 长度大于 4,不会被缓存
s.getAccountByName(
"sbd"
);
// 长度小于 4,会被缓存
s.getAccountByName(
"somebody"
);
// 还是查询数据库
s.getAccountByName(
"sbd"
);
// 会从缓存返回
|
清单 15. 运行结果
1
2
3
|
real querying db...somebody
real querying db...sbd
real querying db...somebody
|
可见对长度大于 4 的账号名 (somebody) 没有缓存,每次都查询数据库。
如果有多个参数,如何进行 key 的组合
假设 AccountService 现在有一个需求,要求根据账号名、密码和是否发送日志查询账号信息,很明显,这里我们需要根据账号名、密码对账号对象进行缓存,而第三个参数“是否发送日志”对缓存没有任何影响。所以,我们可以利用 SpEL 表达式对缓存 key 进行设计
清单 16. Account.java(增加 password 属性)
1
2
3
4
5
6
7
|
private
String password;
public
String getPassword() {
return
password;
}
public
void
setPassword(String password) {
this
.password = password;
}
|
清单 17. AccountService.java(增加 getAccount 方法,支持组合 key)
1
2
3
4
5
6
|
@Cacheable
(value=
"accountCache"
,key=
"#userName.concat(#password)"
)
public
Account getAccount(String userName,String password,
boolean
sendLog) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
return
getFromDB(userName,password);
}
|
注意上面的 key 属性,其中引用了方法的两个参数 userName 和 password,而 sendLog 属性没有考虑,因为其对缓存没有影响。
清单 18. Main.java
1
2
3
4
5
6
7
8
9
10
11
|
public
static
void
main(String[] args) {
ApplicationContext context =
new
ClassPathXmlApplicationContext(
"spring-cache-anno.xml"
);
// 加载 spring 配置文件
AccountService s = (AccountService) context.getBean(
"accountServiceBean"
);
s.getAccount(
"somebody"
,
"123456"
,
true
);
// 应该查询数据库
s.getAccount(
"somebody"
,
"123456"
,
true
);
// 应该走缓存
s.getAccount(
"somebody"
,
"123456"
,
false
);
// 应该走缓存
s.getAccount(
"somebody"
,
"654321"
,
true
);
// 应该查询数据库
s.getAccount(
"somebody"
,
"654321"
,
true
);
// 应该走缓存
}
|
上述测试,是采用了相同的账号,不同的密码组合进行查询,那么一共有两种组合情况,所以针对数据库的查询应该只有两次。
清单 19. 运行结果
1
2
|
real querying db...userName=somebody password=123456
real querying db...userName=somebody password=654321
|
和我们预期的一致。
如何做到:既要保证方法被调用,又希望结果被缓存
根据前面的例子,我们知道,如果使用了 @Cacheable 注释,则当重复使用相同参数调用方法的时候,方法本身不会被调用执行,即方法本身被略过了,取而代之的是方法的结果直接从缓存中找到并返回了。
现实中并不总是如此,有些情况下我们希望方法一定会被调用,因为其除了返回一个结果,还做了其他事情,例如记录日志,调用接口等,这个时候,我们可以用 @CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。
清单 20. AccountService.java
1
|