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>
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();
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)