和 spring 的事务管理类似,spring cache 的关键原理就是 spring AOP,通过 spring AOP,其实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。我们来看一下下面这个图:
上图显示,当客户端“Calling code”调用一个普通类 Plain Object 的 foo() 方法的时候,是直接作用在 pojo 类自身对象上的,客户端拥有的是被调用者的直接的引用。
而 Spring cache 利用了 Spring AOP 的动态代理技术,即当客户端尝试调用 pojo 的 foo()方法的时候,给他的不是 pojo 自身的引用,而是一个动态生成的代理类
如上图所示,这个时候,实际客户端拥有的是一个代理的引用,那么在调用 foo() 方法的时候,会首先调用 proxy 的 foo() 方法,这个时候 proxy 可以整体控制实际的 pojo.foo() 方法的入参和返回值,比如缓存结果,比如直接略过执行实际的 foo() 方法等,都是可以轻松做到的。
直到现在,我们已经学会了如何使用开箱即用的 spring cache,这基本能够满足一般应用对缓存的需求,但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了,还好,spring 也想到了这一点。
我们先不考虑如何持久化缓存,毕竟这种第三方的实现方案很多,我们要考虑的是,怎么利用 spring 提供的扩展点实现我们自己的缓存,且在不改原来已有代码的情况下进行扩展。
首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCache、OSCache,甚至一些内存数据库例如 memcache 或者 h2db 等。下面我举一个简单的例子说明如何做。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package cacheOfAnno;
import java.util.Collection;
import org.springframework.cache.support.AbstractCacheManager;
public class MyCacheManager extends AbstractCacheManager {
private Collection
extends
MyCache> caches;
/**
* Specify the collection of Cache instances to use for this CacheManager.
*/
public void setCaches(Collection
extends
MyCache> caches) {
this.caches = caches;
}
@Override
protected Collection
extends
MyCache> loadCaches() {
return this.caches;
}
}
|
上面的自定义的 CacheManager 实际继承了 spring 内置的 AbstractCacheManager,实际上仅仅管理 MyCache 类的实例。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package cacheOfAnno;
import java.util.HashMap;
import java.util.Map;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
public class MyCache implements Cache {
private String name;
private Map<
String
,Account> store = new HashMap<
String
,Account>();;
public MyCache() {
}
public MyCache(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object getNativeCache() {
return store;
}
@Override
public ValueWrapper get(Object key) {
ValueWrapper result = null;
Account thevalue = store.get(key);
if(thevalue!=null) {
thevalue.setPassword("from mycache:"+name);
result = new SimpleValueWrapper(thevalue);
}
return result;
}
@Override
public void put(Object key, Object value) {
Account thevalue = (Account)value;
store.put((String)key, thevalue);
}
@Override
public void evict(Object key) {
}
@Override
public void clear() {
}
}
|
上面的自定义缓存只实现了很简单的逻辑,但这是我们自己做的,也很令人激动是不是,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。
这还不够,spring 还不知道我们写了这些东西,需要通过 spring*.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
|
<
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
=
"cacheOfAnno.MyCacheManager"
>
<
property
name
=
"caches"
>
<
set
>
<
bean
class
=
"cacheOfAnno.MyCache"
p:name
=
"accountCache"
/>
set
>
property
>
bean
>
beans
>
|
注意上面配置文件的黑体字,这些配置说明了我们的 cacheManager 和我们自己的 cache 实例。
好,什么都不说,测试!
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");
Account account = s.getAccountByName("someone");
System.out.println("passwd="+account.getPassword());
account = s.getAccountByName("someone");
System.out.println("passwd="+account.getPassword());
}
|
上面的测试代码主要是先调用 getAccountByName 进行一次查询,这会调用数据库查询,然后缓存到 mycache 中,然后我打印密码,应该是空的;下面我再次查询 someone 的账号,这个时候会从 mycache 中返回缓存的实例,记得上面的后门么?我们修改了密码,所以这个时候打印的密码应该是一个特殊的值
1
2
3
|
real querying db...someone
passwd=null
passwd=from mycache:accountCache
|
结果符合预期,即第一次查询数据库,且密码为空,第二次打印了一个特殊的密码。说明我们的 myCache 起作用了。
上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。
1
2
3
4
5
6
7
8
9
|
public Account getAccountByName2(String userName) {
return this.getAccountByName(userName);
}
@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache
public Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
return getFromDB(userName);
}
|
上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效
1
2
3
4
5
6
7
8
9
10
|
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"spring-cache-anno.xml");// 加载 spring 配置文件
AccountService s = (AccountService) context.getBean("accountServiceBean");
s.getAccountByName2("someone");
s.getAccountByName2("someone");
s.getAccountByName2("someone");
}
|
1
2
3
|
real querying db...someone
real querying db...someone
real querying db...someone
|
可见,结果是每次都查询数据库,缓存没起作用。要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。
我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下
1
2
3
4
|
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 缓存
public void reload() {
throw new RuntimeException();
}
|
注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"spring-cache-anno.xml");// 加载 spring 配置文件
AccountService s = (AccountService) context.getBean("accountServiceBean");
s.getAccountByName("someone");
s.getAccountByName("someone");
try {
s.reload();
} catch (Exception e) {
}
s.getAccountByName("someone");
}
|
上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。
1
|
real querying db...someone
|
和预期一样。那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。
1
2
3
4
5
|
@CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true)
// 清空 accountCache 缓存
public void reload() {
throw new RuntimeException();
}
|
注意上面的代码,我们在 @CacheEvict 注释中加了 beforeInvocation 属性,确保缓存被清空。
执行相同的测试代码
1
2
|
real querying db...someone
real querying db...someone
|
这样,第一次和第三次都从数据库取数据了,缓存清空有效。
这样,第一次和第三次都从数据库取数据了,缓存清空有效。
和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制,这里限于篇幅不再细述。
有的时候,我们在代码迁移、调试或者部署的时候,恰好没有 cache 容器,比如 memcache 还不具备条件,h2db 还没有装好等,如果这个时候你想调试代码,岂不是要疯掉?这里有一个办法,在不具备缓存条件的时候,在不改代码的情况下,禁用缓存。
方法就是修改 spring*.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
35
36
37
|
<
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
=
"simpleCacheManager"
class
=
"org.springframework.cache.support.SimpleCacheManager"
>
<
property
name
=
"caches"
>
<
set
>
<
bean
class
=
"org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
p:name
=
"default"
/>
set
>
property
>
bean
>
<
bean
id
=
"cacheManager"
class
=
"org.springframework.cache.support.CompositeCacheManager"
>
<
property
name
=
"cacheManagers"
>
<
list
>
<
ref
bean
=
"simpleCacheManager"
/>
list
>
property
>
<
property
name
=
"fallbackToNoOpCache"
value
=
"true"
/>
bean
>
beans
>
|
注意以前的 cacheManager 变为了 simpleCacheManager,且没有配置 accountCache 实例,后面的 cacheManager 的实例是一个 CompositeCacheManager,他利用了前面的 simpleCacheManager 进行查询,如果查询不到,则根据标志位 fallbackToNoOpCache 来判断是否不做任何缓存操作。
1
2
3
|
real querying db...someone
real querying db...someone
real querying db...someone
|
可以看出,缓存失效。每次都查询数据库。因为我们没有配置它需要的 accountCache 实例。
如果将上面 xml 配置文件的 fallbackToNoOpCache 设置为 false,再次运行,则会得到
1
2
3
4
5
|
Exception in thread "main" java.lang.IllegalArgumentException:
Cannot find cache named [accountCache] for CacheableOperation
[public cacheOfAnno.Account
cacheOfAnno.AccountService.getAccountByName(java.lang.String)]
caches=[accountCache] | condition='' | key=''
|
可见,在找不到 accountCache,且没有将 fallbackToNoOpCache 设置为 true 的情况下,系统会抛出异常。
总之,注释驱动的 spring cache 能够极大的减少我们编写常见缓存的代码量,通过少量的注释标签和配置文件,即可达到使代码具备缓存的能力。且具备很好的灵活性和扩展性。但是我们也应该看到,spring cache 由于急于 spring AOP 技术,尤其是动态的 proxy 技术,导致其不能很好的支持方法的内部调用或者非 public 方法的缓存设置,当然这都是可以解决的问题,通过学习这个技术,我们能够认识到,AOP 技术的应用还是很广泛的,如果有兴趣,我相信你也能基于 AOP 实现自己的缓存方案。