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

Posted 编程之艺术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP+Redis自己实现基于注解方式的缓存相关的知识,希望对你有一定的参考价值。

欢迎将文章分享到朋友圈
如需转载,请在后台回复“转载”获取授权


转载是一种动力 分享是一种美德


利用aop功能自己实现基于注解方式的缓存是我自己想出来的,并没有从网上找资料。是一句话让我有了灵感,就是spring cache“通过在现有代码中加入少量它定义的各种注解就能达到缓存方法返回的对象的效果”这句,其中的“缓存方法返回的对象”是这句话的重点。


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


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

本想将demo的代码分享给大家的,但是这些代码是写在我现在做的一个项目中的,所以想要代码的留言邮箱,我将redis下的代码打包发到你邮箱。


不管是数据访问层接口定义的方法的返回值还是业务层接口定义的方法的返回值都是从数据库中获取的,这些方法实现的功能无非就是根据调用该方法时传入的参数从数据库中获取期望的结果返回,所以我们可以利用这一点,在目标方法执行完成之后获取到目标方法的返回结果,然后利用调用目标方法时传递的参数作为缓存的key来缓存目标方法返回的结果,这也就实现了根据传入参数获取期望结果的功能。而在目标方法执行之前根据传入的参数试着从缓存中获取结果,如果获取不到结果再执行目标方法从数据库中获取,如果能从缓存中获取到就直接返回结果了,也就不需要执行目标方法。


在获取数据的时候我们要实现将获取到的数据存到Redis中,那么就需要在数据的删除、更新之后实现清空缓存在Redis中相应的记录了。实现方式就是在执行数据更新(包括删除、修改)的业务方法执行完成之后删除对应数据的缓存。同样的,对数据操作的目标方法肯定要求调用者传入能够定位到具体数据的参数,所以我们也是根据调用目标方法时传入的参数做为删除缓存时定位要删除的数据的key。


要注意的一点就是,查询时缓存数据的key必须要与删除时缓存数据的key相同,比如在查询用户信息的时候是根据用户名来查找的,所以在数据更新的时候也应该是使用用户名来定位到要更新的数据,这样才能实现删除缓存的旧数据。


要实现这一套功能得要借助spring的aop功能。前面在学习spring aop的时候,我们只是利用了spring的aop来实现日记功能,未免低估了spring的aop功能。今天我们就用spring的aop来实现基于注解方法的数据缓存功能,实现代码0入侵。


先来看流程图,画得不规范,但是能理解就行了。

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


根据流程图实现的伪代码。

 /**
     * 利用环绕通知实现@Cacheable@CacheEvict功能
     *
     * @param proceedingJoinPoint
     */

    @Around("cachePointcut()")
    public Object aroundCacheable(ProceedingJoinPoint proceedingJoinPoint) {
            //定义目标方法返回值
            Object result;

            如果目标方法有@MyCacheable注解则
                   result = 试着从缓存中获取数据
                   if(result!=null)获取到缓存,则不执行目标方法
                        return result;
                   else
                        result = 执行目标方法
                        将结果缓存
                        return result;
            如果目标方法没有@MyCacheable注解,但是有@MyCacheEvict注解
                  result = 执行目标方法获取到
                  从缓存中删除记录
                  return result;
            如果目标方法也没有@MyCacheEvict注解
                result = 直接执行目标方法获取返回值
                return result;
    }


