在数据库查询前我想查询是否存在缓存,不存在则查询,这样的重复性操作写在代码里很难看,通过AspectJ的AOP编程,可以很优雅地实现这个缓存过程。
但是在使用过程中,发现Spring自带的@Cacheable注解序列化对象时是使用JDK的序列化工具往Redis里存数据,这样很占Redis的内存,为何不用FastJSON之类的序列化工具序列化对象后往Redis里存JSON字符串呢,更加轻量快捷。
但是JSON在序列化和反序列化的时候需要提供类型,但是类型在SpringCache中并没有提供,那就自己做个吧!
/**
* 缓存注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
/**
* key的生成策略,支持表达式语言,表达式中多个值用"_"分隔
* 不填写默认使用当前方法名
* 例如 #name_#id
* 最终生成SpEL表达式为 #name+'_'+#id
*/
String field() default "";
//JSON序列化的类型
Class type();
//默认缓存时间是一天 60*60*24
long expire() default 86400L;
}
/**
* 删除缓存注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisEvict {
/**
* key生成策略,可填写多个,对应于Hash映射中的field
* 不填写默认删除以当前类全限定名作为key的Hash映射
*/
String[] field() default {};
Class type();
}
/**
* Redis缓存切面处理
*/
@Aspect
@Component
public class RedisCacheAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
private final static Logger logger = Logger.getLogger(RedisCacheAspect.class);
/**
* 获取或添加缓存
*/
@Around("@annotation(com.common.cache.redis.RedisCache)")
public Object RedisCache(final ProceedingJoinPoint jp) throws Throwable {
Method method = getMethod(jp);
RedisCache cache = method.getAnnotation(RedisCache.class);
//根据类名、方法名和参数生成key
final String key = parseKey(cache.field(), method, jp.getArgs());
if (logger.isDebugEnabled()) {
logger.debug("生成key:" + key);
}
//得到被代理的方法上的注解
Class modelType = method.getAnnotation(RedisCache.class).type();
//检查redis是否有缓存
String value = (String) redisTemplate.opsForHash().get(modelType.getName(), key);
Object result;
if (null == value) {
//缓存未命中
if (logger.isDebugEnabled()) {
logger.debug("缓存未命中");
}
//去数据库查询
result = jp.proceed(jp.getArgs());
//把序列化结果放入缓存
redisTemplate.opsForHash().put(modelType.getName(), key, serialize(result));
// 设置失效时间
redisTemplate.expire(modelType.getName(), cache.expire(), TimeUnit.SECONDS);
} else {
//缓存命中
if (logger.isDebugEnabled()) {
logger.debug("缓存命中");
}
//得到被代理方法的返回值类型
Class returnType = ((MethodSignature) jp.getSignature()).getReturnType();
//反序列化从缓存中拿到的json
result = deserialize(value, returnType, modelType);
}
return result;
}
/**
* 删除缓存
*/
@Around("@annotation(com.common.cache.redis.RedisEvict)")
public Object RedisEvict(final ProceedingJoinPoint jp) throws Throwable {
//得到被代理的方法
Method method = getMethod(jp);
//得到被代理方法上的注解
Class modelType = method.getAnnotation(RedisEvict.class).type();
if (logger.isDebugEnabled()) {
logger.debug("清空缓存:" + modelType.getName());
}
//判断是否指定了field
String[] fields = method.getAnnotation(RedisEvict.class).field();
if (fields.length == 0) {
//清除类全限定名对应Hash缓存
redisTemplate.delete(modelType.getName());
} else {
//清除指定的field的缓存
List<Object> objects = new ArrayList<>();
for (String field : fields) {
if (!StringUtils.isEmpty(field)) {
objects.add(field);
}
}
if (objects.size() > 0) {
redisTemplate.opsForHash().delete(modelType.getName(), (Object[]) fields);
}
}
return jp.proceed(jp.getArgs());
}
//FastJSON序列化对象
private String serialize(Object result) {
return JSON.toJSONString(result);
}
//FastJSON反序列化获得对象
@SuppressWarnings("unchecked")
private Object deserialize(String json, Class clazz, Class modelType) {
//返回结果是List对象
if (clazz.isAssignableFrom(List.class)) {
return JSON.parseArray(json, modelType);
}
//返回结果是普通对象
return JSON.parseObject(json, clazz);
}
/**
* 获取被拦截方法对象
* MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
* 而缓存的注解在实现类的方法上
* 所以应该使用反射获取当前对象的方法对象
*/
private Method getMethod(ProceedingJoinPoint pjp) {
//获取参数的类型
Class[] argTypes = ((MethodSignature) pjp.getSignature()).getParameterTypes();
Method method = null;
try {
method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return method;
}
private String parseKey(String field, Method method, Object[] args) {
//SpEL表达式为空默认返回方法名
if (StringUtils.isEmpty(field)) {
return method.getName();
}
//_号分割
String SpEL = field.replace("_", "+'_'+");
//获得被拦截方法参数列表
LocalVariableTableParameterNameDiscoverer nd = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = nd.getParameterNames(method);
//使用SpEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SpEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SpEL上下文中
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return method.getName() + parser.parseExpression(SpEL).getValue(context, String.class);
}
}
最后别忘了在ApplicationContext.xml中配置我们的切面
<!--扫描自定义的缓存切面-->
<context:component-scan base-package="com.common.cache.redis"/>
<!--Aspect切面编程-->
<aop:aspectj-autoproxy/>
这样就可以很轻松地使用自定义的缓存注解了
@RedisCache(field = "#page_#rows_#conditions.pid", type = Dict.class)
public EasyUIPage<Dict> selectByPageWithConditions(Integer page, Integer rows, Dict conditions)