SpringBoot使用AOP实现统计全局接口访问次数详解


Posted in Java/Android onJune 16, 2022

AOP是什么

AOP(Aspect Oriented Programming),也就是面向切面编程,是通过预编译方式和运行期间动态代理实现程序功能的传统已维护的一种技术。

AOP的作用和优势

作用:在程序运行期间,在不修改源代码的情况下对某些方法进行功能增强

优势:减少重复代码,提高开发效率,并且便于维护

常见的动态代理技术

jdk代理:基于接口的动态代理技术

SpringBoot使用AOP实现统计全局接口访问次数详解

cglib代理:基于父类的动态代理技术

SpringBoot使用AOP实现统计全局接口访问次数详解

AOP相关概念

  • List item- Target(目标对象):代理的目标对象
  • Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类
  • Joinpoint(连接点):连接点是指那些被拦截到的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点(可以被增强的方法叫连接点)
  • PointCut(切入点):切入点是指我们要对哪些Joinpoint进行拦截的定义
  • Advice(通知/增强):通知是指拦截到Joinpoint之后所要做的事情就是通知
  • Aspect(切面):是切入点和通知的结合
  • Weaving(织入):把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译器织入和类装载器织入

实现

我在这里采用基于注解形式的的AOP开发。

开发步骤

加入依赖

<!--引入AOP依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--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>

创建目标接口和目标类(内部有切点)

创建切面类(内部有增强方法)

将目标类和切面类的对象创建权交给Spirng

在切面类中使用注解配置织入关系

在配置文件中开启组件扫描和AOP自动代理

SpringBoot使用AOP实现统计全局接口访问次数详解

因为我的项目是SpringBoot Web项目,在这里开启注解就好了。

下面的代码为使用到的原子计数类。

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private static final AtomicCounter atomicCounter = new AtomicCounter();
    /**
     * 单例,不允许外界主动实例化
     */
    private AtomicCounter() {
    }
    public static AtomicCounter getInstance() {
        return atomicCounter;
    }
    private static AtomicInteger counter = new AtomicInteger();
    public int getValue() {
        return counter.get();
    }
    public int increase() {
        return counter.incrementAndGet();
    }
    public int decrease() {
        return counter.decrementAndGet();
    }
    // 清零
    public void toZero(){
        counter.set(0);
    }
}

下面的代码为实现的全局接口监控。

我在项目中简单使用了Redis作缓存,所有你可以看到有Redis相关的操作。

使用 @Before 用于配置前置通知。指定增强的方法在切入点方法之前执行。

使用@ @AfterReturning 用于配置后置通知。指定增强的方法在切入点方法之后执行。

使用@ @AfterThrowing 用于配置异常抛出通知。指定增强的方法在出现异常时执行。

