AOP实现企业级API访问接口监控(通过Google Guava缓存数据)

开发了企业的功能模块,分享给大家参考,若大家看到我的实现有不足之处或有自己的见解欢迎评论区分享٩꒰▽ ꒱۶⁼³₌₃ 学习去咯



前言

需求:

1、记录接口访问成功和失败的次数
2、记录接口访问成功和失败的耗时
3、访问时间戳记录,以5分钟为一个节点,例如:13:14分记录为13:10分
4、公开一个接口,返回Google Guava缓存的数据,以JSON的格式
5、高并发下任然可用,保证线程安全

以上便是具体需求,目前这需求只是存储了具体接口信息,后续可能要去实现监控预警如:接口在当前节点内失败次数超过了阈值、调用接口的耗时超过了阈值等情况需要发送预警(邮箱通知等等)


一、AOP的基本知识

1、什么是AOP?

AOP(Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。

2、有哪些AOP的概念?

Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。

3、AOP包含的几个概念

1)、Jointpoint(连接点):具体的切面点点抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut(切入点),仅作用在方法上。
2)、Advice(通知): 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。
3)、目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。
4)、AOP代理:AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。
5)、Weaving(织入):将增强处理添加到目标对象中,创建一个被增强的对象的过程

总结为一句话就是:在目标对象(target object)的某些方法(jointpoint)添加不同种类的操作(通知、增强操处理),最后通过某些方法(weaving、织入操作)实现一个新的代理目标对象。

4、AOP 有哪些应用场景?

举几个例子:

  • 记录日志(调用方法后记录日志)
  • 监控性能(统计方法运行时间)
  • 权限控制(调用方法前校验是否有权限)
  • 事务管理(调用方法前开启事务,调用方法后提交关闭事务 )
  • 缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用,直接从内存对象返回,不需要查询数据库 )

5、有哪些AOP Advice通知的类型?

特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。

  • 前置通知(Before advice) : 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。
  • 后置通知(After advice) :这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
  • 返回后通知(After return advice) :这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
  • 环绕通知(Around advice) :这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。
  • 抛出异常后通知(After throwing advice) :仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。

二、Google Guava是什么?

这个就是个工具类,很好用,老大让我好好研究,因为他之前在阿里工作的时候也是通过这个进行缓存的。
Google Guava 官网: https://guava.dev/
Google Guava 中文教程: https://wizardforcel.gitbooks.io/guava-tutorial/content/

直接看文档就可以了,在结合网上的demo,基本使用是没问题的。

三、功能实现

1.引入依赖

<!-- AOP -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.4</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.12</version>
</dependency>

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20140107</version>
</dependency>

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2、编写返回内容实体

MonitoringVO.java:

/**
 * @author 一个爱运动的程序员
 */
@Data
public class MonitoringVO {

    /**
     * 请求时间
     */
    private String time;

    /**
     * 接口请求的URL
     */
    private String apiURL;

    /**
     * 接口请求成功的次数
     */
    private Long successfulNum;

    /**
     * 接口请求失败的次数
     */
    private Long failuresNum;

    /**
     * 接口请求成功的累计耗时
     */
    private Long successfulTime;

    /**
     * 接口请求失败的累计耗时
     */
    private Long failuresTime;
}

3、编写AOP

ApiVisitHistory.java:

/**
 * API访问历史统计
 *
 * @author 一个爱运动的程序员
 */
@Component
@Aspect
public class ApiVisitHistory {

    private Logger log = LoggerFactory.getLogger(ApiVisitHistory.class);

    private static ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 定义切面
     * - 此处代表com.example.demo.monitoring.controller包下的所有接口都会被统计
     */
    @Pointcut("execution(* com.example.demo.monitoring.controller..*.*(..))")
    public void pointCut() {

    }

