之前转载过一篇博客
http://blog.youkuaiyun.com/massivestars/article/details/50548006
里面有个缺点,切入的方法参数类型要完全一致,若方法的参数定义为Map,传值为HashMap则会报错
为了灵活配置拦截的方法,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"}]>
}