SpringBoot统计接口被调用次数及限制调用

(一)效果图:

1、被调用接口统计

2、单个接口某天被调用的次数

3、某个ip地址在某天调用的某个接口次数

(二)如何在SpringBoot使用?

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>

        <!-- Spring Boot Redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

  1. AOP相关代码

package com.coupon_test.coupon.utils;

import com.coupon_test.coupon.exception.CouponException;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 接口调用情况监控
 * 1、监控单个接口一天内的调用次数
 * 2、如果抛出异常,则记录异常信息及发生时间
 * 3、对单个IP进行限流,每天对每个接口的调用次数有限
 *
 * @author caojun
 * @date 2023-03-14
 */
@Aspect
@Component
public class ApiCallAdvice {

    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final String FORMAT_PATTERN_DAY = "yyyy-MM-dd";
    private static final String FORMAT_PATTERN_MILLS = "yyyy-MM-dd HH:mm:ss:SSS";

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

    /**
     * 真正执行业务操作前先进行限流的验证
     * 限制维度为:一天内单个IP的访问次数
     * key = URI + IP + date(精确到天)
     * value = 调用次数
     */
    @Before("execution(* com.coupon_test.coupon.controller.*.*(..))")
    public void before(JoinPoint joinPoint) {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取请求的request
        HttpServletRequest request = attributes.getRequest();
        String uri = request.getRequestURI();
        String date = dateFormat(FORMAT_PATTERN_DAY);
        String ip = getRequestIp(request);

        log.info("类名:{}", joinPoint.getSignature().getDeclaringType().getSimpleName());
        log.info("方法名:{}", joinPoint.getSignature().getName());

        if (StringUtils.isEmpty(ip)) {
            throw new CouponException("IP不能为空。");
        }
        // URI+IP+日期 构成以天为维度的key
        String ipKey = uri + "_" + ip + "_" + date;
        if (redisTemplate.hasKey(ipKey)) {
            if (Integer.parseInt(redisTemplate.opsForValue().get(ipKey).toString()) > 99) {    //对调用次数进行限制 0开始
                throw new CouponException("访问失败,已超过访问次数。");
            }
            redisTemplate.opsForValue().increment(ipKey, 1);
        } else {
            stringRedisTemplate.opsForValue().set(ipKey, "1", 1L, TimeUnit.DAYS);
        }
    }

    /**
     * 如果有返回结果,代表一次调用,则对应接口的调用次数加一,统计维度为天
     * (Redis使用Hash结构)
     * key = URI
     * key = date (精确到天)
     * value = 调用次数
     */
    @AfterReturning("execution(* com.coupon_test.coupon.controller.*.*(..))")
    public void afterReturning() {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取请求的request
        HttpServletRequest request = attributes.getRequest();

        String uri = request.getRequestURI();
        String date = dateFormat(FORMAT_PATTERN_DAY);

        if (redisTemplate.hasKey(uri)) {
            redisTemplate.boundHashOps(uri).increment(date, 1);
        } else {
            redisTemplate.boundHashOps(uri).put(date, 1);
        }
    }

    /**
     * 如果调用抛出异常,则缓存异常信息(Redis使用Hash结构)
     * key = URI + “_exception”
     * key = time (精确到毫秒的时间)
     * value = exception 异常信息
     *
     * @param ex 异常信息
     */
    @AfterThrowing(value = "execution(* com.coupon_test.coupon.controller.*.*(..))", throwing = "ex")
    public void afterThrowing(Exception ex) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String uri = request.getRequestURI() + "_exception";
        String time = dateFormat(FORMAT_PATTERN_MILLS);
        String exception = ex.getMessage();

        redisTemplate.boundHashOps(uri).put(time, exception);
    }

    private String getRequestIp(HttpServletRequest request) {
        // 获取请求IP
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip) || "null".equals(ip)) {
            ip = "" + request.getRemoteAddr();
        }
        return ip;
    }

    private String dateFormat(String pattern) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(pattern);
        return dateFormat.format(new Date());
    }
}

before方法是接收请求的地方,这里是controller下面的。

这是对请求的次数进行限制的,从0开始,如上就是限制一个ip调用一个url一天限制一百次,超过就会进行抛出异常。

若想限制ip调用某个接口的次数,直接修改如下代码即可,删除date即可。

如下是redis配置

  redis:
    port: 6379  #redis 端口号  无密码
    host: 127.0.0.1
    lettuce:
      pool:
        max-active: -1
        max-idle: 2000
        max-wait: -1
        min-idle: 1
        time-between-eviction-runs: 5000

(三)controller代码展示

获取所有的接口调用数据

获取某个接口被调用的次数数据

package com.coupon_test.coupon.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("/api/count")
public class InterfaceCallController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/{methodName}")
    public String getCount(@PathVariable String methodName) {
        List<Object> values = redisTemplate.boundHashOps("/api/" + methodName).values();
        return String.format("%s: %s", methodName, values);
    }

    @GetMapping
    public List<String> getAllCount() {
        Set<String> keys = redisTemplate.keys("/api/*_*_*");
        if (keys != null && !keys.isEmpty()) {
            List<String> countList = redisTemplate.opsForValue().multiGet(keys);
            List<String> result = new ArrayList<>();
            for (int i = 0; i < keys.size(); i++) {
                result.add(String.format("%s: %s", keys.toArray()[i], countList.get(i)));
            }
            return result;
        } else {
            return Collections.emptyList();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_46085718/article/details/129661065