Spring Framework 7.0.9 深度剖析-18-缓存抽象
18.1 本章导读
几乎每个 Spring 应用都会用到缓存:查询方法前先查缓存、写操作后清缓存。Spring Framework 从 3.1 开始提供了一套注解驱动的缓存抽象,到 7.0.9 已经演化为一套相当成熟的体系——它与事务抽象(@Transactional)在设计思想上高度同构:都是声明式注解 + AOP 代理 + 拦截器编排,都把"基础设施"和"业务代码"解耦。本章要解决的核心问题是:当你写下一个 @Cacheable 注解,Spring 到底在背后做了什么?
本章所有源码均取自仓库 spring-framework-7.0.9(版本 7.0.9),缓存抽象的主体集中在 spring-context 模块的 org.springframework.cache 包及其四个子包:
org.springframework.cache:顶层 SPI——Cache、CacheManager两个接口;org.springframework.cache.annotation:注解与装配——@EnableCaching、CachingConfigurationSelector、ProxyCachingConfiguration、SpringCacheAnnotationParser、AnnotationCacheOperationSource;org.springframework.cache.interceptor:执行引擎——CacheInterceptor、CacheAspectSupport、AbstractCacheInvoker、KeyGenerator、CacheOperationExpressionEvaluator等;org.springframework.cache.concurrent:开箱即用的ConcurrentMapCache与ConcurrentMapCacheManager;org.springframework.cache.support:支撑类——AbstractValueAdaptingCache、NullValue、SimpleValueWrapper、AbstractCacheManager、NoOpCache等。
读者需要的前置知识:理解 Spring AOP 代理(JDK 动态代理与 CGLIB)、MethodInterceptor 拦截器链、以及 SpEL(Spring Expression Language)的基本用法。读完本章后,你应当能够:说清一次 @Cacheable 方法调用从代理对象到方法返回的完整调用栈;解释 @Cacheable、@CachePut、@CacheEvict 在同一方法上组合时的执行顺序与设计原因;精确说出 sync=true 的四条限制分别在源码哪里校验;定位"缓存不生效"类问题是出在代理、pointcut 匹配、注解解析还是执行编排的哪一环。
本章不编造第三方缓存实现(Redis、Caffeine 等)的内部细节,只讲 Spring 抽象层的 SPI 契约与默认实现;讲到扩展时也只落到接口方法层面。
18.2 核心概念
缓存抽象由一组"接口 + 默认实现 + 注解驱动的执行引擎"构成。先用一张关系图建立全局印象:
应用方法调用
│
▼
代理对象(JDK/CGLIB)
│ AOP 拦截
▼
BeanFactoryCacheOperationSourceAdvisor(advisor,内含 pointcut)
│ advice =
▼
CacheInterceptor ──extends──> CacheAspectSupport ──extends──> AbstractCacheInvoker
│ │
│ ├─ CacheOperationSource(注解→操作元数据)
│ ├─ KeyGenerator(key 生成)
│ ├─ CacheOperationExpressionEvaluator(SpEL)
│ └─ CacheErrorHandler(失败处理)
▼
CacheManager ──管理──> Cache ──底层存储──> ConcurrentHashMap / Redis / ...
下面逐个概念给出它的源码位置与一句话职责。
Cache(spring-context/src/main/java/org/springframework/cache/Cache.java):缓存的顶层抽象接口,代表一个具名的缓存区域(region)。它定义了 getName()、getNativeCache()、get/put/evict/clear 等操作。注意它是 SPI——主要给框架的注解模型用,也可以在应用里直接编程式使用。
Cache.ValueWrapper(Cache.java 的嵌套接口):get(Object) 的返回包装。引入它的根本原因是要区分两种情况:"缓存里没有这个 key"(返回 null)和"缓存里存的值就是 null"(返回一个 ValueWrapper,其 get() 再返回 null)。
CacheManager(org/springframework/cache/CacheManager.java):缓存管理器 SPI,按名字获取 Cache 实例:getCache(String name) 与 getCacheNames()。它本身不存数据,只负责"按名找到/创建缓存区域"。
CacheOperation(interceptor/CacheOperation.java)及其子类族:注解解析后的运行时表示。@Cacheable → CacheableOperation,@CachePut → CachePutOperation,@CacheEvict → CacheEvictOperation。它们是不可变元数据对象,由 Builder 模式构造。
CacheOperationSource(interceptor/CacheOperationSource.java):给定 Method 和目标类,返回该方法上的 CacheOperation 集合。AnnotationCacheOperationSource 是注解版实现,内部带方法级缓存。
KeyGenerator(interceptor/KeyGenerator.java):函数式接口,根据目标对象、方法、参数生成缓存 key。默认实现是 SimpleKeyGenerator。
CacheResolver(interceptor/CacheResolver.java):把"操作上声明的缓存名字符串列表"解析成实际的 Cache 实例集合。默认 SimpleCacheResolver 委托给 CacheManager 按名查找。
CacheErrorHandler(interceptor/CacheErrorHandler.java):缓存读写失败时的错误处理策略。默认 SimpleCacheErrorHandler 直接把异常抛回调用方;LoggingCacheErrorHandler 只记日志、让业务继续跑。
CacheInterceptor / CacheAspectSupport(interceptor/):执行引擎。CacheInterceptor 只是薄薄的 AOP Alliance 适配器,真正的编排逻辑全在父类 CacheAspectSupport.execute(...)。
这组概念之间是标准的策略模式关系:CacheOperationSource 决定"这个方法有哪些缓存操作",KeyGenerator 决定"key 是什么",CacheResolver 决定"操作落在哪些缓存区域上",CacheErrorHandler 决定"出错了怎么办"。这些策略都可以通过 CachingConfigurer 全局覆盖,也可以通过注解属性逐方法覆盖。
18.3 源码剖析
18.3.1 Cache 接口:读、写、删、原子加载四类操作
先看抽象的根。Cache 接口声明在 spring-context/src/main/java/org/springframework/cache/Cache.java,从 3.1 沿用至今。它的 Javadoc 第一句话就点明了定位:"Serves primarily as an SPI for Spring's annotation-based caching model ... and secondarily as an API for direct usage in applications"——它首先是给注解模型用的 SPI,其次才是你可以在代码里直接操作的 API。
最基本的两个标识方法:
public interface Cache {
/** Return the cache name. */
String getName();
/** Return the underlying native cache provider. */
Object getNativeCache();
getName() 返回缓存区域名(对应 @Cacheable("users") 里的 users);getNativeCache() 返回底层原生缓存对象——对 ConcurrentMapCache 就是那个 ConcurrentHashMap,对第三方实现就是它包装的 Redis client / Caffeine 实例。这个逃生口让你在抽象层之外仍能触达原生能力。
读操作有三个重载,签名必须逐字看,因为它们的语义差异是设计的精髓:
@Nullable ValueWrapper get(Object key);
<T> @Nullable T get(Object key, @Nullable Class<T> type);
<T> @Nullable T get(Object key, Callable<T> valueLoader);
第一个 get(Object) 的 Javadoc 写得非常精确:返回 null 表示"缓存里没有这个 key";否则,缓存到的值(它本身可能就是 null)会包在 ValueWrapper 里返回。也就是说——裸 null 与"值为 null 的 ValueWrapper"是两种完全不同的结果。为什么要这么设计?因为底层的 JDK ConcurrentHashMap 不允许存 null value,框架必须用一个内部哨兵对象区分"未命中"与"命中了 null",否则缓存穿透场景(一个查询永远返回 null 的对象)会被放大成每次都打数据库。
第二个 get(Object, Class<T>) 放弃了这种区分能力:Javadoc 明说"This variant of get does not allow for differentiating between a cached null value and no cache entry found at all",命中但类型不符时抛 IllegalStateException。它是给"我确定我要什么类型"的编程式场景用的。
第三个 get(Object, Callable<T>) 是 4.3 引入的原子加载语义:
/**
* Return the value to which this cache maps the specified key, obtaining
* that value from {@code valueLoader} if necessary. This method provides
* a simple substitute for the conventional "if cached, return; otherwise
* create, cache and return" pattern.
* <p>If possible, implementations should ensure that the loading operation
* is synchronized so that the specified {@code valueLoader} is only called
* once in case of concurrent access on the same key.
* <p>If the {@code valueLoader} throws an exception, it is wrapped in
* a {@link ValueRetrievalException}
*/
<T> @Nullable T get(Object key, Callable<T> valueLoader);
注意"If possible, implementations should ensure ... only called once"——接口没有强制加锁,但强烈建议实现保证同一 key 并发下 loader 只被执行一次。这正是 @Cacheable(sync=true) 依赖的语义:Spring 拦截器最终把"执行方法并写入缓存"的动作交给这个方法,由缓存自身的原子性避免缓存击穿(同一热点 key 被并发请求同时回源)。loader 抛异常时包装成 ValueRetrievalException,这是 Cache 的嵌套异常类。
写与删:
void put(Object key, @Nullable Object value);
default @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) { ... }
void evict(Object key);
default boolean evictIfPresent(Object key) { evict(key); return false; }
void clear();
default boolean invalidate() { clear(); return false; }
这里有一组 5.2 引入的、容易被忽略的对偶方法:evict/evictIfPresent、clear/invalidate。普通版本允许"延迟/异步生效"(Javadoc 举例:事务性缓存装饰器里,evict 可能要等事务提交才真正可见);evictIfPresent/invalidate 则要求"立即可见",并返回一个布尔值表示"之前是否真的存在条目"。默认实现是降级调用普通版本并返回 false——框架层(AbstractCacheInvoker.doEvict/doClear)会根据 beforeInvocation 选择调哪一对,我们后面会看到。
6.1 还加了两个响应式入口(默认实现直接抛 UnsupportedOperationException):
default @Nullable CompletableFuture<?> retrieve(Object key) {
throw new UnsupportedOperationException(
getClass().getName() + " does not support CompletableFuture-based retrieval");
}
default <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
throw new UnsupportedOperationException(
getClass().getName() + " does not support CompletableFuture-based retrieval");
}
这是为返回 CompletableFuture 的异步服务方法准备的:缓存读不能阻塞调用线程。ConcurrentMapCache 提供了 best-effort 的实现(基于 ForkJoinPool.commonPool()),后面读它的源码时会对照。
嵌套接口 ValueWrapper 极其简单:
@FunctionalInterface
interface ValueWrapper {
@Nullable Object get();
}
一个函数式接口,只有 get() 一个方法。它的全部意义就是上面说的"区分未命中与 null 值"。
18.3.2 CacheManager:按名取缓存
CacheManager(spring-context/src/main/java/org/springframework/cache/CacheManager.java)只有两个抽象方法加一个 7.0.2 新增的默认方法:
public interface CacheManager {
@Nullable Cache getCache(String name);
Collection<String> getCacheNames();
/** @since 7.0.2 */
default void resetCaches() {
for (String cacheName : getCacheNames()) {
Cache cache = getCache(cacheName);
if (cache != null) {
cache.clear();
}
}
}
}
getCache 的 Javadoc 有一句关键话:"the cache may be lazily created at runtime if the native provider supports it"——名字查不到时,管理器可以现场创建一个缓存区域再返回。@Nullable 意味着也可能返回 null("不存在且不能创建")。这个 nullable 是有下游后果的:AbstractCacheResolver.resolveCaches 在拿到 null 时会直接抛 IllegalArgumentException,我们后面会看到。
resetCaches() 是 7.0.2 的新默认方法:清空所有已注册缓存的内容(注意默认实现是逐个 clear(),保留缓存区域本身)。它在 7.0.9 的两个具体实现里都被覆盖了——ConcurrentMapCacheManager.resetCaches()(6.2.14 引入)在 dynamic 模式下会直接 cacheMap.clear() 把区域也丢掉;AbstractCacheManager.resetCaches() 则包了一层同步块。这个"接口给默认行为、子类按场景覆盖"的设计值得注意:resetCaches 语义本身在演进中(从"清空内容"扩展到"连区域一起重建"),默认方法保证了老实现不必改动也能编译通过。
18.3.3 ConcurrentMapCacheManager:动态创建与静态预注册
接口看完,来看 Spring 自带的最简实现。ConcurrentMapCacheManager(spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java)是一个"不讲究但能用"的管理器,Javadoc 自己都承认:"This is by no means a sophisticated CacheManager; it comes with no cache configuration options"——它适合测试和简单场景,生产环境想要本地缓存该上 Caffeine。
它的字段设计直接说明了它的工作方式:
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
private volatile boolean dynamic = true;
private boolean allowNullValues = true;
private boolean storeByValue = false;
private @Nullable SerializationDelegate serialization;
cacheMap:ConcurrentHashMap,key 是缓存名,value 是Cache实例。这就是"所有缓存区域的注册表"。dynamic:volatile 布尔值,默认true。true = 动态模式(运行时按需创建新缓存区域);false = 静态模式(只认预先注册过的名字)。allowNullValues:默认 true,即允许缓存 null 值。storeByValue/serialization:是否按值拷贝存储(开启后缓存值必须可序列化,写入前先做一次序列化-反序列化,防止外部修改缓存对象)。
核心方法 getCache 只有五行:
@Override
public @Nullable Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache);
}
return cache;
}
逐段读:先快路径查 cacheMap;没查到且处于动态模式时,用 ConcurrentMap.computeIfAbsent 原子地创建并登记——方法引用 this::createConcurrentMapCache 作为创建函数。computeIfAbsent 的语义保证了并发下同一名字只创建一次。而在静态模式下(dynamic=false),查不到就返回 null,绝不会现场造一个——下游 AbstractCacheResolver 拿到 null 就会抛异常,这就是"缓存名字写错时应用启动后第一次调用才报错"的根源。
静态模式怎么进入?看 setCacheNames:
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
for (String name : cacheNames) {
this.cacheMap.put(name, createConcurrentMapCache(name));
}
this.dynamic = false;
}
else {
this.dynamic = true;
}
}
传入非空集合 = 立即为每个名字建好 ConcurrentMapCache 放进 cacheMap,然后把 dynamic 翻成 false;传 null = 恢复动态模式。注意 Javadoc 里 7.0.9 的一句注释:这个方法"replaces existing caches of the given names ... but does not remove unrelated existing caches. For a full reset, consider calling resetCaches() before"——它只替换同名缓存,不清掉无关的旧缓存。
createConcurrentMapCache 是给子类留的扩展点:
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256), isAllowNullValues(), actualSerialization);
}
每个缓存区域自带一个 new ConcurrentHashMap<>(256),allowNullValues 与序列化委托按当前配置传入。
另外两个 6.x 后加的方法值得一提:
/** @since 6.2.14 */
@Override
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.clear();
}
}
/** @since 6.1.15 */
public void removeCache(String name) {
this.cacheMap.remove(name);
}
resetCaches:先把每个缓存的内容清空;动态模式下干脆把 cacheMap 整体清掉(缓存区域销毁,下次用到再建)——这正好对应 CacheManager.resetCaches() Javadoc 里说的两种语义("drop all caches" vs "keep regions but empty")。
18.3.4 ConcurrentMapCache:get/put/evict/clear 的逐行实现
ConcurrentMapCache(spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java)继承 AbstractValueAdaptingCache,内部就是一个 ConcurrentMap:
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
private final ConcurrentMap<Object, Object> store;
private final @Nullable SerializationDelegate serialization;
构造函数链最终汇聚到:
protected ConcurrentMapCache(String name, ConcurrentMap<Object> store,
boolean allowNullValues, @Nullable SerializationDelegate serialization) {
super(allowNullValues);
Assert.notNull(name, "Name must not be null");
Assert.notNull(store, "Store must not be null");
this.name = name;
this.store = store;
this.serialization = serialization;
}
allowNullValues 传给父类;store 就是底层 map;serialization 非 null 即开启按值拷贝。
最有意思的是原子加载版本的 get:
@SuppressWarnings("unchecked")
@Override
public <T> @Nullable T get(Object key, Callable<T> valueLoader) {
return (T) fromStoreValue(this.store.computeIfAbsent(key, k -> {
try {
return toStoreValue(valueLoader.call());
}
catch (Throwable ex) {
throw new ValueRetrievalException(key, valueLoader, ex);
}
}));
}
这就是 @Cacheable(sync=true) 的最终落点。ConcurrentHashMap.computeIfAbsent 本身的语义是:该 key 的映射不存在时,用给定函数算出一个值并原子写入——在 ConcurrentHashMap 里,这个 compute 对同一个 key 是加锁的(bin-level locking),所以同一热点 key 并发下 loader 只会跑一次,这正是接口 Javadoc 要求的"synchronized so that the valueLoader is only called once"。loader 的返回值先经 toStoreValue 转换成存储格式(处理 null 哨兵、处理序列化),从 map 取出后再经 fromStoreValue 还原成用户值。loader 抛任何 Throwable 都被捕获包成 ValueRetrievalException——注意它是在 lambda 里抛出的,会从 computeIfAbsent 里透传出来。
其余方法简单但要注意细节:
@Override
public void put(Object key, @Nullable Object value) {
this.store.put(key, toStoreValue(value));
}
@Override
public @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
Object existing = this.store.putIfAbsent(key, toStoreValue(value));
return toValueWrapper(existing);
}
@Override
public void evict(Object key) {
this.store.remove(key);
}
@Override
public boolean evictIfPresent(Object key) {
return (this.store.remove(key) != null);
}
@Override
public void clear() {
this.store.clear();
}
@Override
public boolean invalidate() {
boolean notEmpty = !this.store.isEmpty();
this.store.clear();
return notEmpty;
}
逐段看:
put:写入前先toStoreValue——用户传 null 时存的是哨兵(下一小节展开)。putIfAbsent:用底层 map 原生的原子putIfAbsent,再把"旧值"(可能为 null 表示之前没条目)用toValueWrapper包成ValueWrapper。这比Cache接口的默认实现(先 get 再 put 两步)更优,是真正的原子操作。evict就是 remove;evictIfPresent则利用remove(key)的返回值判断"之前是否存在条目"——这就是接口默认方法做不到、必须子类覆盖的那个"可靠 prior presence 判断"。clear全清;invalidate先记!isEmpty()再清,返回"之前是否非空"。- 抽象的
lookup只有一行:return this.store.get(key);——它是AbstractValueAdaptingCache.get(Object)的钩子,父类拿到原始存储值后负责 null 还原与包装。
两个 retrieve 响应式方法也贴一下,它们展示了"内存缓存怎么做异步":
@Override
public @Nullable CompletableFuture<?> retrieve(Object key) {
Object value = lookup(key);
return (value != null ? CompletableFuture.completedFuture(
isAllowNullValues() ? toValueWrapper(value) : fromStoreValue(value)) : null);
}
@SuppressWarnings("unchecked")
@Override
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
return CompletableFuture.supplyAsync(() ->
(T) fromStoreValue(this.store.computeIfAbsent(key, k -> toStoreValue(valueLoader.get().join()))));
}
同步路径上:内存查得到就用 completedFuture 立刻完成(命中),查不到直接返回 null("immediately determined miss",对应 Cache.retrieve Javadoc 里说的两种 miss 语义);加载路径则把 loader 的 future join 进 computeIfAbsent,再包一层 supplyAsync 丢到 common pool——因为本地 map 本身没有非阻塞 API,所谓"异步"只是把同步操作挪出调用线程而已。
18.3.5 null 值的秘密:AbstractValueAdaptingCache 与 NullValue
前面反复提到"用户 null → 存储哨兵",现在看这个适配层。AbstractValueAdaptingCache(spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java)的两个钩子是全部秘密所在:
protected Object toStoreValue(@Nullable Object userValue) {
if (userValue == null) {
if (this.allowNullValues) {
return NullValue.INSTANCE;
}
throw new IllegalArgumentException(
"Cache '" + getName() + "' is configured to not allow null values but null was provided");
}
return userValue;
}
protected @Nullable Object fromStoreValue(@Nullable Object storeValue) {
if (this.allowNullValues && storeValue instanceof NullValue) {
return null;
}
return storeValue;
}
写入时:用户值是 null 且 allowNullValues=true → 换成 NullValue.INSTANCE 这个单例哨兵存进 map;如果 allowNullValues=false → 直接抛 IllegalArgumentException。读出时:存的是哨兵且开了允许 null → 还原成 null。一来一回,ConcurrentHashMap 从头到尾没碰过真正的 null value,但用户感知上"方法返回 null 被缓存住了"。
再看 get(Object) 在父类里的模板实现:
@Override
public @Nullable ValueWrapper get(Object key) {
return toValueWrapper(lookup(key));
}
protected Cache.@Nullable ValueWrapper toValueWrapper(@Nullable Object storeValue) {
return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
}
这就闭环了:map 里没条目 → lookup 返回 null → toValueWrapper(null) 返回 null(未命中);map 里有条目(哪怕值是哨兵)→ 包成 SimpleValueWrapper(命中,.get() 内部再经 fromStoreValue 还原成 null)。接口 Javadoc 说的两种结果,在这两行代码里精确落地。
哨兵 NullValue(support/NullValue.java)的实现也值得逐行看:
public final class NullValue implements Serializable {
public static final Object INSTANCE = new NullValue();
private static final long serialVersionUID = 1L;
private NullValue() {
}
private Object readResolve() {
return INSTANCE;
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || other == null);
}
@Override
public int hashCode() {
return NullValue.class.hashCode();
}
@Override
public String toString() {
return "null";
}
}
四个设计点:
- 构造器私有 +
public static final INSTANCE:全 JVM 只有一个哨兵实例,==即可判等; readResolve()返回INSTANCE:序列化反序列化后仍然是同一个单例,不会冒出第二个对象;equals(null)返回 true:哨兵 equals 任何 null 都成立——这是为了配合某些底层存储(如老版本缓存)在反序列化/比较时把它当 null 对待;toString()返回"null":日志里看到它就是 null 的样子,排查时不会困惑。
而 SimpleValueWrapper(support/SimpleValueWrapper.java)就是个朴素的不可变持有器:构造时收下 value,get() 原样返回,equals/hashCode 委托给内部值。没有任何技巧。
18.3.6 装配链:从 @EnableCaching 到三个基础设施 Bean
缓存抽象的启用入口是 @EnableCaching。它本身是一个"开关注解",全部魔力在元注解 @Import 上:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
逐属性:
proxyTargetClass:默认 false,即默认走 JDK 接口代理;只有mode=PROXY时生效。Javadoc 特别提醒:设为 true 会影响所有需要代理的 bean(包括@Transactional的),而不仅是缓存方法。mode:默认AdviceMode.PROXY——基于 Spring AOP 代理织入;另一个值ASPECTJ走 AspectJ 字节码织入(需要spring-aspects模块,此时自调用也能被拦截)。order:缓存 advisor 的优先级,默认最低优先级——意味着默认情况下缓存通知排在事务通知外层之后、内层之前?实际顺序由order决定,默认最低优先级 = 最后执行(最内层),这就是为什么"缓存写在事务提交之前"——后面专门讲。
@Import(CachingConfigurationSelector.class) 把选择器拉进容器。CachingConfigurationSelector(annotation/CachingConfigurationSelector.java)继承 AdviceModeImportSelector<EnableCaching>,职责是根据 mode 值决定装配哪套配置类:
public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
private static final String PROXY_JCACHE_CONFIGURATION_CLASS =
"org.springframework.cache.jcache.config.ProxyJCacheConfiguration";
private static final String CACHE_ASPECT_CONFIGURATION_CLASS_NAME =
"org.springframework.cache.aspectj.AspectJCachingConfiguration";
private static final String JCACHE_ASPECT_CONFIGURATION_CLASS_NAME =
"org.springframework.cache.aspectj.AspectJJCacheConfiguration";
private static final boolean JSR_107_PRESENT;
private static final boolean JCACHE_IMPL_PRESENT;
static {
ClassLoader classLoader = CachingConfigurationSelector.class.getClassLoader();
JSR_107_PRESENT = ClassUtils.isPresent("javax.cache.Cache", classLoader);
JCACHE_IMPL_PRESENT = ClassUtils.isPresent(PROXY_JCACHE_CONFIGURATION_CLASS, classLoader);
}
@Override
public String[] selectImports(AdviceMode adviceMode) {
return switch (adviceMode) {
case PROXY -> getProxyImports();
case ASPECTJ -> getAspectJImports();
};
}
private String[] getProxyImports() {
List<String> result = new ArrayList<>(3);
result.add(AutoProxyRegistrar.class.getName());
result.add(ProxyCachingConfiguration.class.getName());
if (JSR_107_PRESENT && JCACHE_IMPL_PRESENT) {
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return StringUtils.toStringArray(result);
}
逐段读:
- 静态块做了两次类存在性探测:classpath 上有没有 JSR-107(
javax.cache.Cache,注意 7.0.9 里探测的仍是旧包名javax.cache而非jakarta.cache)、有没有 Spring 自带的ProxyJCacheConfiguration。这是典型的"可选依赖软探测"——缓存模块不强制 JCache,存在才装。 selectImports是 switch 表达式(7.0.9 用的是现代 Java 语法),PROXY/ASPECTJ 两条路。- PROXY 路径导入三个东西:
AutoProxyRegistrar(给容器注册基础设施自动代理创建器)、ProxyCachingConfiguration(缓存专用配置)、以及——如果 JSR-107 都在——JCache 的配置。注意ProxyJCacheConfiguration这个类名在本仓库里是字符串常量引用的,它不在spring-context的缓存包下,属于可选模块。 - ASPECTJ 路径则导入
AspectJCachingConfiguration(同样以全限定名字符串给出,类在spring-aspects模块,本模块里只是引用名)。
真正注册 bean 的是 ProxyCachingConfiguration(annotation/ProxyCachingConfiguration.java)。它只有三个 @Bean 方法,每个都标了 @Role(ROLE_INFRASTRUCTURE)——基础设施角色,应用代码不该碰:
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor(
CacheOperationSource cacheOperationSource, CacheInterceptor cacheInterceptor) {
BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
advisor.setCacheOperationSource(cacheOperationSource);
advisor.setAdvice(cacheInterceptor);
if (this.enableCaching != null) {
advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
}
return advisor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheOperationSource cacheOperationSource() {
// Accept protected @Cacheable etc methods on CGLIB proxies, as of 6.0.
return new AnnotationCacheOperationSource(false);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) {
CacheInterceptor interceptor = new CacheInterceptor();
interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
interceptor.setCacheOperationSource(cacheOperationSource);
return interceptor;
}
}
三个 bean 各司其职:
cacheAdvisor:bean 名是CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME,查常量类(config/CacheManagementConfigUtils.java)可知字面值是"org.springframework.cache.config.internalCacheAdvisor"。它是一个BeanFactoryCacheOperationSourceAdvisor,内部挂好CacheOperationSource(决定谁被代理)和CacheInterceptor(advice,真正干活的),并把@EnableCaching(order=...)的值设为 advisor 优先级。cacheOperationSource:注意构造参数是new AnnotationCacheOperationSource(false)——注释明确写着"Accept protected @Cacheable etc methods on CGLIB proxies, as of 6.0"。也就是说 6.0 之后,CGLIB 代理下 protected 方法上的缓存注解也会被识别(参数 false = 不要求必须 public)。这一点在"常见问题"一节还会回到。cacheInterceptor:new 出拦截器后先configure(...)注入四个策略(errorHandler、keyGenerator、cacheResolver、cacheManager——全部是Supplier,懒解析),再挂上 operationSource。
它的父类 AbstractCachingConfiguration(annotation/AbstractCachingConfiguration.java)负责两件事:接住 @EnableCaching 的属性、以及对接 CachingConfigurer。看关键两段:
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.enableCaching = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableCaching.class.getName()));
if (this.enableCaching == null) {
throw new IllegalArgumentException(
"@EnableCaching is not present on importing class " + importMetadata.getClassName());
}
}
@Autowired
void setConfigurers(ObjectProvider<CachingConfigurer> configurers) {
Supplier<@Nullable CachingConfigurer> configurer = () -> {
List<CachingConfigurer> candidates = configurers.stream().toList();
if (CollectionUtils.isEmpty(candidates)) {
return null;
}
if (candidates.size() > 1) {
throw new IllegalStateException(candidates.size() + " implementations of " +
"CachingConfigurer were found when only 1 was expected. " +
"Refactor the configuration such that CachingConfigurer is " +
"implemented only once or not at all.");
}
return candidates.get(0);
};
useCachingConfigurer(new CachingConfigurerSupplier(configurer));
}
setImportMetadata 来自 ImportAware 接口——选择器把 @EnableCaching 的注解属性(mode/proxyTargetClass/order)读出来存进 enableCaching,供上面 cacheAdvisor 方法取 order 用。setConfigurers 是 @Autowired 注入:容器里搜 CachingConfigurer 类型的 bean,最多只允许一个——找到两个直接抛 IllegalStateException,要求你重构配置。这就是为什么 CachingConfigurer 被设计成"全有或全无":它是全局定制入口,两个实现会让框架无所适从。
useCachingConfigurer 把四个回调(cacheManager/cacheResolver/keyGenerator/errorHandler)包成四个 Supplier 存下来,延迟到真正需要时才解析:
protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) {
this.cacheManager = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheManager);
this.cacheResolver = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheResolver);
this.keyGenerator = cachingConfigurerSupplier.adapt(CachingConfigurer::keyGenerator);
this.errorHandler = cachingConfigurerSupplier.adapt(CachingConfigurer::errorHandler);
}
CachingConfigurerSupplier.adapt 的逻辑是:configurer 存在才调对应方法,不存在就给 null——这套 Supplier 链一路传到 CacheAspectSupport.configure(),最终决定每个策略的"用户定制值 or 默认值"。
18.3.7 advisor 与 pointcut:哪些 bean 会被织入代理
BeanFactoryCacheOperationSourceAdvisor(interceptor/BeanFactoryCacheOperationSourceAdvisor.java)本体极薄:
public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut();
public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
this.pointcut.setCacheOperationSource(cacheOperationSource);
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
}
它继承 AbstractBeanFactoryPointcutAdvisor,advice 就是前面注册的 CacheInterceptor(ProxyCachingConfiguration.cacheAdvisor 里 advisor.setAdvice(cacheInterceptor)),pointcut 是内部那个 CacheOperationSourcePointcut。
匹配逻辑在 CacheOperationSourcePointcut(interceptor/CacheOperationSourcePointcut.java):
final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {
private @Nullable CacheOperationSource cacheOperationSource;
@Override
public boolean matches(Method method, Class<?> targetClass) {
return (this.cacheOperationSource == null ||
this.cacheOperationSource.hasCacheOperations(method, targetClass));
}
方法级匹配就一句话:这个方法上有没有缓存操作?有就代理。它继承 StaticMethodMatcherPointcut——意味着匹配结果只取决于方法与目标类,与调用参数无关(静态匹配),代理创建时算一次即可,不会每次调用都重新判定。
类级过滤器多一层保护:
private final class CacheOperationSourceClassFilter implements ClassFilter {
@Override
public boolean matches(Class<?> clazz) {
if (CacheManager.class.isAssignableFrom(clazz)) {
return false;
}
return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz));
}
两条规则:实现了 CacheManager 接口的类永不被缓存代理(避免递归);其余类先问 isCandidateClass——这个方法我们后面在 SpringCacheAnnotationParser 里会看到,它会快速排除"类上根本没有任何缓存注解"的类,避免对每个 bean 都做昂贵的全方法扫描。
至此,装配链闭环:@EnableCaching → 选择器 → 三个基础设施 bean(advisor / operationSource / interceptor)→ 自动代理创建器给"方法上有缓存操作的 bean"织入代理。运行时的一切,都从 CacheInterceptor.invoke 开始。
18.3.8 CacheInterceptor.invoke:AOP 边界与异常包装
运行时调用的第一站是 CacheInterceptor(interceptor/CacheInterceptor.java)。它本身只有一个 invoke 方法,全部价值在那个 aopAllianceInvoker 上:
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
@Override
public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
Object target = invocation.getThis();
Assert.state(target != null, "Target must not be null");
try {
return execute(aopAllianceInvoker, target, method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
}
逐段读:
- 把"继续调用链、最终执行目标方法"这件事,包装成一个
CacheOperationInvokerlambda。为什么要包一层?因为invocation.proceed()声明抛出Throwable,而缓存编排逻辑(CacheAspectSupport里大量方法)不想被throws Throwable污染。于是把受检异常包进CacheOperationInvoker.ThrowableWrapper这个运行时异常里,编排层内部一路当 RuntimeException 传递,最后在 invoke 的最外层解包——catch (ThrowableWrapper th) { throw th.getOriginal(); }把原始异常原样抛回调用方。 target来自invocation.getThis()——即代理背后的真实对象。Assert.state(target != null)是防御性检查。- 然后调用父类
execute(invoker, target, method, args)。所有缓存逻辑都在那里。
记住这个 ThrowableWrapper 协议:业务方法抛出的任何异常,都先被包成 RuntimeException 穿过整个缓存编排,最后再拆包还原。这对理解"异常时缓存到底写不写、evict 到底执不执行"至关重要。
18.3.9 execute 的总编排:七个步骤
CacheAspectSupport(interceptor/CacheAspectSupport.java)是全章最重要的类。先看它的入口 execute(invoker, target, method, args):
protected @Nullable Object execute(CacheOperationInvoker invoker, Object target, Method method, @Nullable Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {
return execute(invoker, method,
new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
}
return invokeOperation(invoker);
}
逐段:
initialized开关:只有走完afterSingletonsInstantiated(全部单例就绪后成功拿到 CacheManager)才置 true。没初始化好就直接放行方法——注释说这是"to cope with cases where the AJ is pulled in automatically":基础设施虽然被注册了但还没配好时,缓存静默降级为不缓存,而不是报错。ultimateTargetClass:穿透代理拿到最终目标类(如果是代理链,剥到最里层)。注解匹配要对着真实类做。- 查操作元数据:
cacheOperationSource.getCacheOperations(method, targetClass)返回这方法上所有CacheOperation(后面注解解析一节讲它怎么来的)。为空 = 这方法没有缓存语义,直接invokeOperation(invoker)放行。 - 有操作 → 包成
CacheOperationContexts(一次性把每个 operation 对应的运行上下文都建好),进私有的execute主流程。
私有主流程只有十六行,但浓缩了全部顺序逻辑:
private @Nullable Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
if (contexts.isSynchronized()) {
// Special handling of synchronized invocation
return executeSynchronized(invoker, method, contexts);
}
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached value matching the conditions
Object cacheHit = findCachedValue(invoker, method, contexts);
if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) {
return evaluate(cacheHit, invoker, method, contexts);
}
return cacheHit;
}
这就是本章的"心脏"。按源码顺序把非同步路径的步骤列出来:
- 同步旁路:
contexts.isSynchronized()为真(即存在sync=true的@Cacheable)→ 整个流程切到executeSynchronized,不走后面的多操作编排。sync 的限制我们后面单独讲。 - 前置 eviction:
processCacheEvicts(evictOperations, beforeInvocation=true, NO_RESULT)——先执行所有@CacheEvict(beforeInvocation=true)的清理。此时方法还没执行,result 还是哨兵NO_RESULT。 - 命中检查:
findCachedValue(...)遍历所有@Cacheable上下文,看哪个缓存里已经有值。 - 分支:
cacheHit为 null(未命中)或是ValueWrapper(命中但包装着,可能要再经过 unless/put 逻辑)→ 进evaluate;其他情况(比如命中的是一个已经解好的值)→ 直接返回。
注意步骤 2 在步骤 3 之前——这就是"@CacheEvict(beforeInvocation=true) 先清缓存、再查缓存"的顺序,源码层面就是这么排的。
CacheOperationContexts:一次调用的上下文集合
构造它的代码值得细看:
private class CacheOperationContexts {
private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
private final boolean sync;
boolean processed;
public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
@Nullable Object[] args, Object target, Class<?> targetClass) {
this.contexts = new LinkedMultiValueMap<>(operations.size());
for (CacheOperation op : operations) {
this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
}
this.sync = determineSyncFlag(method);
}
public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
Collection<CacheOperationContext> result = this.contexts.get(operationClass);
return (result != null ? result : Collections.emptyList());
}
- 用
LinkedMultiValueMap按 operation 类型分组:key 是CacheableOperation.class/CachePutOperation.class/CacheEvictOperation.class,value 是该类型的所有上下文。get(CacheableOperation.class)这种调用就能取出一类操作——主流程里按类型分批处理就是靠它。 - 构造每个上下文时调
getOperationContext→getCacheOperationMetadata→ 去metadataCache拿/建元数据(含 keyGenerator、cacheResolver 的解析结果)→ newCacheOperationContext。注意:构造 context 的那一刻就已经把缓存集合解析出来了(CacheOperationContext构造器里this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver)),也就是说这一步就要成功拿到所有Cache实例,拿不到(空集合)立刻抛IllegalStateException。 determineSyncFlag(method)在构造时一次性判定 sync 语义并做全部合法性校验。processed标记用于响应式"延迟命中"场景的重入防护,同步路径里暂时用不到。
determineSyncFlag:sync=true 的四条限制
这是面试和实践都高频的点,源码在 CacheOperationContexts.determineSyncFlag:
private boolean determineSyncFlag(Method method) {
List<CacheOperationContext> cacheableContexts = this.contexts.get(CacheableOperation.class);
if (cacheableContexts == null) { // no @Cacheable operation at all
return false;
}
boolean syncEnabled = false;
for (CacheOperationContext context : cacheableContexts) {
if (context.getOperation() instanceof CacheableOperation cacheable && cacheable.isSync()) {
syncEnabled = true;
break;
}
}
if (syncEnabled) {
if (this.contexts.size() > 1) {
throw new IllegalStateException(
"A sync=true operation cannot be combined with other cache operations on '" + method + "'");
}
if (cacheableContexts.size() > 1) {
throw new IllegalStateException(
"Only one sync=true operation is allowed on '" + method + "'");
}
CacheOperationContext cacheableContext = cacheableContexts.iterator().next();
CacheOperation operation = cacheableContext.getOperation();
if (cacheableContext.getCaches().size() > 1) {
throw new IllegalStateException(
"A sync=true operation is restricted to a single cache on '" + operation + "'");
}
if (operation instanceof CacheableOperation cacheable && StringUtils.hasText(cacheable.getUnless())) {
throw new IllegalStateException(
"A sync=true operation does not support the unless attribute on '" + operation + "'");
}
return true;
}
return false;
}
逐条件读——这是 @Cacheable(sync=true) 的全部运行时约束,全部在构造上下文时校验,违反即抛 IllegalStateException:
- 不能与其他类型操作共存:
contexts.size() > 1→ "A sync=true operation cannot be combined with other cache operations"。也就是说 sync 的@Cacheable上不能再叠@CachePut或@CacheEvict。 - 整个方法只能有一个 sync 的 @Cacheable:
cacheableContexts.size() > 1→ "Only one sync=true operation is allowed"(@Caching里塞两个 sync 的 Cacheable 会炸)。 - 只能落在单个缓存上:
getCaches().size() > 1→ "restricted to a single cache"。原因后面讲:原子加载get(key, Callable)只能对着一个缓存做,多缓存无法保证同一 key 只加载一次。 - 不能用 unless:
StringUtils.hasText(cacheable.getUnless())→ "does not support the unless attribute"。这正是任务里提到的"sync 与 unless 不兼容"——以 7.0.9 源码为准,确为不兼容,运行时直接报错。
为什么有这些限制?因为 sync 模式走的是 Cache.get(key, Callable) 原子加载语义:"如果没命中,调 loader 算出值、原子写入、返回"。这个模型里方法执行与缓存写入被合并成一步,没有"后置判断 unless""写完再 put 到别的缓存""写完再 evict"这些二次动作的位置。框架宁可在启动后第一次调用时硬报错,也不做语义模糊的妥协。
18.3.10 sync 路径:executeSynchronized 的原子加载
同步旁路的方法体(去掉响应式分支后)如下:
@SuppressWarnings({ "unchecked", "rawtypes" })
private @Nullable Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
// ... CompletableFuture 分支,见下文
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
try {
return wrapCacheValue(method, doGet(cache, key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
// or potentially also an IllegalArgumentException etc.
ReflectionUtils.rethrowRuntimeException(ex.getCause());
// Never reached
return null;
}
}
else {
// No caching required, just call the underlying method
return invokeOperation(invoker);
}
}
逐段读:
- 因为前面校验过"只有一个 Cacheable 上下文、一个缓存",这里直接
iterator().next()取出,不用担心空集合或多元素。 - condition 预判:
isConditionPassing(context, NO_RESULT)——此时还没有方法结果,#result不可用(condition 里本来也不该引用 result)。condition 不过 → 不缓存,直接invokeOperation。 - 生成 key:
generateKey(context, NO_RESULT)。注意 sync 模式下 key 在方法执行之前就算好——因为整个"算 key → 查/写缓存"要被合并进一次get(key, Callable),loader 内部不能再改 key。 - 核心一行:
doGet(cache, key, () -> unwrapReturnValue(invokeOperation(invoker)))。这一行展开就是:调cache.get(key, valueLoader)(经过AbstractCacheInvoker的错误处理包装);缓存命中 → 直接返回缓存值,loader 不执行;未命中 → 执行 loader(即真实方法),返回值经unwrapReturnValue解掉 Optional 外壳后交给ConcurrentMapCache的computeIfAbsent原子写入。方法执行与缓存写入在这里是一次原子操作——这就是 sync=true 防击穿的本质。 - 异常解包:loader 里业务方法抛的异常被包成
ValueRetrievalException(前面ConcurrentMapCache.get的 lambda 里看过),这里 catch 住后ReflectionUtils.rethrowRuntimeException(ex.getCause())把原始异常抛出去。注释说得直白:业务异常直接透传,不吞。 - 两个响应式分支(
CompletableFuture返回类型 / ReactorMono/Flux)走doRetrieve或ReactiveCachingHandler,思路相同但全程不阻塞:命中就从 future 里取值,未命中就异步执行方法、完成后写缓存。本章主线是同步路径,响应式分支知道"存在且语义对等"即可。
最后 wrapCacheValue 处理一个细节:
private @Nullable Object wrapCacheValue(Method method, @Nullable Object cacheValue) {
if (method.getReturnType() == Optional.class &&
(cacheValue == null || cacheValue.getClass() != Optional.class)) {
return Optional.ofNullable(cacheValue);
}
return cacheValue;
}
方法返回 Optional 时,缓存里存的是解包后的普通值(甚至是 null),返回前要重新包回 Optional.ofNullable(...)——这是为了和"方法真的返回 Optional"保持对称(写入前 unwrapReturnValue 已经把 Optional 拆掉了)。
18.3.11 命中检查:findCachedValue 与 findInCaches
回到非同步路径。前置 evict 之后,主流程调 findCachedValue:
private @Nullable Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Object cached = findInCaches(context, key, invoker, method, contexts);
if (cached != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames());
}
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
逐段:
- 遍历所有
@Cacheable上下文(@Caching组合多个 Cacheable 时会有多个)。 - 每个先做 condition 预判(
NO_RESULT,此时还没执行方法);condition 不过 → 这个 Cacheable 直接跳过,连 key 都不算。 - 算出 key,调
findInCaches在该上下文的所有缓存区域里找。 - 找到任何东西就立即返回(第一个命中的 Cacheable 胜出,不再看后面的 Cacheable);全找完都没找到返回 null。
findInCaches 在多缓存区域间的遍历:
private @Nullable Object findInCaches(CacheOperationContext context, Object key,
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
for (Cache cache : context.getCaches()) {
// ... CompletableFuture 分支 ...
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.findInCaches(
context, cache, key, invoker, method, contexts);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
Cache.ValueWrapper result = doGet(cache, key);
if (result != null) {
return result;
}
}
return null;
}
同步路径核心就是:按顺序在每个 Cache 上 doGet(cache, key)——返回 ValueWrapper 即命中,立即返回;null 即该区域未命中,继续下一个区域。多缓存区域是"读时逐个试、谁先命中用谁"的关系,不是冗余副本复制写。
doGet(cache, key) 来自父类 AbstractCacheInvoker,这里先记一笔(下一小节详解):它包了 try-catch,缓存读异常交给 CacheErrorHandler;如果错误处理器不抛异常,它返回 null——读异常被"模拟成未命中",然后走回源。
18.3.12 evaluate:方法执行、put 收集、后置 evict
findCachedValue 的结果送到 evaluate(cacheHit, invoker, method, contexts),这是编排的后半段:
private @Nullable Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method,
CacheOperationContexts contexts) {
// Re-invocation in reactive pipeline after late cache hit determination?
if (contexts.processed) {
return cacheHit;
}
Object cacheValue;
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = unwrapCacheValue(cacheHit);
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect puts from any @Cacheable miss, if no cached value is found
List<CachePutRequest> cachePutRequests = new ArrayList<>(1);
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
Object returnOverride = cachePutRequest.apply(cacheValue);
if (returnOverride != null) {
returnValue = returnOverride;
}
}
// Process any late evictions
Object returnOverride = processCacheEvicts(
contexts.get(CacheEvictOperation.class), false, returnValue);
if (returnOverride != null) {
returnValue = returnOverride;
}
// Mark as processed for re-invocation after late cache hit determination
contexts.processed = true;
return returnValue;
}
这十五行是"方法到底执不执行、缓存写不写、最后 evict 怎么排"的全部答案,逐段拆:
分支一(命中且没有 @CachePut):cacheHit != null && !hasCachePut(contexts)。注意这个 hasCachePut 的语义——它判断"有没有至少一个 condition 通过的 @CachePut"。两个条件同时满足时,连方法都不执行,直接把缓存值解包、包回 Optional 返回。这是纯命中的最快路径。
分支二(否则):必须执行方法。returnValue = invokeOperation(invoker)——真实方法在这里跑(异常会以 ThrowableWrapper 形式冒泡)。cacheValue = unwrapReturnValue(returnValue)——Optional 解包,准备写入缓存。
收集 put 请求:
- 如果未命中(
cacheHit == null):把所有@Cacheable(condition 通过的)包装成CachePutRequest——未命中后要把结果写回缓存,这就是"读穿透 → 回源 → 回填"。 - 无条件收集所有
@CachePut:@CachePut的语义就是"无论如何都执行方法、再把结果写缓存",它不检查命中。
collectPutRequests 内部就是过一遍 condition:
private void collectPutRequests(Collection<CacheOperationContext> contexts,
@Nullable Object result, Collection<CachePutRequest> putRequests) {
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
putRequests.add(new CachePutRequest(context));
}
}
}
注意这里传给 condition 求值的 result 已经是方法返回值了——condition 在 put 阶段引用 #result 是允许的(虽然 condition 主要用于"方法前预判",但 put 收集发生在方法后,此时 result 可用)。
执行 put 请求:逐个 cachePutRequest.apply(cacheValue)。CachePutRequest.apply → performCachePut:
public void performCachePut(@Nullable Object value) {
if (this.context.canPutToCache(value)) {
Object key = this.context.getGeneratedKey();
if (key == null) {
key = generateKey(this.context, value);
}
if (logger.isTraceEnabled()) {
logger.trace("Creating cache entry for key '" + key + "' in cache(s) " +
this.context.getCacheNames());
}
for (Cache cache : this.context.getCaches()) {
doPut(cache, key, value);
}
}
}
canPutToCache(value) 就是 unless 判断:unless 表达式为 true → 不入缓存("除非满足条件,否则缓存")。key 可能在前面命中检查阶段已经生成过(getGeneratedKey() 非 null 就复用),没生成过就现在带着 result 生成(unless/key 表达式此时可引用 #result)。然后对所有缓存区域 doPut。
后置 eviction:processCacheEvicts(evictOperations, beforeInvocation=false, returnValue)——所有 @CacheEvict(beforeInvocation=false)(默认值)在方法成功返回、缓存写完之后才执行,此时 #result 可用。
最后 contexts.processed = true 标记——这是给响应式"延迟命中重入"用的同步路径收尾。
18.3.13 hasCachePut:为什么命中了还要执行方法
evaluate 第一个分支的条件 !hasCachePut(contexts) 值得单独讲。它的实现:
private boolean hasCachePut(CacheOperationContexts contexts) {
// Evaluate the conditions *without* the result object because we don't have it yet...
Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
Collection<CacheOperationContext> excluded = new ArrayList<>(1);
for (CacheOperationContext context : cachePutContexts) {
try {
if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
excluded.add(context);
}
}
catch (VariableNotAvailableException ex) {
// Ignoring failure due to missing result, consider the cache put has to proceed
}
}
// Check if all puts have been excluded by condition
return (cachePutContexts.size() != excluded.size());
}
问题场景:一个方法上同时有 @Cacheable 和 @CachePut。此时即使 @Cacheable 命中了,方法也必须执行——因为 @CachePut 的契约就是"每次都执行、每次都更新缓存"。怎么判断这个?数一下:所有 @CachePut 里,condition 不通过的有几个;如果不是全部被排除,就认为"至少有一个 put 要生效",命中也要走方法。
注意两个细节:
- 此时还没执行方法,
#result不存在——传RESULT_UNAVAILABLE。CacheEvaluationContext.lookupVariable一旦发现这个变量被标记为不可用,访问即抛VariableNotAvailableException。 - catch 住
VariableNotAvailableException后不排除这个 put——注释说"Ignoring failure due to missing result, consider the cache put has to proceed"。逻辑很务实:你的 condition 里引用了#result(比如condition="#result != null"),在没有 result 的情况下无法判定,那就保守地认为 put 要执行,让方法真的跑一遍再说。这是"不确定就回源"的安全默认。
这也解释了一个常见困惑:为什么 @Cacheable 和 @CachePut 同时标注时,每次都查库? 因为命中路径在 hasCachePut 返回 true 时根本走不到,方法必执行,然后结果写进两边的缓存。这不是 bug,是两个注解语义叠加的必然结果。
18.3.14 processCacheEvicts:beforeInvocation 的两次调度
eviction 在主流程里被调两次,靠 beforeInvocation 参数区分:
private @Nullable Object processCacheEvicts(Collection<CacheOperationContext> contexts, boolean beforeInvocation,
@Nullable Object result) {
if (contexts.isEmpty()) {
return null;
}
List<CacheOperationContext> applicable = contexts.stream()
.filter(context -> (context.metadata.operation instanceof CacheEvictOperation evict &&
beforeInvocation == evict.isBeforeInvocation())).toList();
if (applicable.isEmpty()) {
return null;
}
// ... CompletableFuture / reactive 分支 ...
performCacheEvicts(applicable, result);
return null;
}
第一次(主流程开头)传 true:只处理 beforeInvocation=true 的 evict,此时 result 是 NO_RESULT;第二次(evaluate 尾部)传 false:只处理 beforeInvocation=false 的 evict,此时 result 是方法返回值。filter 条件 beforeInvocation == evict.isBeforeInvocation() 把两类操作精确分流。
真正执行在 performCacheEvicts:
private void performCacheEvicts(List<CacheOperationContext> contexts, @Nullable Object result) {
for (CacheOperationContext context : contexts) {
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
if (isConditionPassing(context, result)) {
Object key = context.getGeneratedKey();
for (Cache cache : context.getCaches()) {
if (operation.isCacheWide()) {
logInvalidating(context, operation, null);
doClear(cache, operation.isBeforeInvocation());
}
else {
if (key == null) {
key = generateKey(context, result);
}
logInvalidating(context, operation, key);
doEvict(cache, key, operation.isBeforeInvocation());
}
}
}
}
}
逐段:
- 每个 evict 先过 condition(后置 evict 的 condition 此时可引用
#result)。 allEntries=true(isCacheWide())→ 对每个缓存doClear——清空整个缓存区域,不需要 key。- 否则按 key 删:优先复用前面算好的 key(
getGeneratedKey(),命中检查阶段算过的那个);如果之前没算过(比如纯 evict 方法,没有 @Cacheable,key 是在这里首次带 result 计算)→generateKey(context, result)。 doEvict/doClear的第二个参数就是operation.isBeforeInvocation()——它决定走evict还是evictIfPresent、clear还是invalidate(下一小节看AbstractCacheInvoker)。
beforeInvocation 的语义差异(源码层面):默认 false = 方法成功返回后才清缓存——好处是"方法执行失败抛异常时,旧缓存还在",不会把好缓存误删;代价是"方法成功但事务还没提交时缓存就被清了"——这是缓存与事务组合的经典坑,后面专门讲。设为 true = 方法一进入就清,不管方法成败——适合"先清后算"的场景,但要接受异常时缓存已空的事实。
18.3.15 AbstractCacheInvoker:错误处理如何改写异常语义
前面处处出现的 doGet/doPut/doEvict/doClear 都在父类 AbstractCacheInvoker(interceptor/AbstractCacheInvoker.java)里。它是"调用缓存 API"与"错误处理策略"之间的薄薄一层:
public abstract class AbstractCacheInvoker {
protected SingletonSupplier<CacheErrorHandler> errorHandler;
protected AbstractCacheInvoker() {
this.errorHandler = SingletonSupplier.of(SimpleCacheErrorHandler::new);
}
默认错误处理器是 SimpleCacheErrorHandler。逐个看 do* 方法:
protected Cache.@Nullable ValueWrapper doGet(Cache cache, Object key) {
try {
return cache.get(key);
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null; // If the exception is handled, return a cache miss
}
}
读缓存异常:先交 errorHandler。默认实现是原样抛出,所以这一行 throw 直接冒到最外层;但如果用户换了 LoggingCacheErrorHandler(只记日志不抛),catch 块继续执行 return null——注释写明:"If the exception is handled, return a cache miss",即读失败被伪装成未命中,继续回源查库。这就是"缓存宕机不打爆业务"的源码实现。
带 loader 的 doGet:
protected <T> @Nullable T doGet(Cache cache, Object key, Callable<T> valueLoader) {
try {
return cache.get(key, valueLoader);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
try {
return valueLoader.call();
}
catch (Exception ex2) {
throw new Cache.ValueRetrievalException(key, valueLoader, ex);
}
}
}
两种异常分流:ValueRetrievalException(loader 内部、即业务方法抛的)原样透传——这是业务异常,不该被错误处理器吞掉;其他 RuntimeException(缓存本身的故障)才走 errorHandler,handler 不抛的话降级为直接调 loader 回源——读路径彻底失效也要让业务跑通。
写、删、清:
protected void doPut(Cache cache, Object key, @Nullable Object value) {
try {
cache.put(key, value);
}
catch (RuntimeException ex) {
getErrorHandler().handleCachePutError(ex, cache, key, value);
}
}
protected void doEvict(Cache cache, Object key, boolean immediate) {
try {
if (immediate) {
cache.evictIfPresent(key);
}
else {
cache.evict(key);
}
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheEvictError(ex, cache, key);
}
}
protected void doClear(Cache cache, boolean immediate) {
try {
if (immediate) {
cache.invalidate();
}
else {
cache.clear();
}
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheClearError(ex, cache);
}
}
写路径的语义与读不同:put/evict/clear 出错时,errorHandler 不抛 = 这个写操作静默丢失,业务方法正常返回。注意 immediate 参数就是 beforeInvocation——performCacheEvicts 里传进来的那个布尔:true 走 evictIfPresent/invalidate(立即可见版),false 走 evict/clear(允许延迟生效版)。
CacheErrorHandler 接口本身四个方法(handleCacheGetError / PutError / EvictError / ClearError),两个现成实现:
public class SimpleCacheErrorHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
throw exception;
}
// put / evict / clear 三个实现同样是 throw exception
}
默认策略:全部原样抛出。缓存出问题 = 调用方感知。另一个 LoggingCacheErrorHandler(5.3.16 引入)只打 warn 日志、不抛——生产上常见的"缓存降级"策略就是换它。
18.3.16 元数据缓存:getCacheOperationMetadata 如何解析 keyGenerator 与 cacheResolver
每个 CacheOperation(注解解析产物)上可能写着 keyGenerator、cacheManager、cacheResolver 的 bean 名字符串。这些名字对应的 bean 对象在哪一刻解析?答案在 getCacheOperationMetadata:
protected CacheOperationMetadata getCacheOperationMetadata(
CacheOperation operation, Method method, Class<?> targetClass) {
CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
if (metadata == null) {
KeyGenerator operationKeyGenerator;
if (StringUtils.hasText(operation.getKeyGenerator())) {
operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
}
else {
operationKeyGenerator = getKeyGenerator();
}
CacheResolver operationCacheResolver;
if (StringUtils.hasText(operation.getCacheResolver())) {
operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
}
else if (StringUtils.hasText(operation.getCacheManager())) {
CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
operationCacheResolver = new SimpleCacheResolver(cacheManager);
}
else {
operationCacheResolver = getCacheResolver();
Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
}
metadata = new CacheOperationMetadata(operation, method, targetClass,
operationKeyGenerator, operationCacheResolver);
this.metadataCache.put(cacheKey, metadata);
}
return metadata;
}
解析优先级清晰:
- keyGenerator:operation 上显式指定了 bean 名 →
getBean(名字, KeyGenerator.class)按名取 bean;否则用全局默认(getKeyGenerator(),默认SimpleKeyGenerator)。 - cacheResolver:operation 上指定了 resolver 名 → 按名取;否则指定了 cacheManager 名 → 现场
new SimpleCacheResolver(cacheManager)包一层;都没有 → 用全局默认 resolver。 - 解析结果连同 operation/method/targetClass 打包成
CacheOperationMetadata,以CacheOperationCacheKey为 key 存进metadataCache(ConcurrentHashMap(1024))——每个方法的解析结果一生效一次,之后不再重复查 bean。
getBean 的实现是 BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory, serviceType, name)——按名字限定取 bean,取不到会抛 NoSuchBeanDefinitionException。
全局 resolver 的兜底发生在 afterSingletonsInstantiated:
@Override
public void afterSingletonsInstantiated() {
if (getCacheResolver() == null) {
// Lazily initialize cache resolver via default cache manager
Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
try {
setCacheManager(this.beanFactory.getBean(CacheManager.class));
}
catch (NoUniqueBeanDefinitionException ex) {
// ... 拼成友好消息后重抛
}
catch (NoSuchBeanDefinitionException ex) {
throw new NoSuchBeanDefinitionException(CacheManager.class, "no CacheResolver specified - " +
"register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
}
}
this.initialized = true;
}
所有单例就绪后:如果你既没自定义 resolver 也没在 CachingConfigurer 里给,那就按类型找唯一的 CacheManager bean——找到多个(NoUniqueBeanDefinitionException)或零个(NoSuchBeanDefinitionException)都给出明确错误消息。这就是"@EnableCaching 必须配一个 CacheManager"这条规则的执行点。成功后 initialized=true,execute 入口才放行。
18.3.17 key 的生成:KeyGenerator、SimpleKeyGenerator 与 SimpleKey
先看接口:
@FunctionalInterface
public interface KeyGenerator {
Object generate(Object target, Method method, @Nullable Object... params);
}
函数式接口,三个参数:目标对象、方法、参数数组(varargs)。
默认实现 SimpleKeyGenerator(interceptor/SimpleKeyGenerator.java)的规则四行就说完:
@Override
public Object generate(Object target, Method method, @Nullable Object... params) {
return generateKey((KotlinDetector.isSuspendingFunction(method) ?
Arrays.copyOf(params, params.length - 1) : params));
}
public static Object generateKey(@Nullable Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
规则逐条:
- Kotlin 挂起函数:最后一个参数是编译器塞的
Continuation,生成 key 前把它丢掉——这是对 Kotlin 的特殊照顾,Java 方法无此问题。 - 无参方法:返回
SimpleKey.EMPTY单例。同一个类里所有无参缓存方法共用 EMPTY 吗?不——key 只是 key,缓存区域不同;同一缓存区域里两个无参方法会撞 key!这是个经典坑,"常见问题"一节展开。 - 单参且非 null、非数组:直接把这个参数本身当 key。好处是天然与参数类型一致(比如
findUser(Long id)的 key 就是那个 Long);风险见后。 - 其余情况(多参 / 单参为 null / 单参是数组):包装成
SimpleKey。
SimpleKey(interceptor/SimpleKey.java)的 equals/hashCode 值得看:
public class SimpleKey implements Serializable {
public static final SimpleKey EMPTY = new SimpleKey();
private final @Nullable Object[] params;
// Effectively final, just re-calculated on deserialization
private transient int hashCode;
public SimpleKey(@Nullable Object... elements) {
Assert.notNull(elements, "Elements must not be null");
this.params = elements.clone();
this.hashCode = calculateHash(this.params);
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof SimpleKey that && Arrays.deepEquals(this.params, that.params)));
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// Re-calculate hashCode field on deserialization
this.hashCode = calculateHash(this.params);
}
private static int calculateHash(@Nullable Object[] params) {
int hash = Arrays.deepHashCode(params);
hash = (hash ^ (hash >>> 16)) * 0x85ebca6b;
hash = (hash ^ (hash >>> 13)) * 0xc2b2ae35;
return hash ^ (hash >>> 16);
}
}
三个设计点:
Arrays.deepEquals:参数数组深度比较——两个SimpleKey(1, "a")相等。- hashCode 预计算并缓存:构造时算好存 transient 字段;反序列化时
readObject里重算。注意用了 MurmurHash3 的 finalizer 混合常量(0x85ebca6b、0xc2b2ae35)打散deepHashCode的分布——减少多参数 key 的哈希冲突。 params.clone():防御性拷贝,外部改不了内部数组。
CacheAspectSupport.generateKey 还有一道保险:
private Object generateKey(CacheOperationContext context, @Nullable Object result) {
Object key = context.generateKey(result);
if (key == null) {
throw new IllegalArgumentException("""
Null key returned for cache operation [%s]. If you are using named parameters, \
ensure that the compiler uses the '-parameters' flag."""
.formatted(context.metadata.operation));
}
...
return key;
}
key 算出来是 null 直接抛异常,错误消息专门提示 -parameters 编译参数——因为 SpEL 里用 #p0/#a0 之外的命名参数(#userName)需要反射拿参数名,没有 -parameters 或 LocalVariableTableParameterNameDiscoverer 的调试信息时拿不到名字,表达式求值就会失败。
18.3.18 SpEL 求值:key / condition / unless 的表达式上下文
注解里写的 key = "#user.id"、condition = "#age > 18"、unless = "#result == null" 都是 SpEL 表达式。求值器是 CacheOperationExpressionEvaluator(interceptor/CacheOperationExpressionEvaluator.java):
class CacheOperationExpressionEvaluator extends CachedExpressionEvaluator {
/** Indicate that there is no result variable. */
public static final Object NO_RESULT = new Object();
/** Indicate that the result variable cannot be used at all. */
public static final Object RESULT_UNAVAILABLE = new Object();
/** The name of the variable holding the result object. */
public static final String RESULT_VARIABLE = "result";
private final Map<ExpressionKey, Expression> keyCache = new ConcurrentHashMap<>(64);
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<ExpressionKey, Expression> unlessCache = new ConcurrentHashMap<>(64);
三个哨兵/常量先说清楚:
NO_RESULT:一个"假"对象,表示"现在还没有方法结果"。方法执行前的 condition/key 求值都传它——它不是 null,createEvaluationContext里靠result != NO_RESULT判断要不要注册result变量。RESULT_UNAVAILABLE:另一个假对象,表示"result现在不可用,而且表达式不许碰它"。它会被注册进"不可用变量"黑名单,谁访问#result就抛VariableNotAvailableException——hasCachePut那一段用的就是它。RESULT_VARIABLE = "result":SpEL 变量名,即#result。
三个 ConcurrentHashMap 是表达式缓存:同一方法同一表达式字符串,只解析一次成 Expression 对象,之后复用。这是 CachedExpressionEvaluator 父类提供的能力——SpEL 解析不便宜,绝不能每次调用都重新 parse。
建上下文的方法:
public EvaluationContext createEvaluationContext(Collection<? extends Cache> caches,
Method method, @Nullable Object[] args, Object target, Class<?> targetClass, Method targetMethod,
@Nullable Object result) {
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(
caches, method, args, target, targetClass);
CacheEvaluationContext evaluationContext = this.evaluationContextFactory
.forOperation(rootObject, targetMethod, args);
if (result == RESULT_UNAVAILABLE) {
evaluationContext.addUnavailableVariable(RESULT_VARIABLE);
}
else if (result != NO_RESULT) {
evaluationContext.setVariable(RESULT_VARIABLE, result);
}
return evaluationContext;
}
逐段:
- 先造 root 对象
CacheExpressionRootObject——它包装了 caches/method/args/target/targetClass,这就是 SpEL 的#root。 forOperation造CacheEvaluationContext(它继承MethodBasedEvaluationContext,负责把#a0/#p0、命名参数变量延迟装进来)。- 三态处理 result:不可用 → 加黑名单;有真实结果 →
setVariable("result", result);NO_RESULT → 都不做(#result就是未定义变量,访问即报错)。
CacheExpressionRootObject 的 getter 列表决定了 #root.xxx 能写什么:
class CacheExpressionRootObject {
public Collection<? extends Cache> getCaches() { ... }
public Method getMethod() { ... }
public String getMethodName() { ... }
public @Nullable Object[] getArgs() { ... }
public Object getTarget() { ... }
public Class<?> getTargetClass() { ... }
}
即 #root.method、#root.methodName、#root.target、#root.targetClass、#root.args、#root.caches。而 #a0/#p0/#a1.nested.field 这些是 MethodBasedEvaluationContext 提供的——它把参数数组按下标注册成 a0,a1,... 和 p0,p1,...(两者等价),命名参数名(如 #userName)也在其中,靠 ParameterNameDiscoverer 发现。
#result 何时可用?总结源码里三处调用点:
| 调用点 | 传入 result | #result 状态 |
|---|---|---|
findCachedValue 里 condition/key | NO_RESULT | 未定义,访问报错 |
executeSynchronized 里 condition/key | NO_RESULT | 未定义 |
performCacheEvicts 后置 evict 的 condition | 方法返回值 | 可用 |
collectPutRequests 的 condition、canPutToCache 的 unless、performCachePut 的 key | 方法返回值 | 可用 |
hasCachePut 预判 | RESULT_UNAVAILABLE | 黑名单,访问抛 VariableNotAvailableException |
这张表直接回答"condition 和 unless 的区别":
condition(方法前预判):在findCachedValue/executeSynchronized里以NO_RESULT求值——此时方法还没跑,#result不可用,只能用入参。它是"要不要走缓存"的开关。unless(方法后判定):在CacheOperationContext.canPutToCache里以真实返回值求值——#result可用,用来决定"这次结果要不要写进缓存"。- 但注意:
condition在 put 收集/后置 evict 阶段也会再被求值一次(带 result),所以 condition 里写#result不是绝对不行——只是它第一次(方法前)求值时没有 result,写了就会抛错。实践上 condition 只放方法前可判断的条件,unless 才是引用#result的合法位置。
CacheEvaluationContext 的黑名单机制最后看一眼:
class CacheEvaluationContext extends MethodBasedEvaluationContext {
private final Set<String> unavailableVariables = new HashSet<>(1);
public void addUnavailableVariable(String name) {
this.unavailableVariables.add(name);
}
@Override
public @Nullable Object lookupVariable(String name) {
if (this.unavailableVariables.contains(name)) {
throw new VariableNotAvailableException(name);
}
return super.lookupVariable(name);
}
}
RESULT_UNAVAILABLE 被注册后,任何访问 #result 的表达式在 lookup 阶段直接抛 VariableNotAvailableException——这个异常在 hasCachePut 里被 catch 住转成"保守认为 put 要执行"。
18.3.19 注解解析:SpringCacheAnnotationParser
运行时还有另一半工作:把注解翻译成 CacheOperation 对象。入口是 SpringCacheAnnotationParser(annotation/SpringCacheAnnotationParser.java)。它先声明自己认哪四个注解:
private static final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS =
Set.of(Cacheable.class, CacheEvict.class, CachePut.class, Caching.class);
@Override
public boolean isCandidateClass(Class<?> targetClass) {
return AnnotationUtils.isCandidateClass(targetClass, CACHE_OPERATION_ANNOTATIONS);
}
isCandidateClass 被 pointcut 的类过滤器调用——快速判断一个类"是否可能带这些注解",避免对每个 bean 全方法扫描。
解析方法分两路(类上、方法上),都构造一个 DefaultCacheConfig(持有 @CacheConfig 的类级默认值)再进入私有方法:
private @Nullable Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
Collection<CacheOperation> ops = parseCacheAnnotations(cachingConfig, ae, false);
if (ops != null && ops.size() > 1) {
// More than one operation found -> local declarations override interface-declared ones...
Collection<CacheOperation> localOps = parseCacheAnnotations(cachingConfig, ae, true);
if (localOps != null) {
return localOps;
}
}
return ops;
}
这层重解析有个重要语义:全量找到的操作超过一个时,再用 localOnly=true(getAllMergedAnnotations,只看本地声明、排除接口继承来的)解析一遍;如果本地声明有操作,就以本地为准——本地声明覆盖接口声明。这解决"接口方法上写了 @Cacheable,实现类又写了一个"时以谁为准的问题。
核心解析循环:
private @Nullable Collection<CacheOperation> parseCacheAnnotations(
DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {
Collection<? extends Annotation> annotations = (localOnly ?
AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
if (annotations.isEmpty()) {
return null;
}
Collection<CacheOperation> ops = new ArrayList<>(1);
annotations.stream().filter(Cacheable.class::isInstance).map(Cacheable.class::cast).forEach(
cacheable -> ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable)));
annotations.stream().filter(CacheEvict.class::isInstance).map(CacheEvict.class::cast).forEach(
cacheEvict -> ops.add(parseEvictAnnotation(ae, cachingConfig, cacheEvict)));
annotations.stream().filter(CachePut.class::isInstance).map(CachePut.class::cast).forEach(
cachePut -> ops.add(parsePutAnnotation(ae, cachingConfig, cachePut)));
annotations.stream().filter(Caching.class::isInstance).map(Caching.class::cast).forEach(
caching -> parseCachingAnnotation(ae, cachingConfig, caching, ops));
return ops;
}
用 AnnotatedElementUtils.findAllMergedAnnotations 做全注解发现——它会处理组合注解、元注解、桥接方法等复杂情况。然后按类型分流:Cacheable → parseCacheableAnnotation,以此类推。注意顺序:先收集所有 Cacheable、再 CacheEvict、再 CachePut、最后展开 @Caching。但这个收集顺序不决定执行顺序——执行顺序是 CacheOperationContexts 构造时按 op.getClass() 分进 LinkedMultiValueMap 后,由主流程固定(evict-before → cacheable 查 → put → evict-after),与注解书写顺序无关。
看一个解析方法(Cacheable):
private CacheableOperation parseCacheableAnnotation(
AnnotatedElement ae, DefaultCacheConfig defaultConfig, Cacheable cacheable) {
CacheableOperation.Builder builder = new CacheableOperation.Builder();
builder.setName(ae.toString());
builder.setCacheNames(cacheable.cacheNames());
builder.setCondition(cacheable.condition());
builder.setUnless(cacheable.unless());
builder.setKey(cacheable.key());
builder.setKeyGenerator(cacheable.keyGenerator());
builder.setCacheManager(cacheable.cacheManager());
builder.setCacheResolver(cacheable.cacheResolver());
builder.setSync(cacheable.sync());
defaultConfig.applyDefault(builder);
CacheableOperation op = builder.build();
validateCacheOperation(ae, op);
return op;
}
逐属性对照 @Cacheable 注解定义(annotation/Cacheable.java,属性均默认空串/false):
cacheNames()(即注解里的value/cacheNames别名)→setCacheNames;condition()→setCondition;unless()→setUnless;key()→setKey;keyGenerator()→setKeyGenerator;cacheManager()→setCacheManager;cacheResolver()→setCacheResolver;sync()→setSync。
@CacheEvict 的解析多两个属性:allEntries() → setCacheWide,beforeInvocation() → setBeforeInvocation(其余属性与 Cacheable 相同但没有 unless——evict 本来就不写缓存)。@CachePut 与 Cacheable 属性相同(key/condition/unless/...)但没有 sync。
@Caching 的展开:
private void parseCachingAnnotation(
AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching, Collection<CacheOperation> ops) {
Cacheable[] cacheables = caching.cacheable();
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable));
}
CacheEvict[] cacheEvicts = caching.evict();
for (CacheEvict cacheEvict : cacheEvicts) {
ops.add(parseEvictAnnotation(ae, defaultConfig, cacheEvict));
}
CachePut[] cachePuts = caching.put();
for (CachePut cachePut : cachePuts) {
ops.add(parsePutAnnotation(ae, defaultConfig, cachePut));
}
}
@Caching 就是三个数组装包器——逐个拆出来当独立 operation 加进集合。所以"@Caching(cacheable={...}, evict={...}, put={...}) 与直接在方法上叠注解"在运行时产物上完全等价。
18.3.20 校验与 @CacheConfig 合并语义
解析完每个 operation 后立刻校验:
private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) {
if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache annotation configuration on '" +
ae + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
throw new IllegalStateException("Invalid cache annotation configuration on '" +
ae + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " +
"These attributes are mutually exclusive: the cache manager is used to configure a" +
"default cache resolver if none is set. If a cache resolver is set, the cache manager" +
"won't be used.");
}
}
两组互斥校验:key 与 keyGenerator 二选一(一个是 SpEL 表达式,一个是 bean 名,同时写不知听谁的);cacheManager 与 cacheResolver 二选一。违反即抛 IllegalStateException——启动后第一次解析到该方法就报。
类级默认值的合并在 DefaultCacheConfig.applyDefault:
private static class DefaultCacheConfig {
private final Class<?> target;
private String @Nullable [] cacheNames;
private @Nullable String keyGenerator;
private @Nullable String cacheManager;
private @Nullable String cacheResolver;
private boolean initialized = false;
public void applyDefault(CacheOperation.Builder builder) {
if (!this.initialized) {
CacheConfig annotation = AnnotatedElementUtils.findMergedAnnotation(this.target, CacheConfig.class);
if (annotation != null) {
this.cacheNames = annotation.cacheNames();
this.keyGenerator = annotation.keyGenerator();
this.cacheManager = annotation.cacheManager();
this.cacheResolver = annotation.cacheResolver();
}
this.initialized = true;
}
if (builder.getCacheNames().isEmpty() && this.cacheNames != null) {
builder.setCacheNames(this.cacheNames);
}
if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator()) &&
StringUtils.hasText(this.keyGenerator)) {
builder.setKeyGenerator(this.keyGenerator);
}
if (StringUtils.hasText(builder.getCacheManager()) || StringUtils.hasText(builder.getCacheResolver())) {
// One of these is set so we should not inherit anything
}
else if (StringUtils.hasText(this.cacheResolver)) {
builder.setCacheResolver(this.cacheResolver);
}
else if (StringUtils.hasText(this.cacheManager)) {
builder.setCacheManager(this.cacheManager);
}
}
}
合并规则逐条:
- 第一次用时才读
@CacheConfig(initialized标记,懒加载)——@CacheConfig放在类上,提供该类所有缓存操作的默认 cacheNames/keyGenerator/cacheManager/cacheResolver。 - cacheNames:方法上没写、类上写了 → 继承类级。方法上写了就以方法为准。
- keyGenerator:方法上既没写 key 表达式、也没写 keyGenerator → 继承类级 keyGenerator。注意条件是"两个都没写"——你一旦手写了
key="#xxx",类级 keyGenerator 就不再继承。 - cacheManager/cacheResolver:方法上写了任一个 → 不继承类级任何东西;都没写 → 优先继承类级 resolver,没有再继承类级 manager。优先级是 resolver 覆盖 manager。
18.3.21 AbstractFallbackCacheOperationSource:查找回退策略与缓存
注解解析的上层调度在 AbstractFallbackCacheOperationSource(interceptor/AbstractFallbackCacheOperationSource.java)。它先建了一个 operation 缓存:
/** Canonical value held in cache to indicate no cache operation was found... */
private static final Collection<CacheOperation> NULL_CACHING_MARKER = Collections.emptyList();
private final Map<Object, Collection<CacheOperation>> operationCache = new ConcurrentHashMap<>(1024);
连"这方法没有缓存操作"也缓存——用 NULL_CACHING_MARKER 空集合当哨兵。否则 pointcut 每匹配一次就要重扫一次类。
主查找逻辑 computeCacheOperations:
private @Nullable Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
// The method may be on an interface, but we need metadata from the target class.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
if (opDef != null) {
return opDef;
}
// Second try is the caching operation on the target class.
opDef = findCacheOperations(specificMethod.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
opDef = findCacheOperations(method);
if (opDef != null) {
return opDef;
}
// Last fallback is the class of the original method.
opDef = findCacheOperations(method.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
}
return null;
}
回退顺序四级:
allowPublicMethodsOnly() && !public→ 直接 null(proxy 模式默认AnnotationCacheOperationSource(false),即不要求 public——ProxyCachingConfiguration里传的 false);- 目标类上的具体方法;
- 目标类本身(类级注解/接口默认方法);
- 如果代理进来的方法与具体方法不同(接口方法 vs 实现方法),回退到原方法 → 原方法声明类。
Object 的方法(toString/hashCode 等)在更外层直接排除——不会给 toString 生成缓存。AnnotationCacheOperationSource 只是把 findCacheOperations(Method/Class) 委托给 SpringCacheAnnotationParser,并持有 publicMethodsOnly 标志(默认 true,但 proxy 装配传了 false)。
18.3.22 响应式返回类型的缓存路径(补充)
7.0.9 的 CacheAspectSupport 对返回 CompletableFuture、Reactor Mono/Flux 的方法有专门分支,前面主线里略过,这里补齐关键源码——它解释了"为什么异步方法上也能直接加 @Cacheable"。
类顶部有两个开关:
public static final String IGNORE_REACTIVESTREAMS_PROPERTY_NAME = "spring.cache.reactivestreams.ignore";
private static final boolean SHOULD_IGNORE_REACTIVE_STREAMS =
SpringProperties.getFlag(IGNORE_REACTIVESTREAMS_PROPERTY_NAME);
private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
"org.reactivestreams.Publisher", CacheAspectSupport.class.getClassLoader());
classpath 上有 Reactive Streams(且没设 spring.cache.reactivestreams.ignore=true)时,构造器才 new 出 ReactiveCachingHandler:
protected CacheAspectSupport() {
this.reactiveCachingHandler =
(REACTIVE_STREAMS_PRESENT && !SHOULD_IGNORE_REACTIVE_STREAMS ? new ReactiveCachingHandler() : null);
}
findInCaches 里的 CompletableFuture 分支:
for (Cache cache : context.getCaches()) {
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
CompletableFuture<?> result = doRetrieve(cache, key);
if (result != null) {
return result.exceptionallyCompose(ex -> {
if (!(ex instanceof RuntimeException rex)) {
return CompletableFuture.failedFuture(ex);
}
try {
getErrorHandler().handleCacheGetError(rex, cache, key);
return CompletableFuture.completedFuture(null);
}
catch (Throwable ex2) {
return CompletableFuture.failedFuture(ex2);
}
}).thenCompose(value -> (CompletableFuture<?>) evaluate(
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
invoker, method, contexts));
}
else {
continue;
}
}
语义同步路径一一对应:doRetrieve 对应 doGet,返回 null = 立即确定未命中(continue 下一个缓存区域);命中则在 future 完成后再进入 evaluate——注意 evaluate 开头那句 if (contexts.processed) return cacheHit; 就是为这次"延迟重入"准备的:第一次 evaluate 走的是未命中分支(method 还没真跑),等 future 解析出值后重入时不能再执行一遍方法,直接返回解包后的值。错误处理也挂在 future 的 exceptionallyCompose 上——读缓存失败时交 errorHandler,不抛则当未命中继续。
CachePutRequest.apply 的异步分支同理:
public @Nullable Object apply(@Nullable Object result) {
if (result instanceof CompletableFuture<?> future) {
return future.whenComplete((value, ex) -> {
if (ex == null) {
performCachePut(value);
}
}
}
...
}
方法返回 future、且成功完成时才 performCachePut——future 异常完成(ex != null)则不写缓存。这个细节很重要:异步方法失败不产生脏缓存。
ReactiveCachingHandler 内部(Mono/Flux 分支)思路相同:命中就从缓存的 list/future 重建响应式流;未命中就执行原流、在 doOnSuccess 里 collect 后写缓存;Flux 用 CachePutListSubscriber 把整组元素收集成 List 再缓存——即 Flux 方法缓存的是"事件序列的全集快照",不是逐个元素。这些分支都以 NOT_HANDLED 哨兵返回"我不处理",让同步路径兜底。
18.3.23 其余支撑类速览
NamedCacheResolver(interceptor/NamedCacheResolver.java):与 SimpleCacheResolver 的区别在于缓存名字在 resolver bean 上静态配置(setCacheNames),不从 operation 读——适合"一组操作共享同一批缓存名"的场景。
CompositeCacheManager(support/CompositeCacheManager.java):持有一组 CacheManager,getCache(name) 按顺序逐个问,第一个能给出该缓存的管理器胜出。多后端混合(本地 + 远程)时用它统一入口。
SimpleCacheManager(support/SimpleCacheManager.java):纯静态注册表——启动时手工 setCaches(...) 一批 Cache 实例,运行时不再创建。测试场景最常用。
NoOpCache / NoOpCacheManager(support/):NoOpCache 的所有读写方法都是空操作,get 永远返回 null——"缓存关了"的实现。NoOpCacheManager 对任何名字都返回一个 NoOpCache。需要临时关掉缓存(比如排查"是不是缓存导致的 bug")时换它即可,业务代码零改动。
CacheProxyFactoryBean(interceptor/CacheProxyFactoryBean.java):旧式工厂 bean,用于手工给单个 bean 套缓存代理——@EnableCaching 普及后基本不用了,保留只为兼容老配置。
CacheOperationSource 接口本身只有两个方法:getCacheOperations(method, targetClass) 与 hasCacheOperations(method, targetClass)——pointcut 匹配用后者,执行用前者。
18.3.24 @CacheEvict 与 @CachePut 注解属性的逐字对照
最后把三个写操作注解的属性集对照一次源码定义(annotation/CacheEvict.java、CachePut.java),补全 18.3.19 未展开的部分:
@CacheEvict:
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
boolean allEntries() default false;
boolean beforeInvocation() default false;
注意它没有 unless、没有 sync——evict 不产生缓存写入,自然不需要"写完再反悔"的 unless;condition 存在,但它在前置 evict(NO_RESULT)和后置 evict(真实 result)两个阶段都会被求值。
@CachePut:
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
它与 @Cacheable 的属性差一个 sync——@CachePut 没有同步模式(它每次都执行方法,没有"防击穿"的需求)。
@Caching 三个数组装包器:
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
@CacheConfig 四个类级默认:cacheNames()/keyGenerator()/cacheManager()/cacheResolver(),合并规则已在 18.3.20 拆解。
这四张属性表加上 validateCacheOperation 的两组互斥校验(key↔keyGenerator、cacheManager↔cacheResolver),就是写注解时全部的"语法规则"——其余行为差异全部来自 CacheAspectSupport 的固定编排,而非注解属性。
18.4 运行机制
源码读散了之后,需要把它们串成一次完整的运行。下面按场景给时序。
18.4.1 一次普通 @Cacheable 调用的完整时序
设一个最简单的场景:
@Cacheable(cacheNames = "users", key = "#id")
public User findById(Long id) { ... }
调用方拿到的是代理对象。完整调用栈(方法名为简化):
caller
└─ proxyFindById(id) JDK/CGLIB 代理
└─ DynamicAdvisedInterceptor.intercept
└─ 匹配 BeanFactoryCacheOperationSourceAdvisor.pointcut
(CacheOperationSourcePointcut.matches → hasCacheOperations → true)
└─ CacheInterceptor.invoke(invocation)
├─ aopAllianceInvoker = () -> invocation.proceed() 包 ThrowableWrapper
└─ CacheAspectSupport.execute(invoker, target, method, args)
├─ ultimateTargetClass → UserService.class
├─ operationSource.getCacheOperations → [CacheableOperation{users,#id}]
├─ new CacheOperationContexts(...)
│ ├─ getCacheOperationMetadata → metadataCache 命中/新建
│ │ (keyGenerator=SimpleKeyGenerator, resolver=SimpleCacheResolver)
│ └─ context 构造 → resolver.resolveCaches → Cache "users"
├─ contexts.isSynchronized()? → false
├─ processCacheEvicts(before=true) → 无 evict 操作,跳过
├─ findCachedValue:
│ ├─ condition? 无 → 通过
│ ├─ key = SpEL("#id") = 1001
│ └─ doGet(users, 1001)
│ ├─ 命中 → 返回 ValueWrapper(User) → return
│ └─ 未命中 → null
└─ evaluate(null, invoker, ...):
├─ cacheHit==null → invokeOperation(invoker)
│ └─ invocation.proceed() → 真正 findById 查库 → user
├─ cacheValue = unwrapReturnValue(user)
├─ collectPutRequests(@Cacheable 未命中) → [CachePutRequest]
├─ request.apply:
│ ├─ canPutToCache? unless 无 → true
│ ├─ key 已有(1001)
│ └─ doPut(users, 1001, user) → ConcurrentMapCache.put
│ └─ store.put(1001, toStoreValue(user))
├─ processCacheEvicts(before=false) → 无,跳过
└─ return user
几个值得记住的设计点:
- 查操作元数据、解析缓存集合只在上下文构造时做一次,随后 key 生成、缓存读写都复用——热路径上没有重复 bean 查询。
- 命中与未命中的分叉点在
evaluate的第一个 if:命中且无@CachePut→ 方法根本不执行;未命中 → 方法执行、结果回填。 - 回填动作本质上是"给 @Cacheable 未命中自动补了一次 put"——
collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, ...)这行把"读穿透后回填"变成了 put 流程的一部分,与@CachePut走同一个performCachePut。 - 未命中路径里
key在findCachedValue阶段已经算过一次;回填时getGeneratedKey()复用,不会重复求 SpEL。
18.4.2 @Caching 组合与 @Cacheable + @CachePut 并存
假设:
@Caching(
cacheable = { @Cacheable(cacheNames = "users", key = "#id") },
put = { @CachePut(cacheNames = "users", key = "#id") },
evict = { @CacheEvict(cacheNames = "userStats", allEntries = true) }
)
public User update(Long id, User input) { ... }
上下文构造后三类操作齐备。按主流程顺序走:
processCacheEvicts(before=true):userStats的 evict 默认beforeInvocation=false,不在这一步处理——跳过。findCachedValue:查users缓存。- 命中:
cacheHit != null。 evaluate第一个 if:cacheHit != null && !hasCachePut(contexts)——但这里有@CachePut!hasCachePut返回 true → 条件不成立 → 方法照样执行。这就是"@Cacheable命中了也挡不住@CachePut要跑方法"的源码逻辑。
- 命中:
- 方法执行返回新 user。
cacheHit != null→collectPutRequests(CacheableOperation)被跳过(只在未命中时回填)。collectPutRequests(CachePutOperation)收集到那个 put →performCachePut→ 把新 user 写进users缓存(覆盖旧值)。processCacheEvicts(before=false, result)→userStats整区 clear。
结论:组合注解里,方法永远执行一次;读缓存只是"能不能少执行一次"的优化,写缓存才是 put 的本职。多个操作的叠加不改变"方法执行次数为 1"这一点——这点和很多人直觉里的"注解是拦截面"不同。
18.4.3 @CacheEvict(allEntries = true) 的清理语义
allEntries=true 在 performCacheEvicts 里走 doClear 分支:
- 不需要 key(整区清空,与参数无关);
doClear(cache, beforeInvocation):beforeInvocation=false(默认)→cache.clear();true→cache.invalidate();- 执行时机由
beforeInvocation决定:true = 方法进门前清;false = 方法成功返回后清(方法抛异常则不清)。
配合 processCacheEvicts 的 filter:beforeInvocation == evict.isBeforeInvocation()——所以 allEntries=true 且 beforeInvocation=true 的 evict,发生在所有缓存读写之前;而默认 false 的发生在所有 put 写完之后。这个顺序在源码里是硬编码的,不随注解书写顺序改变。
18.4.4 缓存与事务的组合时序
这是生产环境最容易出问题的组合。设:
@CacheEvict(cacheNames = "users", key = "#id") // beforeInvocation=false(默认)
@Transactional
public void updateUser(Long id, User input) { ... }
两个 advisor 同时存在时,谁先谁后由 order 决定。@EnableCaching(order=LOWEST_PRECEDENCE) 意味着缓存 advice 默认优先级最低——即事务在外层、缓存在内层。运行顺序:
事务拦截器(外层)begin
└─ 缓存拦截器(内层):
├─ 前置 evict(before=true 的话在这,默认 false 不在)
├─ 执行方法(业务写库)
├─ 后置 evict(默认)→ users 缓存里 id=1 的条目此刻就被删了
└─ return
事务拦截器 commit
问题出在:后置 evict 在事务 commit 之前就执行了。此时数据库新值还没落盘,但缓存已经被清空——一个并发读请求在 evict 之后、commit 之前进来,会回源读到旧值(事务未提交,别的连接看不到新数据),然后把旧值回填进缓存。等事务 commit 后,缓存里反而是旧数据,直到下次 evict 才纠正。这就是"缓存与事务提交时机不一致"的经典脏读窗口。
源码层面这个时序是怎么来的:CacheAspectSupport.evaluate 里 processCacheEvicts(before=false, ...) 紧跟在方法执行后、return 前——它根本不知道外面包着事务。事务的提交点在更外层拦截器返回之后。要规避,要么把 evict 设为 beforeInvocation=true(方法进门前就清,事务失败也会清——窗口变成"清缓存 → 方法写库 → 提交",并发读仍可能读到旧库值回填,但至少不会把新事务的未提交错误状态掩盖);要么用事务同步回调在 afterCommit 里手工 evict——Spring 抽象本身默认不提供 afterCommit 时机,这是 SPI 层的留白。
18.4.5 同步(sync=true)路径的并发时序
对比普通路径,sync 把"查 → 回源 → 回填"合并进了一次 computeIfAbsent:
线程 A: doGet(users, 1001, loader)
└─ ConcurrentMapCache.get:
store.computeIfAbsent(1001, k -> toStoreValue(loader.call()))
├─ map 锁定 bin 1001
├─ 线程 B 同时 computeIfAbsent(1001, ...) → 阻塞在同一 bin
├─ loader.call() = 真实查库 → user
├─ store.put(1001, NullValue/user)
└─ 返回
线程 B: computeIfAbsent 拿到 A 写入的值 → loader 不执行
这就是"防击穿":热点 key 过期瞬间,并发请求只有一个回源。注意它的代价也来自源码——前面 determineSyncFlag 的四条限制:不能组合其他操作、只能单缓存、不能 unless。因为整个"回源+写入"被压缩进一个原子单元后,没有任何"写完之后再调整"的余地。
18.4.6 错误发生时的时序分支
以默认 SimpleCacheErrorHandler 为准:
- 读缓存抛异常 →
doGetcatch → handler 原样抛 → 整个调用失败,方法不执行。缓存故障 = 业务故障。 - 写缓存抛异常 →
doPutcatch → handler 原样抛 → 方法已经执行完了,异常这时才冒出来——业务方法成功了,但调用方收到缓存写失败异常。这是默认策略很"刚"的地方。 - 如果换成
LoggingCacheErrorHandler:读异常 →doGet返回 null → 当未命中继续回源;写异常 → 吞掉记日志,方法正常返回。缓存降级对业务透明。
18.5 扩展用法与最佳实践
18.5.1 自定义 CacheManager:SPI 契约
Spring 不绑定任何具体缓存产品——要接 Redis、多级缓存,都是自己实现两个接口。契约很薄:
CacheManager 只有两个方法必须实现:
@Nullable Cache getCache(String name);
Collection<String> getCacheNames();
实现时的关键决策点从源码里可以反推出来:
- 名字查不到时怎么办:返回 null(静态注册表)还是现场创建(动态)?
ConcurrentMapCacheManager用dynamic标志区分;AbstractCacheManager提供了模板——loadCaches()启动时加载一批,getMissingCache(name)给运行时按需创建留了钩子,getCache里 double-check 后登记(前面读过它的源码:先快路径cacheMap.get,miss 再getMissingCache,同步块内二次检查后decorateCache登记)。自己写管理器时,继承AbstractCacheManager然后实现loadCaches()与按需时覆盖getMissingCache()是最省事的路径。 - null 值策略:自己实现
Cache时,要么直接继承AbstractValueAdaptingCache(自动获得toStoreValue/fromStoreValue的 null 哨兵逻辑),要么自己保证get(key)的返回遵守"裸 null = 未命中"的契约。不遵守这个契约会导致findInCaches的命中判断出错。 - 原子加载:
get(key, Callable)要尽量保证同一 key 并发只调一次 loader——这是@Cacheable(sync=true)的全部价值依赖。底层用本地ConcurrentHashMap.computeIfAbsent(如ConcurrentMapCache)或带锁/单flight 的客户端(如 Redis 的 SETNX + 分布式锁)都可以,但语义必须对上。 evictIfPresent/invalidate:如果你的缓存装饰器支持延迟删除(比如事务内缓存),应该覆盖这两个方法返回可靠的 prior-presence 判断;默认实现返回 false 是降级行为。
多级缓存(本地 Caffeine + Redis)的组织方式本质是:写操作同时打两层、读操作先查本地再查远程、远程 miss 再回源。这个编排不是 Spring 注解模型提供的——需要自己实现 Cache 接口,在 get(key, Callable) 里做"本地 miss → 远程 get → 远程 miss → loader 回源 → 写远程 → 写本地"。接口契约不变,复杂度全在实现里。
18.5.2 @CacheConfig:类级默认值
@CacheConfig(annotation/CacheConfig.java)放类上,属性就是 cacheNames/keyGenerator/cacheManager/cacheResolver 四项。合并规则前面读 DefaultCacheConfig.applyDefault 时已经拆解:方法上显式写的优先,没写的从类级继承。典型用法:
@CacheConfig(cacheNames = "users", keyGenerator = "myKeyGenerator")
@Service
public class UserService {
@Cacheable // 自动用 users + myKeyGenerator
public User find(Long id) { ... }
@CachePut(cacheNames = "users") // 显式写,仍用类级 keyGenerator
public User update(Long id, User u) { ... }
}
好处是一个业务域的缓存命名集中管理,改一处全类生效。
18.5.3 CachingConfigurer:全局策略覆盖
四个默认方法(annotation/CachingConfigurer.java):cacheManager()、cacheResolver()、keyGenerator()、errorHandler()。实现这个接口并交给容器,AbstractCachingConfiguration.setConfigurers 会拿它替换默认值。注意两条源码纪律:
- 全容器只能有一个
CachingConfigurer——setConfigurers里找到两个直接抛IllegalStateException。 - 这些方法的返回值会被包成
Supplier延迟解析,所以即使CachingConfigurer实现类本身依赖缓存相关 bean,也不会有循环依赖问题。
自定义 KeyGenerator 是最常见的扩展:
public interface KeyGenerator {
Object generate(Object target, Method method, @Nullable Object... params);
}
比如你的参数里有 DTO 对象,默认 SimpleKeyGenerator 会把整个 DTO 当 SimpleKey——你可以在自定义 generator 里提取 DTO 的 id 字段做 key。
18.5.4 缓存预热
启动后想让热点 key 提前进缓存?框架没有专门的预热 API,但机制很直接:拿 CacheManager 取到 Cache,然后 cache.put(key, value)。CacheManager.getCache("users") 在 dynamic 模式下会自动创建该区域——这也意味着代码里任何一处拼错缓存名,在 dynamic 模式下都会静默创建一个新空缓存,生产环境建议用 setCacheNames(...) 切到静态模式,名字写错直接抛错。
预热代码可以写在 ApplicationRunner 里:查一遍热点数据,逐个 cache.put。注意 put 的值要和"方法返回值经 unwrapReturnValue 处理后的形态"一致——即普通对象(Optional 已解包),框架读出来时会按 wrapCacheValue 再包回去。
18.5.5 错误处理策略的取舍
默认 SimpleCacheErrorHandler 把异常原样抛出——缓存故障直接打挂业务。生产上更常见的是 LoggingCacheErrorHandler:只记 warn 日志。从 AbstractCacheInvoker 的源码行为看它的实际效果:
- 读失败 → 伪装未命中 → 回源查库:数据库顶住,业务可用,但缓存等于没开;
- 写失败 → 静默丢写:数据库是新的,缓存是旧的——这就是脏缓存窗口,必须靠
@CacheEvict的下游清理或短 TTL 兜底。
所以选错误处理器本质是在选"故障时保一致性还是保可用性"。没有绝对正确的答案,按业务读多写少的比例定。
18.6 常见问题与坑
18.6.1 自调用失效
同类内一个方法调另一个带 @Cacheable 的方法,缓存不生效。源码解释:proxy 模式下,缓存 advice 织在代理对象上——this.internalCall() 走的是目标对象自身的引用,根本不经过代理,CacheInterceptor.invoke 压根没被调用。AbstractFallbackCacheOperationSource.computeCacheOperations 解析得再正确,也没人触发它。
修法:把调用方也移到另一个 bean;或注入自己的代理(@Lazy Self self)调 self.method();或 @EnableCaching(mode = AdviceMode.ASPECTJ) 切字节码织入——CachingConfigurationSelector.getAspectJImports 会导入 AspectJCachingConfiguration,自调用也能被织入,但代价是引入 spring-aspects 和编译期/加载期织入。
18.6.2 缓存穿透与 allowNullValues
一个查询永远返回 null 的方法,开了缓存后每次都回源吗?看 ConcurrentMapCache 的链路:方法返回 null → evaluate 走未命中分支 → collectPutRequests 收集 → performCachePut → canPutToCache(unless 无)→ doPut(users, key, null) → ConcurrentMapCache.put → store.put(key, toStoreValue(null))。toStoreValue 在 allowNullValues=true(默认)时存的是 NullValue.INSTANCE。下次同样 key 进来,lookup 返回哨兵对象(非 null)→ toValueWrapper 包成 wrapper → 命中。
如果 allowNullValues=false:toStoreValue(null) 直接抛 IllegalArgumentException("Cache '...' is configured to not allow null values but null was provided")——写 null 就报错。也就是说这个开关一开,"永远返回 null 的方法"连缓存都用不了。这是 ConcurrentMapCacheManager.setAllowNullValues(false) 的真实代价,不是文档里的"优化项"。
18.6.3 key 冲突
两个层面:
- 无参方法共享
SimpleKey.EMPTY:同一缓存区域里,@Cacheable cacheNames="config" public String getA()和public String getB()的 key 都是 EMPTY——getA的结果会被getB读出来。SimpleKeyGenerator.generateKey的源码明确params.length == 0 → SimpleKey.EMPTY,不带方法名做区分。多参数方法同理:两个(Long a, Long b)方法的 SimpleKey 数组元素相同就相等。 - 单参直接用参数本身:
findById(Long id)和findByToken(String token)如果用同一个缓存区域,Long 1001与String "1001"的 equals/hashCode 天然不同,安全;但如果两个方法都用Long参数、且值域重叠,就撞。
实践纪律:不同业务用不同 cacheNames 隔离;同区域多方法时手写 key 表达式带上方法语义,如 key = "#root.methodName + '_' + #id"。
18.6.4 @CachePut 与 @Cacheable 同方法组合的顺序
前面运行机制一节已拆解:方法恒执行一次;@Cacheable 的命中检查在 findCachedValue,但只要存在 condition 通过的 @CachePut(hasCachePut),evaluate 就放弃"纯命中返回"分支,方法必跑,结果由 @CachePut 写缓存。顺序是源码硬编码的:前置 evict → 命中检查 → 方法执行 → put 收集(@Cacheable 未命中回填 + @CachePut)→ 后置 evict,与注解书写顺序无关。
18.6.5 sync 模式的限制
以 determineSyncFlag 源码为准,违反即抛 IllegalStateException:
- 不能与
@CachePut/@CacheEvict同方法; - 同方法只能有一个 sync 的
@Cacheable; - 只能解析出单个
Cache; - 不能设
unless。
常见疑问"sinc=true 时 unless 为什么不行":因为 unless 是"方法执行后判断要不要写缓存",而 sync 模式把方法执行与写入合并进了一次 Cache.get(key, Callable) 原子调用——loader 一旦执行完值就已经在 map 里了,没有"执行完再反悔不写"的步骤。框架选择启动时直接报错,而不是静默忽略 unless。
18.6.6 缓存与事务的提交时机
见 18.4.4:默认 beforeInvocation=false 的 evict 在事务提交前执行,存在"evict 后、commit 前的并发回源把旧值回填"的窗口。缓解思路:对写方法的 evict 用 beforeInvocation=true(先清后写,代价是方法异常也会清缓存);或自定义 evict 时机到 afterCommit;或缩短热点 key 的 TTL。
18.6.7 非 public 方法上的注解
AbstractFallbackCacheOperationSource.computeCacheOperations 里 allowPublicMethodsOnly() && !Modifier.isPublic → null。而 ProxyCachingConfiguration 注册的是 new AnnotationCacheOperationSource(false)——6.0 之后 CGLIB 代理下 protected 方法也认。但注意:
- private 方法永远不行:CGLIB 子类化也拦不住 private(根本不可继承);
- JDK 接口代理下,方法必须通过接口暴露;
- 即便注解被识别,
this.自调用照样绕过代理。
最稳妥的纪律:缓存注解只写在 public、且通过代理对象调用的方法上。
18.7 本章小结
本章从两个 SPI 接口讲起,沿着"注解 → 装配 → 代理 → 拦截器 → 编排 → 缓存实现"这条线走完了 Spring 7.0.9 缓存抽象的全貌。收束成几条源码级结论:
- 抽象分层:
Cache(一个缓存区域的读写删)与CacheManager(按名管理区域)是底层 SPI;CacheOperation(注解元数据)、KeyGenerator(key)、CacheResolver(名字→Cache 实例)、CacheErrorHandler(失败策略)是执行层的四个可替换策略;CacheInterceptor/CacheAspectSupport是编排核心。 Cache.get(Object)返回ValueWrapper而非裸值,是为了区分"未命中"与"命中 null";null 值在AbstractValueAdaptingCache.toStoreValue里换成NullValue.INSTANCE哨兵,使不支持 null 的ConcurrentHashMap也能缓存"方法返回 null"这一事实。- 装配链:
@EnableCaching通过@Import(CachingConfigurationSelector.class)触发;PROXY 模式下注册三个基础设施 bean——internalCacheAdvisor(advisor)、cacheOperationSource(AnnotationCacheOperationSource(false),6.0 起 CGLIB 下认 protected)、cacheInterceptor;CachingConfigurer全容器至多一个,四个策略全部懒解析。 CacheAspectSupport.execute的固定顺序:前置 evict(beforeInvocation=true)→findCachedValue逐@Cacheable查缓存 →evaluate:命中且无@CachePut则直接返回,否则执行方法 → 收集 put(@Cacheable 未命中回填 + @CachePut)→ 后置 evict(beforeInvocation=false,此时#result可用)。sync=true 时整个编排切到executeSynchronized,用Cache.get(key, Callable)的原子加载防击穿。- sync=true 的四条限制(
determineSyncFlag校验):不可与其他操作共存、仅一个 sync 操作、仅单缓存、不可用 unless——都是启动即报错。 - SpEL 三态 result:
NO_RESULT(方法前,#result未定义)、真实返回值(方法后,可用)、RESULT_UNAVAILABLE(黑名单,访问即抛VariableNotAvailableException,用于hasCachePut保守预判)。 - 默认错误策略是"刚"的:
SimpleCacheErrorHandler原样抛异常;换LoggingCacheErrorHandler后读失败伪装未命中回源、写失败静默丢失——这是可用性与一致性的取舍。 - 高频坑:自调用绕过代理、dynamic 模式下拼错缓存名静默建新区域、无参方法共享
SimpleKey.EMPTY、@Cacheable+@CachePut同方法时方法恒执行一次、默认 evict 在事务提交前执行造成脏读窗口、private 方法注解永不生效。
本章关键类清单
| 类名 | 模块与相对路径 | 职责一句话 |
|---|---|---|
Cache | spring-context/src/main/java/org/springframework/cache/Cache.java | 缓存区域 SPI:getName/getNativeCache/get/put/evict/clear 及 ValueWrapper 嵌套接口 |
CacheManager | spring-context/src/main/java/org/springframework/cache/CacheManager.java | 缓存管理器 SPI:按名取 Cache、列举全部缓存名、resetCaches |
ConcurrentMapCache | spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java | 基于 ConcurrentHashMap 的 Cache 实现,get(key,Callable) 走 computeIfAbsent 原子加载 |
ConcurrentMapCacheManager | spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java | 动态/静态两种模式的缓存管理器,按需 createConcurrentMapCache |
AbstractValueAdaptingCache | spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java | null 值哨兵适配基类:toStoreValue/fromStoreValue/toValueWrapper |
NullValue | spring-context/src/main/java/org/springframework/cache/support/NullValue.java | 用户 null 的存储哨兵单例,readResolve 保证反序列化唯一 |
SimpleValueWrapper | spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java | ValueWrapper 的朴素持有实现 |
AbstractCacheManager | spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java | 静态缓存管理器模板:启动 loadCaches + 运行时 getMissingCache 双检登记 |
EnableCaching | spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java | 开关注解,@Import(CachingConfigurationSelector.class),mode/proxyTargetClass/order |
CachingConfigurationSelector | spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java | 按 AdviceMode 选择 PROXY/ASPECTJ 配置,软探测 JSR-107 |
ProxyCachingConfiguration | spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java | 注册 advisor/operationSource/interceptor 三个基础设施 bean |
AbstractCachingConfiguration | spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java | 接住 @EnableCaching 属性,对接唯一 CachingConfigurer |
CachingConfigurer | spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java | 全局策略回调接口:cacheManager/cacheResolver/keyGenerator/errorHandler |
SpringCacheAnnotationParser | spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java | 四个缓存注解 → CacheOperation,含 @CacheConfig 默认合并与互斥校验 |
AnnotationCacheOperationSource | spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java | 注解版 CacheOperationSource,持有 publicMethodsOnly 标志 |
AbstractFallbackCacheOperationSource | spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java | 方法→操作的四级回退查找 + operationCache 缓存 + 无操作哨兵 |
CacheInterceptor | spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java | AOP Alliance 适配器:ThrowableWrapper 包装 proceed,转交 execute |
CacheAspectSupport | spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java | 执行编排核心:execute/findCachedValue/evaluate/evict 调度/sync 路径 |
AbstractCacheInvoker | spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheInvoker.java | doGet/doPut/doEvict/doClear:缓存异常分流到 CacheErrorHandler |
SimpleCacheErrorHandler | spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java | 默认错误策略:异常原样抛出 |
LoggingCacheErrorHandler | spring-context/src/main/java/org/springframework/cache/interceptor/LoggingCacheErrorHandler.java | 降级策略:只记 warn 日志,业务继续 |
KeyGenerator | spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java | key 生成 SPI(函数式接口) |
SimpleKeyGenerator | spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java | 默认 key 生成:无参 EMPTY/单参本体/其余 SimpleKey |
SimpleKey | spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java | 多参数 key:deepEquals + MurmurHash3 混合 hashCode |
CacheOperationExpressionEvaluator | spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java | SpEL 求值器:key/condition/unless 三个表达式缓存 + NO_RESULT/RESULT_UNAVAILABLE |
CacheEvaluationContext | spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java | SpEL 上下文:unavailableVariables 黑名单,访问即抛 VariableNotAvailableException |
CacheExpressionRootObject | spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java | #root 根对象:method/methodName/target/targetClass/args/caches |
CacheableOperation/CachePutOperation/CacheEvictOperation | spring-context/src/main/java/org/springframework/cache/interceptor/ | 注解元数据不可变对象族(Builder 构造) |
SimpleCacheResolver/AbstractCacheResolver | spring-context/src/main/java/org/springframework/cache/interceptor/ | 名字→Cache 实例解析:按 operation.getCacheNames() 向 CacheManager 查找 |
BeanFactoryCacheOperationSourceAdvisor | spring-context/src/main/java/org/springframework/cache/interceptor/BeanFactoryCacheOperationSourceAdvisor.java | advisor:pointcut 委托 CacheOperationSource.hasCacheOperations |
CacheOperationSourcePointcut | spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java | 静态方法匹配点切 + 排除 CacheManager 实现类的类过滤器 |
CacheManagementConfigUtils | spring-context/src/main/java/org/springframework/cache/config/CacheManagementConfigUtils.java | 基础设施 bean 名常量(internalCacheAdvisor 等) |