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

本文详细介绍Spring Boot中缓存功能的应用,包括缓存注解的使用方法、配置及实现原理,通过示例展示了如何利用Spring Boot自动化配置来管理EhCache缓存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  •  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();
}
View Code

      新建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);
}
View Code

备注:大家可以先不配置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;
    }
}
View Code

 新建单元测试类:

@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());
    }
    
}
View Code

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

从日志中,我们发现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 + "]";
    }
}
View Code

   在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());
    }
    
}
View Code

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

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

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

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

 

转载于:https://www.cnblogs.com/guanzhyan/p/8401115.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值