使用Spring AOP注解实现Redis缓存 适合复杂业务场合

2 篇文章 0 订阅
2 篇文章 0 订阅

之前转载过一篇博客

http://blog.csdn.net/massivestars/article/details/50548006

里面有个缺点,切入的方法参数类型要完全一致,若方法的参数定义为Map,传值为HashMap则会报错


我根据实际环境作了改进,支持切入方法参数为Map、Javabean、基本类型(要定义成对象),也增加了一些时间参数和缓存配置项。

为了灵活配置拦截的方法,aop使用xml配置.


定义时间片段的枚举

package org.massive.redis.constant;

/**
 * Created by Massive on 2016/1/9.
 */
public enum DateUnit {
    SECONDS,MINUTES,HOURS,DAYS,MONTHS,YEARS
}


Cacheable的注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {

    // 类目 用于定义是以什么开头的
    String category() default "";

    // 要用来解释的key值
    String key();

    // 过期时间数值,默认-1为永久
    int expire() default -1;
    
    // 时间单位,默认为秒
    DateUnit dateUnit() default DateUnit.SECONDS;

}


Aop拦截类

package org.massive.redis.aop;

import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.massive.redis.annotation.Cacheable;
import org.massive.redis.constant.DateUnit;
import org.massive.redis.constant.SystemCacheProperties;
import org.massive.redis.util.AopUtils;
import org.massive.redis.util.RedisAccess;
import org.massive.redis.util.SpringExpressionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

/**
 * Created by Massive on 2016/1/9.
 */
public class CacheAopAspect {

    private final static Logger log =  Logger.getLogger(CacheAopAspect.class);

    RedisAccess redisAccess;

    public RedisAccess getRedisAccess() {
        return redisAccess;
    }

    public void setRedisAccess(RedisAccess redisAccess) {
        this.redisAccess = redisAccess;
    }

    public Object doCacheable(ProceedingJoinPoint pjp) throws Throwable {
        Object result=null;
        Method method = AopUtils.getMethod(pjp);

        Cacheable cacheable = method.getAnnotation(Cacheable.class);

        Boolean isCacheEnable = "enable".equals(SystemCacheProperties.getProperty("system.cache.enable"));

        if(cacheable != null && !isCacheEnable) {
            log.debug("没有开启缓存");
        }

        //-----------------------------------------------------------------------
        // 如果拦截的方法中没有Cacheable注解
        // 或者system.cache.enable的开关没打开
        // 则直接执行方法并返回结果
        //-----------------------------------------------------------------------
        if (cacheable == null || !isCacheEnable) {
            try {
                result = pjp.proceed();
            } catch (Throwable e) {
                e.printStackTrace();
            }
            return result;
        }

        String key = cacheable.key();

        //----------------------------------------------------------
        // 用SpEL解释key值
        //----------------------------------------------------------
        String keyVal = SpringExpressionUtils.parseKey(key, method, pjp.getArgs());

        if (!StringUtils.isEmpty(cacheable.category())){
            keyVal = cacheable.category() + "_" + keyVal;
        } else {
            //----------------------------------------------------------
            // 如果cacheable的注解中category为空取 类名+方法名
            //----------------------------------------------------------
            keyVal = pjp.getTarget().getClass().getSimpleName() + "_"
                    + method.getName() + "_" + keyVal;
        }

        Class returnType = ((MethodSignature)pjp.getSignature()).getReturnType();


        //-----------------------------------------------------------------------
        // 从redis读取keyVal,并且转换成returnType的类型
        //-----------------------------------------------------------------------
        result = redisAccess.get(keyVal, returnType);

        if (result == null) {
            try {
                //-----------------------------------------------------------------------
                // 如果redis没有数据则执行拦截的方法体
                //-----------------------------------------------------------------------
                result = pjp.proceed();
                int expireSeconds = 0;

                //-----------------------------------------------------------------------
                // 如果Cacheable注解中的expire为默认(默认值为-1)
                // 并且systemCache.properties中的system.cache.expire.default.enable开关为true
                // 则取system.cache.expire.default.seconds的值为缓存的数据
                //-----------------------------------------------------------------------
                if (cacheable.expire() == -1 &&
                        "enable".equals(SystemCacheProperties.getProperty("system.cache.expire.default.enable"))) {
                    expireSeconds = new Integer(SystemCacheProperties.getProperty("system.cache.expire.default.seconds"));
                } else {
                    expireSeconds = getExpireSeconds(cacheable);
                }
                //-----------------------------------------------------------------------
                // 把拦截的方法体得到的数据设置进redis,过期时间为计算出来的expireSeconds
                //-----------------------------------------------------------------------
                redisAccess.set(keyVal, result, expireSeconds);
                log.debug("已缓存缓存:key=" +  keyVal);
            } catch (Throwable e) {
                e.printStackTrace();
            }
            return result;
        }
        log.debug("========从缓存中读取");
        log.debug("=======:key   = " + key);
        log.debug("=======:keyVal= " + keyVal);
        log.debug("=======:val   = " + result);
        return result;
    }

    /**
     * 计算根据Cacheable注解的expire和DateUnit计算要缓存的秒数
     * @param cacheable
     * @return
     */
    public int getExpireSeconds(Cacheable cacheable) {
        int expire = cacheable.expire();
        DateUnit unit = cacheable.dateUnit();
        if (expire <= 0) {
            return 0;
        }
        if (unit == DateUnit.MINUTES) {
            return expire * 60;
        } else if(unit == DateUnit.HOURS) {
            return expire * 60 * 60;
        } else if(unit == DateUnit.DAYS) {
            return expire * 60 * 60 * 24;
        } else if(unit == DateUnit.MONTHS) {
            return expire * 60 * 60 * 24 * 30;
        } else if(unit == DateUnit.YEARS) {
            return expire * 60 * 60 * 24 * 365;
        }
        return expire;
    }


}


redis的访问类

package org.massive.redis.util;

import com.alibaba.fastjson.JSONObject;
import org.springframework.stereotype.Repository;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;


/**
 * Created by Massive on 2016/1/9.
 */
public class RedisAccess {

    private JedisPool jedisPool;

    public JedisPool getJedisPool() {
        return jedisPool;
    }

    public void setJedisPool(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    public void set(String key,Object o,Integer seconds){
        Jedis jedis = jedisPool.getResource();
        jedis.set(key, JSONObject.toJSONString(o));
        if (seconds != null && seconds > 0) {
            jedis.expire(key,seconds);
        }
        jedis.close();
    }

    public String get(String key) {
        Jedis jedis = jedisPool.getResource();
        String text = jedis.get(key);
        jedis.close();
        return text;
    }


    public <T> T get(String key,Class<T> clazz){
        String text = get(key);
        T result = JSONObject.parseObject(text, clazz);
        return result;
    }

    public void del(String key) {
        Jedis jedis = jedisPool.getResource();
        jedis.del(key);
        jedis.close();
    }

    /**
     * 清空某个DB的数据
     */
    public void flushDB() {
        Jedis jedis = jedisPool.getResource();
        jedis.flushDB();
        jedis.close();
    }

}




AopUtils类

package org.massive.redis.util;

import org.aspectj.lang.ProceedingJoinPoint;

import java.lang.reflect.Method;

/**
 * Created by Massive on 2016/8/11.
 */
public class AopUtils {


    /**
     * 获取被拦截方法对象
     * MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
     * 而缓存的注解在实现类的方法上
     * 所以应该使用反射获取当前对象的方法对象
     * @param pjp
     * @return
     * @throws NoSuchMethodException
     */
    public static Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
        //--------------------------------------------------------------------------
        // 获取参数的类型
        //--------------------------------------------------------------------------
        Object[] args = pjp.getArgs();
        Class[] argTypes = new Class[pjp.getArgs().length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }

        String methodName = pjp.getSignature().getName();
        Class<?> targetClass = pjp.getTarget().getClass();
        Method[] methods = targetClass.getMethods();

        //--------------------------------------------------------------------------
        // 查找Class<?>里函数名称、参数数量、参数类型(相同或子类)都和拦截的method相同的Method
        //--------------------------------------------------------------------------
        Method method = null;
        for (int i = 0; i < methods.length; i++){
            if (methods[i].getName() == methodName){
                Class<?>[] parameterTypes = methods[i].getParameterTypes();
                boolean isSameMethod = true;

                // 如果相比较的两个method的参数长度不一样,则结束本次循环,与下一个method比较
                if (args.length != parameterTypes.length) {
                    continue;
                }

                //--------------------------------------------------------------------------
                // 比较两个method的每个参数,是不是同一类型或者传入对象的类型是形参的子类
                //--------------------------------------------------------------------------
                for (int j = 0;parameterTypes != null && j < parameterTypes.length ;j++) {
                    if (parameterTypes[j] != argTypes[j] && !parameterTypes[j].isAssignableFrom(argTypes[j])) {
                        isSameMethod = false;
                        break;
                    }
                }
                if (isSameMethod) {
                    method = methods[i];
                    break;
                }
            }
        }
        return method;
    }


}



SPEL表达解释工具类

package org.massive.redis.util;

import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.lang.reflect.Method;

/**
 * Created by Massive on 2016/8/11.
 */
public class SpringExpressionUtils {

    /**
     * 获取缓存的key
     * key 定义在注解上,支持SPEL表达式
     * 注: method的参数支持Javabean和Map
     *      method的基本类型要定义为对象,否则没法读取到名称
     *
     * example1:
     *      Phone phone = new Phone();
     *      "#{phone.cpu}"  为对象的取值
     * example2:
     *      Map apple = new HashMap(); apple.put("name","good apple");
     *      "#{apple[name]}"  为map的取值
     * example3:
     *      "#{phone.cpu}_#{apple[name]}"
     *
     * @param key
     * @param method
     * @param args
     * @return
     */
    public static String parseKey(String key, Method method, Object[] args) {
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u =
                new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);

        //使用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]);
        }
        ParserContext parserContext = new TemplateParserContext();

        //----------------------------------------------------------
        // 把 #{ 替换成 #{# ,以适配SpEl模板的格式
        //----------------------------------------------------------
        Object returnVal =
                parser.parseExpression(key.replace("#{","#{#"), parserContext).getValue(context, Object.class);
        return returnVal == null ? null: returnVal.toString();
    }

}



systemCache.properties

# ==============================================
# ===============  REDIS CONFIG  ===============
# ==============================================
# parameters for redis.clients.jedis.JedisPoolConfig
redis.pool.maxTotal=50
redis.pool.maxIdle=10
redis.pool.maxWaitMillis=1000
redis.pool.testOnBorrow=true

# parameters from redis.clients.jedis.JedisPool
redis.server.host=121.40.211.170
redis.server.port=6379
redis.server.timeout=2000
redis.server.auth=lxm_binf_mmm_asd
#===============================================


# ==============================================
# ===============  CACHE OPTIONS ===============
# ==============================================
# determine the cache is opened or not
# options: enable,disable
system.cache.enable=enable

# redis database range is 1-16
system.cache.database=3

# determine the cache expire is opened or not
# options: enable,disable
system.cache.expire.default.enable=enable

# The time cache exists
system.cache.expire.default.seconds=3600
#===============================================






applicationContext.xml