要实现这一流程的功能需要解决以下问题:

  1. 如何获取到目标方法所属的类,注意不是代理类,要获取到被代理的业务类,因为我们的注解是加在原业务类的目标方法上的。

    Class target = proceedingJoinPoint.getTarget().getClass();


  2. 如何获取到目标方法。可以通过环绕通知传递进来的ProceedingJoinPoint对象获取到目标方法名,千万不要直接通过ProceedingJoinPoint对象获取目标方法:proceedingJoinPoint.getSignature(),这样获取到的是代理类的目标方法。

    //获取到目标方法名
    String methodName = proceedingJoinPoint.getSignature().getName();
    //获取到目标方法
    Method method = target.getClass().getMethod(methodName, argsClass);


  3. 如何获取方法上的注解

    //获取到方法上指定的@MyCacheable注解
    Annotation cacheable = method.getAnnotation(MyCacheable.class);

    //获取到方法上指定的@MyCacheEvict注解
    Annotation cacheEvict = method.getAnnotation(MyCacheEvict.class);


  4. 如何获取注解上的属性的值

    //获取注解的缓存名称和需要从缓存中删除的返回结果的key
    String[] cacheNames;
    String cacheKey;
    cacheNames = ((MyCacheEvict) cacheEvict).cacheNames();
    cacheKey = ((MyCacheEvict) cacheEvict).key();


  5. 如何获取到目标方法的参数

    //根据目录对象和方法名方法参数类型获取目标方法上的注解
    Object[] args = proceedingJoinPoint.getArgs();
    Class[] argsClass = new Class[args.length];
    for (int index = 0index < args.length; index++) {
          argsClass[index] = args[index].getClass();
    }


  6. 如何获取目标方法的返回值类型

    //获取目标方法返回值类型
    Class<?> resultType = method.getReturnType();
    System.err.println("目标方法返回值类型===>" + resultType.getName());




自定义的@MyCacheable注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyCacheable {

    String[] cacheNames() default {};

    String key() default "";

}


自定义的@MyCacheEvict注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyCacheEvict {

    String[] cacheNames() default {};

    String key() default "";

    boolean allEntries() default false;

}


先要创建一个切面类,我这里命名为RedisCacheAspect。使用@Component注解标志该类为一个bean,spring初始化时会实例化这个bean,使用@Aspect声明这是一个切面类bean。

@Aspect
@Component
public class RedisCacheAspect {

    /**
     * 利用redis实现数据缓存,这个接口的实现类是RedisServiceImpl
     * 这个接口的功能相当于Mybatis的mapper接口。
     */

    @Autowired
    private RedisService redisService;

    /**
     * 定义切点,这里定义切入所有业务类的任何方法
     */

    @Pointcut("execution(* wjy.weiai7lv.service.*..*.*(..))")
    private void cachePointcut() {
    }

    /**
     * 利用环绕通知实现spring cache的@Cacheable@CacheEvict功能
     *
     * @param proceedingJoinPoint
     */

    @Around("cachePointcut()")
    public Object aroundCacheable(ProceedingJoinPoint proceedingJoinPoint) {

    }

}


为什么使用环绕方法呢?因为只有环绕方法才能满足我们的需求。我们需要能够在方法执行前拦截方法,通过判断是否让目标方法执行,在环绕方法中我们可以中断目标方法的执行,而且环绕方法中我们还可以拿到目标方法的返回值。


由于使用@Component注解RedisCacheAspect为一个bean,所以需要将该类所在的包加到spring开启注解扫描指定的base-pachage中。

<!-- RedisCacheAspect所在的包是wjy.weiai7lv.redis -->
 <context:component-scan base-package="[省略其它],wjy.weiai7lv.redis"/>


下面给出RedisCacheAspect的全部代码。

package wjy.weiai7lv.redis;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 实现0侵入业务代码方式使用redis缓存数据
 * @author 吴就业
 *  作者声明:写这个类的目的只是让各位好理解spring cache的实现原理,不推荐使用哦
 */

@Aspect
@Component
public class RedisCacheAspect {

    /**
     * 利用redis实现数据缓存,这个接口的实现类是RedisServiceImpl
     * 这个接口的功能相当于Mybatis的mapper接口。
     */

    @Autowired
    private RedisService redisService;

    /**
     * 定义切点
     */

    @Pointcut("execution(* wjy.weiai7lv.service.*..*.*(..))")
    private void cachePointcut() {
    }

    /**
     * 利用环绕通知实现@Cacheable和@CacheEvict功能
     *
     * @param proceedingJoinPoint
     */

