大家在项目中应该经常使用到缓存,一般都是先从缓存中拿数据,拿得到就直接返回;拿不到然后才从数据库中获取数据。一般代码都是和业务代码耦合在一起的。那么能不能把缓存的代码抽离开来呢?答案是可以的,看了看Spring Cache的实现,它是基于注解与AOP around通知来实现的。
我们先来看看缓存在业务代码中的一般实现:
1) get
Object cacheValue = cache.get("key");
if(null != cacheValue){
return cacheValue;
} else {
Object dbValue = getFromInDb("key");
cache.set("key", value);
return dbValue;
}
2) delete
Object cacheValue = cache.get("key");
if(null != cacheValue){
cache.delete("key");
db.deleteByKey("key")
} else {
db.deleteByKey("key")
}
这样缓存模块就与业务模块太耦合了。下面就是我们项目中基于spring cache原理的简易实现,让我们来看一看具体的代码:
1、RedisCacheGet – 缓存获取方法
这个类是用于方法来获取缓存的方法。里面可以调你存放Redis的DBIndex,你存放的key,其中key支持Spring EL表达式。
package com.spring.carl.cache.redis.annotation
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RedisCacheGet {
//枚举类型
public enum DataType {
CLASS, JSON
}
/**
* 保存数据库
*
* @return
*/
public int DBIndex() default 0;
/**
* key值
*
* @return
*/
public String key();
/**
* 缓存过期时间默认为不过期,过期时间手动去设定,单位为 S
* 0:不限制保存时长
*
* @return
*/
public int expire() default 0;
/**
* force是否强制刷新数据
*
* @return
*/
public boolean force() default false;
/**
* 数据类型
*
* @return
*/
public DataType dataType() default DataType.JSON;
}
2、RedisCacheClean – 缓存删除方法
这个类是用于消除缓存的方法。里面可以调你存放Redis的DBIndex,你存放的key,其中key支持Spring EL表达式,支持批量删除操作。
package com.spring.carl.cache.redis.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RedisCacheClean {
/**
* 保存数据库
*
* @return
*/
public int DBIndex();
/**
* key值
*
* @return
*/
public String[] key();
/**
* 是否批量
*
* @return
*/
public boolean isBatch() default false;
}
3、RedisCacheAspect
这个类是处理缓存注解的方法,同时支持key为spring EL表达式的实现。
package com.spring.carl.cache.redis.annotation;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.carl.cache.redis.client.JedisClient;
import com.spring.carl.jackson.JacksonUtil;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@Aspect
@Component
@Log4j2
public class RedisCacheAspect {
private static final int ONEDAY = 60 * 60 * 24; //24h
@Autowired
private JedisClient jedisClient;
@Around("@annotation(RedisCacheGet)")
public Object cacheGet(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
log.info("redisCache get method.name {}",method.getName());
return goRedisCacheGet(args, method, joinPoint);
}
@Around("@annotation(RedisCacheClean)")
public Object cacheClean(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
log.info("redisCache clean method.name {}",method.getName());
return goRedisCacheClean(args, method, joinPoint);
}
/**
* @param args
* @param method
* @param joinPoint
* @return
* @throws Throwable
* @description redisCacheGet注解解析方法
*/
public Object goRedisCacheGet(Object[] args, Method method, ProceedingJoinPoint joinPoint) throws Throwable {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
//如果有这个注解,则获取注解类
RedisCacheGet methodType = method.getAnnotation(RedisCacheGet.class);
String key = parser.parseExpression(methodType.key()).getValue(context, String.class);
log.info("redisCache key {}",key);
if (methodType.dataType() == RedisCacheGet.DataType.JSON) {//JSON形式保存
if (methodType.force() == true) {//强制更新数据
Object object = joinPoint.proceed(args);
if (object != null) {
setRedisValueJson(methodType, key, object);
}
//返回值类型
return object;
} else {
if (jedisClient.exists(methodType.DBIndex(), key)) {
String json = jedisClient.get(methodType.DBIndex(), key);
try {
log.debug("redis" + method.getReturnType() + "key{}" + key + "\t value{}" + json);
if (method.getGenericReturnType().toString().contains("List") ||
method.getGenericReturnType().toString().contains("Set") ||
method.getGenericReturnType().toString().contains("Map")) {
ObjectMapper mapper = JacksonUtil.getInstance().getObjectMapper();
String clazzName = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0].toString().substring(6);
Object o = Class.forName(clazzName).newInstance();
JavaType javaType = getCollectionType(mapper, method.getReturnType(), o.getClass());
return JacksonUtil.getInstance().json2JavaType(json, javaType);
} else {
return JacksonUtil.getInstance().json2Bean(json, method.getReturnType());
}
} catch (Exception e) {
log.error(e.getMessage());
}
return null;
} else {//查询数据,缓存,返回对象
Object object = joinPoint.proceed(args);
if (object != null) {
setRedisValueJson(methodType, key, object);
}
return object;
}
}
} else {//CLASS形式保存
if (methodType.force() == true) {//强制更新数据
Object object = joinPoint.proceed(args);
if (object != null) {
setRedisValueClass(methodType, key, object);
}
//返回值类型
return object;
} else {
if (jedisClient.exists(methodType.DBIndex(), key)) {//对象存在直接返回
return jedisClient.getObject(methodType.DBIndex(), key);
} else {//查询数据,缓存,返回对象
Object object = joinPoint.proceed(args);
if (object != null) {
setRedisValueClass(methodType, key, object);
}
return object;
}
}
}
}
/**
* @param methodType
* @param key
* @param object
*/
private void setRedisValueClass(RedisCacheGet methodType, String key, Object object) {
//设置缓存时长
if (methodType.expire() == 0) {
jedisClient.set(methodType.DBIndex(), key, object);
} else if (methodType.expire() == 1) {
jedisClient.set(methodType.DBIndex(), key, ONEDAY, object);
} else {
jedisClient.set(methodType.DBIndex(), key, methodType.expire(), object);
}
}
/**
* @param methodType
* @param key
* @param object
*/
private void setRedisValueJson(RedisCacheGet methodType, String key, Object object) {
String jsonStr = JacksonUtil.getInstance().bean2Json(object);
if (methodType.expire() == 0) {//0:永不过期
jedisClient.set(methodType.DBIndex(), key, jsonStr);
} else if (methodType.expire() == 1) {//1:过期时间为24h
jedisClient.set(methodType.DBIndex(), key, ONEDAY, jsonStr);
} else {//手动指定
jedisClient.set(methodType.DBIndex(), key, methodType.expire(), jsonStr);
}
}
/**
* @param args
* @param method
* @param joinPoint
* @return
* @throws Throwable
* @description redisCache 清除方法
*/
public Object goRedisCacheClean(Object[] args, Method method, ProceedingJoinPoint joinPoint) throws Throwable {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//把方法参数放入SPEL上下文中
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
//如果有这个注解,则获取注解类
Object object = joinPoint.proceed(args);
//如果有这个注解,则获取注解类
RedisCacheClean methodType = method.getAnnotation(RedisCacheClean.class);
if(methodType.isBatch()){
for (String str : methodType.key()) {
String keyStr = parser.parseExpression(str).getValue(context, String.class);
String[] keys = keyStr.split(",");
for (String key : keys) {
jedisClient.del(methodType.DBIndex(), key);
}
}
} else {
for (String str : methodType.key()) {
String key = parser.parseExpression(str).getValue(context, String.class);
jedisClient.del(methodType.DBIndex(), key);
}
}
return object;
}
private static JavaType getCollectionType(ObjectMapper mapper, Class<?> collectionClass, Class<?>... elementClasses) {
return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
}
}
4、使用方法
具体注解的使用方法。
package com.spring.carl.onlineorder.order.manager.Impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.spring.carl.common.cache.redis.annotation.RedisCacheClean;
import com.spring.carl.common.cache.redis.annotation.RedisCacheGet;
import com.spring.carl.common.constants.RedisDBIndexConstants;
import com.spring.carl.onlineorder.order.domain.OrderExt;
import com.spring.carl.onlineorder.order.domain.vo.OrderExtQueryBean;
import com.spring.carl.onlineorder.order.manager.OrderExtManager;
import com.spring.carl.onlineorder.order.service.OrderExtService;
/**
* 订单扩展信息管理接口实现类
*
*/
@Service
public class OrderExtManagerImpl implements OrderExtManager{
@Autowired
private OrderExtService orderExtService;
@Override
@RedisCacheClean(key="'store_order_ext_'+#orderExt.merchantId + '_' + #orderExt.orderId",
DBIndex = RedisDBIndexConstants.ONLINEORDER)
public void insertOrderExt(OrderExt orderExt) {
this.orderExtService.save(orderExt);
}
@Override
@RedisCacheClean(key="'store_order_ext_'+#merchantId + '_' + #orderId",
DBIndex = RedisDBIndexConstants.ONLINEORDER)
public void insertOrderExts(Long merchantId, Long orderId,
List<OrderExt> orderExts) {
if(orderExts == null || orderExts.isEmpty()){
return ;
}
this.orderExtService.saveBatch(merchantId, orderExts);
}
@Override
@RedisCacheGet(key="'store_order_ext_'+#merchantId + '_' + #orderId",
DBIndex = RedisDBIndexConstants.ONLINEORDER)
public List<OrderExt> getOrderExts(Long merchantId, Long orderId) {
OrderExtQueryBean queryBean = new OrderExtQueryBean();
queryBean.setMerchantId(merchantId);
queryBean.setOrderId(orderId);
List<OrderExt> orderExts =
this.orderExtService.find(queryBean);
return orderExts;
}
}
这样只需要在需要使用注解的上面添加相就的注解可以了。实现了业务代码与缓存代码的解藕。