☰
JCache(JSR-107)在Java EE与Spring Boot中的集成与启用指南
2026/10/6 4:59:06 网站建设 项目流程

面试题刷到这道“Java EE 或 Spring Boot 环境中,如何集成和启用 JCache?”时,我愣了一下。倒不是不会用,而是“Java EE 环境”和“Spring Boot 环境”这两个词放在一起,正好踩中了很多人的知识盲区:大家习惯了在 Spring Boot 里点一个 @Cacheable,但要解释清楚 JCache(JSR-107)到底是什么、它和 Spring Cache 抽象层是什么关系、在 Java EE 容器里怎么玩,很多同学就支支吾吾了。

这道题出得很有水平,属于典型的“会用但讲不清”陷阱。平时项目里缓存用得再多,如果底层原理没吃透,面试一深挖就会露馅。今天我不打算背标准答案,而是把这套东西从规范本身、集成步骤、配置细节到常见坑位完整拆一遍。适合正在准备中高级 Java 面试的同学,也适合想把 Spring Boot 缓存用好、不被缓存框架绑死的开发。所有代码和配置都是我在实际项目里跑过、踩过坑之后整理出来的,可以直接参考。

1. JCache 到底是什么:先搞清楚这套 API 存在的意义

1.1 没有标准之前,缓存代码是怎么被绑死的

先说一个很容易被忽略的背景。JCache 不是 Spring 的东西,它是 Java 官方的缓存标准,JSR-107 规范定义了javax.cache包下的一套 API。在我的早年间工作经历里,项目里用缓存基本就是 Ehcache 2.x、Guava Cache、Hazelcast 各玩各的,每种缓存都有自己的一套get/put/expire/listener接口。

打个比方:那时候的 Java 缓存世界就像每家手机厂商都有自己的充电接口,手机没问题,但出门得带好几根线。后来大家看不下去了,JCP 组织就在 2014 年搞出了 JCache API,统一了缓存的编程模型。从此以后,不管底层是 Ehcache、Infinispan、Hazelcast 还是 Caffeine(只要实现了 JSR-107),业务代码写的是同一套接口。换缓存实现,代码改动量能缩到最小,这对中间件厂商和大型企业来说都是实打实的红利。

所以面试时别一上来就说“JCache是一种缓存”,这种回答会显得很外行。要表达出“JCache 是一套标准,一套契约,不是具体某一个产品,更不是 Redis”。这句话是整道题的基石。

1.2 JSR-107 规范定义的六大核心 API

JCache 的核心 API 都在javax.cache包下,面试中能把以下几点讲到,就足以证明你是真看过规范的。

  • CachingProvider:缓存的厂商入口,用于创建、获取和管理 CacheManager。每个厂商都有自己的 CachingProvider 实现,例如 Ehcache 的org.ehcache.jsr107.EhcacheCachingProvider。
  • CacheManager:缓存管理器,可以理解为一个容器。一个 CacheManager 下面可以有多个 Cache。它负责 Cache 的生命周期管理,比如创建、关闭、获取。
  • Cache:真正存数据的接口,泛型为Cache<K, V>。它定义了一整套类似 Map 的操作,但多了缓存特有的语义:putIfAbsent、replace、getAndPut、invoke原子操作等。这里要注意它和 ConcurrentMap 的区别:Cache 的操作有分布式的上下文,并且支持通过 CacheLoader、CacheWriter 实现读写穿透。
  • Cache.Entry:缓存中的条目,包含 key 和 value。
  • ExpiryPolicy:过期策略,控制条目什么时候过期。
  • CacheLoader / CacheWriter:用于实现 read-through 和 write-through 模式。缓存没有数据时自动从数据源加载,写缓存时同步写数据源。
  • CacheEntryListener:条目级监听器,能监听创建、更新、删除、过期、淘汰等事件。