    @Around("cachePointcut()")
    public Object aroundCacheable(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            Object result;

            //获取目标方法所属对象的类名,通过getTarget可获取到被代理的目标对象
            String className = proceedingJoinPoint.getTarget().getClass().getName();
            System.err.println("目标方法所属对象类名===>" + className);
            //获取到目标方法名
            String methodName = proceedingJoinPoint.getSignature().getName();
            System.err.println("目标方法名===>" + methodName);

            //根据目录对象和方法名方法参数类型获取目标方法上的注解
            Object[] args = proceedingJoinPoint.getArgs();
            Class[] argsClass = new Class[args.length];
            for (int index = 0; index < args.length; index++) {
                argsClass[index] = args[index].getClass();
            }
            //获取到方法
            Method method = proceedingJoinPoint.getTarget().getClass().getMethod(methodName, argsClass);
            //获取到方法上指定的@MyCacheable注解
            Annotation cacheable = method.getAnnotation(MyCacheable.class);
            if (cacheable == null) {
                //没有@MyCacheable这个注解直接放行目标方法并获取返回值
                result = proceedingJoinPoint.proceed();

                /////////////////////////////////////////////////////////////////////////////
                /// 目标方法没有@MyCacheable的时候判断是不是有@MyCacheEvict,
                /// 如果有就需要在目标方法执行完成之后处理删缓存
                ////////////////////////////////////////////////////////////////////////////
                //获取到方法上指定的@MyCacheEvict注解
                Annotation cacheEvict = method.getAnnotation(MyCacheEvict.class);
                if (cacheEvict == null) {
                    //没有这个注解直接放行
                    return result;
                }
                //获取注解的缓存名称和需要从缓存中删除的返回结果的key
                String[] cacheNames;
                String cacheKey;
                cacheNames = ((MyCacheEvict) cacheEvict).cacheNames();
                cacheKey = ((MyCacheEvict) cacheEvict).key();
                if (cacheKey.charAt(0) == '&') {
                    //cacheKey去掉'&'之后就是参数下标,从参数中获取指定下标的参数的值
                    cacheKey = args[Integer.valueOf(cacheKey.substring(1, cacheKey.length()))].toString();
                }
                //将目标方法的返回值从缓存redis中移除
                redisService.removeDate(cacheKey);
                if(((MyCacheEvict) cacheEvict).allEntries()==true){
                    //cacheNames是一个数组
                    for (String cacheName : cacheNames) {
                        //从key=cacheName的集合中移除一个记录
                        redisService.removeObjectFromSet(cacheName, cacheKey);
                    }
                }
                /////////////////////////////////////////////////////////////////////////
                /// end @MyCacheEvict
                ////////////////////////////////////////////////////////////////////////

                return result;
            }

            //获取注解的缓存名称和缓存返回结果的key
            String[] cacheNames;
            String cacheKey;
            cacheNames = ((MyCacheable) cacheable).cacheNames();
            cacheKey = ((MyCacheable) cacheable).key();
            if (cacheKey.charAt(0) == '&') {
                //cacheKey去掉'&'之后就是参数下标,从参数中获取指定下标的参数的值
                cacheKey = args[Integer.valueOf(cacheKey.substring(1, cacheKey.length()))].toString();
            }

            //获取目标方法返回值类型
            Class<?> resultType = method.getReturnType();
            System.err.println("目标方法返回值类型===>" + resultType.getName());

            //从redis中获取数据,如果获取不到再执行目标方法
            if (resultType.isAssignableFrom(Set.class)) {
                //如果返回值类型实现set接口,那么就是set类型
                result = redisService.queryList(cacheKey);
            } else if (resultType.isAssignableFrom(Map.class)) {
                result = redisService.queryMap(cacheKey);
            } else {
                result = redisService.queryObject(cacheKey);
            }


            if (result != nullreturn result;

            //执行目标方法并获取返回值
            result = proceedingJoinPoint.proceed();
            System.err.println("redis缓存aop环绕通知["
                    + result.getClass().getName());

            //将目标方法的返回值缓存到redis中
            if (resultType.isAssignableFrom(Set.class)) {
                //如果返回值类型实现set接口,那么就是set类型
                redisService.savaList(cacheKey, (List<? extends Object>) result);
            } else if (resultType.isAssignableFrom(Map.class)) {
                redisService.savaMap(cacheKey, (Map<String, ? extends Object>) result);
            } else {
                redisService.savaObject(cacheKey, result);
            }

            //cacheNames是一个数组
            for (String cacheName : cacheNames) {
                //向key=cacheName的集合中添加一个记录
                redisService.addObjecyToSet(cacheName, cacheKey);
            }
            return result;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return null;
        }
    }

}


