首页 > 代码库 > Spring通过AOP实现对Redis的缓存同步
Spring通过AOP实现对Redis的缓存同步
废话不多说
说下思路:使用aop注解,在Service实现类添加需要用到redis的方法上,当每次请求过来则对其进行拦截,如果是查询则从redis进行get key,如果是update则删除key,防止脏数据或者历史数据出现。建议aop不懂的同学或者SPEL也不太熟悉的先去看看资料再回过来看,会事半功倍。
1.首先贴上核心注解类
@Retention(RetentionPolicy.RUNTIME)@Target({ ElementType.METHOD })public @interface RedisLogService { enum CACHE_OPERATION { FIND, // 查询缓存操作 UPDATE, // 需要执行修改缓存的操作 INSERT; // 需要执行新增缓存的操作 } /** 存储的分组 */ String[] group(); /** 当前缓存操作类型 */ CACHE_OPERATION cacheOperation() default CACHE_OPERATION.FIND; /** 存储的Key 默认加入类名跟方法名 */ String key() default ""; /** 是否使用缓存 */ boolean use() default true; /** 超时时间 */ int expire() default 0; enum LOG_OPERATION { ON, // 开启日志记录 OFF, // 关闭日志记录 } /** 当前缓存操作类型 */ LOG_OPERATION logOperation() default LOG_OPERATION.ON; /** 操作名称 */ String name() default ""; /** 操作参数 */ String param() default ""; /** 日志参数 操作人操作IP,操作IP归属地 */ String logParam() default "";
解释下部分参数
enum CACHE_OPERATION {
FIND, // 查询缓存操作
UPDATE, // 需要执行修改缓存的操作
INSERT; // 需要执行新增缓存的操作
}
默认是Find,如果是update则在Service上面的注解进行改变就可
2.Service使用方法
@RedisLogService(group = {
"group.news" }, key = "#record", name = "网站维护-公司新闻管理-分页查询公司新闻", param = "#record", logParam = "#map")
在这里我是以查询为例子,这里我只加了5个参数,第一个是group是组的意思,用来作为前缀标识某个模块,例如新闻模块统一是group.news开头;第二个key可以理解为存储在redis的key,后续会做详细解答;第三个name就不解释了,展示而已;第四个param则是操作参数;第五个是日志的,这个不用管。
3.贴上具体拦截类
@Aspect@Order(value = 1)@Component("redisLogServiceInterceptor")public class RedisLogServiceInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RedisLogServiceInterceptor.class); @Autowired private UserLogRecordService userLogRecordService; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * * * @Title: execute * @Description: 切入点业务逻辑 * @param proceedingJoinPoint * @return */ @Around("@annotation(RedisLogService)") public Object execute(ProceedingJoinPoint proceedingJoinPoint) throws ServiceException { Object result = null; try { Method method = getMethod(proceedingJoinPoint); // 获取注解对象 RedisLogService redisLogService = method.getAnnotation(RedisLogService.class); // 判断是否使用缓存 boolean useRedis = redisLogService.use(); if (useRedis) { // 使用redis ValueOperations<String, Object> operations = redisTemplate.opsForValue(); // 判断当前操作 switch (redisLogService.cacheOperation()) { case FIND: result = executeDefault(redisLogService, operations, proceedingJoinPoint, method); break; case UPDATE: result = executeUpdate(redisLogService, operations, proceedingJoinPoint); break; case INSERT: result = executeInsert(redisLogService, operations, proceedingJoinPoint); break; default: result = proceedingJoinPoint.proceed(); break; } } else { result = proceedingJoinPoint.proceed(); } } catch (ServiceException e) { throw e; } catch (Throwable e) { throw new ServiceException(new Result<Object>("500", e.getMessage()), e); } return result; }
/**
*
* @Title: getMethod
* @Description: 获取被拦截方法对象
* @param joinPoint
* @return
*/
protected Method getMethod(JoinPoint joinPoint) throws Exception {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
return method;
}
可以看到这里通过@Around环绕切面这个注解,因为@Around是可以同时在所拦截方法的前后执行一段逻辑,比方说我在查询前先去redis查,发现没有就从数据库查,查完了我还需要返回再把结果set到redis,所以说这里就起到了很大的作用。
4.下面贴上查询的具体实现方法
/** * * @Title: executeDefault * @Description: 默认操作的执行 * @param redisLogService * @param result * @param operations * @param proceedingJoinPoint * @param method * @throws Throwable */ @SuppressWarnings("unchecked") private Object executeDefault(RedisLogService redisLogService, ValueOperations<String, Object> operations, ProceedingJoinPoint proceedingJoinPoint, Method method) throws Throwable { Object result = null; Object[] args = proceedingJoinPoint.getArgs(); // 获取被拦截方法参数名列表(使用Spring支持类库) LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = u.getParameterNames(method); // 获取key的后缀的参数名 String key = redisLogService.key(); if (StringUtils.isNotBlank(key)) { // 使用SPEL进行key的解析 ExpressionParser parser = new SpelExpressionParser(); // SPEL上下文 StandardEvaluationContext context = new StandardEvaluationContext(); // 把方法参数放入SPEL上下文中 for (int i = 0; i < paraNameArr.length; i++) { context.setVariable(paraNameArr[i], args[i]); } Object object = parser.parseExpression(key).getValue(context); if (null != object) { if (object instanceof Map<?, ?>) { key = GzdtlStringUtil.transMapToString((Map<String, Object>) object); } else if (object instanceof Collection<?>) { Collection<Object> collection = (Collection<Object>) object; StringBuffer stringBuffer = new StringBuffer(); for (Object o : collection) { stringBuffer.append(o.toString()); } key = stringBuffer.toString(); } else { key = object.toString(); } } } String className = proceedingJoinPoint.getTarget().getClass().getName(); if (className.indexOf(".") >= 0) { className = className.substring(className.lastIndexOf(".") + 1, className.length()); } String methodName = method.getName(); String[] group = redisLogService.group(); if (null != group && group.length > 0) { if (StringUtils.isNotBlank(key)) { key = group[0] + ":" + className + ":" + methodName + ":" + key; } else { key = group[0] + ":" + className + ":" + methodName; } } else { if (StringUtils.isNotBlank(key)) { key = "group" + ":" + className + ":" + methodName + ":" + key; } else { key = "group" + ":" + className + ":" + methodName; } } result = operations.get(key); // 如果缓存没有数据则更新缓存 if (result == null) { result = proceedingJoinPoint.proceed(); int expire = redisLogService.expire(); // 更新缓存 if (expire > 0) { operations.set(key, result, expire, TimeUnit.SECONDS); } else { operations.set(key, result); } } return result; }
解释一下 Object[] args = proceedingJoinPoint.getArgs(); 这里是从方法内取出参数
这里强调一下关于proceedingJoinPoint.getArgs();是有条件的,
public Result<PageInfo<WebInfoBase>> findPageByParam(WebInfoFindParam record, Map<String, String> map)
上面这个方法我是传入的是实体bean,别名一定要写record,不然后面解析的时候会解析不到,map是我的记录日志的参数,暂时不用管。
接着使用SPEL进行解析取出查询参数。
组装Key是grop+类名+方法名+传入的参数组成唯一的key。
最后是从redis get这个key,发现没有就放过让其查询完毕成功return后回到aop拦截器进行set,这样就成功把key和value保存到redis。
后续再补上如果是update要怎么同步呢,开头说过其实是删除这个key,因为update后内容都发生了变化。
Spring通过AOP实现对Redis的缓存同步