那这里我补充一个看源码的小技巧:Cache 接口里的invoke方法和CacheEntryProcessor是 JCache 独有的强大能力,它允许在锁定的缓存条目上执行任意操作。面试如果被问“如何原子地更新缓存里的某个对象”,不要说自己写 synchronized,而要说用cache.invoke(key, entryProcessor),这一下就拉开了档次。

1.3 Spring Cache 和 JCache 是不是同一种东西

这是大多数面试者最混淆的点。Spring Cache 是 Spring 框架提供的一个缓存抽象层,定义了一套注解(@Cacheable、@CacheEvict等)和统一的 CacheManager/Cache接口,用来屏蔽底层不同缓存实现。它不依赖 JCache 标准也能正常工作,底层完全可以是 ConcurrentMap、Redis、Caffeine 等,Spring 只是做了一层封装,帮你把“缓存操作”揉进了 AOP 逻辑里。

JCache 则是 Java 官方的缓存编程标准,它是定义在语言生态层面的一套接口。Spring Cache 抽象层完全可以适配到 JCache 上,此时 Spring 的@Cacheable注解底层调用的就是javax.cache接口,但反过来 JCache 的注解(比如@CacheResult)也可以在 Java EE 容器中脱离 Spring 独立使用。

面试时可以给面试官画个关系:业务代码 → Spring Cache 抽象(注解) → JCache API(标准) → 具体缓存实现(Ehcache/Infinispan/Hazelcast)。这里不要用“替代”来描述,要说“适配与集成”。Spring Boot 中把spring.cache.type配成jcache,做的事情就是用 Spring 这个壳去包住 JCache 这个标准,最终让具体缓存厂商的实现落地。

2. 在 Spring Boot 中集成 JCache:最容易被误解的配置步骤

2.1 依赖引入与版本避坑

Spring Boot 集成 JCache 不需要引入一堆花里胡哨的东西,基础依赖就两个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version> </dependency>

这里我特意选 Ehcache 3.x 作为示例,因为它的 JSR-107 支持做得最完整,文档也全。用它做学习对象,能少走很多弯路。spring-boot-starter-cache会自带一个spring-context-support,里面已经包含了 Spring 对 JCache 的适配器JCacheCacheManager,所以不需要再加额外的适配依赖。

如果你是直接用javax.cache原生 API,而不是走 Spring 注解,那我建议显式加上这个依赖:

<dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> <version>1.1.1</version> </dependency>

版本这块有一个特别容易被坑的点:虽然 Java EE 已经改成 Jakarta EE,但JCache 的包名至今依然是javax.cache,没有跟随 jakarta 重命名。这意味着在 Spring Boot 3.x(基于 jakarta.* 的那批)里,你引入 Ehcache 后照样能正常使用 javax.cache 接口,不用担心包冲突。我第一次切 Spring Boot 3 时还担心这事,实测下来是好的。

2.2 配置文件里的三件事:type、provider、cache-names

在application.yml中,JCache 配置主要关心的就是这么几个:

spring: cache: type: jcache jcache: provider: org.ehcache.jsr107.EhcacheCachingProvider cache-names: - userCache - goodsCache redis: # 如果同时配了 redis 缓存,这里用于其他用途,别和 jcache 混淆
  • spring.cache.type=jcache明确告诉 Spring Boot,CacheManager 要选用 JCache 适配器。
  • spring.cache.jcache.provider指定 CachingProvider 的全限定类名。如果 classpath 下只放了一个 JCache 实现,这一项甚至可以不配,Spring Boot 会通过ServiceLoader自动发现。但如果像某些项目那样同时引入了 Ehcache 和 Hazelcast,就必须显式指定,否则启动时会直接报IllegalArgumentException,告诉你找到了多个 CachingProvider。
  • spring.cache.cache-names用于预创建有哪些缓存。实际项目中我建议在这里把核心缓存名列出来,而不是放任业务代码随便写名字。理由很简单:不列出来,Spring 会在第一次访问某个缓存名时自动创建,配置化管理程度低,后面不好做统一的过期策略绑定。