使用方式完全跟spring cache的使用方式一样。使用自定义的方式不用配置什么缓存管理者,也不用开启cache注解功能。下面给出在业务类中的使用例子。


/**
     * 查询用户信息
     * @param username 用户名
     * @return
     */

    //@MyCacheable注解标注该方法查询的结果存入缓存,再次访问时直接读取缓存中的数据。
    //      cacheNames:该缓存名称下所缓存的所有的key是一个set类型集合。
    //             (可以使用redis-cli的type key命令查看指定的key是什么类型的数据)
    //      key:就是缓存该方法返回的数据的key    
    //使用自定义实现方法开启下面注解
    @MyCacheable(cacheNames = "userCache", key = "&0")
    public User getUserWithUsername(String username) {
        if (StringUtils.strIsNull(username))
            return null;
        return this.userDao.getUserWithUsername(username);
    }

    /**
     * 删除用户信息
     * @param username
     */

    //使用自定义实现方法开启下面注解
    @MyCacheEvict(cacheNames = "userCache",key = "&0",allEntries = true)
    public void delUserWithUsername(String username){
        //删除用户记录
    }


spring整合redis的配置及使用还是再贴一次吧

[配置]

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/cache
        http://www.springframework.org/schema/cache/spring-cache.xsd"
>


    <!-- 配置JedisPoolConfig实例,JedisPoolConfig是连接池的配置 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.maxTotal}" />
        <property name="maxIdle" value="${redis.maxIdle}" />
        <property name="minIdle" value="${redis.minIdle}"/>
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>

    <!-- 配置JedisConnectionFactory,连接池工厂 -->
    <bean id="jedisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">

        <!-- 连接信息 -->
        <property name="hostName" value="${redis.host}" />
        <property name="port" value="${redis.port}" />
        <!-- 数据库 -->
        <property name="database" value="${redis.dbIndex}" />
        <!-- 使用连接池 -->
        <property name="usePool" value="true"/>
        <property name="poolConfig" ref="poolConfig" />
    </bean>

    <!-- key序列化策略,class需要实现RedisSerializer接口 -->
    <bean id="keySerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    <!-- value序列化策略,class需要实现RedisSerializer接口 -->
    <bean id="valueSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>

    <!-- 配置操作redis的RedisTemplate对象
        spring-data-redis封装了RedisTemplate对象来进行对Redis的各种操作,它支持所有的Redis原生的api。
        RedisUtils中就是用这个对象来进行数据的写入和查询操作的。
            redisTemplate.boundValueOps();//操作Object类型数据
            redisTemplate.boundHashOps();//操作hash类型数据
            redisTemplate.boundListOps();//操作list类型数据
            ......
     -->

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer" ref="keySerializer"/>
        <property name="valueSerializer" ref="valueSerializer"/>
        <!-- map类型数据序列号策略 -->
        <property name="hashKeySerializer" ref="keySerializer"/>
        <property name="hashValueSerializer" ref="valueSerializer"/>
        <!-- 配置是否支持事务 -->
        <property name="enableTransactionSupport" value="true"/>
    </bean>
</beans>


[RedisUtils]

package wjy.weiai7lv.redis;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * 声明为一个普通的bean
 */

@Component
public class RedisUtils {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * 清空指定key的记录
     * @param key
     */

    public void clearKey(String key){
        redisTemplate.delete(key);
    }

    /**
     * 从redis读取一个对象数据
     * @param key
     * @return
     */

    public Object getObjectFromRedis(String key){
        BoundValueOperations<String,Object> boundValueOperations = redisTemplate.boundValueOps(key);
        return boundValueOperations.get();
    }

    /**
     * 向redis写入一个列表数据
     * @param key
     * @param value
     */

    public void setObjectToRedis(String key,Object value){
        BoundValueOperations<String,Object> boundValueOperations = redisTemplate.boundValueOps(key);
        boundValueOperations.set(value);
    }

    /**
     * 从redis读取一个列表数据
     * @param key
     * @return
     */

