引言:为什么需要声明式缓存?
- 背景痛点:传统代码中缓存逻辑与业务逻辑高度耦合,存在重复代码、维护困难等问题(如手动判断缓存存在性、序列化/反序列化操作)
- 解决方案:通过注解+AOP实现缓存逻辑与业务解耦,开发者只需关注业务,通过注解配置缓存策略(如过期时间、防击穿机制等)
- 技术价值:提升代码可读性、降低维护成本、支持动态缓存策略扩展。
核心流程设计:- 方法调用 → 切面拦截 → 生成缓存Key → 查询Redis →
- └ 命中 → 直接返回缓存数据
- └ 未命中 → 加锁查DB → 结果写入Redis → 返回数据
复制代码 二、核心实现步骤
1. 定义自定义缓存注解(如@RedisCache)
- package com.mixchains.ytboot.common.annotation;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- import java.util.concurrent.TimeUnit;
- /**
- * @author 卫相yang
- * OverSion03
- */
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface RedisCache {
- /**
- * Redis键前缀(支持SpEL表达式)
- */
- String key();
- /**
- * 过期时间(默认1天)
- */
- long expire() default 1;
- /**
- * 时间单位(默认天)
- */
- TimeUnit timeUnit() default TimeUnit.DAYS;
- /**
- * 是否缓存空值(防穿透)
- */
- boolean cacheNull() default true;
- }
复制代码 2. 编写AOP切面(核心逻辑)
切面职责:
- 缓存Key生成:拼接类名、方法名、参数哈希(MD5或SpEL动态参数)本次使用的是SpEL
- 缓存查询:优先从Redis读取,使用FastJson等工具反序列化
空值缓存:缓存值并设置短过期时间,防止恶意攻击- package com.mixchains.ytboot.common.aspect;
- import com.alibaba.fastjson.JSON;
- import com.mixchains.ytboot.common.annotation.RedisCache;
- import io.micrometer.core.instrument.util.StringUtils;
- import lombok.extern.slf4j.Slf4j;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.reflect.MethodSignature;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.core.DefaultParameterNameDiscoverer;
- import org.springframework.core.ParameterNameDiscoverer;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.expression.EvaluationContext;
- import org.springframework.expression.ExpressionParser;
- import org.springframework.expression.spel.standard.SpelExpressionParser;
- import org.springframework.expression.spel.support.StandardEvaluationContext;
- import org.springframework.stereotype.Component;
- import java.lang.reflect.Method;
- import java.lang.reflect.Type;
- /**
- * @author 卫相yang
- * OverSion03
- */
- @Aspect
- @Component
- @Slf4j
- public class RedisCacheAspect {
- @Autowired
- private RedisTemplate<String, String> redisTemplate;
- private final ExpressionParser parser = new SpelExpressionParser();
- private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
- @Around("@annotation(redisCache)")
- public Object around(ProceedingJoinPoint joinPoint, RedisCache redisCache) throws Throwable {
- Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
- // 解析SpEL表达式生成完整key
- String key = parseKey(redisCache.key(), method, joinPoint.getArgs());
- // 尝试从缓存获取
- String cachedValue = redisTemplate.opsForValue().get(key);
- if (StringUtils.isNotBlank(cachedValue)) {
- Type returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
- return JSON.parseObject(cachedValue, returnType);
- }
- // 执行原方法
- Object result = joinPoint.proceed();
- // 处理缓存存储
- if (result != null || redisCache.cacheNull()) {
- String valueToCache = result != null ?
- JSON.toJSONString(result) :
- (redisCache.cacheNull() ? "[]" : null);
- if (valueToCache != null) {
- redisTemplate.opsForValue().set(
- key,
- valueToCache,
- redisCache.expire(),
- redisCache.timeUnit()
- );
- }
- }
- return result;
- }
- private String parseKey(String keyTemplate, Method method, Object[] args) {
- String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
- EvaluationContext context = new StandardEvaluationContext();
- if (paramNames != null) {
- for (int i = 0; i < paramNames.length; i++) {
- context.setVariable(paramNames[i], args[i]);
- }
- }
- return parser.parseExpression(keyTemplate).getValue(context, String.class);
- }
- }
复制代码 代码片段示例:- @RedisCache(
- key = "'category:homeSecond:' + #categoryType", //缓存的Key + 动态参数
- expire = 1, //过期时间
- timeUnit = TimeUnit.DAYS // 时间单位
- )
- @Override
- public ReturnVO<List<GoodsCategory>> listHomeSecondGoodsCategory(Integer level, Integer categoryType) {
- // 数据库查询
- List<GoodsCategory> dbList = goodsCategoryMapper.selectList(
- new LambdaQueryWrapper<GoodsCategory>()
- .eq(GoodsCategory::getCategoryLevel, level)
- .eq(GoodsCategory::getCategoryType, categoryType)
- .eq(GoodsCategory::getIsHomePage, 1)
- .orderByDesc(GoodsCategory::getHomeSort)
- );
- // 设置父级UUID(可优化为批量查询)
- List<Long> parentIds = dbList.stream().map(GoodsCategory::getParentId).distinct().collect(Collectors.toList());
- Map<Long, String> parentMap = goodsCategoryMapper.selectBatchIds(parentIds)
- .stream()
- .collect(Collectors.toMap(GoodsCategory::getId, GoodsCategory::getUuid));
- dbList.forEach(item -> item.setParentUuid(parentMap.get(item.getParentId())));
- return ReturnVO.ok("列出首页二级分类", dbList);
- }
复制代码 最终效果:
到此这篇关于Redis+自定义注解+AOP实现声明式注解缓存查询的示例的文章就介绍到这了,更多相关Redis 声明式注解缓存查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/database/338869uoe.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |