AOP 实现 Redis 缓存切面解析
这段代码是一个基于 Spring AOP + Redis 的通用缓存切面实现,通过自定义 @Cache 注解实现方法级别的缓存读写,减少重复数据库查询。
一、核心结构与依赖
1. 切面基础定义
java
运行
@Aspect
@Component
@Slf4j
public class CacheAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Pointcut("@annotation(com.mszlu.blog.common.cache.Cache)")
public void pt(){}
}
@Aspect:标记为切面类,定义切点与通知逻辑@Component:交给 Spring 管理,切面才能生效@Slf4j:Lombok 日志注解,简化日志输出@Pointcut:切点表达式,匹配所有标注@Cache注解的方法
二、核心流程:环绕通知(@Around)
1. 步骤拆解
① 获取方法信息与参数
java
运行
Signature signature = pjp.getSignature();
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = signature.getName();
Class[] parameterTypes = new Class[pjp.getArgs().length];
Object[] args = pjp.getArgs();
String params = "";
for(int i=0; i<args.length; i++) {
if(args[i] != null) {
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
} else {
parameterTypes[i] = null;
}
}
// 参数MD5加密,避免key过长
if (StringUtils.isNotEmpty(params)) {
params = DigestUtils.md5Hex(params);
}
- 用
ProceedingJoinPoint获取目标类名、方法名、参数列表 - 参数转为 JSON 字符串后,用 MD5 加密生成唯一标识,避免 Redis key 过长
- 后续用
class+method+params拼接 Redis key,保证不同方法 / 参数的缓存隔离
② 获取@Cache注解配置
java
运行
Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
Cache annotation = method.getAnnotation(Cache.class);
long expire = annotation.expire();
String name = annotation.name();
- 通过反射获取方法上的
@Cache注解,读取name(缓存前缀)和expire(过期时间)
③ 拼接 Redis Key
java
运行
String redisKey = name + "::" + className + "::" + methodName + "::" + params;
Key 格式:缓存前缀::类名::方法名::参数MD5值,确保唯一性
④ 缓存读取(命中则直接返回)
java
运行
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)) {
log.info("走了缓存~~~,{}", redisKey);
Result result = JSON.parseObject(redisValue, Result.class);
return result;
}
- 从 Redis 查询 key,若存在则直接解析 JSON 为
Result对象并返回 - 跳过目标方法执行,减少 DB 查询
⑤ 目标方法执行 + 缓存写入
java
运行
Object proceed = pjp.proceed();
redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存~~~ {},{}", className, methodName);
return proceed;
- 调用
pjp.proceed()执行原方法,获取返回结果 - 将结果转为 JSON 字符串,存入 Redis 并设置过期时间
- 返回原方法结果,后续相同请求会直接命中缓存
⑥ 异常处理
java
运行
catch (Throwable throwable) {
throwable.printStackTrace();
return Result.fail(code: -999, msg: "系统错误");
}
- 捕获所有异常,打印栈信息并返回统一错误结果,避免因缓存逻辑异常导致接口直接报错
三、关键优化与设计亮点
- 通用缓存 Key 设计 用
name+className+methodName+params拼接 Key,支持多模块、多方法的缓存隔离,参数 MD5 加密避免 key 过长和特殊字符问题。 - 无侵入式缓存 业务方法仅需加
@Cache(expire=xxx, name="xxx")注解,无需修改业务代码,解耦缓存逻辑。 - 缓存过期控制 注解中配置的
expire参数直接控制 Redis 过期时间,支持不同方法设置不同缓存有效期。 - 异常降级缓存逻辑异常时,捕获错误并返回友好提示,不影响核心业务(也可改为直接执行原方法,降级为 "缓存失效")。
四、可优化点(面试 / 项目拓展)
-
参数类型获取问题 当前用
getMethod(methodName, parameterTypes)获取方法时,若参数为 null,parameterTypes[i] = null会导致反射报错。优化方案:用org.aspectj.lang.reflect.MethodSignature直接获取方法对象:java
运行
MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); -
空值缓存防穿透 若 DB 查询结果为 null,当前逻辑不会写入缓存,导致缓存穿透。可添加空值缓存(设置较短过期时间):
java
运行
if (proceed == null) { redisTemplate.opsForValue().set(redisKey, "null", Duration.ofMillis(30000)); } -
缓存击穿 / 雪崩优化 可添加本地锁(如
synchronized)或分布式锁(Redis SETNX),防止热点 key 过期时大量请求打向 DB。 -
序列化统一 直接用
JSON.toJSONString序列化,建议配置RedisTemplate的序列化器(如 Jackson2JsonRedisSerializer),保证序列化一致性。
五、使用示例
在业务方法上添加@Cache注解即可生效:
java
运行
@Cache(name = "article", expire = 600000) // 缓存10分钟
public Result listArticle(PageParams pageParams) {
// 原业务代码(如DB查询)
return Result.success(records);
}
调用时,第一次请求执行原方法并写入 Redis,后续相同参数请求直接返回缓存结果。