Spring Boot Ehcache

EhCache 是一个比较成的Java缓存框架,最早从 hibernate 发展而来,是进程中的缓存系统,它提供了内存、磁盘文件存储,以及分布式存储方案等多种灵活的cache管理方案。

Sprging boot 对 Ehcache 的使用提供支持,所以在 spring boot 中只需要简单配置即可使用Ehcache实现数据缓存处理。

相关注解

@CacheConfig

用于标注在类上,可以存放该类中所有缓存的公有属性,比如设置缓存的名字

1
2
3
4
5

@CacheConfig(cacheName = "users")
public interface UserService(
// ...
)

配置了该数据访问对象中返回的内容将存储于名为 users 的缓存对象中,我们也可以不使用该注解,直接通过 @cacheable 自己配置缓存集的名字来定要。

@Cacheable

应用到读取数据的方法上,即可缓存的方法,如查找方法,先从缓存中读取,如果没有再调用相应方法获取数据,然后把数据添加到缓存中。该注解主要有下面借个参数。

  • value、cacheNames:两个等同的参数(cacheNames为 spring4新增,作为value的别名),用于指定缓存存储的集合名。由于spring4中新增了@CacheConfig,因此在spring3中原本必须有的value属性,也成了非必需项了。
  • key:缓存对象存储在 Map 集合中的 key 值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需要使用 SpEl 表达式,比如 @Cacheable(key = "#p0") 使用函数的第一个参数作为缓存的key值。
  • condition:缓存对象的条件,非必需,也需要使用SpEl表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存。
  • unless:另外一个缓存条件参数,非必需,也需要使用SpEL表达式。它不同于 condition 参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断,所以它可以通过对 result 进行判断。
  • keyGenerator:用于指定 key 生成器,非必需。若需要指定一个自定义的 key 生成器,我们需要去实现 org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是,该参数与key是互斥的。
  • cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用。
  • cacheResolver:用于指定使用哪个缓存解析器,非必需。需要通过 org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。

@CachePut

应用到写数据的方法上,如 新增/修改方法(此放必须返回需要写到缓存中的数据),调用方法时会自动把相应的数据放入缓存,@CachePut的参数与@Cacheable类似。

1
2
3
4
@CachePut(value="users", key = "#user.id")
public User add(User user) {
// ...
}

@CacheEvict

应用到删除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据

1
2
@cacheEvict(value = "uses", key="#id")
void delete(final Integer id);

除了同@Cacheable一样的参数之外,@CacheEvict还有下面两个参数:

  • allEntries:非必需,默认是false。当为 true 时,会移除所有数据。
  • beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。

@Caching

组合多个 Cache 注解使用,如

1
2
3
4
5
6
7
@Caching(
put = {
@CachePut(value="user", key="#user.id")
@CachePut(value="user", key="#user.username")
@CachePut(value="user", key="#user.age")
}
)

环境配置

添加依赖

  • ehcache
  • spring-boot-starter-cache
pom.xml
1
2
3
4
5
6
7
8
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

缓存配置

src/main/resources 目录下添加文件 ehcache.xml ,内容如下

ehcache.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
38
39
40
41
42
43
44
45
46
<ehcache name="mycache">
<!--
如果不使用磁盘缓存,只需要将 diskStore 注释掉即可;
如果使用磁盘缓存,需要在ehcache.xml的ehcache元素下定义一个 diskStore元素并指定其 path 属性;
-->
<diskStore path="c:\java\cache" />

<!--
name: 缓存名称
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:磁盘最大缓存数目
eternal:对象是否永久有效,一旦设置了,timeout将不起作用
overflowToDisk:是否保存到磁盘,当系统宕机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,表示可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,表示可闲置时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据。默认值是false。
diskSpoolBufferSizeMB:设置DiskStore(磁盘缓存)的缓存区大小,默认是30MB,每个cache都应该有自己的一个缓存区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElemsInMemo限制时,会根据指定的策略去清理内存,默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:
可选策略:
LRU 最近最少使用,默认策略
FIFO 先进先出
LFU 最少访问次数
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU" />