@Component
@Aspect
public class GlobalActuator {
    private static final Logger log = LoggerFactory.getLogger(GlobalActuator.class);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    ThreadLocal<Long> startTime = new ThreadLocal<>();
    ConcurrentHashMap<Object, Object> countMap = new ConcurrentHashMap<Object, Object>();
    /**
     * 匹配控制层层通知 这里监控controller下的所有接口
     */
    @Pointcut("execution(* com.sf.controller.*Controller.*(..))")
    private void controllerPt() {
    }
    /**
     * 在接口原有的方法执行前,将会首先执行此处的代码
     */
    @Before("com.sf.actuator.GlobalActuator.controllerPt()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        //获取传入目标方法的参数
        Object[] args = joinPoint.getArgs();
    }
    /**
     * 只有正常返回才会执行此方法
     * 如果程序执行失败,则不执行此方法
     */
    @AfterReturning(returning = "returnVal", pointcut = "com.sf.actuator.GlobalActuator.controllerPt()")
    public void doAfterReturning(JoinPoint joinPoint, Object returnVal) throws Throwable {
        Signature signature = joinPoint.getSignature();
        String declaringName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        String mapKey = declaringName + methodName;
        // 执行成功则计数加一
        int increase = AtomicCounter.getInstance().increase();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        synchronized (this) {
        //在项目启动时,需要在Redis中读取原有的接口请求次数
            if (countMap.size() == 0) {
                JSONObject jsonObject = RedisUtils.objFromRedis(StringConst.INTERFACE_ACTUATOR);
                if (jsonObject != null) {
                    Set<String> strings = jsonObject.keySet();
                    for (String string : strings) {
                        Object o = jsonObject.get(string);
                        countMap.putIfAbsent(string, o);
                    }
                }
            }
        }
        // 如果此次访问的接口不在countMap,放入countMap
        countMap.putIfAbsent(mapKey, 0);
        countMap.compute(mapKey, (key, value) -> (Integer) value + 1);
        synchronized (this) {
            // 内存计数达到30 更新redis
            if (increase == 30) {
                RedisUtils.objToRedis(StringConst.INTERFACE_ACTUATOR, countMap, Constants.AVA_REDIS_TIMEOUT);
                //删除过期时间
                stringRedisTemplate.persist(StringConst.INTERFACE_ACTUATOR);
                //计数器置为0
                AtomicCounter.getInstance().toZero();
            }
        }
        //log.info("方法执行次数:" + mapKey + "------>" + countMap.get(mapKey));
        //log.info("URI:[{}], 耗费时间:[{}] ms", request.getRequestURI(), System.currentTimeMillis() - startTime.get());
    }
    /**
     * 当接口报错时执行此方法
     */
    @AfterThrowing(pointcut = "com.sf.actuator.GlobalActuator.controllerPt()")
    public void doAfterThrowing(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.info("接口访问失败,URI:[{}], 耗费时间:[{}] ms", request.getRequestURI(), System.currentTimeMillis() - startTime.get());
    }
}

这里再给出Controller层代码。

