首页 > 代码库 > 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;    }
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的缓存同步