Spring Boot中使用EhCache实现缓存支持

时间:2022-09-24 16:03:44
  •  SpringBoot提供数据缓存功能的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。,相信非常多人已经用过cache了。因为数据库的IO瓶颈。一般情况下我们都会引入非常多的缓存策略,例如引入redis,引入hibernate的二级缓存等。SpringBoot在annotation的层面给我们实现了cache,得益于Spring的AOP。所有的缓存配置只是在annotation层面配置,完全没有侵入到我们的代码当中,就像我们的声明式事务一样。
  • Spring定义了CacheManager和Cache接口统一不同的缓存技术。其中CacheManager是Spring提供的各种缓存技术的抽象接口。而Cache接口包含缓存的各种操作,当然我们一般情况下不会直接操作Cache接口。

Spring针对不同的缓存技术,需要实现不同的cacheManager,Spring定义了如下的cacheManger实现:

 
CacheManger 描述
SimpleCacheManager 使用简单的Collection来存储缓存,主要用于测试
ConcurrentMapCacheManager 使用ConcurrentMap作为缓存技术(默认)
NoOpCacheManager 测试用
EhCacheCacheManager 使用EhCache作为缓存技术,以前在hibernate的时候经常用
GuavaCacheManager 使用google guava的GuavaCache作为缓存技术
HazelcastCacheManager 使用Hazelcast作为缓存技术
JCacheCacheManager 使用JCache标准的实现作为缓存技术,如Apache Commons JCS
RedisCacheManager 使用Redis作为缓存技术

Cache注解详解

@CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "users"):配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。
@Cacheable 在方法执行前Spring先是否有缓存数据,如果有直接返回。如果没有数据,调用方法并将方法返回值存放在缓存当中。
@Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:

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

@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会将方法返回值放入缓存,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析。

@CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:

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

配置缓存

  • 引入缓存依赖

在pom.xml中引入cache依赖,添加如下内容:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 开启缓存

在Spring Boot主类中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),开启缓存功能,如下:

@SpringBootApplication
@ComponentScan(basePackages={"com.gzh.*"})
@EnableCaching
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} }
  • 使用注解缓存数据

在数据访问接口中,增加缓存配置注解,如:

