自定义aop实现Cacheable注解(零拷贝), CacheItemGet,CacheMapGet,CacheMapPut
一.开发背景
众所周知, 在SpringBoot中有一个管理Redis缓存的注解Cacheable, 可以很方便的获取缓存
但是针对Cacheable的痛点,缓存的数据在客户端中查看,如果遇到错误很难排查错误。并且Cacheable会将整个数据获取到JVM 内存中, 会对应用系统造成负担.如果数据量很大的话, 还可能会造成内存溢出问题.
Cacheable不方便使用的点
针对Map类型做处理,并且Redis中也想存储成Map集合。
针对Redis的Set集合也可以开发类似的组件
二.实现思路
使用aop来实现, 在切面中在执行目标方法时先执行对缓存的操作,然后再判断是否执行目标方法.
对缓存的操作, 按照自己的设计来实现, 比如, 返回值是一个Map类型的化, 执行对Map的操作, 如果返回值是一个List的话, 执行对List的操作.
另外, 我们还需要对参数进行解析和替换, 例如, 我们可能需要动态的去指定缓存的Key, 或者Map中的Key来根据参数进行生成,这个时候我们需要动态进行参数绑定,
还有一个需要注意的点是, 对缓存进行PUT操作时, 需要加锁, 防止缓存击穿或者缓存穿透现象的发生.
三.具体实现
1、定义注解
1)、获取map中某一项值的注解
这个注解用于获取指定redis key下指定Map key的数据
package com.biubiu.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheItemGet {
String key();
String hKey();
}
2)、获取map集合的注解
这个注解用于获取指定redis key下的整个Map数据, 返回的是Map结构
package com.biubiu.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheMapGet {
String key();
long expire() default 12 * 60 * 60L;
boolean parse() default false;
}
3)、更新map的注解
package com.biubiu.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheMapPut {
String key();
long expire() default 12 * 60 * 60L;
boolean parse() default false;
}
2、redisTemplate
package com.biubiu;
@Configuration
public class Config extends WebMvcConfigurationSupport {
@Bean(name = "hashRedisTemplate")
public RedisTemplate hashRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(lettuceConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
3、aop切面
获取Map 中某一项数据的切面
package com.biubiu.aop;
import com.biubiu.annotation.CacheItemGet;
import com.biubiu.util.CommonUtil;
import com.biubiu.util.MD5;
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.Qualifier;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
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 javax.annotation.Resource;
import java.lang.reflect.Method;
@Aspect
@Component
public class CacheItemGetAspect {
@Qualifier("hashRedisTemplate")
@Resource
private RedisTemplate redisTemplate;
@Around("@annotation(com.biubiu.annotation.CacheItemGet)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
CacheItemGet cacheItemGet = method.getAnnotation(CacheItemGet.class);
String key = cacheItemGet.key();
String hKeyEl = cacheItemGet.hKey();
//创建解析器
ExpressionParser parser = new SpelExpressionParser();
Expression hKeyExpression = parser.parseExpression(hKeyEl);
//设置解析上下文有哪些占位符。
EvaluationContext context = new StandardEvaluationContext();
//获取方法参数
Object[] args = joinPoint.getArgs();
String[] parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(method);
for(int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
//解析得到 item的 key
String hKeyValue = hKeyExpression.getValue(context).toString();
String hKey = MD5.getMD5Str(hKeyValue);
HashOperations ops = redisTemplate.opsForHash();
Object value = ops.get(key, hKey);
if(value != null) {
return value.toString();
}
return joinPoint.proceed();
}
}
获取整个Map 结构的切面
package com.biubiu.aop;
import com.biubiu.annotation.CacheMapGet;
import com.biubiu.annotation.CacheMapPut;
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.Qualifier;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class CacheMapGetAspect {
@Qualifier("hashRedisTemplate")
@Resource
private RedisTemplate redisTemplate;
@Around("@annotation(com.biubiu.annotation.CacheMapGet)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
CacheMapGet cacheMap = method.getAnnotation(CacheMapGet.class);
String key = cacheMap.key();
//强制刷缓存
HashOperations ops = redisTemplate.opsForHash();
Object val;
Map value = ops.entries(key);
if(value.size() != 0) {
return value;
}
//加锁
synchronized (this) {
value = ops.entries(key);
if(value.size() != 0) {
return value;
}
//执行目标方法
val = joinPoint.proceed();
ops.delete(key);
//把值设置回去
ops.putAll(key, (Map) val);
redisTemplate.expire(key, cacheMap.expire(), TimeUnit.SECONDS);
return val;
}
}
}
更新缓存的切面
package com.biubiu.aop;
import com.biubiu.annotation.CacheMapPut;
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.Qualifier;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class CacheMapPutAspect {
@Qualifier("hashRedisTemplate")
@Resource
private RedisTemplate redisTemplate;
@Around("@annotation(com.biubiu.annotation.CacheMapPut)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
CacheMapPut cacheMap = method.getAnnotation(CacheMapPut.class);
String key = cacheMap.key();
//强制刷缓存
HashOperations ops = redisTemplate.opsForHash();
Object val;
//加锁
synchronized (this) {
//执行目标方法
val = joinPoint.proceed();
redisTemplate.delete(key);
//把值设置回去
ops.putAll(key, (Map) val);
redisTemplate.expire(key, cacheMap.expire(), TimeUnit.SECONDS);
return val;
}
}
}
4、使用方法
@CacheItemGet(key = "biubiu-field-hash", hKey = "#tableName+#colName")
public String getValue(String tableName, String colName) {
//省略...
}
@CacheMapGet(key = "biubiu-field-hash", expire = 6 * 60 * 60L)
public Map getFieldTypeMap() {
//省略...
}
@CacheMapPut(key = "biubiu-field-hash", expire = 6 * 60 * 60L)
public Map putFieldTypeMap() {
//省略...
}
5、MD5工具类
package com.biubiu.util;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
/**
* @Author yule.zhang
* @CreateTime 2019-6-16下午05:28:11
* @Version 1.0
* @Explanation 用MD5对数据进行加密
*/
public class MD5 {
static final Logger log = LogManager.getLogger(MD5.class);
MessageDigest md5;
static final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
public MD5() {
try {
// 获得MD5摘要算法的 MessageDigest 对象
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
log.error("创建MD5对象出错, ", e);
throw new IllegalArgumentException("创建md5对象时出错");
}
}
public synchronized String getMD5(String s) {
return this.getMD5(s.getBytes()).toLowerCase();
}
public synchronized String getMD5(byte[] btInput) {
try {
// 使用指定的字节更新摘要
md5.update(btInput);
// 获得密文
byte[] md = md5.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str).toLowerCase();
} catch (Exception e) {
log.error("生成MD5码时出错,", e);
throw new IllegalArgumentException("生成MD5出错");
}
}
/**
* 获取32位的MD5加密
* @param sourceStr
* @return
*/
public static String getMD5Str(String sourceStr) {
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sourceStr.getBytes(Charset.forName("utf-8")));
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
result = buf.toString();
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
}
return result;
}
}