    /**
     * 在接口原有的方法执行前,将会首先执行此处的代码
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        //获取传入目标方法的参数
        log.info("类名:{}", joinPoint.getSignature().getDeclaringType().getSimpleName());
        log.info("方法名:{}", joinPoint.getSignature().getName());
    }

    /**
     * 只有正常返回才会执行此方法
     * 如果程序执行失败,则不执行此方法
     */
    @Async
    @AfterReturning(returning = "returnVal", pointcut = "pointCut()")
    public void doAfterReturning(JoinPoint joinPoint, Object returnVal) throws ExecutionException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String key = AtomicCounter.getTimeStamp() + "--" + AtomicCounter.getApiURL(request);
        // 成功计数+1
        AtomicCounter.increaseSucceed(AtomicCounter.getCountObject(key));
        // 耗时计算
        Long succeedTime = System.currentTimeMillis() - startTime.get();
        log.info("接口访问成功,URI:[{}], 耗费时间:[{}] ms", request.getRequestURI(), succeedTime);
        // 存储API接口访问信息
        AtomicCounter.setSuccessfulTime(key, succeedTime);
    }

    /**
     * 当接口报错时执行此方法
     */
    @AfterThrowing(pointcut = "pointCut()")
    public void doAfterThrowing(JoinPoint joinPoint) throws ExecutionException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String key = AtomicCounter.getTimeStamp() + "--" + AtomicCounter.getApiURL(request);
        // 失败计数+1
        AtomicCounter.increaseFail(AtomicCounter.getCountObject(key));
        // 耗时计算
        Long failTime = System.currentTimeMillis() - startTime.get();
        log.info("接口访问失败,URI:[{}], 耗费时间:[{}] ms", request.getRequestURI(), failTime);
        AtomicCounter.setMultiMap(key, failTime);
    }
}

4、编写存储工具类

AtomicCounter java:

/**
 * 记录API接口访问成功/失败的计数及耗时
 *
 * @author 一个爱运动的程序员
 */
public class AtomicCounter {

    /**
     * Guava Cache 缓存API访问记录
     */
    private static Cache<String, ConcurrentHashMap<AtomicCounter, MonitoringVO>> cache = CacheBuilder.newBuilder().expireAfterWrite(60 * 10, TimeUnit.SECONDS).build();

    /**
	 * 调用的计数对象
	 *
	 * @param key
	 * @return
	 */
	public static ConcurrentHashMap<AtomicCounter, MonitoringVO> getCountObject(String key) {
        ConcurrentHashMap<AtomicCounter, MonitoringVO> ifPresent = cache.getIfPresent(key);
        if (ifPresent == null || ifPresent.isEmpty()) {
            ifPresent = new ConcurrentHashMap<>();
            AtomicCounter atomicCounter = new AtomicCounter();
            MonitoringVO monitoringVO = new MonitoringVO();
            String[] split = key.split("--");
            monitoringVO.setTime(split[0]);
            monitoringVO.setApiURL(split[1]);
            monitoringVO.setSuccessfulNum(0L);
            monitoringVO.setFailuresNum(0L);
            monitoringVO.setSuccessfulTime(0L);
            monitoringVO.setFailuresTime(0L);
            ifPresent.put(atomicCounter, monitoringVO);
            cache.put(key, ifPresent);
		}
		return ifPresent;
	}