@Mapper
public interface UserMapper { @Select("SELECT * FROM T_USER WHERE id = #{id}")
UserVO findById(@Param("id") int id); @Select("SELECT * FROM T_USER WHERE name = #{name,jdbcType=VARCHAR}")
UserVO findByName(@Param("name") String name); @Results({
@Result(property="id",column="id"),
@Result(property = "name", column = "name"),
@Result(property = "age", column = "age")
})
@Select("SELECT id, name, age FROM T_USER")
List<UserVO> findAll(); @Insert("INSERT INTO T_USER(name, age) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age); @Update("UPDATE T_USER SET age=#{age,jdbcType=INTEGER},name=#{name,jdbcType=VARCHAR} WHERE id=#{id,jdbcType=INTEGER}")
void update(UserVO userVO); @Delete("DELETE FROM T_USER WHERE id =#{id,jdbcType=INTEGER}")
void delete(int id); @Delete("DELETE FROM T_USER")
void deleteAll();
}

新建IUserService接口类,如下:

@CacheConfig(cacheNames="user")
public interface IUserService { //有一个尤其需要注意的坑:Spring默认的SimpleKeyGenerator是不会将函数名组合进key中的,即多个方法设置@Cacheable("databaseCache"),输出的key是一样的 /**
* 新增一个用户
* @param name
* @param age
*/
@CachePut(value="user",keyGenerator="cacheKeyGenerator")
void create(String name, int age); /**
* 根据id删除一个用户
* @param name
*/
@CacheEvict(value="user",keyGenerator="cacheKeyGenerator")
void deleteById(int id); /**
* 删除所有信息
*/
@CacheEvict(keyGenerator="cacheKeyGenerator",allEntries=true)
void deleteAll(); /**
* 更新用户信息
* @param userVO
*/
@CachePut(keyGenerator="cacheKeyGenerator",cacheNames="user")
void update(UserVO userVO); /**
* 获取用户列表
* @return
*/
@Cacheable(keyGenerator="cacheKeyGenerator")
List<UserVO> findAll(); /**
* 根据Id查询用户信息
* @param id
* @return
*/
@Cacheable(keyGenerator="cacheKeyGenerator")
UserVO findById(int id); /**
* 根据名称查询用户信息
* @param name
* @return
*/
@Cacheable(keyGenerator="cacheKeyGenerator")
UserVO findByName(String name);
}

备注:大家可以先不配置keyGenerator属性,可以指定简单key。keyGenerator属性是我后边测试所用。

UserService实现类:

@Service
public class UserService implements IUserService { @Autowired
private UserMapper mapper; @Override
public void create(String name, int age) {
mapper.insert(name, age);
} @Override
public void deleteById(int id) {
mapper.delete(id);
} @Override
public void deleteAll() {
mapper.deleteAll();
} @Override
public List<UserVO> findAll() {
List<UserVO> list = mapper.findAll();
return list;
} @Override
public void update(UserVO userVO) {
mapper.update(userVO);
} @Override
public UserVO findById(int id) {
UserVO userVO = mapper.findById(id);
return userVO;
} @Override
public UserVO findByName(String name) {
UserVO userVO = mapper.findByName(name);
return userVO;
}
}

新建单元测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class CacheApplicationTest { private static final Logger LOG = Logger.getLogger(CacheApplicationTest.class); @Autowired
private IUserService iUserService; @Autowired
private CacheManager cacheManager; @Before
public void setUp(){
//检查使用的Cache
LOG.info("Cache Manager ===== "+cacheManager.getClass().getName());
//删除所有数据
iUserService.deleteAll();
//添加用户信息
iUserService.create("guanguan", 20);
iUserService.create("lindapang", 21);
iUserService.create("xiaoyan", 18);
} @Test
public void userAddTest(){
UserVO user = iUserService.findByName("guanguan");
LOG.info("第一次查询用户信息=="+user.toString());
user = iUserService.findByName("guanguan");
LOG.info("第二次查询用户信息=="+user.toString());
} }

执行单元测试,可以在控制台看到输入如下内容:

Spring Boot中使用EhCache实现缓存支持

从日志中,我们发现spring boot开启的缓存已经生效,第一次都执行了访问数据库的操作,第二次执行缓存。

完成上边案例后,大家肯定会想,spring boot是如何实现缓存的,使用的是什么缓存,带着这个疑问,我们继续往下看。

其实常规的SpringBoot已经为我们自动配置了EhCache、Collection、Guava、ConcurrentMap等缓存,默认使用SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:

• Generic
• JCache
• EhCache
• Hazelcast
• Infinispan
• Redis
• Guava
• Simple

除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。SpringBoot的application.properties配置文件,使用spring.cache前缀的属性进行配置。

spring.cache.type=#缓存的技术类型
spring.cache.cache-names=应用程序启动创建缓存的名称
spring.cache.ehcache.config=ehcache的配置文件位置
spring.cache.infinispan.config=infinispan的配置文件位置
spring.cache.jcache.config=jcache配置文件位置
spring.cache.jcache.provider=当多个jcache实现类时,指定选择jcache的实现类

这里不适用默认的ConcurrentMapCache 而是使用 EhCache,看看如何配置来使用EhCache进行缓存管理。

配置EhCache缓存

  • 添加EhCache缓存依赖
<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.4</version>
</dependency>
  • 创建EhCache缓存配配置文件ehcache.xml

在src/main/resources目录下创建:ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 指定一个文件目录,当EHCache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
<diskStore path="java.io.tmpdir"/> <!-- 设定缓存的默认数据过期策略 --> <cache name="users" maxElementsInMemory="10000"
maxEntriesLocalHeap="200M"
timeToLiveSeconds="600"/> <defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="120"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"
diskExpiryThreadIntervalSeconds="120"/> <!-- maxElementsInMemory 内存中最大缓存对象数,必须的配置 -->
<!-- maxEntriesLocalHeap是用来限制当前缓存在堆内存上所能保存的最大元素数量的-->
<!-- eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false -->
<!-- maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大 -->
<!-- overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,
会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。-->
<!-- diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。-->
<!-- diskPersistent:是否缓存虚拟机重启期数据 -->
<!-- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒 --> <!-- timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,
如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,
EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,
则表示对象可以无限期地处于空闲状态 --> <!-- timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,
如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,
EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,
则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义 --> <!-- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,
Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、
FIFO(先进先出)、LFU(最少访问次数)。--> </ehcache>
  • 在application.properties添加如下配置
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
  • 在Spring Boot启动类上添加@EnableCaching注解

至此,我们所有的配置项都基本上配置完成。回到刚才IUserService接口类,我们使用缓存注解时使用到缓存的key,这个key是用来分辨同一个缓存中的缓存数据的。key是可以自己制定的,也可以通过自定义一个KeyGenerator来进行生成。

注解上key的几种形式如下:

基本形式:

1.@Cacheable(value="cacheName", key"#id")

2.public ResultDTO method(int id);

组合形式:

1.@Cacheable(value="cacheName", key"T(String).valueOf(#name).concat('-').concat(#password))

2.public ResultDTO method(int name, String password);

对象形式:

1.@Cacheable(value="cacheName", key"#user.id)

2.public ResultDTO method(User user);

自定义Key生成器:

1.@Cacheable(value="gomeo2oCache", keyGenerator = "keyGenerator")

2.public ResultDTO method(User user);

这里我们探讨下最后一种,自定义key。key可以为任何对象,我们要考虑的只有一件事,两个key对象,如何判断他们是否相等。所以很自然的我们想到重新实现它的hashCode和equals方法即可。

自定义keyGenerator

自定义的key生成器,我们需要去实现

org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的。

      CacheKeyGenerator类代码如下:

@Component("cacheKeyGenerator")
public class CacheKeyGenerator implements KeyGenerator{ @Override
public Object generate(Object target, Method method, Object... params) {
Object key=new BaseCacheKey(target,method,params);
return key.toString();
}
}

BaseCacheKey类代码如下:

public class BaseCacheKey implements Serializable {

    /**
*
*/
private static final long serialVersionUID = -8517453845729052981L; private static final Logger LOG = Logger.getLogger(BaseCacheKey.class); private final Object[] params;
private final int hashCode;
private final String className;
private final String methodName; public BaseCacheKey(Object target, Method method, Object[] elements) {
this.className = target.getClass().getName();
this.methodName = getMethodName(method);
this.params = new Object[elements.length];
System.arraycopy(elements, 0, this.params, 0, elements.length);
this.hashCode = generatorHashCode();
} private String getMethodName(Method method) {
StringBuilder builder = new StringBuilder(method.getName());
Class<?>[] types = method.getParameterTypes();
if (types.length != 0) {
builder.append("(");
for (Class<?> type : types) {
String name = type.getName();
builder.append(name + ",");
}
builder.append(")");
}
return builder.toString();
} @Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BaseCacheKey o = (BaseCacheKey) obj;
if (this.hashCode != o.hashCode())
return false;
if (!Optional.ofNullable(o.className).orElse("").equals(this.className))
return false;
if (!Optional.ofNullable(o.methodName).orElse("").equals(this.methodName))
return false;
if (!Arrays.equals(params, o.params))
return false;
return true;
} @Override
public final int hashCode() {
return hashCode;
} private int generatorHashCode() {
final int prime = 31;
int result = 1;
result = prime * result + hashCode;
result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());
result = prime * result + Arrays.deepHashCode(params);
result = prime * result + ((className == null) ? 0 : className.hashCode());
return result;
} @Override
public String toString() {
LOG.info(Arrays.toString(params));
LOG.info(Arrays.deepToString(params));
return "BaseCacheKey [params=" + Arrays.deepToString(params) + ", className=" + className + ", methodName="
+ methodName + "]";
}
}

在IUserService接口类注解中使用keyGenerator="cacheKeyGenerator"。

  测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class CacheApplicationTest { private static final Logger LOG = Logger.getLogger(CacheApplicationTest.class); @Autowired
private IUserService iUserService; @Autowired
private CacheManager cacheManager; @Before
public void setUp(){
//检查使用的Cache
LOG.info("Cache Manager ===== "+cacheManager.getClass().getName());
//删除所有数据
iUserService.deleteAll();
//添加用户信息
iUserService.create("guanguan", 20);
iUserService.create("lindapang", 21);
iUserService.create("xiaoyan", 18);
} @Test
public void userAddTest(){
UserVO user = iUserService.findByName("guanguan");
LOG.info("第一次查询用户信息=="+user.toString());
user = iUserService.findByName("guanguan");
LOG.info("第二次查询用户信息=="+user.toString());
} }

测试结果如下,可以看到缓存依旧生效:

Cache Manager ===== org.springframework.cache.ehcache.EhCacheCacheManager

Spring Boot中使用EhCache实现缓存支持

可以观察到,此时CacheManager的实例是
org.springframework.data.redis.cache.RedisCacheManager,
在第一次查询的时候,执行了select语句;第二次查询没有执行select语句,说明是从缓存中获得了结果。

不过由于EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的。在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了。在Spring Boot的缓存支持中使用Redis进行数据缓存。