    public List<Object> getListFromRedis(String key){
        BoundListOperations<String,Object> boundValueOperations = redisTemplate.boundListOps(key);
        return boundValueOperations.range(0,boundValueOperations.size());
    }

    /**
     * 向redis写入一个对象数据
     * @param key
     * @param value
     */

    public void setListToRedis(String key,List<Object> value){
        BoundListOperations<String,Object> boundValueOperations = redisTemplate.boundListOps(key);
        boundValueOperations.rightPushAll(value);
    }


    /**
     * 从redis读取一个map数据
     * @param key
     * @return
     */

    public Map<StringObject> getMapFromRedis(String key){
        BoundHashOperations<String,String,Object> boundValueOperations = redisTemplate.boundHashOps(key);
        return boundValueOperations.entries();
    }

    /**
     * 向redis写入一个map数据
     * @param key
     * @param value
     */

    public void setMapToRedis(String key, Map<StringObject> value){
        BoundHashOperations<String,String,Object> boundValueOperations = redisTemplate.boundHashOps(key);
        boundValueOperations.putAll(value);
    }


    /**
     * 将元素objecy添加到Set集合中
     * @param key
     * @param object
     */

    public void addObjecyToSet(String key,Object object){
        BoundSetOperations operations = redisTemplate.boundSetOps(key);
        //向集合添加一个成员
        operations.add(object);
    }

    /**
     * 将object从Set集合中删除
     * @param key
     * @param object
     */

    public void removeObjectFromSet(String key,Object object){
        BoundSetOperations operations = redisTemplate.boundSetOps(key);
        //移除集合中一个成员
        operations.remove(object);
    }

}


[RedisService]

package wjy.weiai7lv.redis;


import java.util.List;
import java.util.Map;

/**
 * redis服务接口
 */

public interface RedisService {

    //移除是通用的
    void removeDate(String key);

    //Object类型
    <T> void savaObject(String key,T value);
    <T> T queryObject(String key);

    //Set类型
    <T> void addObjecyToSet(String key,Object object);
    <T> void removeObjectFromSet(String key,Object object);

    //List类型,可用于栈、队列
    <T> List<T> queryList(String key);
    <T> void savaList(String key,List<T> value);

    //map类型数据,我规定map只能使用字符串做key
    <T> Map<String,T> queryMap(String key);
    <T> void savaMap(String key, Map<String,T> value);

}


[RedisServiceImpl]

package wjy.weiai7lv.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class RedisServiceImpl implements RedisService {

    @Autowired
    private RedisUtils redisUtils;

    public void removeDate(String key) {
        redisUtils.clearKey(key);
    }

    public <T> void savaObject(String key, T value) {
        redisUtils.setObjectToRedis(key, value);
    }

    public <T> T queryObject(String key) {
        return (T) redisUtils.getObjectFromRedis(key);
    }

    public <T> void addObjecyToSet(String key, Object object) {
        redisUtils.addObjecyToSet(key, object);
    }

    public <T> void removeObjectFromSet(String key, Object object) {
        redisUtils.removeObjectFromSet(key, object);
    }

    public <T> List<T> queryList(String key) {
        return (List<T>) redisUtils.getListFromRedis(key);
    }

    public <T> void savaList(String key, List<T> value) {
        redisUtils.setListToRedis(key, (List<Object>) value);
    }

    public <T> Map<String, T> queryMap(String key) {
        return (Map<String, T>) redisUtils.getMapFromRedis(key);
    }

    public <T> void savaMap(String key, Map<String, T> value) {
        redisUtils.setMapToRedis(key, (Map<StringObject>) value);
    }

}


有关spring整合redis实现数据缓存的另外两篇文章:

  • Spring整合Redis注解方式实现缓存

  • SSM项目整合Redis实现数据缓存


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



以上是关于AOP+Redis自己实现基于注解方式的缓存的主要内容,如果未能解决你的问题,请参考以下文章

ssm+redis整合(通过aop自定义注解方式)

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

博学谷学习记录超强总结,用心分享 | SpringCache常用注解介绍+集成redis

AOP-代理拦截实现Redis缓存

AOP-代理拦截实现Redis缓存

AOP-代理拦截实现Redis缓存