五一闲在家里没事干,,整点活;
前言
大概功能就是在需要缓存返回值的方法上加上注解,当方法被调用的时候自动查询缓存,命中缓存则自动返回缓存,未命中缓存则执行原本逻辑,并在执行后自动缓存新的数据
概述
大概就是通过自定义缓存Cache
,然后aop切面扫描所有使用这个注解的方法,并通过缓存枚举CacheEnum
进行管理key
值以及过期时间
,再通过另一个注解CacheSuffix
标注需要拼接到key值之后作为特征参数的方法参数;
后缀拼接部分,因为pjp获取到的方法参数
和方法参数的注解[]数组
索引位置是对应的
,所以检测到指定注解的那组注解的索引
就是其参数在args[]所在的索引
代码部分
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
注解
// 标记方法缓存返回值的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
CacheEnum cacheEnum();
}
// 标记方法参数成为缓存key值特征参数
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheSuffix {
}
枚举
@Getter
@AllArgsConstructor
public enum CacheEnum {
USERNAME("用户名缓存","USERNAME",5000L),
USERINFO("用户信息缓存","USERINFO",10000L),
;
private final String remark; // 备注
private final String key;
private final Long expire; // 过期时间,毫秒
}
缓存类
@Slf4j
public class MyCaChe {
// 获取一个单例方便调用
public static MyCaChe caChe = new MyCaChe();
// 数据容器
private static ConcurrentMap<String, Container> data;
// 获取缓存
public Object get(String key) {
Container container = data.get(key);
if (ObjectUtil.isEmpty(container))
return null;
// 获取时如果读到过期的就执行删除并返回null
Long ttl = container.getTtl();
long cu = System.currentTimeMillis();
if (ttl < cu && container.getTtl() != -1) {
data.remove(key); // 过期就删除
return null;
}
return container.getData();
}
// 设置缓存
public void set(String key, Object o, Long timeout) {
// kv,如果有过期时间就把到期时间戳设置为当前时间戳+过期时间
data.put(key, Container.builder().data(o).ttl(System.currentTimeMillis() + timeout).build());
}
public void set(String key, Object o) {
// 无过期时间版,ttl设置为-1,默认-1不删除
data.put(key, Container.builder().data(o).ttl(-1L).build());
}
public MyCaChe() {
// 创建的时候设置data为线程安全的map,同时执行异步清理过期数据的定时任务
data = new ConcurrentHashMap<>();
new Thread(() -> {
while (true)
clear();
}).start();
}
// 清理过期元素
public synchronized void clear() {
try {
Set<Map.Entry<String, Container>> entries = data.entrySet();
int startCount = entries.size();
// 过滤过期的元素重新赋值给缓存池
data = entries.stream()
// 设置需要保留的数据的条件,[ttl=-1或者ttl大于当前时间戳]
.filter(en -> en.getValue().getTtl() == -1L || en.getValue().getTtl() > System.currentTimeMillis())
.collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
log.info("执行缓存清理,本次清理了:{}个失效缓存,现有缓存数量:{}", startCount - data.size(), data.size());
// 等待1分钟,重新开始,这里只是做个demo就不用定时任务了
wait(60000);
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException("等待出现异常");
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
// 带有有效期的缓存容器
class Container {
private Object data; // 数据
private Long ttl; // 到期时间戳
}
AOP
@Aspect
@Component
@AllArgsConstructor
public class CacheAop {
@Around("@annotation(com.do.leProject.code.annotation.Cache)")
public Object enumCacheAroundAspect(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature(); // 获取签名签名,这里强转为方法签名,因为我们知道这里切片位于方法内
Method method = signature.getMethod(); // 获取方法
// Annotation[] annotations = method.getDeclaredAnnotations(); // 获取所有注解
Cache cache = method.getDeclaredAnnotation(Cache.class); // 获取缓存注解
if (cache == null) // 如果没有这个注解就是不需要缓存直接执行原逻辑
return pjp.proceed();
CacheEnum caCheEnum = cache.cacheEnum(); // 获取注解上的注解枚举参数
String cacheKey = getCacheKey(cache, pjp); // 计算应该使用的缓存key值
// Object cacheData = getCache(caCheEnum.getKey()); // 通过枚举获取key进行缓存查询
Object cacheData = getCache(cacheKey);
if (ObjectUtil.isNotEmpty(cacheData)) // 如果缓存不为空,本方法直接返回缓存的内容跳过后续逻辑,
return cacheData;
Object methodData = pjp.proceed(); // 到此缓存为空,直接执行本方法原逻辑获取返回值
setCache(cacheKey, methodData, caCheEnum.getExpire()); // 将新获得的返回值写入缓存,key以及过期时间从枚举中获取
return methodData; // 方法返回最新的执行结果
}
// 获取请求应该使用的缓存key
public String getCacheKey(Cache cache, ProceedingJoinPoint pjp) {
// 参数前如果有使用CacheSuffix注解则该参数为加入后缀的参数
StringJoiner keyJoiner = new StringJoiner(";;");
keyJoiner.add(cache.cacheEnum().getKey());
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
// 这里是获取所有参数的注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// 这里获取所有参数
Object[] args = pjp.getArgs();
// 参数的顺序索引和方法参数注解[]的顺序是一致的,所以索引相对
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotation = parameterAnnotations[i];
for (Annotation an : annotation) {
// 如果这一组注解(一个参数有n个注解)有我们想要的注解,则就是我们要的那个参数,就把他对应索引的参数值取出拼接到key之后
if (an instanceof CacheSuffix)
keyJoiner.add(String.valueOf(args[i]));
}
}
return keyJoiner.toString();
}
// 获取缓存,这里大家可以视情况改成使用其他缓存中间件,我这里就简单的写了个map来演示
private Object getCache(String key) {
return MyCaChe.caChe.get(key);
}
// 写入缓存,这里大家可以视情况改成使用其他缓存中间件,我这里就简单的写了个map来演示
private void setCache(String key, Object data, Long exp) {
MyCaChe.caChe.set(key, data, exp);
}
}
测试部分代码
Controller
@RestController
@RequestMapping("/cache")
@AllArgsConstructor
public class CacheEnumTestController {
private final CacheEnumService cacheEnumService;
@GetMapping("/getName/{id}")
public String getName(@PathVariable String id) {
return cacheEnumService.getName(id,"name",3);
}
}
Service
@Service
public class CacheEnumService {
// 鹅,,,这样不走代理aop无法有效命中这里,所以一定要将这里bean交给spring-ioc
// public static CacheEnumService cacheEnumService = new CacheEnumService();
@Cache(cacheEnum = CacheEnum.USERNAME)
public String getName(@CacheSuffix String id,String nas,int age) {
StringJoiner name = new StringJoiner(",", "【", "】");
name.add("zhangsan");
name.add("id:" + id);
name.add("age:" + new Random().nextInt(100));
return name.toString();
}
}
补充
缓存容器部分,这里只是临时写的测试工具,实际中使用redis或者缓存工具类,不要整那个死循环清理;
枚举只需要按照规则定义即可;
缓存读取写入部分,需要按照使用的工具或者中间件重写;
不指定缓存key的后缀
时默认就使用枚举的key
值了;
日志上报部分
日志上报封装实体
这个上报内容视情况而定
/**
* 请求日志上报实体
*/
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class RequestLogReport {
private Integer httpStatus; // 响应状态码
private String reqUri; // 请求uri
private String reqRemotePath; // 请求的ip地址
private Object requestParam; // 请求参数
private Long responseTime; // 请求响应耗时
private LocalDateTime reqDateTime; // 请求到达时间
private String deviceName; // 处理请求的设备名称
}
日志上报以及打印部分AOP代码
// 请求日志打印以及日志上报切面
@Aspect
@Component
@AllArgsConstructor
public class RequestLogAop {
private static final Logger log = LoggerFactory.getLogger(RequestLogAop.class);
private static final Map<String, String> ENV = System.getenv();
private final HttpServletRequest req;
private final HttpServletResponse rsp;
// 定义日志切点,使用正则匹配需要增强的方法
@Pointcut("execution(public * com.do.leProject.codeStyle.code.controller.*Controller.*(..))")
public void requestLogoPoint() {
}
// 使用上方定义的切点
@Around("requestLogoPoint()")
public Object enumCacheAroundAspect(ProceedingJoinPoint pjp) throws Throwable {
log.info("【Request】:Url:{};请求参数:{}", req.getRequestURI(), pjp.getArgs());
long start = System.currentTimeMillis();
Object result = pjp.proceed();
log.info("【Response[{}]】:响应结果:{}", rsp.getStatus(), result);
// 日志上报至ES
this.requestReport(RequestLogReport.builder()
.deviceName(ENV.get("USERDOMAIN"))
.reqRemotePath(req.getRemoteAddr())
.reqUri(req.getRequestURI())
.requestParam(pjp.getArgs())
.reqDateTime(LocalDateTime.now())
.httpStatus(rsp.getStatus())
.responseTime(System.currentTimeMillis() - start)
.build());
return result;
}
@Async
void requestReport(RequestLogReport requestLogReport) {
log.info("请求日志上报=>{}", JSONUtil.toJsonStr(requestLogReport));
}
}
效果
结语
啊啊啊啊 。。。又是个无聊的五一啊。。。