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 相关文章推荐
浅析NIO系列之TCP
Jun 15 Java/Android
详解Spring Boot使用系统参数表提升系统的灵活性
Jun 30 Java/Android
SpringBoot集成Druid连接池连接MySQL8.0.11
Jul 02 Java/Android
使用logback实现按自己的需求打印日志到自定义的文件里
Aug 30 Java/Android
关于maven依赖 ${xxx.version}报错问题
Jan 18 Java/Android
关于Mybatis中SQL节点的深入解析
Mar 19 Java/Android
Android超详细讲解组件ScrollView的使用
Mar 31 Java/Android
零基础学java之方法的定义与调用详解
Apr 10 Java/Android
SpringCloud项目如何解决log4j2漏洞
Apr 10 Java/Android
Android开发之底部导航栏的快速实现
Apr 28 Java/Android
Qt数据库应用之实现图片转pdf
Jun 01 Java/Android
一文搞懂Java中的注解和反射
Jun 21 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
php数字转汉字代码(算法)
2011/10/08 PHP
phpExcel中文帮助手册之常用功能指南
2014/08/18 PHP
Yii2如何批量添加数据
2016/05/17 PHP
Yii2压缩PHP中模板代码的输出问题
2018/08/28 PHP
laravel 解决Eloquent ORM的save方法无法插入数据的问题
2019/10/21 PHP
JavaScript this 深入理解
2009/07/30 Javascript
javascript encodeURI和encodeURIComponent的比较
2010/04/03 Javascript
在JQuery dialog里的服务器控件 事件失效问题
2010/12/08 Javascript
Ext JS 4实现带week(星期)的日期选择控件(实战一)
2013/08/21 Javascript
javascript的document.referrer浏览器支持、失效情况总结
2014/07/18 Javascript
javascript正则表达式参数/g与/i及/gi的使用指南
2014/08/27 Javascript
JavaScript不刷新实现浏览器的前进后退功能
2014/11/05 Javascript
JS实现往下不断流动网页背景的方法
2015/02/27 Javascript
jquery实现焦点图片随机切换效果的方法
2015/03/12 Javascript
jQuery+HTML5加入购物车代码分享
2020/10/29 Javascript
javascript中的3种继承实现方法
2016/01/27 Javascript
浅谈javascript中的call、apply、bind
2016/03/06 Javascript
WebApi+Bootstrap+KnockoutJs打造单页面程序
2016/05/16 Javascript
Angular表单验证实例详解
2016/10/20 Javascript
Vue.js使用v-show和v-if的注意事项
2016/12/13 Javascript
前端js弹出框组件使用方法
2020/08/24 Javascript
轻松理解JavaScript之AJAX
2017/03/15 Javascript
JS字符串补全方法padStart()和padEnd()
2020/05/27 Javascript
python进阶教程之异常处理
2014/08/30 Python
使用python进行波形及频谱绘制的方法
2019/06/17 Python
Python CategoricalDtype自定义排序实现原理解析
2020/09/11 Python
博士学位自我鉴定范文
2013/12/26 职场文书
司法助理专业自荐书
2014/06/13 职场文书
医院党的群众路线教育实践活动学习心得体会
2014/10/30 职场文书
领导参观欢迎词
2015/01/26 职场文书
烈士陵园扫墓感想
2015/08/07 职场文书
2019年大学推荐信
2019/06/24 职场文书
送给教师们,到底该如何写好教学反思?
2019/07/02 职场文书
85句关于理想的名言警句大全
2019/08/22 职场文书
详解gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)
2021/11/27 Vue.js
tomcat的catalina.out日志按自定义时间格式进行分割的操作方法
2022/04/02 Servers