• JAVA中使用最广泛的本地缓存?Ehcache的自信从何而来2 —— Ehcache的各种项目集成与使用初体验


    Ehcache的依赖集成与配置

    依赖引入

    集成使用Ehcache的第一步,就是要引入对应的依赖包。对于Maven项目而言,可以在pom.xml中添加对应依赖:

    1. <dependency>
    2. <groupId>org.ehcache</groupId>
    3. <artifactId>ehcache</artifactId>
    4. <version>3.10.0</version>
    5. </dependency>
    6. 复制代码

    依赖添加完成后,还需要对缓存进行配置后方可使用。

    缓存的配置与创建

    使用代码配置与创建Ehcache

    Ehcache支持在代码中手动创建缓存对象,并指定对应缓存参数信息。在使用之前,需要先了解几个关键代码类:

    类名具体说明
    CacheManagerBuilderCacheManager对象的构造器对象,可以方便的指定相关参数然后创建出符合条件的CacheManager对象。
    ResourcePoolsBuilder用于指定缓存的存储形式(ResourcePools)的配置构造器对象,可以指定缓存是堆内缓存、堆外缓存、磁盘缓存或者多者的组合,以及各个类型缓存的容量信息、是否持久化等信息。
    CacheConfiguration用于承载所有指定的关于缓存的配置属性值。
    CacheConfigurationBuilder用于生成最终缓存总体配置信息的构造器,可以指定缓存存储形式(ResourcePools)、过期策略(ExpiryPolicy)、键值类型等等各种属性值。

    通过组合使用上述Builder构造器,我们便可以在代码中完成对缓存Cache属性的设置。比如下面这样:

    1. public static void main(String[] args) {
    2. CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
    3. .with(CacheManagerBuilder.persistence("d:\\myCache\\"))
    4. .build(true);
    5. // 指定缓存的存储形式,采用多级缓存,并开启缓存持久化操作
    6. ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
    7. .heap(1, MemoryUnit.MB)
    8. .disk(10, MemoryUnit.GB, true)
    9. .build();
    10. // 封装缓存配置对象,指定了键值类型、指定了使用TTL与TTI联合的过期淘汰策略
    11. CacheConfiguration<Integer, String> cacheConfiguration =
    12. CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class, resourcePools)
    13. .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
    14. .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(5)))
    15. .build();
    16. // 使用给定的配置参数,创建指定名称的缓存对象
    17. Cache<Integer, String> myCache = cacheManager.createCache("myCache", cacheConfiguration);
    18. }
    19. 复制代码

    上面的示例中,我们创建了一个基于heap + disk二级缓存对象,并开启了缓存的持久化,以及指定了持久化结果文件的存储路径。

    基于XML配置Ehcache

    因为Ehcache在创建缓存的时候可以指定的参数较多,如果通过上面的代码方式指定配置,略显繁琐且不够清晰直观,并且当需要创建多个不同的缓存对象的时候比较麻烦。好在Ehcache还提供了一种通过XML来进行参数配置的途径,并且支持在一个xml中配置多个不同的缓存对象信息。

    在项目的resource目录下添加个Ehcache的配置文件,比如取名ehcache.xml,项目层级结构示意如下:

    然后我们在ehcache.xml中添加配置内容。内容示例如下:

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
    3. xmlns='http://www.ehcache.org/v3'
    4. xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
    5. http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd">
    6. <persistence directory="D:\myCache"/>
    7. <cache alias="myCache">
    8. <key-type>java.lang.Integer</key-type>
    9. <value-type>java.lang.String</value-type>
    10. <expiry>
    11. <tti unit="minutes">5</tti>
    12. </expiry>
    13. <resources>
    14. <heap unit="MB">10</heap>
    15. <offheap unit="MB">50</offheap>
    16. <disk persistent="true" unit="MB">500</disk>
    17. </resources>
    18. </cache>
    19. </config>
    20. 复制代码

    上面演示的Ehcache3.x版本中的配置实现方式(配置文件与Ehcache2.x存在较大差异,不要混用,运行会报错),在xml中指定了myCache的key与value对应的类型,指定了基于TTI的5分钟过期淘汰策略,并规定了采用heap + offheap + disk三级缓存机制,此外还开启了缓存持久化能力,并指定了持久化文件的存储路径。

    通过xml配置的方式,可以很直观的看出这个缓存对象的所有关键属性约束,也是相比于代码中直接配置的方式更有优势的一个地方。在xml配置文件中,也可以同时配置多个缓存对象信息。此外,为了简化配置,Ehcache还支持通过来将一些公用的配置信息抽取出来成为模板,然后各个Cache独立配置的时候只需要增量配置各自差异化的部分即可,当然也可以基于给定的模板进行个性化的修改覆写配置。

    比如下面这个配置文件,配置了两个Cache对象信息,复用了同一个配置模板,然后各自针对模板中不符合自己的配置进行了重新改写。

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
    3. xmlns='http://www.ehcache.org/v3'
    4. xsi:schemaLocation="
    5. http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
    6. http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd">
    7. <persistence directory="D:\myCache"/>
    8. <cache-template name="myTemplate">
    9. <key-type>java.lang.String</key-type>
    10. <value-type>java.lang.String</value-type>
    11. <expiry>
    12. <ttl unit="minutes">30</ttl>
    13. </expiry>
    14. <resources>
    15. <heap unit="MB">10</heap>
    16. <disk unit="GB" persistent="true">2</disk>
    17. </resources>
    18. </cache-template>
    19. <cache alias="myCache" uses-template="myTemplate">
    20. <key-type>java.lang.Integer</key-type>
    21. </cache>
    22. <cache alias="myCache2" uses-template="myTemplate">
    23. <expiry>
    24. <ttl unit="minutes">60</ttl>
    25. </expiry>
    26. </cache>
    27. </config>
    28. 复制代码

    配置完成之后,我们还需要在代码中指定使用此配置文件进行CacheManager创建与配置,并且完成CacheManager的init初始化操作。

    1. public Cache<Integer, String> createCacheWithXml() {
    2. // 获取配置文件
    3. URL xmlConfigUrl = this.getClass().getClassLoader().getResource("./ehcache.xml");
    4. // 解析对应的配置文件并创建CacheManager对象
    5. XmlConfiguration xmlConfiguration = new XmlConfiguration(xmlConfigUrl);
    6. CacheManager cacheManager = CacheManagerBuilder.newCacheManager(xmlConfiguration);
    7. // 执行初始化操作
    8. cacheManager.init();
    9. // 直接从CacheManager中根据名称获取对应的缓存对象
    10. return cacheManager.getCache("myCache", Integer.class, String.class);
    11. }
    12. 复制代码

    这样,Ehcache的集成与配置就算完成了,接下来直接获取Cache对象并对其进行操作即可。

    1. public static void main(String[] args) {
    2. EhcacheService ehcacheService = new EhcacheService();
    3. Cache<Integer, String> cache = ehcacheService.createCacheWithXml();
    4. cache.put(1, "value1");
    5. System.out.println(cache.get(1));
    6. }
    7. 复制代码

    当然,Ehcache3.x版本中使用xml方式配置的时候,有几个坑需要提防,避免踩坑。

    1. 对于过期时间的设定只允许选择ttl或者tti中的一者,不允许两者同时存在——而通过代码配置的时候则没有这个问题。如果在xml中同时指定ttl与tti则运行的时候会抛异常。

    1. 节点下面配置的时候,节点需要放在节点的前面,否则会报错Schema校验失败

    业务中使用

    缓存设置并创建完成后,业务代码中便可以通过Ehcache提供的接口,进行缓存数据的相关操作。业务使用是通过对Cache对象的操作来进行的,Cache提供的API接口与JDK中的Map接口极其相似,所以在使用上毫无门槛,可以直接上手。

    实际编码中,根据业务的实际诉求,通过Cache提供的API接口来完成缓存数据的增删改查操作。

    1. public static void main(String[] args) {
    2. EhcacheService ehcacheService = new EhcacheService();
    3. Cache<Integer, String> cache = ehcacheService.getCache();
    4. // 存入单条记录到缓存中
    5. cache.put(1, "value1");
    6. Map<Integer, String> values = new HashMap<>();
    7. values.put(2, "value2");
    8. values.put(3, "value3");
    9. // 批量向缓存中写入数据
    10. cache.putAll(values);
    11. // 当缓存不存在的时候才写入缓存
    12. cache.putIfAbsent(2, "value2");
    13. // 查询单条记录
    14. System.out.println(cache.get(2));
    15. // 批量查询操作
    16. System.out.println(cache.getAll(Stream.of(1,2,3).collect(Collectors.toSet())));
    17. // 移除单条记录
    18. cache.remove(1);
    19. System.out.println(cache.get(1));
    20. // 清空缓存记录
    21. cache.clear();
    22. System.out.println(cache.get(1));
    23. }
    24. 复制代码

    从上述代码可以看出,EhCache具体使用起来与普通Map操作无异。虽然使用简单,但是这样也存在个问题就是业务代码所有使用缓存的地方,都需要强依赖Ehcache的具体接口,导致业务代码与Ehcache的依赖耦合度太高,后续如果想要更换缓存组件时,难度会非常大。

    在前面的文章《聊一聊JAVA中的缓存规范 —— 虽迟但到的JCache API与天生不俗的Spring Cache》中有介绍过JAVA业界的缓存标准规范,主要有JSR107标准与Spring Cache标准,如果可以通过标准的接口方式进行访问,这样就可以解决与EhCache深度耦合的问题了。令人欣慰的是,Ehcache同时提供了对JSR107与Spring Cache规范的支持

    下面一起看下如何通过JSR107规范接口以及Spring Cache的标准来使用Ehcache。

    通过JCache API来使用Ehcache

    依赖集成与配置

    如果要使用JCache标准方式来使用,需要额外引入JCache对应依赖包:

    1. <dependency>
    2. <groupId>javax.cache</groupId>
    3. <artifactId>cache-api</artifactId>
    4. <version>1.1.1</version>
    5. </dependency>
    6. 复制代码

    按照JCache的规范,必须通过CacheManager才能获取到Cache对象(这一点与Ehcache相同),而CacheManager则又需要通过CacheProvider来获取。

    遵循这一原则,我们可以按照JCache的方式来得到Cache对象:

    1. import javax.cache.Cache;
    2. import javax.cache.CacheManager;
    3. import javax.cache.Caching;
    4. import javax.cache.configuration.MutableConfiguration;
    5. import javax.cache.expiry.CreatedExpiryPolicy;
    6. import javax.cache.expiry.Duration;
    7. import javax.cache.spi.CachingProvider;
    8. public class JsrCacheService {
    9. public Cache<Integer, String> getCache() {
    10. CachingProvider cachingProvider = Caching.getCachingProvide();
    11. CacheManager cacheManager = cachingProvider.getCacheManager();
    12. MutableConfiguration<Integer, String> configuration =
    13. new MutableConfiguration<Integer, String>()
    14. .setTypes(Integer.class, String.class)
    15. .setStoreByValue(false)
    16. .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE));
    17. Cache<Integer, String> myCache = cacheManager.createCach ("myCache", configuration);
    18. System.out.println(myCache.getClass().getCanonicalName());
    19. return myCache;
    20. }
    21. }
    22. 复制代码

    import的内容可以看出上述代码没有调用到任何Ehcache的类,调用上述代码执行并打印出构建出来的Cache对象具体类型如下,可以看出的的确确创建出来的是Ehcache提供的Eh107Cache类:

    1. org.ehcache.jsr107.Eh107Cache
    2. 复制代码

    这是为什么呢?其实原理很简单,之前介绍JCache API的文章中也有解释过。JCache中的CacheProvider其实是一个SPI接口,Ehcache实现并向JVM中注册了这一接口,所以JVM可以直接加载使用了Ehcache提供的实际能力。翻看下Ehcache的源码,我们也可以找到其SPI注册对应的配置信息:

    这里还有一个需要注意的点,因为SPI接口有可能被多个组件实现,而且可能会有多个组件同时往JVM中注册了javax.cache.spi.CachingProvider这一SPI接口的实现类,这种情况下,上述代码执行的时候会报错,因为没有指定具体使用哪一个SPI,所以JVM出现了选择困难症,只能抛异常了:

    所以为了避免这种情况的发生,我们可以在获取CacheProvider的时候,指定加载使用Ehcache提供的具体实现类org.ehcache.jsr107.EhcacheCachingProvider即可。

    1. CachingProvider cachingProvider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
    2. 复制代码

    上面代码中,使用了JCache的MutableConfiguration类来实现缓存配置的设定。作为通用规范,JCache仅定义了所有缓存实现者需要实现的功能的最小集,而Ehcache除了JCache提供的最低限度缓存功能外,还有很多其余缓存不具备的增强特性。如果需要使用这些特性,则需要使用Ehcache自己的缓存配置类来实现。

    举个例子,MutableConfiguration只能设定基于内存缓存的一些行为参数,而如果需要配置Ehcache提供的heap+offheap+disk三级缓存能力,或者是要开启Ehcache的持久化能力,则MutableConfiguration就有点爱莫能助,只能Ehcache亲自出马了。

    比如下面这样:

    1. public Cache<Integer, String> getCache() {
    2. CacheConfiguration<Integer, String> cacheConfiguration =
    3. CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class,
    4. ResourcePoolsBuilder.heap(10).offheap(20, MemoryUnit.MB)).build();
    5. EhcacheCachingProvider cachingProvider = (EhcacheCachingProvider) Caching.getCachingProvider();
    6. CacheManager cacheManager = cachingProvider.getCacheManager();
    7. return cacheManager.createCache("myCache",
    8. Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration));
    9. }
    10. 复制代码

    当然,也可以在JCache中继续使用Ehcache的xml配置方式。如下示意:

    1. public Cache<Integer, String> getCache3() throwsURISyntaxException {
    2. CachingProvider cachingProvider = Caching.getCachingProvider();
    3. CacheManager manager = cachingProvider.getCacheManager(
    4. getClass().getClassLoader().getResource("./ehcache.xml").toURI(),
    5. getClass().getClassLoader());
    6. return manager.getCache("myCache", Integer.class, String.class);
    7. }
    8. 复制代码

    相比于使用纯粹的JCache API方式,上述两种使用Ehcache自己配置的方式可以享受到Ehcache提供的一些高级特性。但代价就是业务代码与Ehcache的解耦不是那么彻底,好在这些依赖仅在创建缓存的地方,对整体代码的耦合度影响不是很高,属于可接受的范围。

    业务中使用

    完成了通过JCache API获取Cache对象,然后业务层代码中,便可以基于Cache对象提供的一系列方法,对缓存的具体内容进行操作了。

    1. public static void main(String[] args) throws Exception {
    2. JsrCacheService service = new JsrCacheService();
    3. Cache<Integer, String> cache = service.getCache();
    4. cache.put(1,"value1");
    5. cache.put(2,"value2");
    6. System.out.println(cache.get(1));
    7. cache.remove(1);
    8. System.out.println(cache.containsKey(1));
    9. }
    10. 复制代码

    在Spring中集成Ehcache

    作为JAVA领域霸主级别的存在,Spring凭借其优良的设计与出色的表现俘获了大批开发人员青睐,大部分项目都使用Spring作为基础框架来简化编码逻辑。Ehcache可以整合到Spring中,并搭配Spring Cache的标准化注解,让代码可以以一种更加优雅的方式来实现缓存的操作。

    依赖集成与配置

    以SpringBoot项目为例进行说明,首先需要引入对应的依赖包。对于maven项目,在pom.xml中添加如下配置:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-cache</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>org.ehcache</groupId>
    7. <artifactId>ehcache</artifactId>
    8. </dependency>
    9. 复制代码

    依赖引入之后,我们需要在配置文件中指定使用Ehcache作为集成的缓存能力提供者,并且可以指定ehcache.xml独立的配置文件(ehcache.xml配置文件需要放置在resource目录下):

    1. spring.cache.type=ehcache
    2. spring.cache.ehcache.config=./ehcache.xml
    3. 复制代码

    然后我们需要在项目启动类上添加上@EnableCaching来声明开启缓存能力:

    1. @SpringBootApplication
    2. @EnableCaching
    3. public class CrawlerApplication {
    4. // ...
    5. }
    6. 复制代码

    到这里,对于Ehcache2.x版本而言,就已经完成集成预配置操作,可以直接在代码中进行操作与使用了。但是对于Ehcache3.x版本而言,由于Spring并未提供对应的CacheManager对其进行支持,如果这个时候我们直接启动程序,会在启动的时候就被无情的泼上一盆冷水:

    为了实现Ehcache3.xSpring的集成,解决上述的问题,需要做一些额外的适配逻辑。根据报错信息,首先可以想到的就是手动实现cacheManager的创建与初始化。而由于Spring Cache提供了对JSR107规范的支持,且Ehcache3.x也全面符合JSR107规范,所以我们可以将三者结合起来,以JSR107规范作为桥梁,实现SpringBoot与Ehcache3.x的集成。

    这个方案也即目前比较常用的"SpringBoot + JCache + Ehcache"组合模式。首先需要在前面已有实现的基础上,额外增加对JCache的依赖:

    1. <dependency>
    2. <groupId>javax.cache</groupId>
    3. <artifactId>cache-api</artifactId>
    4. <version>1.1.1</version>
    5. </dependency>
    6. 复制代码

    其次,需要修改下application.properties配置文件,将Spring Cache声明使用的缓存类型改为JCache

    1. spring.cache.type=jcache
    2. spring.cache.jcache.config=./ehcache.xml
    3. 复制代码

    上面的配置看着略显魔幻,也是很多不清楚原有的小伙伴们会比较疑惑的地方(我曾经刚在项目中看到这种写法的时候,就一度怀疑是别人代码配置写错了)。但是经过上述的原因阐述,应该就明白其中的寓意了。

    接下来,需要在项目中手动指定使用ehcache.xml配置文件来构建cacheManager对象。

    1. @Configuration
    2. public class EhcacheConfig {
    3. @Bean
    4. public JCacheManagerFactoryBean cacheManagerFactoryBean() throws Exception {
    5. JCacheManagerFactoryBean factoryBean = new JCacheManagerFactoryBean();
    6. factoryBean.setCacheManagerUri(getClass().getClassLoader().getResource("ehcache.xml").toURI());
    7. return factoryBean;
    8. }
    9. @Bean
    10. public CacheManager cacheManager(javax.cache.CacheManager cacheManager) {
    11. JCacheCacheManager cacheCacheManager = new JCacheCacheManager();
    12. cacheCacheManager.setCacheManager(cacheManager);
    13. return cacheCacheManager;
    14. }
    15. }
    16. 复制代码

    这样,就完成了通过JCache桥接来实现Spring中使用Ehcache3.x版本的目的了。

    支持Spring Cache注解操作

    完成了Spring与Ehcache的整合之后,便可以使用Spring Cache提供的标准注解来实现对Ehcache缓存的操作。

    首先需了解Spring Cache几个常用的注解及其含义:

    注解含义说明
    @EnableCaching开启使用缓存能力
    @Cacheable添加相关内容到缓存中
    @CachePut更新相关缓存记录
    @CacheEvict删除指定的缓存记录,如果需要清空指定容器的全部缓存记录,可以指定allEntities=true来实现

    通过注解的方式,可以轻松的实现将某个方法调用的入参与响应映射自动缓存起来,基于AOP机制,实现了对业务逻辑无侵入式的静默缓存处理。

    1. @Service
    2. @Slf4j
    3. public class TestService {
    4. @Cacheable(cacheNames = "myCache", key = "#id")
    5. public String queryById(int id) {
    6. log.info("queryById方法被执行");
    7. return "value" + id;
    8. }
    9. @CachePut(cacheNames = "myCache", key = "#id")
    10. public String updateIdValue(int id, String newValue) {
    11. log.info("updateIdValue方法被执行");
    12. return newValue;
    13. }
    14. @CacheEvict(cacheNames = "myCache", key = "#id")
    15. public void deleteById(int id) {
    16. log.info("deleteById方法被执行");
    17. }
    18. }
    19. 复制代码

    通过注解的方式指定了各个方法需要配套执行的缓存操作,具体业务代码里面则聚焦于自身逻辑,无需操心缓存的具体实现。可以通过下面的代码测试下集成后的效果:

    1. @GetMapping("/test")
    2. public String test() {
    3. String value = testService.queryById(123);
    4. System.out.println("第一次查询,结果:" + value);
    5. value = testService.queryById(123);
    6. System.out.println("第二次查询,结果:" +value);
    7. testService.updateIdValue(123, "newValue123");
    8. value = testService.queryById(123);
    9. System.out.println("更新后重新查询,结果:" + value);
    10. testService.deleteById(123);
    11. value = testService.queryById(123);
    12. System.out.println("删除后重新查询,结果:" + value);
    13. return "OK";
    14. }
    15. 复制代码

    执行结果如下:

    1. queryById方法被执行
    2. 第一次查询,结果:value123
    3. 第二次查询,结果:value123
    4. updateIdValue方法被执行
    5. 更新后重新查询,结果:newValue123
    6. deleteById方法被执行
    7. queryById方法被执行
    8. 删除后重新查询,结果:newValue123
    9. 复制代码

    从测试结果可以看出,查询之后方法的入参与返回值被做了缓存,再次去查询的时候并没有真正的执行具体的查询操作方法,而调用删除方法之后再次查询,又会触发了真正的查询方法的执行。

    小结回顾

    好啦,关于Ehcache的各种配置、以及通过JSR107或者Spring Cache规范集成到项目中使用的相关内容,就介绍到这里了。不知道小伙伴们是否对Ehcache的使用有了进一步的了解呢?而关于Ehcache,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。

    📣 补充说明1

    本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。

    如果有兴趣,也欢迎关注此专栏。

  • 相关阅读:
    极狐GitLab CI 助力 .Net 项目研发效率和质量双提升
    五张图带你理解 RocketMQ 顺序消息实现机制
    6、Linux:一起玩转vi/vim编辑命令
    JAVA深化篇_33——线程并发协作(生产者/消费者模式)
    [管理与领导-93]:IT基层管理者 - 扩展技能 - 5 - 职场丛林法则 -7- 复杂问题分析能力与复杂问题的解决能力:系统化思维
    设置host
    【计算机网络】 TCP——四次挥手
    opencv重点知识
    web3 前端dapp从redux过滤出 (我创建与别人创建)正在执行的订单 并展示在Table上
    [架构之路-223]:数据管理能力成熟度评估模型DCMM简介
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127958389