我准备开一个系列,就是写一些在简要的学习项目中可能会用到的奇奇怪怪的功能,比如线程池或者统一异常处理类 取名为【项目demo】系列 然后将会同步到GitHub中:https://github.com/Livorth/FunctionalLearning
自定义Redis缓存注解实现统一缓存
Spring Cache
这里推荐看 统一缓存帝国,实战 Spring Cache!,不能说感受良多,只能说受益匪浅
但是讲Spring Cache不是我的本意,所以Spring Cache的部分就不过多介绍,只简单描述使用过程
- 添加依赖:spring-boot-starter-cache
- 配置缓存类型,我推荐用caffeine或者redis,后面以redis为例
- 在主启动类或者任意配置类上加上
@EnableCaching 注解 - 最后在指定方法上添加
@Cacheable 缓存注解接口,注意他有以以下参数(参考博客)
cacheNames/value :用来指定缓存组件的名字key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写)keyGenerator :key 的生成器,推荐重写。 key 和 keyGenerator 二选一使用cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。condition :可以用来指定符合条件的情况下才缓存unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过 #result 获取方法结果)sync :是否使用异步模式。
SpringBoot系列之缓存使用教程介绍了其他相关的注解
自定义Redis缓存注解
自定义注解的实现,也就是使用AOP进行切片处理
这里我主要参考
上面这个相对简单,下面这个相当复杂,不过我还是一下面这个为蓝本来写的
自定义缓存注解RedisCache
因为是自定义的,所以可以根据自己的需求来进行变动,我按照我自己的想法进行了一定的改动
package cn.livorth.functionallearning.common.cache;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface RedisCache {
String nameSpace() default "";
String key() default "";
long expireTime() default 1 * 60 * 1000;
boolean read() default true;
}
具体AOP代理方法类RedisCacheAspect
和常规的AOP代理方法类相同,主要的是其中实现的具体逻辑
- 根据相关信息生成key
- 通过key在redis中查询数据是非已经存在
- 如果不存在,则去数据库中查找,如果数据库中都不存在,则要考虑内存穿透的问题了
- 如果存在,则返回redis中对应的数据,同时记得需要反序列化,毕竟存储的时候已经统一JSON化了
其中有两个地方需要注意
- key的生成方案,仅仅考传入的"key"作为key肯定是不够的,必然会重复,由于我暂时对springEL表达式不是很了解,所以我是用的是MD5加密同时进行拼接的方式来生成的
- 反序列的过程,比如泛型的判断
package cn.livorth.functionallearning.common.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
public class RedisCacheAspect {
@Resource
private RedisHandler handler;
@Pointcut(value = "@annotation(cn.livorth.functionallearning.common.cache.RedisCache)")
public void redisCache() {
}
@Around(value = "redisCache()")
private Object saveCache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("<======拦截到redisCache方法:{}.{}======>" ,
proceedingJoinPoint.getTarget().getClass().getName(), proceedingJoinPoint.getSignature().getName());
Method m = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
Method methodWithAnnotations = proceedingJoinPoint.getTarget().getClass().getDeclaredMethod(
proceedingJoinPoint.getSignature().getName(), m.getParameterTypes());
Object result;
RedisCache annotation = methodWithAnnotations.getDeclaredAnnotation(RedisCache.class);
String key = parseKey(methodWithAnnotations, proceedingJoinPoint.getArgs(), annotation.key(), annotation.nameSpace());
log.info("<====== 通过key:{}从redis中查询 ======>", key);
String cache = handler.getCache(key);
if (cache == null) {
log.info("<====== Redis 中不存在该记录,从数据库查找 ======>");
result = proceedingJoinPoint.proceed();
if (result != null) {
long expireTime = annotation.expireTime();
if (expireTime != -1) {
handler.saveCache(key, result, expireTime, TimeUnit.SECONDS);
} else {
handler.saveCache(key, result);
}
}
return result;
} else {
return deSerialize(m, cache);
}
}
private Object deSerialize(Method m, String cache) {
Class returnTypeClass = m.getReturnType();
log.info("从缓存中获取数据:{},返回类型为:{}" , cache, returnTypeClass);
Object object = null;
Type returnType = m.getGenericReturnType();
if(returnType instanceof ParameterizedType){
ParameterizedType type = (ParameterizedType) returnType;
Type[] typeArguments = type.getActualTypeArguments();
for(Type typeArgument : typeArguments){
Class typeArgClass = (Class) typeArgument;
log.info("<======获取到泛型:{}" , typeArgClass.getName());
object = JSON.parseArray(cache, typeArgClass);
}
}else {
object = JSON.parseObject(cache, returnTypeClass);
}
return object;
}
private String parseKey(Method method, Object[] argValues, String key, String nameSpace) {
StringBuilder prefix = new StringBuilder();
prefix.append(nameSpace).append(".").append(key);
prefix.append(".").append(method.getName());
StringBuilder sb = new StringBuilder();
for (Object obj : argValues) {
sb.append(obj.toString());
}
return prefix.append(DigestUtils.md5DigestAsHex(sb.toString().getBytes())).toString();
}
@Component
class RedisHandler {
@Resource
RedisTemplate<String, String> cache;
<T> void saveCache(String key, T t, long expireTime, TimeUnit unit) {
String value = JSON.toJSONString(t);
log.info("<====== 存入Redis 数据:{}", value);
cache.opsForValue().set(key, value, expireTime, unit);
}
<T> void saveCache(String key, T t) {
String value = JSON.toJSONString(t, SerializerFeature.WRITE_MAP_NULL_FEATURES);
cache.opsForValue().set(key, value);
}
void removeCache(String key) {
cache.delete(key);
}
String getCache(String key) {
return cache.opsForValue().get(key);
}
}
}
注意这里面的RedisHandler其实可以使用之前自己写的redis封装类RedisUtils
然后就是布隆过滤器的使用,其实我只知道概念,具体使用暂时还不清楚
进行测试
说是测试也只是加个注解的事情
UserController.java
@LogAnnotation(logModule = "user", logType = "select", logDescription = "通过分页获取所有用户信息")
@RedisCache(nameSpace = "user", key = "getAllUserByPage")
@GetMapping("page/{thePage}/{pageSize}")
public List<User> getAllUserByPage(@PathVariable("thePage") int thePage, @PathVariable("pageSize") int pageSize){
Page<User> page = new Page<>(thePage, pageSize);
return userService.getAllUserByPage(page);
}
然后进行多次访问并查看日志信息
http://localhost:8888/user/page/3/5 第一次是对数据库中信息进行访问,第二次测试从缓存中找
写在后面
注解自定义,代理方法类自己实现,很多东西的决定权在自己手上
要想简单写可以写的很简单,要写复杂那都不是几百行可以搞定的
只能说看需求来吧,我这也只能起到一个抛砖引玉的作用
我的项目demo系列都会同步到GitHub上,欢迎围观
https://github.com/Livorth/FunctionalLearning
|