自定义 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<String, Object> hashRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> 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<String, Object> 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<String, String, Object> 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<String, Object> 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<String, String, Object> ops = redisTemplate.opsForHash();
Object val;
Map<String, Object> 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<String, Object>) 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<String, Object> 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<String, String, Object> ops = redisTemplate.opsForHash();
Object val;
//加锁
synchronized (this) {
//执行目标方法
val = joinPoint.proceed();
redisTemplate.delete(key);
//把值设置回去
ops.putAll(key, (Map<String, Object>) 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<String, String> getFieldTypeMap() {
//省略...
}
@CacheMapPut(key = "biubiu-field-hash", expire = 6 * 60 * 60L)
public Map<String, String> 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;
}
}
版权声明: 本文为 InfoQ 作者【张音乐】的原创文章。
原文链接:【http://xie.infoq.cn/article/0ef639d08a6f0e7a3f21cfa12】。未经作者许可,禁止转载。
张音乐
求你关注我,别不识抬举.别逼我跪下来求你. 2021.03.28 加入
还未添加个人简介
评论