Spring通过AOP实现对Redis的缓存同步

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;
    }
View Code

 解释一下  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的缓存同步的主要内容,如果未能解决你的问题,请参考以下文章

springboot 2.x整合redis,spring aop实现接口缓存

注解及AOP实现Redis缓存组件

AOP-代理拦截实现Redis缓存

AOP-代理拦截实现Redis缓存

AOP-代理拦截实现Redis缓存

AOP+Redis自己实现基于注解方式的缓存