<!-- 指定的缓存配置 -->
<cache
name="users"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>

springboot 缓存配置

application.yml
1
2
3
4
5
6
7
8
9

# Ehcache缓存配置
# 数据源配置
spring:
## 缓存配置
cache:
encache:
config: classpath:ehcache.xml

启动缓存

在项目的入口类中,添加 @EnableCaching 注解,启动缓存

1
2
3
4
5
6
@SpringBootApplication
@EnableCaching
@MapperScan("com.xxx.dao")
public calss Starter{
SpringApplicatin.run(Starter.class);
}

使用缓存

bean对象实现序列化

目标缓存对象需要实现序列化,如实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example.po;


import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
* 用户
*/
@Data
public class User implements Serializable {
private Integer userId;

@NotBlank(message = "用户名不能为空")
private String userName;

@NotBlank(message = "密码不能为空")
@Length(min =6, max = 10, message="密码长度至少6位但不超过10位")
private String userPwd;
}

使用注解缓存实现

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package org.example.service;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.commons.lang3.StringUtils;
import org.example.dao.UserMapper;
import org.example.po.User;
import org.example.query.UserQuery;
import org.example.util.AssertUtil;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;

/**
* 用户业务类
*/
@Service
public class UserService {
@Resource
private UserMapper userMapper;

/**
* 通过用户名获取用户信息
* @param userName 用户名
* @return 用户信息
*/
public User findUserByName(String userName){

return this.userMapper.findUserByName(userName);
}

/**
* 通过用户Id获取用户信息
* @param userId 用户Id
* @return
*/
@Cacheable(value="users", key = "#userId")
public User findUserById(Integer userId){
return this.userMapper.findUserById(userId);
}

/**
* 分页查询用户列表
* @param userQuery
* @return
*/
@Cacheable(value="users", key="#userQuery.userName + '-' + #userQuery.pageNum + '-' + #userQuery.pageSize")
public PageInfo<User> findUserByParam(UserQuery userQuery){
// 开启分页
PageHelper.startPage(userQuery.getPageNum(), userQuery.getPageSize());
// 查询数据
List<User> userList = this.userMapper.findUserByParam(userQuery);
// 分页查询
return new PageInfo<User>(userList);
}

/**
* 添加用户
* @param user
* @return
*/
@CachePut(value = "users", key = "#user.userId")
public User addUser(User user) {
// 参数校验
AssertUtil.isTrue(user == null, "用户信息不能为空");
AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()),"用户姓名不能为空");
AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()),"密码不能为空");
AssertUtil.isTrue(this.findUserByName(user.getUserName()) != null, "用户名不能重复");

// 执行添加操作
AssertUtil.isTrue(this.userMapper.addUser(user) < 1, "添加用户失败");

return user;
}

/**
* 修改用户
* @param user
*/
@Transactional(propagation = Propagation.REQUIRED)
@CachePut(value = "users", key = "#user.userId")
public User updateUser(User user){
// 参数校验
AssertUtil.isTrue(user == null, "用户信息不能为空");
AssertUtil.isTrue(user.getUserId() == null,"用户数据异常");
AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()),"用户姓名不能为空");
AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()),"密码不能为空");
User checkUser = this.userMapper.findUserByName(user.getUserName());
AssertUtil.isTrue(checkUser != null && !checkUser.getUserId().equals(user.getUserId()), "用户名已存在");


// 执行添加操作
AssertUtil.isTrue(this.userMapper.updateUser(user) < 1, "修改用户失败");
return user;
}

/**
* 删除用户
* @param userId 用户Id
*/
@CacheEvict(value = "users", key = "#userId")
public void deleteUserById(Integer userId){
AssertUtil.isTrue( userId == null ||this.userMapper.findUserById(userId) == null, "用户不存在");
AssertUtil.isTrue(this.userMapper.deleteUserById(userId) < 1, "删除用户失败");
}
}