概述
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
其特点总结如下:
- 通过少量的配置 annotation 注释即可使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
- 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过其实现任何方法的缓存支持
- 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
实现
-
maven配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1 <dependency>
2 <groupId>aspectj</groupId>
3 <artifactId>aspectjrt</artifactId>
4 <version>1.5.4</version>
5 </dependency>
6 <dependency>
7 <groupId>org.aspectj</groupId>
8 <artifactId>aspectjweaver</artifactId>
9 <version>1.8.10</version>
10 </dependency>
11 <dependency>
12 <groupId>org.springframework</groupId>
13 <artifactId>spring-context</artifactId>
14 <version>${spring.version}</version>
15 </dependency>
16
-
本项目采用纯java的配置,以下是配置spring cache的代码,缓存管理采用spring自带SimpleCacheManager,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1
2@Configuration
3@EnableCaching //开启缓存注解
4public class SpringCacheConfig {
5
6 /**
7 * 使用SimpleCacheManager管理缓存
8 * @return SimpleCacheManager
9 */
10 @Bean
11 public SimpleCacheManager cacheManager() {
12 SimpleCacheManager cacheManager = new SimpleCacheManager();
13 cacheManager.setCaches(Arrays.asList(
14 //新建2个缓存实例
15 new ConcurrentMapCache[]{
16 new ConcurrentMapCache("default"),
17 new ConcurrentMapCache("test"),
18 new ConcurrentMapCache("users")}));
19 return cacheManager;
20 }
21
@Cacheable、@CachePut、@CacheEvict 注释介绍
通过上面的例子,我们可以看到 spring cache 主要使用两个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict,我们总结一下其作用和配置方法。
*表 1. @Cacheable 作用和配置方法
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存*
@Cacheable 主要的参数
value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=”testcache”,key=”#userName”)
condition
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
@CachePut 作用和配置方法
value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=”testcache”,key=”#userName”)
condition
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
表 3. @CacheEvict 作用和配置方法
value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”}
key
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
例如:@Cacheable(value=”testcache”,key=”#userName”)
condition
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
allEntries
是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
例如:@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
@CachEvict(value=”testcache”,beforeInvocation=true)
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 1@Service("userService")
2public class UserService {
3
4 @Autowired
5 private UserRepository userRepository;
6
7 @Autowired
8 private CacheManager cacheManager;
9
10 private Logger logger = LoggerFactory.getLogger(UserService.class);
11
12 @Cacheable(cacheNames = "users")
13 public List<User> getUsers(){
14 logger.info("getUsers for userService");
15 Map<String, Object> param = new HashMap();
16 return userRepository.getUserList(param);
17 }
18
19 @CachePut(cacheNames = "users")
20 public User saveUser(User user){
21 logger.info("save user at userRepository !");
22 return userRepository.save(user);
23 }
24
25
基本原理
和 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
23 1package cacheOfAnno;
2
3 import java.util.Collection;
4
5 import org.springframework.cache.support.AbstractCacheManager;
6
7 public class MyCacheManager extends AbstractCacheManager {
8 private Collection<? extends MyCache> caches;
9
10 /**
11 * Specify the collection of Cache instances to use for this CacheManager.
12 */
13 public void setCaches(Collection<? extends MyCache> caches) {
14 this.caches = caches;
15 }
16
17 @Override
18 protected Collection<? extends MyCache> loadCaches() {
19 return this.caches;
20 }
21
22 }
23
上面的自定义的 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
59 1package cacheOfAnno;
2
3 import java.util.HashMap;
4 import java.util.Map;
5
6 import org.springframework.cache.Cache;
7 import org.springframework.cache.support.SimpleValueWrapper;
8
9 public class MyCache implements Cache {
10 private String name;
11 private Map<String,Account> store = new HashMap<String,Account>();;
12
13 public MyCache() {
14 }
15
16 public MyCache(String name) {
17 this.name = name;
18 }
19
20 @Override
21 public String getName() {
22 return name;
23 }
24
25 public void setName(String name) {
26 this.name = name;
27 }
28
29 @Override
30 public Object getNativeCache() {
31 return store;
32 }
33
34 @Override
35 public ValueWrapper get(Object key) {
36 ValueWrapper result = null;
37 Account thevalue = store.get(key);
38 if(thevalue!=null) {
39 thevalue.setPassword("from mycache:"+name);
40 result = new SimpleValueWrapper(thevalue);
41 }
42 return result;
43 }
44
45 @Override
46 public void put(Object key, Object value) {
47 Account thevalue = (Account)value;
48 store.put((String)key, thevalue);
49 }
50
51 @Override
52 public void evict(Object key) {
53 }
54
55 @Override
56 public void clear() {
57 }
58 }
59
上面的自定义缓存只实现了很简单的逻辑,但这是我们自己做的,也很令人激动是不是,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。
注意和限制
基于 proxy 的 spring aop 带来的内部调用问题
上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。
可见,结果是每次都查询数据库,缓存没起作用。要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。
@CacheEvict 的可靠性问题
我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下
非 public 方法问题
和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制,这里限于篇幅不再细述。