spring 集成的一套标准的cache框架不能很好的定义缓存的有效期, 必须提前单独的对每个缓存key指定过期时间.这个操作还是不太灵活.
下面将利用spring-aop (aspectj) 来完成自定义缓存框架的简单实现
自定义缓存实现
定义缓存注解 @MajjCache
/**
* 缓存标示注解
*
* @Author JingjingMa
* @Date 2019/9/15 12:40
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MajjCache {
/**
* 缓存键的名称
* 支持占位符替换
* <p>
* ${c} = className
* ${m} = methodName
* ${p} = param[0].toString+param[1].toString+...+param[n].toString
* ${cmp} = ${c}.${m}[${p}]
* #{\d} = p[\d] 比如param[0]第0个参数值
* </p>
*
* @return
*/
String name() default "";
/**
* 缓存时间(单位秒s,默认1小时)
* ps: 如果type=Request,则此处的ttl无效
*
* @return
*/
int ttl() default 60 * 60;
/**
* 缓存类型(默认Redis)
*
* @return
*/
Type type() default Type.Redis;
/**
* 缓存类型
*/
enum Type {
Redis, Request
}
}
从上述的注解可以看出支持的功能还是很强的,后续还可以不断增强
- cache-key 支持表达式
- 根据动态key设置有效期
- 支持缓存方式[Redis,Request,...]
利用aop功能,织入缓存
@Component
@Aspect
@Slf4j
public class MajjCacheAop {
@Autowired
StringRedisTemplate redisTemplate;
Pattern pattern = Pattern.compile("\\#\\{\\d+\\}");
/**
* 定义AOP扫描路径
* 第一个注解只扫描aopTest方法
*/
@Pointcut(value = "@annotation(cn.majingjing.tmblog.common.anno.MajjCache)")
public void cache() {
log.info("---MajjCacheAop-cache()---");
}
@Around("cache()")
public Object around(ProceedingJoinPoint joinPoint) throws Exception {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
Object[] args = joinPoint.getArgs();
Class<?> returnType = method.getReturnType();
MajjCache majjCache = method.getAnnotation(MajjCache.class);
String cacheKey = cacheKey(majjCache, className, methodName, args);
Object proceed;
if (cacheKey.length() > 0) {
//从缓存中取数据
proceed = cacheGet(majjCache, cacheKey, returnType);
if (Tools.isNull(proceed)) {
//从实际方法中获取数据
proceed = joinPoint.proceed();
//往缓存中写数据
cacheSet(majjCache, cacheKey, proceed);
}
} else {
log.warn("{}.{}的缓存配置错误,请检查name?", className, methodName);
//从实际方法中获取数据
proceed = joinPoint.proceed();
}
return proceed;
} catch (Throwable e) {
log.error("", e);
throw new Exception(e);
}
}
/**
* 往缓存中写数据
*
* @param majjCache
* @param cacheKey
* @param cacheObject
*/
private void cacheSet(MajjCache majjCache, String cacheKey, Object cacheObject) {
if (majjCache.type().equals(MajjCache.Type.Redis)) {
String _cacheObject = JSONObject.toJSONString(cacheObject);
redisTemplate.opsForValue().set(cacheKey, _cacheObject, majjCache.ttl(), TimeUnit.SECONDS);
} else if (majjCache.type().equals(MajjCache.Type.Request)) {
HttpServletRequest request = SpringContextServletHolder.getRequest();
request.setAttribute(cacheKey, cacheObject);
}
}
/**
* 从缓存中获取数据
*
* @param cacheKey
* @param majjCache
* @return
*/
private <T> T cacheGet(MajjCache majjCache, String cacheKey, Class<T> clz) {
if (majjCache.type().equals(MajjCache.Type.Redis)) {
// 没有被缓存
if (!redisTemplate.hasKey(cacheKey)) {
return null;
}
String cacheObject = redisTemplate.opsForValue().get(cacheKey);
T t = JSONObject.parseObject(cacheObject, clz);
return t;
} else if (majjCache.type().equals(MajjCache.Type.Request)) {
HttpServletRequest request = SpringContextServletHolder.getRequest();
Object cacheObject = request.getAttribute(cacheKey);
return (T) cacheObject;
}
return null;
}
/**
* cache-key 生成
*
* @param majjCache
* @param className
* @param methodName
* @param args
* @return
*/
private String cacheKey(MajjCache majjCache, String className, String methodName, Object... args) {
String cacheName = majjCache.name();
if (Tools.isEmpty(cacheName)) {
return "";
}
//${}
if(cacheName.contains("${")){
// ${cmp}
if (majjCache.name().contains("${cmp}")) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(className);
stringBuilder.append(".");
stringBuilder.append(methodName);
stringBuilder.append("[");
for (Object obj : args) {
stringBuilder.append(obj.toString());
}
stringBuilder.append("]");
cacheName = cacheName.replace("${cmp}", stringBuilder.toString());
}
// ${c}
if (majjCache.name().contains("${c}")) {
cacheName = cacheName.replace("${c}", className);
}
// ${m}
if (majjCache.name().contains("${m}")) {
cacheName = cacheName.replace("${m}", methodName);
}
// ${p}
if (majjCache.name().contains("${p}")) {
StringBuilder stringBuilder = new StringBuilder();
for (Object obj : args) {
stringBuilder.append(obj.toString());
}
cacheName = cacheName.replace("${p}", stringBuilder.toString());
}
}
//#{}
if(cacheName.contains("#{")){
Matcher matcher = pattern.matcher(cacheName);
while (matcher.find()) {
String key = matcher.group();
int argIndex = Integer.parseInt(key.substring(2, key.length() - 1));
System.out.println(key + "\t" + argIndex);
cacheName = cacheName.replace(key, args[argIndex].toString());
}
}
return cacheName;
}
}
由于只是简单示例,此处代码揉杂在一个aop类里面
如果用于生产环境请考虑如下优化
- 简单的使用
StringRedisTemplate
来完成复合对象的存储,实际项目可自定义数据类型的模版 - 可以根据不同的
MajjCache.Type
来写个多态 cache-key
生成可以参考SpEL
表达式- 需考虑异常的处理
织入缓存切入点,在需要的方法上增加缓存注解
@FeignClient(value = "xxx",path = "/xxx-xxx")
public interface BlogService extends BlogApi {
@Override
@MajjCache(name = IConstants.CacheKey.BLOG_HOTLIST, ttl = 24*60*60)
@GetMapping("/blog/aaa")
List<xxx> aaa(@RequestParam(value = "rows", defaultValue = "10") Integer rows);
@Override
//@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60*60)
xxx bbb(@RequestParam(value = "aid") String aid,
@RequestParam(value = "token") String token);
@Override
@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 60)
xxx ccc(@PathVariable(value = "aid") String aid);
@Override
@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 10)
xxx prev(@PathVariable(value = "aid") String aid);
@Override
@MajjCache(name = "tmblog:web:blog:#{0}:${m}", ttl = 5)
xxx ddd(@PathVariable(value = "aid") String aid);
}
如上代码已经完成了,在feignclient 请求服务时,如果有@MajjCache
则会优先检查缓存,如果命中则不会调用feign服务.还实现了缓存时间动态指定的功能.