首页 > 文章列表 > SpringBoot如何使用AOP实现统计全局接口访问次数

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

springboot aop
340 2023-05-21

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

AOP是什么

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

AOP的作用和优势

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

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

常见的动态代理技术

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

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

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 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中可以看到接口的请求次数。

Web最终效果图如下: