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

来源:互联网 发布:数据标准方案 编辑:程序博客网 时间:2024/06/07 21:53

之前转载过一篇博客

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.JedisPoolConfigredis.pool.maxTotal=50redis.pool.maxIdle=10redis.pool.maxWaitMillis=1000redis.pool.testOnBorrow=true# parameters from redis.clients.jedis.JedisPoolredis.server.host=121.40.211.170redis.server.port=6379redis.server.timeout=2000redis.server.auth=lxm_binf_mmm_asd#===============================================# ==============================================# ===============  CACHE OPTIONS ===============# ==============================================# determine the cache is opened or not# options: enable,disablesystem.cache.enable=enable# redis database range is 1-16system.cache.database=3# determine the cache expire is opened or not# options: enable,disablesystem.cache.expire.default.enable=enable# The time cache existssystem.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.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsdhttp://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. */@Servicepublic 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"}]>}





1 0
原创粉丝点击