更规范的做法是放一个ehcache.xml到 classpath 下,在 XML 里定义缓存模板和过期策略:

<config xmlns="http://www.ehcache.org/v3" xmlns:jsr107="http://www.ehcache.org/v3/jsr107"> <service> <jsr107:defaults enable-management="true" enable-statistics="true"/> </service> <cache alias="userCache"> <expiry> <ttl unit="seconds">300</ttl> </expiry> <heap unit="entries">10000</heap> </cache> <cache alias="goodsCache"> <expiry> <ttl unit="seconds">600</ttl> </expiry> <heap unit="entries">50000</heap> </cache> </config>

只要这个文件放在 classpath 根目录,Ehcache 的 JCache provider 会自动加载。配置生效后,你可以在启动日志里看到类似 “Spring configured JCacheCacheManager” 的提示。

2.3 用 Spring 注解还是用原生 JCache API

我见过很多项目两种都在用,但用混了。先说 Spring 注解方式,最典型的用法:

@Service public class UserService { @Cacheable(cacheNames = "userCache", key = "#id") public User getUserById(Long id) { // 模拟数据库查询 return userMapper.selectById(id); } @CacheEvict(cacheNames = "userCache", key = "#id") public void deleteUser(Long id) { userMapper.deleteById(id); } }

使用前还要在配置类或启动类上加一个@EnableCaching:

@Configuration @EnableCaching public class CacheConfig { }

这个注解别漏,漏了你会非常困惑:代码里 @Cacheable 写得整整齐齐,缓存就是不生效。

再说原生 JCache API。如果说注解适合业务入口,那原生 API 更适合做一些精细控制。比如你要在代码里显式拿到缓存对象做putIfAbsent,或者是批量加载、监听事件的场景:

import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; CachingProvider provider = Caching.getCachingProvider(); CacheManager cacheManager = provider.getCacheManager(); Cache<String, User> userCache = cacheManager.getCache("userCache", String.class, User.class); userCache.put("1001", new User("张三")); User user = userCache.get("1001");

要提醒一句:Caching.getCachingProvider()如果没有指定参数,会返回第一个被ServiceLoader加载到的 provider。如果 classpath 下存在多个实现,这种裸调法不够健壮,建议改成:

Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider")

面试时可以说清楚:Spring 注解适合降低使用成本、声明式拦截;原生 JCache API 适合在组件底层做更细粒度的策略控制、批量预热、原子更新操作。两者不是非此即彼,可以共存。

2.4 JCacheConfigurer 手工装配的隐藏能力

Spring Boot 自动配置对大多数项目够用,但有时你有定制需求:比如想动态决定 CacheManager 使用哪个 uri 路径,或是在 CacheManager 创建后追加自定义的配置。这种情况下可以实现JCacheConfigurer接口:

@Configuration @EnableCaching public class CustomCacheConfig implements JCacheConfigurer { @Override public CacheManager cacheManager() { CachingProvider provider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider"); return provider.getCacheManager( getClass().getResource("/ehcache-custom.xml"), getClass().getClassLoader() ); } }

不过这个场景平时真的用不着,绝大多数项目直接靠application.yml和ehcache.xml就足够了。之所以提出来,是因为面试里可能会被追问“如果自动配置不满足需求你怎么处理”,你答出JCacheConfigurer重写cacheManager(),面试官就知道你是真的读过 Spring Boot 源码。

3. 在 Java EE 环境中集成 JCache:CDI 和标准容器的姿势

3.1 Java EE 规范对 JCache 的定位

Java EE(现在叫 Jakarta EE)规范中并没有强制要求每个应用服务器内置一个 JCache 实现,但提供了非常自然的集成方式:CDI 依赖注入。你可以在容器里直接注入CacheManager或Cache对象,标准容器会负责它们的生命周期和配置。

以 WildFly、Payara 这样的服务器为例,它们内部本身就整合了 Infinispan 之类的缓存引擎,并且暴露了 JCache 的 CachingProvider。你只需要在代码里:

@Inject private javax.cache.CacheManager cacheManager; @Inject private javax.cache.Cache<String, User> userCache;

是不是比 Spring Boot 更简洁?这就是 CDI 的威力,但前提是你的运行时环境已经提供了 JCache 实现。如果是普通的 Tomcat 或 Jetty,容器本身不提供,你就得自己往 WEB-INF/lib 里塞依赖,然后通过监听器启动时初始化 CacheManager。

另一个 Java EE 典型的玩法是用 JCache 自带的@CacheResult注解做方法级缓存。这是 JSR-107 的 CDI 扩展部分,使用方式和 Spring 的 @Cacheable 很像:

@CacheResult(cacheName = "userCache") public User loadUser(String userId) { return dao.findById(userId); }

还有配套的@CacheRemove、@CacheRemoveAll、@CachePut注解。需要注意的是,这些注解是javax.cache.annotation包下的,不是 Spring 的org.springframework.cache.annotation,两者千万别混着用。一个 Java EE 项目里,要么走 Spring 注解、要么走 JCache 注解,混用会导致拦截器不认,出现“缓存看似写了但根本没触发”的诡异效果。

3.2 在 Java EE 中启用 JCache 的完整链路

我把常见的启用步骤整理一下,场景是“使用一个标准的 Java EE 应用服务器,同时使用 JCache CDI 方式”。

第一步,确保部署描述里声明的 bean 归档是开启的。绝大多数场景只需要在WEB-INF/beans.xml里写一个空配置:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" bean-discovery-mode="all"> </beans>

bean-discovery-mode="all"表示所有类都可以被 CDI 管理。之前踩过一个坑,如果用的是默认注解模式,CDI 不会扫描那些没有 bean 定义注解的类,@Inject CacheManager就会一直报 unsatisfied dependency。

第二步,确认应用服务器已经加载了 JCache provider。如果没加载,需要在自己的模块里加入如 Ehcache 的依赖,并且确保META-INF/services/javax.cache.spi.CachingProvider文件能被发现。

第三步,通过 CDI 方式注入CacheManager,然后正常写业务代码。真正到生产环境,动态配置缓存参数一般放在服务器的系统属性或独立配置文件里,让运维能调过期时间、堆大小,而不用改代码重启。

Java EE 里使用 JCache 时有一个特别明显的优势:它和 CDI 事件、事务机制天然集成。缓存条目监听器可以和 CDI 事件联动,做数据变更通知。这一点在微服务拆分的项目里很好用:某个缓存 key 更新时,通过CacheEntryCreatedListener触发一个 CDI 事件,让其他流程接着干活。

3.3 Java EE 和 Spring Boot 的实现差异

如果把两边的方案放一张表里对比,下面这些差异面试时能说出来会很加分:

对比维度Java EE / Jakarta EESpring Boot
集成方式CDI 注入,声明在 beans.xmlSpring 自动配置 + @EnableCaching
常用注解@CacheResult、@CacheRemove@Cacheable、@CacheEvict、@CachePut
依赖来源应用服务器内置或手动加入 libspring-boot-starter-cache + 具体实现
CacheManager 获取@Inject 或 JNDIJCacheCacheManager 自动装配
扩展入口CDI 事件、InterceptorJCacheConfigurer、CacheErrorHandler、自定义 KeyGenerator

这张表并不代表谁好谁坏,而是体现了两套生态的设计哲学。Java EE 偏重规范和容器托管,Spring Boot 偏重自动化和约定优于配置。如果面试官问“为什么在 Spring Boot 里也能用 JCache?”,你只要点出“Spring Boot 实现了 JCache 的适配器,本质上是把标准接口重新包装成了 Spring 管理的 Bean”就够了。

4. JCache 核心机制深入:几个能拉开差距的原理级细节

4.1 ExpiryPolicy:过期策略不是配完就完事

JCache 的过期策略由ExpiryPolicy接口控制,标准实现里一般有几个值:ETERNAL(永不过期)、Modified(创建或更新后固定 TTL)、Accessed(读取或更新后固定 TTL)。配置时对Accessed别滥用,因为它意味着每次 get 都会续期,在高并发热点场景下会带来额外的更新时间戳开销。

真正想做得精细,需要自己实现接口。有一次我在一个购物车场景中,需求是未登录用户加入购物车的临时数据 30 分钟过期,但只要用户每次访问就自动续期;登录后用户数据改成 2 小时有效期。这时标准 TTL 不够用,就要写自定义 ExpiryPolicy:

public class CartExpiryPolicy implements ExpiryPolicy { @Override public Duration getExpiryForCreatedEntry() { return Duration.ofSeconds(1800); } @Override public Duration getExpiryForUpdatedEntry() { return Duration.ofSeconds(1800); } @Override public Duration getExpiryForAccessedEntry() { return Duration.ofSeconds(1800); } }

然后在创建 Cache 时通过MutableConfiguration关联:

MutableConfiguration<String, Cart> config = new MutableConfiguration<>(); config.setExpiryPolicyFactory(() -> new CartExpiryPolicy());

有一个容易忽略的小细节:ExpiryPolicy 的工厂是通过FactoryBuilder创建的,每次创建 Cache 时会生成新的策略实例。如果策略内部带有状态(比如记录上一次访问时间),必须注意线程安全问题。这也是为什么标准 API 里更推荐使用无状态的 Duration 配置。

4.2 CacheLoader 和 CacheWriter:贯穿读与写的缓存模式

JCache 里最有含金量的两个接口就是CacheLoader和CacheWriter,它们实现了 read-through 和 write-through 模式。

CacheLoader的语义可以类比你第一次查数据时,发现缓存没有,主动去数据库捞。标准 JCache 把这个过程自动化了:只要缓存的配置是 read-through,并且 cache.get(key) 没命中,JCache 实现会自动调用对应的 CacheLoader.load(key)。这个机制对防止缓存穿透很有用,因为打了 read-through,恶意请求打进来时,最终压力会落在 CacheLoader 那一层,开发者可以在这个统一入口增加并发控制和防御逻辑。

CacheWriter则是写缓存的同时把数据同步到数据库等持久层,适合“以缓存为主、数据库为最终存储”的场景。接口是:

public class UserCacheWriter implements CacheWriter<Long, User> { @Override public void write(Cache.Entry<? extends Long, ? extends User> entry) { userMapper.save(entry.getKey(), entry.getValue()); } @Override public void delete(Object key) { userMapper.deleteById((Long) key); } }

实际用的时候,配置中要把isReadThrough、isWriteThrough开启:

MutableConfiguration<Long, User> config = new MutableConfiguration<>(); config.setTypes(Long.class, User.class) .setCacheLoaderFactory(() -> new UserCacheLoader()) .setCacheWriterFactory(() -> new UserCacheWriter()) .setReadThrough(true) .setWriteThrough(true);

我个人的建议是:业务单纯的读多写少场景不要乱开 write-through,它会把每一次缓存写操作都变成同步的存储层操作,写放大很严重。更常见的设计是 read-through 配合 CacheLoader,写的时候用 CacheWriter 做异步补偿,或者干脆只更新缓存、由业务层保证最终一致性。

4.3 监听器与 CacheStatistics:别忽视这两个辅助能力

CacheEntryListener一般被归类为功能增强,但因为工作中排查问题经常需要它,我单独说几点。监听器支持的事件类型包括 CREATED、UPDATED、REMOVED、EXPIRED、EVICTED。注册方式:

CacheEntryCreatedListener<Long, User> createdListener = (events) -> { for (CacheEntryEvent<? extends Long, ? extends User> event : events) { // 处理批量事件 } }; cache.registerCacheEntryListener( new MutableCacheEntryListenerConfiguration<>( () -> createdListener, null, // filter true, // old value required false // synchronous ) );

注意这里synchronous=false表示异步监听,生产环境除非有强一致的需求,否则我建议使用异步。监听器在大型缓存库里处理的可能是成百上千个事件,同步执行会拖慢缓存主流程。

CacheStatistics则是做性能判断的关键依赖。JCache 的统计走 JMX 方向,CacheManagerMXBean和CacheStatisticsMXBean可以暴露缓存命中率、平均 get 时间、淘汰数量等数据。配置时只需要在 ehcache.xml 的 jsr107 节点打开 enable-statistics,然后通过 JConsole 或 Spring Boot Admin 的 JMX 面板查看。在系统性能调优或者面试聊“如何判断缓存是否需要扩容”时,这些数据说话比猜靠谱得多。

5. 实战里那些容易踩的坑:这些经验比配置本身更值钱

5.1 缓存穿透、击穿、雪崩在 JCache 下的应对

我在刚开始部署 JCache 时,天真地以为加入缓存就万事大吉,结果线上真实流量一冲,次要问题全暴露了。三个经典场景在 JCache 里怎么处理,我给出个人实操结论:

  • 缓存穿透:查一个根本不存在的数据,每次请求都穿过缓存到达数据库。JCache 的Cache.get对于不存在的数据返回 null,此时会触发 CacheLoader 加载,但加载结果也是 null 的话,JCache 并不会缓存 null。最快的处理方式是在 CacheLoader 中做空值标记,或者用 Optional 包一层,配合@Cacheable(unless = "#result == null")让空值不进缓存。
  • 缓存击穿:热点 key 失效的瞬间大量请求进来。可以用 JCache 的putIfAbsent配合自定义加载锁,只让一个线程去加载数据。其实 Ehcache 等实现内部对单 key 的读加载做了加锁,所以不需要自己再写一套 distributed lock。
  • 缓存雪崩:大量 key 在同一个时间段过期。解决思路是配置过期时间时加一个随机量,错开过期时间。但 JCache 的 ttl 配置是固定值,如果平台没有随机 TTL 扩展,你只能在创建 Cache 后对不同的 key 显式设置不同的 ExpiryPolicy,或者结合业务对 key 做分组到不同 Cache。

5.2 本地缓存与分布式缓存的边界问题

JCache 本身只是一套 API,它不规定底层是本地内存还是分布式集群。Ehcache 3.x 默认是本地堆缓存;Hazelcast、Infinispan 则有分布式实现,同样符合 JSR-107。这就引出一个重要问题:你在 Spring Boot 里同时依赖了 spring-boot-starter-cache 和 ehcache,默认得到的 JCacheCacheManager 只是节点本地缓存。当你的应用部署了多个实例,用户请求被负载均衡打到不同节点时,每个节点看到的缓存不共享。

我之前有个血泪教训:用户登录 token 缓存在本地 JCache 里,Redis 负责其他数据,结果用户登录 session 在节点 A 保存后,下一次请求被负载均衡转发到节点 B,直接从缓存中查不到,登录态丢失。后来我采用的方案是:本地 JCache 只保存“不要求全局一致”的热点数据,比如字典表、商品详情;全局共享、强一致要求高的数据放到分布式缓存组件。JCache 里的分布式实现也可以解决这个问题,但需要你认真评估网络序列化、数据一致性带来的复杂度。

5.3 注解失效、自调用、序列化这些老坑一个都别踩

先说注解失效问题。Spring 的@Cacheable基于 AOP 代理实现,如果在一个类内部调用带缓存注解的方法,例如:

public void processUser(Long id) { this.getUserById(id); // 缓存不生效 }

因为调用发生在同一个类内部,对象引用是原始对象而非代理对象,拦截器根本不会执行。解决方式很简单:把方法拆到另一个 Bean,或者在配置里开启 exposeProxy 后使用AopContext.currentProxy()。这个坑我面试时问了很多人,十个有四个没意识到。

然后是序列化问题。Ehcache 等 JCache 实现默认会把 value 变成字节流存储。如果你的 value 对象没有实现Serializable,运行起来会抛NonSerializableException这类错误。这个坑在本地堆缓存时并不明显,因为某些实现拿到的是对象引用;一旦开启磁盘持久化,或者切换到分布式的 JCache 实现,就会立刻暴露。所以写缓存的实体建议都老老实实实现 Serializable,并且定义好 serialVersionUID。

最后是事务一致性问题。Spring 的事务提交通常发生在缓存注解逻辑之后,如果你的业务是:先查数据库,更新数据库,再更新缓存,但你用的是默认事务边界,缓存更新可能发生在数据库事务提交之前。如果数据库事务后来回滚了,缓存里的数据就是脏的。我个人习惯是把缓存更新动作放在事务提交后的@TransactionalEventListener(phase = AFTER_COMMIT)里执行,保证最终一致性。

5.4 缓存预热与冷启动问题

JCache 提供了loadAll方法可以批量加载,但它只负责把数据从 CacheLoader 拉到缓存里,不负责决定哪些是要加载的热门 key。很多人一上来就把全表 loadAll,结果缓存膨胀、内存被打满。

合理的做法是结合业务数据统计,确定热点 key 集合,在应用启动完成之后用一个后台线程慢慢加载,最好还要控制并发加载速度。比如这样:

ExecutorService executor = Executors.newFixedThreadPool(4); ArrayList<Long> hotKeys = analyzeHotKeys(); for (List<Long> batch : Lists.partition(hotKeys, 100)) { executor.submit(() -> cache.loadAll(batch, true, null)); }

6. 面试回答框架:30 秒讲清楚、3 分钟讲深入

回到那道每日一题。面试官问“在 Java EE 或 Spring Boot 环境中,如何集成和启用 JCache?”,答题时要有主次感:

第一步,先一句话定义 JCache。它是 JSR-107 标准的 Java 缓存 API,不是具体缓存产品,而是一套统一的缓存编程接口,核心元素包括 CachingProvider、CacheManager、Cache、ExpiryPolicy、CacheLoader/CacheWriter 等。

第二步,分环境说明集成方式。Java EE 环境中,可以通过 CDI 直接注入 CacheManager 或 Cache,配合@CacheResult、@CacheRemove注解使用;Spring Boot 环境中,引入spring-boot-starter-cache和 JCache 实现(如 Ehcache 3.x),配置spring.cache.type=jcache,指定spring.cache.jcache.provider,加入@EnableCaching后即可使用@Cacheable。

第三步,补充自动发现多 provider 的处理细节。如果 classpath 存在多个 CachingProvider,必须在配置里指定spring.cache.jcache.provider,否则启动报错。

第四步,进入加分项。主动说说CacheLoader与CacheWriter对 read-through / write-through 的支持,CacheEntryListener和 CacheStatistics 在运行时监控中的作用。再提到 Spring Cache 是抽象层、JCache 是标准 API,二者是适配关系,这番话足以让面试官感觉到你不是背题。

最后如果面试官追问“JCache 和 Spring Cache 怎么选”,我的个人倾向是:普通业务接口用 Spring Cache 注解足够,它和事务、SpEL、错误处理结合得更好;如果你在写公共缓存组件、缓存中间件,需要标准 API 层面的可移植性时,直接基于 javax.cache 编程才是正道。这也是 JCache 存在的最大价值。

我在经历了本地缓存到多级缓存、从 Ehcache 2.x 迁到 Ehcache 3.x 的折腾后,最大的体会就是:缓存框架本身从来不是核心难点,难点在于你对数据一致性、过期时间、并发模型的理解。JCache 作为一套标准,最有意义的不是省了几行代码,而是逼着你把上面这些问题按统一的方式思考一遍。面试题刷到这块时,别只背配置,把规范读一遍、把 CacheLoader 玩熟,收获会大得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询