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


    •  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进行数据缓存。

  • 相关阅读:
    java web项目防止多用户重复登录解决方案
    java提高篇(二一)-----ArrayList
    转:为什么需要htons(), ntohl(), ntohs(),htons() 函数
    转:对于linux下system()函数的深度理解(整理)
    转:sprintf与snprintf
    转: fscanf()函数详解
    转:fopen()函数
    转:struct sockaddr与struct sockaddr_in ,struct sockaddr_un的区别和联系
    转:BZERO()等的区别
    转:Linux内存管理之mmap详解
  • 原文地址:https://www.cnblogs.com/guanzhyan/p/8401115.html
Copyright © 2020-2023  润新知