@GetMapping("/interface/{intCount}")
    @ApiOperation(value = "查找接口成功访问次数(默认倒序)")
    public Result<List<InterfaceDto>> findInterfaceCount(
            @ApiParam(name = "intCount", value = "需要的接口数") @PathVariable Integer intCount
    ) {
        HashMap<String, Integer> hashMap = new HashMap<>();
        JSONObject jsonObject = RedisUtils.objFromRedis(StringConst.INTERFACE_ACTUATOR);
        if (jsonObject != null) {
            Set<String> strings = jsonObject.keySet();
            for (String string : strings) {
                Integer o = (Integer) jsonObject.get(string);
                hashMap.putIfAbsent(string, o);
            }
        }
        //根据value倒序
        Map<String, Integer> sortedMap = hashMap.entrySet().stream().sorted(Collections.reverseOrder(Map.Entry.comparingByValue()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
        //返回列表
        List<InterfaceDto> resultList = new ArrayList<>();
        //排序后中的map中所有的key
        Object[] objects = sortedMap.keySet().toArray();
        for (int i = 0; i < intCount; i++) {
            InterfaceDto interfaceDto = new InterfaceDto();
            interfaceDto.setName((String) objects[i]);
            interfaceDto.setCount(sortedMap.get((String) objects[i]));
            resultList.add(interfaceDto);
        }
        return Result.success(resultList);
    }

项目运行一段时间后,在Redis中可以看到接口的请求次数。

SpringBoot使用AOP实现统计全局接口访问次数详解

Web最终效果图如下:

SpringBoot使用AOP实现统计全局接口访问次数详解

到此这篇关于SpringBoot使用AOP实现统计全局接口访问次数详解的文章就介绍到这了,更多相关SpringBoot AOP内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!


Tags in this post...

Java/Android 相关文章推荐
spring项目中切面及AOP的使用方法
Jun 26 Java/Android
探讨Java中的深浅拷贝问题
Jun 26 Java/Android
一篇文章带你复习java知识点
Jun 28 Java/Android
Java SSH 秘钥连接mysql数据库的方法
Jun 28 Java/Android
Spring Boot mybatis-config 和 log4j 输出sql 日志的方式
Jul 26 Java/Android
使用springMVC所需要的pom配置
Sep 15 Java/Android
springboot如何接收application/x-www-form-urlencoded类型的请求
Nov 02 Java/Android
关于@OnetoMany关系映射的排序问题,使用注解@OrderBy
Dec 06 Java/Android
java objectUtils 使用可能会出现的问题
Feb 28 Java/Android
Java十分钟精通进阶适配器模式
Apr 06 Java/Android
Spring Boot配合PageHelper优化大表查询数据分页
Apr 20 Java/Android
openGauss数据库JDBC环境连接配置的详细过程(Eclipse)
Jun 01 Java/Android
Java中的Kotlin 内部类原理
Jun 16 #Java/Android
Spring Security动态权限的实现方法详解
Java实现注册登录跳转
Jun 16 #Java/Android
Java界面编程实现界面跳转
springboot实现string转json json里面带数组
Jun 16 #Java/Android
Android Gradle 插件自定义Plugin实现注意事项
Jun 16 #Java/Android
Java完整实现记事本代码
Jun 16 #Java/Android
You might like
蝙蝠侠:侠影之谜
2020/03/04 欧美动漫
Re:从零开始的异世界生活 第2季 开播啦
2020/07/24 日漫
php 分页函数multi() discuz
2009/06/21 PHP
PHP学习笔记之一
2011/01/17 PHP
php实现的click captcha点击验证码类实例
2014/09/23 PHP
如何在Laravel5.8中正确地应用Repository设计模式
2019/11/26 PHP
PHP常用header头定义代码示例汇总
2020/08/29 PHP
lyhucSelect基于Jquery的Select数据联动插件
2011/03/29 Javascript
JavaScript 上万关键字瞬间匹配实现代码
2013/07/07 Javascript
js修改input的type属性问题探讨
2013/10/12 Javascript
js正则表达exec与match的区别说明
2014/01/29 Javascript
JS实现网站菜单拖拽移位效果的方法
2015/09/24 Javascript
JS实现新浪博客左侧的Blog管理菜单效果代码
2015/10/22 Javascript
BootStrap的JS插件之轮播效果案例详解
2016/05/16 Javascript
JavaScript实现窗口抖动效果
2016/10/19 Javascript
微信小程序侧边栏滑动特效(左右滑动)
2017/01/23 Javascript
详解react、redux、react-redux之间的关系
2018/04/11 Javascript
发布Angular应用至生产环境的方法
2018/12/10 Javascript
vue实现可视化可拖放的自定义表单的示例代码
2019/03/20 Javascript
如何对react hooks进行单元测试的方法
2019/08/14 Javascript
vue 根据选择的月份动态展示日期对应的星期几
2021/02/06 Vue.js
编写Python CGI脚本的教程
2015/06/29 Python
python中使用iterrows()对dataframe进行遍历的实例
2018/06/09 Python
python实现opencv+scoket网络实时图传
2020/03/20 Python
python Matplotlib数据可视化(2):详解三大容器对象与常用设置
2020/09/30 Python
python subprocess pipe 实时输出日志的操作
2020/12/05 Python
BASIC HOUSE官方旗舰店:韩国著名的服装品牌
2018/09/27 全球购物
HOTEL INFO英国:搜索全球酒店
2019/08/08 全球购物
过滤器的用法
2013/10/08 面试题
求职信怎么写
2014/05/23 职场文书
党员学习群众路线教育实践活动对照检查材料
2014/09/23 职场文书
党的群众路线教育实践活动查摆剖析材料
2014/10/10 职场文书
事业单位工作人员年度考核个人总结
2015/02/12 职场文书
道歉的话语大全
2015/05/12 职场文书
死亡诗社观后感
2015/06/05 职场文书
golang中的空slice案例
2021/04/27 Golang