<?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:context="http://www.springframework.org/schema/context"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"
	   default-autowire="byType">

	<context:property-placeholder file-encoding="utf-8" location="classpath*:systemCache.properties"/>

	<!-- 开启使用注解注入bean -->
	<context:annotation-config/>
	<!-- 扫描base-package定义的目录,注解注入bean -->
	<context:component-scan base-package="org.massive"/>

	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxTotal" value="${redis.pool.maxTotal}" />
		<property name="maxIdle" value="${redis.pool.maxIdle}" />
		<property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />
		<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
	</bean>

	<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
		<constructor-arg index="0" ref="jedisPoolConfig"/>
		<constructor-arg index="1" value="${redis.server.host}" />
		<constructor-arg index="2" value="${redis.server.port}"/>
		<constructor-arg index="3" value="${redis.server.timeout}"/>
		<constructor-arg index="4" value="${redis.server.auth}"/>
		<constructor-arg index="5" value="${system.cache.database}"/>
	</bean>

	<!-- redis访问类 -->
	<bean id="redisAccess" class="org.massive.redis.util.RedisAccess">
		<property name="jedisPool" ref="jedisPool"/>
	</bean>

	<bean id="cacheAopAspect" class="org.massive.redis.aop.CacheAopAspect">
		<property name="redisAccess" ref="redisAccess"/>
	</bean>

	<!-- 这里使用不使用注解是为了灵活配置要拦截的方法,使用注解的话要修改源代码 -->
	<!-- 强烈推荐使用xml配置的方式 -->
	<!-- 拦截所有org.massive.*.service 和 org.massive.*.cache 包下所有的方法 -->
	<aop:config proxy-target-class="true">
		<aop:aspect ref="cacheAopAspect">
			<aop:pointcut id="doCacheAopPointcut"
						  expression="(execution(* org.massive.*.service.*.*(..))
					or execution(* org.massive.*.cache.*.*(..)))"/>
			<aop:around pointcut-ref="doCacheAopPointcut" method="doCacheable"/>
		</aop:aspect>
	</aop:config>


	<bean id="cacheAopEvict" class="org.massive.redis.aop.CacheAopEvict">
		<property name="redisAccess" ref="redisAccess"/>
	</bean>
	<!-- 拦截所有org.massive.*.service 和 org.massive.*.cache 包下所有的方法 -->
	<aop:config proxy-target-class="true">
		<aop:aspect ref="cacheAopEvict">
			<aop:pointcut id="doCacheEvictPointcut"
						  expression="(execution(* org.massive.*.service.*.*(..))
					or execution(* org.massive.*.cache.*.*(..)))"/>
			<aop:around pointcut-ref="doCacheEvictPointcut" method="doCacheEvict"/>
		</aop:aspect>
	</aop:config>

</beans>






下面编写测试用例进行测试

定义一个测试用的Javabean

package org.massive.redis.samples;

/**
 * Created by Massive on 2016/1/10.
 */
public class Player {

    public String userName;
    public int age;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Player{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}

service类,被AopCacheAspect拦截

package org.massive.redis.service;

import org.massive.redis.annotation.CacheEvict;
import org.massive.redis.annotation.Cacheable;
import org.massive.redis.constant.DateUnit;
import org.massive.redis.samples.Player;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by Massive on 2016/1/9.
 */
@Service
public class CacheAopTestService {

    /**
     * 参数为基本类型
     * @param pageNo
     * @param pageSize
     * @return
     */
    @Cacheable(key = "#{pageNo}_#{pageSize}")
    public List getPrimitiveData(Integer pageNo, Integer pageSize) {
        Player player = new Player();
        player.setUserName("massive");
        List list = new ArrayList();
        list.add(player);
        return list;
    }

    /**
     * 方法参数为Javabean,缓存200秒
     * @param player
     * @return
     */
    @Cacheable(category="player",key="#{player.userName}",expire = 200)
    public Player getBeanData(Player player) {
        System.out.println("this is redis bean test...");
        return player;
    }

    /**
     * 方法参数为Map;expire = 1,dateUnit = DateUnit.HOURS  缓存一小时
     * @param phone
     * @return
     */
    @Cacheable(category = "forMapTest",key = "#{phone[cpu]}_#{phone[ram]}",expire = 1,dateUnit = DateUnit.HOURS)
    public Map getMapData(Map phone) {
        System.out.println("this is redis map test...");
        return phone;
    }

