通过自定义AOP实现二级缓存注解功能
通过自定义AOP实现二级缓存注解功能
面试官问:你的项目中用到AOP了吗?
1 AOP是什么
AOP是面向切面编程
- AOP将通用的代码逻辑从业务代码中抽离出来,然后在运行时自动放到指定位置进行运行
- 让你不用手写重复代码,也能在某些方法执行前后自动加上这些通用逻辑
常用于:
- 日志
- 权限校验
- 事务处理
- 缓存
2 项目中如何使用AOP实现二级缓存的
2.1 概述
核心机制基于自定义注解@L2Cache,通过AOP切面L2CacheAspect拦截标注该注解的方法,实现缓存逻辑。该实现不依赖Spring Cache抽象,而是手动管理Caffeine和Redis实例,支持三种操作模式(FULL、PUT、DELETE)
2.2 配置与依赖注入
二级缓存的配置主要在SimpleCacheProperties和L2CacheConfig中定义,确保Caffeine缓存实例被正确初始化,并注入到切面中。
- SimpleCacheProperties:这是一个@ConfigurationProperties类,从application.properties或YAML配置文件中读取以”l2cache”前缀的属性
- L2CacheConfig:这是一个配置类,使用@EnableCaching启用Spring缓存(虽未直接使用Spring CacheManager,但为兼容性考虑)。它注入SimpleCacheProperties,并创建Caffeine缓存实例
2.3 自定义注解 @L2Cache
注解定义在L2Cache.java中,用于标记目标方法:
@Target(ElementType.METHOD):仅适用于方法。@Retention(RetentionPolicy.RUNTIME):运行时保留,便于AOP反射读取。参数:
cacheName():缓存命名空间,默认”aimin-cache”,用于前缀键(如”aimin-cache::user:1”)。key():必填,SpEL表达式,用于动态生成键(如”#user.id”,基于方法参数)。l2TimeOut():Redis过期时间(秒),默认120。type():操作类型,默认FULL(详见CacheType枚举)。
示例使用:
1 | |
2.4 AOP切面实现 (L2CacheAspect)
切面类使用@Aspect和@Component标记,是Spring管理的Bean。核心是@Around环绕通知,拦截所有标注@L2Cache的方法。实现缓存的逻辑。
2.4.1 切点定义
1 | |
这匹配任何带有@L2Cache的方法。
2.4.2 环绕通知 doAround():
这是一个完整的AOP拦截逻辑,包围目标方法执行(point.proceed())。步骤如下:
获取方法信息和参数:
- 通过MethodSignature获取方法和注解。
- 构建TreeMap<String, Object>映射参数名和值。
根据type执行不同逻辑:
CacheType.PUT(强制更新):
- 执行目标方法(point.proceed(),通常是数据库更新/插入)。
- 将结果写入Redis(set with timeout)。
- 将结果写入Caffeine(put)。
- 返回结果。
- 用途:确保缓存同步,常用于更新操作。
CacheType.DELETE(删除):
- 从Redis删除键。
- 从Caffeine invalidate键。
- 执行目标方法(可能清理数据库)。
- 返回结果。
- 用途:清除失效缓存。
CacheType.FULL(默认,查询+缓存):
- 查询流程:
- 先查Caffeine(getIfPresent):若命中,日志”get data from caffeine”并返回。
- 若未命中,查Redis(opsForValue().get):若命中,日志”get data from redis”,回写到Caffeine,返回。
- 若均未命中,执行目标方法(point.proceed(),日志”get data from database”)。
- 回写流程(若结果非null):
- 写入Redis(set with l2TimeOut)。
- 写入Caffeine(put)。
- 返回结果。
- 空结果(null)不缓存,避免缓存穿透(但代码中未显式处理null,可能需业务层处理)。
- 查询流程:
2.5 整体工作流程示例
假设一个标注@L2Cache(key="#id", type=FULL)的方法getUserById(Long id):
- 调用getUserById(123)。
- AOP拦截,解析键为”aimin-cache::123”。
- 查Caffeine:若命中,返回。
- 查Redis:若命中,回写Caffeine,返回。
- 执行方法:查DB得到User,回写Redis(过期120s)和Caffeine,返回User。
对于更新:使用PUT类型,执行DB更新后强制回写两级缓存。
3. AOP起到的作用
所有缓存相关的重复操作(查缓存、写缓存、删缓存)全部由AOP自动完成
自动拦截方法
你只要在方法上加一个@L2Cache注解,AOP就会自动“抓住”这个方法,不需要你在每个业务方法里手写任何缓存代码。在不改业务代码的情况下,自动加上缓存逻辑
业务方法本来只负责查数据库:1
2
3
4@L2Cache(key = "#id", type = CacheType.FULL)
public User getUserById(Long id) {
return userMapper.selectById(id); // 只写这一句就够了!
}AOP会自动在执行这句代码之前先查 Caffeine → 查 Redis
如果都没命中,才执行上面的数据库查询
查询完后又自动把结果写回 Redis 和 Caffeine
你完全不用管这些,代码看起来超级干净!统一处理所有缓存操作
同样的缓存逻辑(查两级、写两级、删除、强制更新)在项目里可能有几十上百个地方。
用了AOP之后,所有逻辑都集中在L2CacheAspect这一个类里:
→ 以后改缓存策略(比如换Redis过期时间、加布隆过滤器)只需要改一个文件
→ 不会出现“这个方法写了缓存、那个方法忘记写”的低级bug