通过【自定义注解+AOP+枚举】实现自动缓存 + 日志打印以及上报

五一闲在家里没事干,,整点活;

前言

大概功能就是在需要缓存返回值的方法上加上注解,当方法被调用的时候自动查询缓存,命中缓存则自动返回缓存,未命中缓存则执行原本逻辑,并在执行后自动缓存新的数据

概述

在这里插入图片描述

大概就是通过自定义缓存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));
    }
}

效果
在这里插入图片描述

结语

啊啊啊啊 。。。又是个无聊的五一啊。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值