集成使用Ehcache的第一步,就是要引入对应的依赖包。对于Maven项目而言,可以在pom.xml中添加对应依赖:
- <dependency>
- <groupId>org.ehcache</groupId>
- <artifactId>ehcache</artifactId>
- <version>3.10.0</version>
- </dependency>
- 复制代码
依赖添加完成后,还需要对缓存进行配置后方可使用。
使用代码配置与创建Ehcache
Ehcache支持在代码中手动创建缓存对象,并指定对应缓存参数信息。在使用之前,需要先了解几个关键代码类:
类名 | 具体说明 |
---|---|
CacheManagerBuilder | CacheManager对象的构造器对象,可以方便的指定相关参数然后创建出符合条件的CacheManager对象。 |
ResourcePoolsBuilder | 用于指定缓存的存储形式(ResourcePools)的配置构造器对象,可以指定缓存是堆内 缓存、堆外 缓存、磁盘 缓存或者多者的组合,以及各个类型缓存的容量信息、是否持久化等信息。 |
CacheConfiguration | 用于承载所有指定的关于缓存的配置属性值。 |
CacheConfigurationBuilder | 用于生成最终缓存总体配置信息的构造器,可以指定缓存存储形式 (ResourcePools)、过期策略 (ExpiryPolicy)、键值类型 等等各种属性值。 |
通过组合使用上述Builder构造器,我们便可以在代码中完成对缓存Cache属性的设置。比如下面这样:
- public static void main(String[] args) {
- CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
- .with(CacheManagerBuilder.persistence("d:\\myCache\\"))
- .build(true);
- // 指定缓存的存储形式,采用多级缓存,并开启缓存持久化操作
- ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
- .heap(1, MemoryUnit.MB)
- .disk(10, MemoryUnit.GB, true)
- .build();
- // 封装缓存配置对象,指定了键值类型、指定了使用TTL与TTI联合的过期淘汰策略
- CacheConfiguration<Integer, String> cacheConfiguration =
- CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class, resourcePools)
- .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
- .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(5)))
- .build();
- // 使用给定的配置参数,创建指定名称的缓存对象
- Cache<Integer, String> myCache = cacheManager.createCache("myCache", cacheConfiguration);
- }
- 复制代码
上面的示例中,我们创建了一个基于heap + disk
的二级缓存对象,并开启了缓存的持久化,以及指定了持久化结果文件的存储路径。
基于XML配置Ehcache
因为Ehcache在创建缓存的时候可以指定的参数较多,如果通过上面的代码方式指定配置,略显繁琐且不够清晰直观,并且当需要创建多个不同的缓存对象的时候比较麻烦。好在Ehcache还提供了一种通过XML
来进行参数配置的途径,并且支持在一个xml中配置多个不同的缓存对象信息。
在项目的resource目录下添加个Ehcache的配置文件,比如取名ehcache.xml
,项目层级结构示意如下:
然后我们在ehcache.xml中添加配置内容。内容示例如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
- xmlns='http://www.ehcache.org/v3'
- xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
- http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd">
-
- <persistence directory="D:\myCache"/>
-
- <cache alias="myCache">
- <key-type>java.lang.Integer</key-type>
- <value-type>java.lang.String</value-type>
- <expiry>
- <tti unit="minutes">5</tti>
- </expiry>
- <resources>
- <heap unit="MB">10</heap>
- <offheap unit="MB">50</offheap>
- <disk persistent="true" unit="MB">500</disk>
- </resources>
- </cache>
- </config>
- 复制代码
上面演示的Ehcache3.x
版本中的配置实现方式(配置文件与Ehcache2.x存在较大差异,不要混用,运行会报错),在xml中指定了myCache
的key与value对应的类型,指定了基于TTI的5分钟过期淘汰策略,并规定了采用heap + offheap + disk
的三级缓存机制,此外还开启了缓存持久化能力,并指定了持久化文件的存储路径。
通过xml配置的方式,可以很直观的看出这个缓存对象的所有关键属性约束,也是相比于代码中直接配置的方式更有优势的一个地方。在xml配置文件中,也可以同时配置多个缓存对象信息。此外,为了简化配置,Ehcache还支持通过
来将一些公用的配置信息抽取出来成为模板,然后各个Cache独立配置的时候只需要增量配置各自差异化的部分即可,当然也可以基于给定的模板进行个性化的修改覆写配置。
比如下面这个配置文件,配置了两个Cache对象信息,复用了同一个配置模板,然后各自针对模板中不符合自己的配置进行了重新改写。
- <?xml version="1.0" encoding="UTF-8"?>
- <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
- xmlns='http://www.ehcache.org/v3'
- xsi:schemaLocation="
- http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd
- http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.1.xsd">
-
- <persistence directory="D:\myCache"/>
-
- <cache-template name="myTemplate">
- <key-type>java.lang.String</key-type>
- <value-type>java.lang.String</value-type>
- <expiry>
- <ttl unit="minutes">30</ttl>
- </expiry>
- <resources>
- <heap unit="MB">10</heap>
- <disk unit="GB" persistent="true">2</disk>
- </resources>
- </cache-template>
-
- <cache alias="myCache" uses-template="myTemplate">
- <key-type>java.lang.Integer</key-type>
- </cache>
- <cache alias="myCache2" uses-template="myTemplate">
- <expiry>
- <ttl unit="minutes">60</ttl>
- </expiry>
- </cache>
- </config>
- 复制代码
配置完成之后,我们还需要在代码中指定使用此配置文件进行CacheManager创建与配置,并且完成CacheManager的init初始化操作。
- public Cache<Integer, String> createCacheWithXml() {
- // 获取配置文件
- URL xmlConfigUrl = this.getClass().getClassLoader().getResource("./ehcache.xml");
- // 解析对应的配置文件并创建CacheManager对象
- XmlConfiguration xmlConfiguration = new XmlConfiguration(xmlConfigUrl);
- CacheManager cacheManager = CacheManagerBuilder.newCacheManager(xmlConfiguration);
- // 执行初始化操作
- cacheManager.init();
- // 直接从CacheManager中根据名称获取对应的缓存对象
- return cacheManager.getCache("myCache", Integer.class, String.class);
- }
- 复制代码
这样,Ehcache的集成与配置就算完成了,接下来直接获取Cache对象并对其进行操作即可。
- public static void main(String[] args) {
- EhcacheService ehcacheService = new EhcacheService();
- Cache<Integer, String> cache = ehcacheService.createCacheWithXml();
- cache.put(1, "value1");
- System.out.println(cache.get(1));
- }
- 复制代码
当然,Ehcache3.x版本中使用xml方式配置的时候,有几个坑需要提防,避免踩坑。
节点下面配置的时候,
节点需要放在
节点的前面,否则会报错Schema校验失败。缓存设置并创建完成后,业务代码中便可以通过Ehcache提供的接口,进行缓存数据的相关操作。业务使用是通过对Cache对象的操作来进行的,Cache提供的API接口与JDK中的Map接口极其相似,所以在使用上毫无门槛,可以直接上手。
实际编码中,根据业务的实际诉求,通过Cache提供的API接口来完成缓存数据的增删改查操作。
- public static void main(String[] args) {
- EhcacheService ehcacheService = new EhcacheService();
- Cache<Integer, String> cache = ehcacheService.getCache();
- // 存入单条记录到缓存中
- cache.put(1, "value1");
- Map<Integer, String> values = new HashMap<>();
- values.put(2, "value2");
- values.put(3, "value3");
- // 批量向缓存中写入数据
- cache.putAll(values);
- // 当缓存不存在的时候才写入缓存
- cache.putIfAbsent(2, "value2");
- // 查询单条记录
- System.out.println(cache.get(2));
- // 批量查询操作
- System.out.println(cache.getAll(Stream.of(1,2,3).collect(Collectors.toSet())));
- // 移除单条记录
- cache.remove(1);
- System.out.println(cache.get(1));
- // 清空缓存记录
- cache.clear();
- System.out.println(cache.get(1));
- }
- 复制代码
从上述代码可以看出,EhCache具体使用起来与普通Map操作无异。虽然使用简单,但是这样也存在个问题就是业务代码所有使用缓存的地方,都需要强依赖Ehcache的具体接口,导致业务代码与Ehcache的依赖耦合度太高,后续如果想要更换缓存组件时,难度会非常大。
在前面的文章《聊一聊JAVA中的缓存规范 —— 虽迟但到的JCache API与天生不俗的Spring Cache》中有介绍过JAVA业界的缓存标准规范,主要有JSR107
标准与Spring Cache
标准,如果可以通过标准的接口方式进行访问,这样就可以解决与EhCache深度耦合的问题了。令人欣慰的是,Ehcache同时提供了对JSR107与Spring Cache规范的支持!
下面一起看下如何通过JSR107规范接口以及Spring Cache的标准来使用Ehcache。
如果要使用JCache标准方式来使用,需要额外引入JCache对应依赖包:
- <dependency>
- <groupId>javax.cache</groupId>
- <artifactId>cache-api</artifactId>
- <version>1.1.1</version>
- </dependency>
- 复制代码
按照JCache的规范,必须通过CacheManager才能获取到Cache对象(这一点与Ehcache相同),而CacheManager则又需要通过CacheProvider
来获取。
遵循这一原则,我们可以按照JCache的方式来得到Cache对象:
- import javax.cache.Cache;
- import javax.cache.CacheManager;
- import javax.cache.Caching;
- import javax.cache.configuration.MutableConfiguration;
- import javax.cache.expiry.CreatedExpiryPolicy;
- import javax.cache.expiry.Duration;
- import javax.cache.spi.CachingProvider;
-
- public class JsrCacheService {
- public Cache<Integer, String> getCache() {
- CachingProvider cachingProvider = Caching.getCachingProvide();
- CacheManager cacheManager = cachingProvider.getCacheManager();
- MutableConfiguration<Integer, String> configuration =
- new MutableConfiguration<Integer, String>()
- .setTypes(Integer.class, String.class)
- .setStoreByValue(false)
- .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_MINUTE));
- Cache<Integer, String> myCache = cacheManager.createCach ("myCache", configuration);
- System.out.println(myCache.getClass().getCanonicalName());
- return myCache;
- }
- }
- 复制代码
从import
的内容可以看出上述代码没有调用到任何Ehcache的类,调用上述代码执行并打印出构建出来的Cache对象具体类型如下,可以看出的的确确创建出来的是Ehcache提供的Eh107Cache
类:
- org.ehcache.jsr107.Eh107Cache
- 复制代码
这是为什么呢?其实原理很简单,之前介绍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
即可。
- CachingProvider cachingProvider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
- 复制代码
上面代码中,使用了JCache的MutableConfiguration
类来实现缓存配置的设定。作为通用规范,JCache仅定义了所有缓存实现者需要实现的功能的最小集,而Ehcache除了JCache提供的最低限度缓存功能外,还有很多其余缓存不具备的增强特性。如果需要使用这些特性,则需要使用Ehcache自己的缓存配置类来实现。
举个例子,MutableConfiguration只能设定基于内存缓存的一些行为参数,而如果需要配置Ehcache提供的heap+offheap+disk
三级缓存能力,或者是要开启Ehcache的持久化能力,则MutableConfiguration就有点爱莫能助,只能Ehcache亲自出马了。
比如下面这样:
- public Cache<Integer, String> getCache() {
- CacheConfiguration<Integer, String> cacheConfiguration =
- CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, String.class,
- ResourcePoolsBuilder.heap(10).offheap(20, MemoryUnit.MB)).build();
- EhcacheCachingProvider cachingProvider = (EhcacheCachingProvider) Caching.getCachingProvider();
- CacheManager cacheManager = cachingProvider.getCacheManager();
- return cacheManager.createCache("myCache",
- Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfiguration));
- }
- 复制代码
当然,也可以在JCache中继续使用Ehcache的xml配置方式。如下示意:
- public Cache<Integer, String> getCache3() throwsURISyntaxException {
- CachingProvider cachingProvider = Caching.getCachingProvider();
- CacheManager manager = cachingProvider.getCacheManager(
- getClass().getClassLoader().getResource("./ehcache.xml").toURI(),
- getClass().getClassLoader());
- return manager.getCache("myCache", Integer.class, String.class);
- }
- 复制代码
相比于使用纯粹的JCache API
方式,上述两种使用Ehcache自己配置的方式可以享受到Ehcache提供的一些高级特性。但代价就是业务代码与Ehcache的解耦不是那么彻底,好在这些依赖仅在创建缓存的地方,对整体代码的耦合度影响不是很高,属于可接受的范围。
完成了通过JCache API获取Cache对象,然后业务层代码中,便可以基于Cache对象提供的一系列方法,对缓存的具体内容进行操作了。
- public static void main(String[] args) throws Exception {
- JsrCacheService service = new JsrCacheService();
- Cache<Integer, String> cache = service.getCache();
- cache.put(1,"value1");
- cache.put(2,"value2");
- System.out.println(cache.get(1));
- cache.remove(1);
- System.out.println(cache.containsKey(1));
- }
- 复制代码
作为JAVA领域霸主级别的存在,Spring凭借其优良的设计与出色的表现俘获了大批开发人员青睐,大部分项目都使用Spring作为基础框架来简化编码逻辑。Ehcache可以整合到Spring中,并搭配Spring Cache
的标准化注解,让代码可以以一种更加优雅的方式来实现缓存的操作。
以SpringBoot项目为例进行说明,首先需要引入对应的依赖包。对于maven项目,在pom.xml中添加如下配置:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
- <dependency>
- <groupId>org.ehcache</groupId>
- <artifactId>ehcache</artifactId>
- </dependency>
- 复制代码
依赖引入之后,我们需要在配置文件中指定使用Ehcache作为集成的缓存能力提供者,并且可以指定ehcache.xml
独立的配置文件(ehcache.xml配置文件需要放置在resource目录下):
- spring.cache.type=ehcache
- spring.cache.ehcache.config=./ehcache.xml
- 复制代码
然后我们需要在项目启动类上添加上@EnableCaching
来声明开启缓存能力:
- @SpringBootApplication
- @EnableCaching
- public class CrawlerApplication {
- // ...
- }
- 复制代码
到这里,对于Ehcache2.x
版本而言,就已经完成集成预配置操作,可以直接在代码中进行操作与使用了。但是对于Ehcache3.x
版本而言,由于Spring并未提供对应的CacheManager对其进行支持,如果这个时候我们直接启动程序,会在启动的时候就被无情的泼上一盆冷水:
为了实现Ehcache3.x与Spring的集成,解决上述的问题,需要做一些额外的适配逻辑。根据报错信息,首先可以想到的就是手动实现cacheManager的创建与初始化。而由于Spring Cache提供了对JSR107规范的支持,且Ehcache3.x也全面符合JSR107规范,所以我们可以将三者结合起来,以JSR107规范作为桥梁,实现SpringBoot与Ehcache3.x的集成。
这个方案也即目前比较常用的"SpringBoot + JCache + Ehcache
"组合模式。首先需要在前面已有实现的基础上,额外增加对JCache的依赖:
- <dependency>
- <groupId>javax.cache</groupId>
- <artifactId>cache-api</artifactId>
- <version>1.1.1</version>
- </dependency>
- 复制代码
其次,需要修改下application.properties
配置文件,将Spring Cache声明使用的缓存类型改为JCache。
- spring.cache.type=jcache
- spring.cache.jcache.config=./ehcache.xml
- 复制代码
上面的配置看着略显魔幻,也是很多不清楚原有的小伙伴们会比较疑惑的地方(我曾经刚在项目中看到这种写法的时候,就一度怀疑是别人代码配置写错了)。但是经过上述的原因阐述,应该就明白其中的寓意了。
接下来,需要在项目中手动指定使用ehcache.xml配置文件来构建cacheManager对象。
- @Configuration
- public class EhcacheConfig {
- @Bean
- public JCacheManagerFactoryBean cacheManagerFactoryBean() throws Exception {
- JCacheManagerFactoryBean factoryBean = new JCacheManagerFactoryBean();
- factoryBean.setCacheManagerUri(getClass().getClassLoader().getResource("ehcache.xml").toURI());
- return factoryBean;
- }
- @Bean
- public CacheManager cacheManager(javax.cache.CacheManager cacheManager) {
- JCacheCacheManager cacheCacheManager = new JCacheCacheManager();
- cacheCacheManager.setCacheManager(cacheManager);
- return cacheCacheManager;
- }
- }
- 复制代码
这样,就完成了通过JCache桥接来实现Spring中使用Ehcache3.x版本的目的了。
完成了Spring与Ehcache的整合之后,便可以使用Spring Cache提供的标准注解来实现对Ehcache缓存的操作。
首先需了解Spring Cache几个常用的注解及其含义:
注解 | 含义说明 |
---|---|
@EnableCaching | 开启使用缓存能力 |
@Cacheable | 添加相关内容到缓存中 |
@CachePut | 更新相关缓存记录 |
@CacheEvict | 删除指定的缓存记录,如果需要清空指定容器的全部缓存记录,可以指定allEntities=true 来实现 |
通过注解的方式,可以轻松的实现将某个方法调用的入参与响应映射自动缓存起来,基于AOP机制,实现了对业务逻辑无侵入式的静默缓存处理。
- @Service
- @Slf4j
- public class TestService {
- @Cacheable(cacheNames = "myCache", key = "#id")
- public String queryById(int id) {
- log.info("queryById方法被执行");
- return "value" + id;
- }
- @CachePut(cacheNames = "myCache", key = "#id")
- public String updateIdValue(int id, String newValue) {
- log.info("updateIdValue方法被执行");
- return newValue;
- }
- @CacheEvict(cacheNames = "myCache", key = "#id")
- public void deleteById(int id) {
- log.info("deleteById方法被执行");
- }
- }
- 复制代码
通过注解的方式指定了各个方法需要配套执行的缓存操作,具体业务代码里面则聚焦于自身逻辑,无需操心缓存的具体实现。可以通过下面的代码测试下集成后的效果:
- @GetMapping("/test")
- public String test() {
- String value = testService.queryById(123);
- System.out.println("第一次查询,结果:" + value);
- value = testService.queryById(123);
- System.out.println("第二次查询,结果:" +value);
- testService.updateIdValue(123, "newValue123");
- value = testService.queryById(123);
- System.out.println("更新后重新查询,结果:" + value);
- testService.deleteById(123);
- value = testService.queryById(123);
- System.out.println("删除后重新查询,结果:" + value);
- return "OK";
- }
- 复制代码
执行结果如下:
- queryById方法被执行
- 第一次查询,结果:value123
- 第二次查询,结果:value123
- updateIdValue方法被执行
- 更新后重新查询,结果:newValue123
- deleteById方法被执行
- queryById方法被执行
- 删除后重新查询,结果:newValue123
- 复制代码
从测试结果可以看出,查询之后方法的入参与返回值被做了缓存,再次去查询的时候并没有真正的执行具体的查询操作方法,而调用删除方法之后再次查询,又会触发了真正的查询方法的执行。
好啦,关于Ehcache
的各种配置、以及通过JSR107或者Spring Cache规范集成到项目中使用的相关内容,就介绍到这里了。不知道小伙伴们是否对Ehcache的使用有了进一步的了解呢?而关于Ehcache,你是否有自己的一些想法与见解呢?欢迎评论区一起交流下,期待和各位小伙伴们一起切磋、共同成长。
📣 补充说明1 :
本文属于《深入理解缓存原理与实战设计》系列专栏的内容之一。该专栏围绕缓存这个宏大命题进行展开阐述,全方位、系统性地深度剖析各种缓存实现策略与原理、以及缓存的各种用法、各种问题应对策略,并一起探讨下缓存设计的哲学。
如果有兴趣,也欢迎关注此专栏。