    /**
     * 方法参数为复合类型,包括Javabean,Map,Integer等,缓存永存时间
     * @param player
     * @param phone
     * @param pageNo
     * @param pageSize
     * @return
     */
    @Cacheable(category = "mix",key = "#{player.userName}_#{phone[cpu]}_#{phone[ram]}_#{pageNo}_#{pageSize}")
    public List<Map> getMixData(Player player,Map phone,Integer pageNo,Integer pageSize) {
        Map map = new HashMap();
        map.put("type","mix");
        List<Map> list = new ArrayList<Map>();
        list.add(map);
        list.add(phone);
        return list;
    }

    @CacheEvict(category = "forTest",key = "#{map[userName]}")
    public Map updateMapData(Map map) {
        System.out.println("this is evict map test...");
        return map;
    }

}


测试类

package org.massive.redis.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.massive.redis.samples.Player;
import org.massive.redis.service.CacheAopTestService;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by Massive on 2016/1/9.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:applicationContext.xml"})
public class CacheAopTest {


    @Resource
    private JedisPool jedisPool;

    @Resource
    private CacheAopTestService cacheAopTestService;

    @Test
    public void testConnection() {
        Jedis jedis =jedisPool.getResource();
        jedis.set("___mobile", "1388888888");
        System.out.println(jedis.get("mobile"));
    }


    /**
     * 测试method的参数为基本类型
     */
    @Test
    public void testPrimitive() {
        cacheAopTestService.getPrimitiveData(1,10);
    }
    //第一次输出
    //已缓存缓存:key=CacheAopTestService_getPrimitiveData_1_10

    //第二次输出
    //<========从缓存中读取>
    //<=======:key   = #{pageNo}_#{pageSize}>
    //<=======:keyVal= CacheAopTestService_getPrimitiveData_1_10>
    //<=======:val   = [{"age":0,"userName":"massive"}]>



    /**
     * 测试method的参数为Javabean
     */
    @Test
    public void testJavaBean() {
        Player player = new Player();
        player.setUserName("Stephen Curry");
        player.setAge(27);
        cacheAopTestService.getBeanData(player);
        System.out.println(player);
    }
    //第一次输出
    //<已缓存缓存:key=player_Stephen Curry

    //第二次输出
    //<========从缓存中读取>
    //<=======:key   = #{player.userName}>
    //<=======:keyVal= player_Stephen Curry>
    //<=======:val   = Player{userName='Stephen Curry', age=27}>



    /**
     * 测试method的参数为Map
     */
    @Test
    public void testMap() {
        Map phone = new HashMap();
        phone.put("cpu","Intel");
        phone.put("ram","4GB");
        cacheAopTestService.getMapData(phone);
    }
    //第一次输出
    //已缓存缓存:key=forMapTest_Intel_4GB

    //第二次输出
    //<========从缓存中读取>
    //<=======:key   = #{phone[cpu]}_#{phone[ram]}>
    //<=======:keyVal= forMapTest_Intel_4GB>
    //<=======:val   = {ram=4GB, cpu=Intel}>



    /**
     * 测试method的参数和返回都是混合且复杂
     */
    @Test
    public void testMix() {
        Map phone = new HashMap();
        phone.put("cpu","Intel");
        phone.put("ram","4GB");

        Player player = new Player();
        player.setUserName("Curry");
        player.setAge(27);

        cacheAopTestService.getMixData(player,phone,1,100);
    }
    //第一次输出
    //已缓存缓存:key=mix_Curry_Intel_4GB_1_100

    //第二次输出
    //<========从缓存中读取>
    //<=======:key   = #{player.userName}_#{phone[cpu]}_#{phone[ram]}_#{pageNo}_#{pageSize}>
    //<=======:keyVal= mix_Curry_Intel_4GB_1_100>
    //<=======:val   = [{"type":"mix"}, {"ram":"4GB","cpu":"Intel"}]>



}





  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值