    /**
     * 增加访问接口调用成功的次数
     * @param concurrentHashMap
     * @return
     */
    public static void increaseSucceed(ConcurrentHashMap<AtomicCounter, MonitoringVO> concurrentHashMap) {
        MonitoringVO monitoringVO = null;
        for (MonitoringVO m : concurrentHashMap.values()) monitoringVO = m;
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            monitoringVO.setSuccessfulNum(monitoringVO.getSuccessfulNum() + 1);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 更新调用成功的耗时
     * @param key
     * @param successTime
     * @throws ExecutionException
     */
    public static void setSuccessfulTime(String key, Long successTime) {
        ConcurrentHashMap<AtomicCounter, MonitoringVO> map = getCountObject(key);
        MonitoringVO monitoringVO = null;
        for (MonitoringVO m : map.values()) monitoringVO = m;
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            monitoringVO.setSuccessfulTime(monitoringVO.getSuccessfulTime() + successTime);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 增加访问接口调用失败的次数
     * @param concurrentHashMap
     * @return
     */
	public static void increaseFail(ConcurrentHashMap<AtomicCounter, MonitoringVO> concurrentHashMap) {
        MonitoringVO monitoringVO = null;
        for (MonitoringVO m : concurrentHashMap.values()) monitoringVO = m;
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            monitoringVO.setFailuresNum(monitoringVO.getFailuresNum() + 1);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
	}

    /**
     *更新调用失败的耗时
     * @param key
     * @param failTime
     */
    public static void setMultiMap(String key, Long failTime) {
        ConcurrentHashMap<AtomicCounter, MonitoringVO> map = getCountObject(key);
        MonitoringVO monitoringVO = null;
        for (MonitoringVO m : map.values()) monitoringVO = m;
        Lock lock = new ReentrantLock();
        try {
            lock.lock();
            monitoringVO.setFailuresTime(monitoringVO.getFailuresTime() + failTime);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取时间戳,并以5分钟为节点
     * @return
     */
    public static String getTimeStamp() {
        // 获取时间戳
        Long timeStamp = System.currentTimeMillis();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH");
        SimpleDateFormat sdfMinute = new SimpleDateFormat("mm");
        // 时间戳转换成时间
        String sd = sdf.format(new Date(Long.parseLong(String.valueOf(timeStamp))));
        // 时间戳转换成时间
        String sdMinute = sdfMinute.format(new Date(Long.parseLong(String.valueOf(timeStamp))));
        sd += ":" + (Integer.valueOf(sdMinute) - Integer.valueOf(sdMinute) % 5);
        return sd;
    }

    /**
     * 获取API的接口URL
     * @param request
     * @return
     */
    public static String getApiURL(HttpServletRequest request) {
        // 请求路径
        String requestURI = request.getRequestURI();
        return requestURI;
    }

    /**
	 * 获取API监控信息
	 *
	 * @return
	 */
	public static CopyOnWriteArrayList<MonitoringVO> getCacheMap() {
        ConcurrentMap<String, ConcurrentHashMap<AtomicCounter, MonitoringVO>> stringConcurrentHashMapConcurrentMap = cache.asMap();
        CopyOnWriteArrayList<MonitoringVO> list = new CopyOnWriteArrayList<>();
		for (ConcurrentHashMap<AtomicCounter, MonitoringVO> c : stringConcurrentHashMapConcurrentMap.values())
			for (MonitoringVO m : c.values()) list.add(m);
		return list;
	}
}

5、编写测试类

HelloController.java:

@RestController
@RequestMapping("/aop")
public class HelloController {
    static int r = 0;

    @GetMapping("success")
    public String success() {
        return "调用成功";
    }

    @GetMapping("fail")
    public String fail() {
        if (r == 3) {
            r++;
            return "调用错误接口,成功一次";
        } else {
            r++;
            int i = 1 / 0;
            return "测试报错的AOP方法";
        }
    }

    @GetMapping("cache")
    public String get() {
        CopyOnWriteArrayList<MonitoringVO> list = AtomicCounter.getCacheMap();
        return JSONObject.valueToString(list);
    }
}

项目目录:
在这里插入图片描述
运行效果:
在这里插入图片描述

总结

以上就是今天要分享的内容,在线程安全方面,Google Guava Cache缓存本就是线程安全的,所以需要关注的是运算过程的线程安全,本可以使用AtomicInteger等线程安全的整型数据类型,但由于存储的太过麻烦不易处理,最终在处理运算上使用了lock来保证线程安全。今天的分享就到这里,给个三连呗,创作不易(づ。◕ᴗᴗ◕。)づ

附加功能

需求:能自定义给接口增强功能,例如:通过注解的形式给接口提供监控功能

代码如下:
MonitoringAnnotation .java:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MonitoringAnnotation {
}

ApiVisitHistory.java:

@Pointcut("@annotation(com.example.demo.monitoring.annotation.MonitoringAnnotation)")

修改路径即可:
在这里插入图片描述
只需要在我们需要监控的接口方法或类上加上这个注解即可:

@MonitoringAnnotation

源码地址

https://github.com/XIN007-C/api-monitoring/tree/main/aopdemo

如果对大家有帮助,给个start吧 ψ(*`ー´)ψ

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值