Java 实现 对外接口进行aop限流
1.需要的maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.限流注解InterfaceLimit.java
特此注意: 这是一个注解类 @interface
package com.safesoft.config.customlimit;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* [自定义接口限流注解]
*
* @author Kevin.Wan
* @date 2023/8/1
**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface InterfaceLimit {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
double permitsPerSecond () ;
/**
* 获取令牌最大等待时间
*/
long timeout();
/**
* 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
*/
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
}
3.限流注解的切面类LimitAop.java
package com.safesoft.config.customlimit;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import com.safesoft.domain.utils.IPUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import static com.safesoft.domain.common.Constants.DATE_TIME_FORMATTER;
/**
* [基于aop切面实现限流]
*
* @author Kevin.Wan
* @date 2023/8/1
**/
@Slf4j
@Aspect
@Component
public class LimitAop {
/**
* 不同的接口,不同的流量控制
* map的key为 Limiter.key
*/
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Around("@annotation(com.safesoft.config.customlimit.InterfaceLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
//获取当前时间
final String format_time = LocalDateTime.now().format(DATE_TIME_FORMATTER);
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿limit的注解
InterfaceLimit limit = method.getAnnotation(InterfaceLimit.class);
if (limit != null) {
//key作用:不同的接口,不同的流量控制
String key=limit.key();
RateLimiter rateLimiter = null;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key, rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
}
rateLimiter = limitMap.get(key);
// 拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
//获取request
final HttpServletRequest request = getHttpServletRequest();
// 请求的真实ip
final String realIP = IPUtils.getRealIP(request);
// 拿不到命令,直接返回异常提示
if (!acquire) {
log.error("当前请求人数过多进入等待,请求ip:{},请求时间:{}", realIP, format_time);
throw new IllegalStateException("请求次数太频繁,请稍后...");
}
log.info("获取令牌成功,IP:{},时间:{}", realIP, format_time);
}
return joinPoint.proceed();
}
protected final HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
}
4.里面用到了获取访问IP工具类(可以不用)
package com.safesoft.domain.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* @Author Sherlock.shen
* @Date 2021/3/25 17:39
* @Description: 获取访问ip工具类
*/
public class IPUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(IPUtils.class);
private static final String STR_UNKNOWN_LOWER_CASE = "unKnown";
private static final String STR_COMMA = ",";
public static final int NUM_ZERO = 0;
public static final int NUM_ONE = 1;
public static String getRealIP(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != NUM_ZERO && !STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(STR_COMMA) != -NUM_ONE) {
ip = ip.split(STR_COMMA)[NUM_ZERO];
}
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
LOGGER.info("Proxy-Client-IP ip,{}", ip);
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
LOGGER.info("WL-Proxy-Client-IP,{}", ip);
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
LOGGER.info("HTTP_CLIENT_IP ip,{}", ip);
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
LOGGER.info("HTTP_X_FORWARDED_FOR,{}", ip);
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
LOGGER.info("X-Real-IP ip,{}", ip);
}
if (ip == null || ip.length() == NUM_ZERO || STR_UNKNOWN_LOWER_CASE.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
LOGGER.info("getRemoteAddr ip,{}", ip);
}
return ip;
}
}
5.然后就可以使用了 例如
/**
* 水位监测看板数据==》也就是根据设备查询实时数据
*/
@LogAction("对外水位监测看板数据")
@PostMapping(URLConstant.WATER_LEVEL_OPEN_MONITORING)
@InterfaceLimit(key = "getWaterLevel",permitsPerSecond = 5,timeout = 500,timeunit = TimeUnit.MILLISECONDS)
public JSONObject getWaterLevel(@RequestBody @Validated ExternalParam externalParam){
LOGGER.info("对外接口-获取水位监测看板数据");
//stop1 验签
final Boolean aBoolean = externalService.signatureCheck(externalParam);
if (aBoolean.equals(Boolean.FALSE)){
throw new IllegalStateException("验签失败");
}
//stop2 判断是否有组编号
String groupId = "";
if (!Strings.isNullOrEmpty(externalParam.getGroupId())){
groupId = externalParam.getGroupId();
}
//stop3 根据组编号查询设备实时数据
return cockpitService.getWaterLevelRealTimeData(groupId);
}
其中 @InterfaceLimit(key = “getWaterLevel”,permitsPerSecond = 5,timeout = 500,timeunit = TimeUnit.MILLISECONDS) 这个就是
key任意起,不重复即可,,后面代表的是 每秒最多5个请求 每秒的前500毫秒响应