Spring 的声明式缓存注解(@Cacheable
, @CachePut
, @CacheEvict
等)是 AOP 技术在实际应用中最强大、最经典的范例之一,其原理与 @Transactional
非常相似。
核心思想:一个智能的“秘书”
你可以把 @Cacheable
的 AOP 实现想象成一个极其高效的“秘书”。
- 你 (调用方):老板。
- 目标方法 (
findProductById
): 一个需要花费大量时间和精力去调研才能回答的“问题”(比如去档案馆查资料)。 - AOP 代理 (Proxy): 你的秘书。
- 缓存 (Cache): 秘书的“备忘录”或“笔记”。
当你向秘书提出一个问题时(调用方法):
- 秘书首先会翻看自己的备忘录(查缓存)。
- 如果备忘录里有答案(缓存命中),秘书会直接告诉你答案,然后回去继续做其他事。他根本不会去档案馆进行那次耗时又费力的调研。你作为老板,很快就得到了答案,甚至不知道秘书是直接给的还是去查了资料。
- 如果备忘录里没有答案(缓存未命中),秘书会告诉你:“老板,请稍等,我需要去查一下。” 然后他会亲自去档案馆进行调研(执行目标方法)。
- 当他拿到调研结果后,他会非常聪明地先把结果记在自己的备忘录里(放入缓存),以备你下次再问。
- 最后,他才把这个调研结果告诉你。
@Cacheable
的 AOP 实现原理
@Cacheable
的背后是一个功能极其强大的环绕通知 (@Around
),由 Spring 的 CacheInterceptor
实现。
-
代理和拦截:和事务一样,当 Spring 发现一个 Bean 的方法上有
@Cacheable
注解时,会为它创建一个 AOP 代理对象。所有对该方法的调用都会先被这个代理对象拦截。 -
CacheInterceptor
(环绕通知) 开始工作:
这个拦截器在调用真正的目标方法之前,会执行以下逻辑:a. 生成缓存键 (Cache Key):
* 拦截器会解析@Cacheable
注解。
* 它使用value
属性 (如"products"
) 作为缓存的命名空间(或称为缓存区域)。
* 它使用key
属性 (如"#id"
) 中的 SpEL (Spring Expression Language) 表达式来根据方法参数动态生成一个唯一的键。例如,当调用findProductById(123L)
时,#id
会被替换为123L
,最终生成的键可能是"products::123"
。b. 查询缓存:
* 拦截器拿着这个生成的键,去配置好的缓存管理器(CacheManager
,其背后可能是 Redis, Caffeine, EhCache 等)中进行查询。 -
决策点:缓存是否命中?
这是环绕通知威力最大的体现,因为它可以控制目标方法是否执行。-
情况一:缓存命中 (Cache Hit)
- 拦截器在缓存中找到了对应的值。
- 它会立即将这个值返回给调用方。
- 最关键的一步:目标方法
findProductById
根本不会被执行! 这就是所谓的“方法短路 (Short-Circuiting)”。它避免了数据库查询或远程调用。
-
情况二:缓存未命中 (Cache Miss)
- 拦截器在缓存中没有找到任何东西。
- 它会继续执行调用链,即调用
pjp.proceed()
来执行原始的目标方法findProductById
。 - 当目标方法成功执行并返回一个结果后,拦截器会捕获这个返回值。
- 它会将这个返回值以之前生成的缓存键存入缓存中,以备下次使用。
- 最后,将这个返回值交还给调用方。
-
其他缓存注解的 AOP 原理
-
@CachePut
:- 这个注解同样使用 AOP 拦截。
- 但它的逻辑是总会执行目标方法(比如更新数据库的操作)。
- 在方法执行成功后,它会总是将方法的返回值更新到缓存中。
- 它用于确保在数据更新后,缓存中的数据也是最新的。它没有“短路”行为。
-
@CacheEvict
:- 这个注解也使用 AOP 拦截。
- 它的逻辑通常是在目标方法成功执行后(默认),根据
key
去缓存中删除一个或多个条目。 - 它用于在删除或修改数据后,清除相关的旧缓存,确保数据一致性。
总结
注解 | AOP 实现方式 (简化理解) | 是否执行目标方法? |
---|---|---|
@Cacheable | @Around :查缓存 -> (命中) 短路返回 / (未命中) 执行并存入缓存 | 不一定 |
@CachePut | @Around :执行方法 -> 将结果更新到缓存 | 总是执行 |
@CacheEvict | @AfterReturning (默认):执行方法 -> 从缓存中删除条目 | 总是执行 |
所以,Spring 的声明式缓存是 AOP 的实践之一。它通过 AOP 将复杂的、与业务无关的缓存管理逻辑完全分离,让开发者只需关注业务本身,极大地提高了代码的可读性和可维护性。