资讯详情

JCache(JSR-107)在Spring Boot与Java EE中的集成与面试实战指南

📅 2026/10/6 14:13:19 | 华诺云谱 👁 阅读
JCache(JSR-107)在Spring Boot与Java EE中的集成与面试实战指南
高级 Java 的面试里JCacheJSR-107属于那种“看着简单、一问就露馅”的题目。很多同学背过 CacheManager、Cacheable 几个名词但面试官一旦追问“在 Java EE 和 Spring Boot 里分别怎么集成、底层怎么选实现、缓存不生效怎么排查”立马就卡壳。这篇就把 JCache 从规范到落地完整梳理一遍重点讲清 Spring Boot 环境下的集成步骤和 Java EE 场景的接入思路。适合正在准备高级 Java 岗位面试的开发者也适合项目里想引入标准缓存 API 但还没动手的工程师。1. 内容整体设计与思路拆解1.1 JCacheJSR-107到底是什么JCache 是 Java 官方的缓存 API 标准JSR-107 是它的规范编号。它定义了一套统一的缓存编程接口让开发者不依赖具体缓存产品就能完成缓存的读写、管理和配置。在没有 JCache 之前Java 世界里的缓存方案是多点开花的Ehcache 有自己的一套 APIHazelcast 有一套Redis 客户端又有一套。每个项目换缓存实现业务代码就要跟着改一遍。JCache 的目标就是终结这种混乱提供一套类似 JDBC 之于数据库的标准接口。JCache 的核心接口都放在javax.cache包下主要包括CachingProvider、CacheManager、Cache、Cache.Entry这几个角色。简单理解CachingProvider是缓存实现厂商的入口CacheManager是缓存的管理容器Cache才是真正存数据的地方。从面试角度你要能说清楚这套分层结构还要知道Caching.getCachingProvider()这个静态方法是整个 JCache 的启动入口。后面所有 API 调用都从它开始。1.2 面试官想通过这道题考察什么这道题表面问“怎么集成启用”实际上在考察三个层面。第一对 JSR-107 规范的理解程度。面试官想确认你分得清“JCache 规范”和“具体实现”的区别知道 JCache 只是一套接口标准真正干活的是 Ehcache、Hazelcast 这些实现厂商。第二对 Spring Cache 抽象层的认识。Spring 框架自己有一套缓存抽象spring-boot-starter-cache可以自动适配多种缓存方案JCache 只是其中一种。你得知道 Spring 是如何通过JCacheCacheManager把javax.cache.CacheManager桥接成 SpringCacheManager的。第三实际落地能力。光会背概念没用面试官会追问缓存不生效怎么排查、缓存穿透怎么处理、多个 CacheManager 冲突怎么办。这些实践问题才是高级工程师和初级开发者的分水岭。搞清楚这三点等于先抓住了题目的骨架后面填细节就有方向了。2. 核心细节解析与实操要点2.1 四个核心接口一个启动入口JCache 的分层结构非常清晰从上到下依次是接口职责类比CachingProvider创建和管理CacheManager是厂商入口工厂CacheManager管理多个Cache的生命周期和配置仓库CacheK, V真正存储键值对提供读写 API货架Cache.EntryK, V缓存中的键值对条目货物启动入口是Caching这个工具类通过Caching.getCachingProvider()获取CachingProvider实例。如果你的 classpath 上有多个 JCache 实现可以传入类名指定Caching.getCachingProvider(org.ehcache.jsr107.EhcacheCachingProvider)。从CachingProvider拿到CacheManager后就能创建Cache了CachingProvider provider Caching.getCachingProvider(); CacheManager cacheManager provider.getCacheManager(); CacheString, User usersCache cacheManager.createCache(users, new MutableConfigurationString, User() .setExpiryPolicyFactory(TouchedExpiryPolicy.factoryOf(Duration.ONE_MINUTE)) .setStoreByValue(true));这段代码创建了一个名为users的缓存过期策略是访问后一分钟过期。MutableConfiguration是 JCache 提供的配置类支持设置过期策略、存储方式、是否启用统计等选项。这里有个关键点getCacheManager()有个重载方法可以传 URI 和 ClassLoader。默认不传其实就是空 URI 加当前线程的 ClassLoader。在高版本下多个CacheManager可能因为 ClassLoader 不同而互相隔离这个细节在排查问题时经常用到。2.2 Cache 的读写操作和并发控制Cache接口的读写操作和Map很像但有几个容易被忽略的差异。首先是get方法cache.get(key)在 key 不存在时返回null。但如果是“缓存了 null 值”的情况你无法区分是 key 不存在还是 value 本来就是 null。所以 JCache 还提供了getOrDefault和containsKey来配合判断。其次是原子操作。多线程环境下if (cache.get(key) null) { cache.put(key, value); }这种方式存在竞态问题。JCache 提供了getAndPut、putIfAbsent、remove等原子方法// 原子地获取旧值并放入新值 User oldUser cache.getAndPut(user_1, newUser); // 只有 key 不存在时才放入 boolean success cache.putIfAbsent(user_1, newUser); // 只有匹配 key 和旧值时才删除 boolean removed cache.remove(user_1, oldUser);这些原子操作在面试中经常被问到尤其putIfAbsent是实现分布式锁和防缓存穿透的常用手段。2.3 注解驱动与生成 key 的规则JCache 本身是编程式 API但在 Java EE 和 Spring 环境中注解式缓存更受欢迎。Spring 缓存注解的核心是Cacheable、CachePut、CacheEvict。Cacheable在方法执行前先查缓存命中则直接返回CachePut无论缓存是否存在都执行方法并把结果写入缓存CacheEvict用来删除缓存条目。key 的生成规则是高频考点。默认使用SimpleKeyGenerator规则是无参数时 key 为SimpleKey.EMPTY单个参数时 key 就是该参数本身多个参数时 key 是由所有参数组成的SimpleKey对象实际开发中往往需要自定义 key可以在注解里通过key属性用 SpEL 表达式指定Cacheable(cacheNames users, key #id) public User getUser(Long id) { return userRepository.findById(id).orElse(null); } Cacheable(cacheNames users, key #user.username) public User getUserByUsername(User user) { return userRepository.findByUsername(user.getUsername()); } Cacheable(cacheNames users, key #root.methodName _ #id) public User getUserWithPrefix(Long id) { // 缓存 key 形如 getUserWithPrefix_1 }自定义 key 可以避免默认 key 生成器带来的类型混淆问题。例如参数是Long类型的1和字符串类型的1默认生成器会生成不同的 key但如果你只存了一份数据就可能导致误判。2.4 Spring Cache 与 JCache 的关系Spring Cache 是 Spring 框架自己的缓存抽象定义了一套独立的Cache和CacheManager接口。JCache 是 JCP 的标准规范两者有重叠但不等同。Spring 通过适配器模式把 JCache 接进自己的体系JCacheCache实现了 Spring 的Cache接口内部包装javax.cache.CacheJCacheCacheManager实现了 Spring 的CacheManager接口内部管理 JCache 的原生缓存。所以Spring Boot 集成 JCache 的本质就是两步把 JCache 的CacheManager交给JCacheCacheManager适配然后通过 Spring 的注解和抽象去操作它。3. 实操过程与核心环节实现3.1 Spring Boot 环境下的依赖与配置先说明一点Spring Boot 并没有为 JCache 单独提供 starter而是整合在spring-boot-starter-cache里。这个 starter 会引入 Spring 的缓存抽象和自动配置能力。在 pom 里加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency !-- JCache API -- dependency groupIdjavax.cache/groupId artifactIdcache-api/artifactId /dependency !-- 选择 JCache 实现这里用 Ehcache 3 -- dependency groupIdorg.ehcache/groupId artifactIdehcache/artifactId /dependencycache-api是 JSR-107 的标准接口。Spring Boot 2.x 默认版本管理里已经包含了它但显式声明依赖能让团队伙伴更清楚项目用到了 JCache。ehcache是 JCache 的具体实现它同时支持 JCache API 和 Ehcache 原生 API。然后在启动类或配置类上加EnableCachingSpringBootApplication EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }最后在application.yml里指定缓存类型spring: cache: type: jcache jcache: config: classpath:ehcache.xmlspring.cache.typejcache告诉 Spring Boot 自动配置使用 JCache 作为缓存方案。config指定 JCache 实现厂商的配置文件位置。3.2 编写 ehcache.xml 配置文件Ehcache 3 的 XML 配置放在src/main/resources/ehcache.xmlconfig xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlnshttp://www.ehcache.org/v3 xsi:schemaLocationhttp://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.10.xsd cache aliasusers key-typejava.lang.Long/key-type value-typecom.example.pojo.User/value-type expiry ttl unitminutes10/ttl /expiry heap unitentries2000/heap /cache cache aliasuserCache key-typejava.lang.String/key-type value-typejava.lang.Object/value-type expiry tti unitminutes30/tti /expiry heap unitentries5000/heap /cache /config这里定义了users和userCache两个缓存区。alias必须和代码里Cacheable(cacheNames users)的 name 完全一致否则 Spring 会在运行时找不到对应缓存。关于ttl和tti的区别ttlTime To Live是创建后固定存活时长不管有没有被访问ttiTime To Idle是空闲超时只要在超时前被访问过就重新计时适合用户会话类数据。3.3 在 Service 层启用 JCache依赖和配置就绪后在业务代码里用注解即可Service public class UserService { Resource private UserRepository userRepository; Cacheable(cacheNames users, key #id) public User getUserById(Long id) { return userRepository.findById(id).orElse(null); } CacheEvict(cacheNames users, key #id) public void deleteUser(Long id) { userRepository.deleteById(id); } CachePut(cacheNames users, key #user.id) public User updateUser(User user) { return userRepository.save(user); } }getUserById被Cacheable修饰后第一次调用会执行方法体并把返回结果存入users缓存第二次用相同 key 调用时直接从缓存返回不再查库。deleteUser用CacheEvict在删除后清除对应缓存条目。updateUser用CachePut在方法执行后强制更新缓存。注意这里不管缓存里原来有没有数据都会重新写入。验证是否生效可以在启动时加上日志或者直接观察数据库查询次数。更简单的方式是在缓存配置里启用统计Configuration public class CacheConfig { Bean public JCacheManagerCustomizer cacheManagerCustomizer() { return cacheManager - { CachingProvider provider Caching.getCachingProvider(); CacheManager jcacheManager provider.getCacheManager(); for (String cacheName : jcacheManager.getCacheNames()) { CacheObject, Object cache jcacheManager.getCache(cacheName); cache.getConfiguration(MutableConfiguration.class); } }; } }JCacheManagerCustomizer是 Spring Boot 提供的 JCache 专属回调接口会在 JCacheCacheManager创建完毕后执行。借助它可以在启动时对 JCache 的CacheManager做二次定制。3.4 Java EE 环境下的集成思路Java EE 环境不像 Spring Boot 那样有自动配置集成 JCache 需要更手动一些。先加依赖dependency groupIdjavax.cache/groupId artifactIdcache-api/artifactId version1.1.1/version /dependency dependency groupIdorg.ehcache/groupId artifactIdehcache/artifactId version3.10.8/version /dependency dependency groupIdorg.jsr107.ri/groupId artifactIdcache-annotations-ri/artifactId version1.1.1/version /dependencycache-annotations-ri是 JSR-107 注解的参考实现提供CacheResult、CacheRemoveAll等拦截器注解。在使用层面JCache 的 CDI 集成主要靠拦截器RequestScoped public class UserService { Inject private UserRepository userRepository; CacheResult(cacheName users) public User findUser(CacheKey Long id, CacheValue User user) { return userRepository.findById(id).orElse(null); } }然后在beans.xml中启用拦截器beans xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_2.xsd bean-discovery-modeannotated interceptors classorg.jsr107.ri.annotations.interceptor.CacheResultInterceptor/class /interceptors /beansJava EE 环境集成 JCache 的核心障碍是缺少 Spring 那样的自动配置。通常需要在应用启动时显式创建CacheManager并注册为应用级单例或者借助 CDI 的Produces方法暴露CacheManagerApplicationScoped public class CacheManagerProducer { Produces ApplicationScoped public CacheManager createCacheManager() { CachingProvider provider Caching.getCachingProvider(); CacheManager cacheManager provider.getCacheManager(); cacheManager.createCache(users, new MutableConfiguration()); return cacheManager; } }如果你用的是 TomEE、WildFly 这类 Java EE 服务器部分服务器版本自带 JCache 实现或提供额外依赖配置。建议在生产环境先确认服务器的模块加载系统避免缓存实现的类加载器冲突。4. 常见问题与排查技巧实录4.1 缓存不生效先查这三点最经典的场景注解加了、依赖配了、缓存就是不生效方法还是每次都执行。第一确认EnableCaching有没有加。没有这个开关所有缓存注解都是摆设。第二确认方法访问权限。Cacheable基于 Spring AOP 代理实现只有public方法才能被代理。如果方法是私有或包内可见代理类无法拦截缓存自然不生效。第三确认是不是自调用。同一个类里this.getUserById()调用getUserById()走的还是原对象没经过代理。解决方式是把调用放到另一个 Bean 里或者通过AopContext.currentProxy()获取代理对象Service public class UserService { public User getUserWithCache(Long id) { // this 调用不会触发缓存 // User user this.getUserById(id); // 改用代理调用 UserService proxy (UserService) AopContext.currentProxy(); return proxy.getUserById(id); } Cacheable(cacheNames users, key #id) public User getUserById(Long id) { return userRepository.findById(id).orElse(null); } }需要在启动类上额外开启EnableAspectJAutoProxy(proxyTargetClass true, exposeProxy true)。4.2 CacheManager 冲突与配置未加载如果你在项目里手动声明了一个CacheManagerBean同时又使用了JCacheManagerCustomizer有时会出现自定义配置不生效的情况。这是因为 Spring Boot 的JCacheCacheConfiguration会优先使用自动配置创建的CacheManager手动 Bean 可能覆盖自动配置也可能被覆盖。推荐的做法是不要自己定义CacheManagerBean除非你有非常特殊的定制需求。单纯改缓存策略用JCacheManagerCustomizer或直接改 XML 配置文件就够了。另外还有一个常见坑spring.cache.jcache.config指定的 XML 路径写错导致启动时静默回退到默认配置。Spring Boot 不会因为 JCache 配置加载失败而直接报错而是可能降级成简单内存缓存。排查时看启动日志里的CacheConfiguration信息确认确实走了jcache类型。4.3 缓存数据一致性与并发问题缓存与数据库的一致性没有银弹但有几条实践原则。如果是更新操作用CachePut和CacheEvict组合优于先删后插。先更新数据库再删缓存避免缓存里保留旧数据。高并发场景要注意缓存击穿某个 key 过期瞬间大量请求同时打到数据库。解决手段包括互斥锁和提前续期。public User getUserByIdWithLock(Long id) { User user cache.get(user_ id); if (user null) { synchronized (lockObj) { user cache.get(user_ id); if (user null) { user userRepository.findById(id).orElse(null); cache.put(user_ id, user); } } } return user; }这里只展示了本地锁方案因为参与面试的话你只要讲清楚“双重检查加锁”的思路即可。分布式场景升级成 Redisson 的分布式锁原理是一致的。4.4 缓存监控与调优的实战建议在生产环境缓存不是配完就完事。我建议至少监控三个指标命中率、缓存条目数、过期淘汰数。JCache 支持开启统计功能MutableConfigurationLong, User config new MutableConfiguration(); config.setStatisticsEnabled(true);开启后可以通过CacheStatistics获取命中次数、未命中次数、平均获取时间等数据。Spring Boot 可以结合 Micrometer 把这些指标暴露给 Prometheus、Graphite 等监控系统。有些工程师会对 Redis 和 JCache 的使用边界感到困惑。我的观点是JCache 更适合本地堆内缓存数据量小、访问频繁、允许按进程隔离的场景例如字典数据、用户基础信息、权限配置。多个应用实例需要共享缓存时优先考虑 Redis 这类集中式缓存。最后要说的花了不少篇幅把 JCache 在 Java EE 和 Spring Boot 里的集成讲了一遍。实际开发中我见过太多团队在 Spring Boot 里上来就用 Redis忽略了 JCache 这类本地缓存的价值。一个小型单体服务热点数据命中的情况下本地堆缓存能把响应耗时从几十毫秒压到 1 毫秒以内而引入一套 Redis 带来的运维成本和复杂度往往是成倍的。面试时被问到 JCache不需要把整个规范倒背如流但要能讲清楚 JCache 解决了什么问题、它在 Spring Boot 里的自动配置如何生效、缓存不生效时怎么排查。能把这三件事讲明白这道题基本就算过关了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