在 WEB MVC的一些基础配置 中,ApiControlInterceptor是通过一个自定义的InterceptorRegistryCustomizer接口实现来引入的,具体是在InspectConfiguration类中配置的。
ApiControlInterceptor class
@Slf4j
public class ApiControlInterceptor implements HandlerInterceptor {
private int requestCount = 0;
private final AtomicInteger enterCount = new AtomicInteger(0);
private ApiManager apiManager;
private final ApiControlProperties apiControlProps;
private final CacheControl cacheControl;
private final ApplicationContext applicationContext;
private final Set<String> handlerMethods = new HashSet<>(512);
private final Map<HandlerMethodKey, Long> invokeTimeTrace = new ConcurrentHashMap<>(1024);
private final Map<String, ApiStatistics> invokeDuration = new ConcurrentHashMap<>(1024);
private Map<String, ApiStatistics> rateValve = new HashMap<>(1024);
private volatile long loggingBacklogTime = 0;
public ApiControlInterceptor(ApiControlProperties apiControlProps, CacheControl cacheControl, ApplicationContext applicationContext) {
this.apiControlProps = apiControlProps;
this.cacheControl = cacheControl;
this.applicationContext = applicationContext;
this.apiControlProps.initModuleIsEnabled();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
requestCount++;
long startTime = System.currentTimeMillis();
if (this.apiManager == null) {
this.apiManager = applicationContext.getBean(ApiManager.class);
}
String header = request.getHeader("X-Request-From");
if(Objects.equals(header, "gateway")){
if(apiControlProps.isStatisticalWeight()) {
this.apiManager.requestToRedis(
request.getHeader("deviceId"),
request.getHeader("Host"),
request.getHeader("client-type")
);
}
}
if (requestCount % apiControlProps.getReportUnusedRate() == 0) {
this.apiManager.markAsUsed(handlerMethods);
}
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
handlerMethods.add(method.getDeclaringClass().getName() + "." + method.getName());
}
int backlogValue = this.enterCount.incrementAndGet();
if (backlogValue > apiControlProps.getLoggingBacklogThreshold() && startTime - loggingBacklogTime > 5000) {
loggingBacklogTime = startTime;
Map<String, long[]> map = new HashMap<>();
for (HandlerMethodKey hm : invokeTimeTrace.keySet()) {
String methodName = hm.getMethod().getDeclaringClass().getName() + "." + hm.getMethod().getName();
String parameterTypes = Arrays.stream(hm.getMethod().getParameterTypes())
.map(Class::getName)
.collect(Collectors.joining(","));
String key = methodName + "(" + parameterTypes + ")";
long avgDuration = 0;
ApiStatistics apiStatistics = rateValve.get(key);
if (apiStatistics != null) {
avgDuration = apiStatistics.getAvgDuration();
}
long[] data = map.putIfAbsent(key, new long[]{1L, avgDuration});
// 如果key存在,则put失败成功,value则不可能为null
if (data != null) {
data[0] = data[0] + 1;
}
}
String text = map.entrySet().stream()
.map(entry -> entry.getKey() + " => [请求积压:" + entry.getValue()[0] + ", 平均耗时:" + entry.getValue()[1] + "ms]")
.collect(Collectors.joining("\n\t"));
log.info("用户请求积压详情,数量:({}, {}) \n\t{}", backlogValue, invokeTimeTrace.size(), text);
}
if(apiControlProps.isEnabledApiControl()) {
if (requestCount % apiControlProps.getReportStatisticsRate() == 0) {
this.rateValve = this.apiManager.pushAndFetchApiStatistics(invokeDuration);
}
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
HandlerMethodKey hmk = new HandlerMethodKey(hm);
invokeTimeTrace.put(hmk, startTime);
String methodName = hm.getMethod().getDeclaringClass().getName() + "." + hm.getMethod().getName();
String parameterTypes = Arrays.stream(hm.getMethod().getParameterTypes()).map(Class::getName).collect(Collectors.joining(","));
String key = methodName + "(" + parameterTypes + ")";
ApiStatistics invoke = new ApiStatistics();
invoke.setTotalCount(1);
ApiStatistics absent = invokeDuration.putIfAbsent(key, invoke);
// 如果key存在,则put失败成功,value则不可能为null
if (absent != null) {
absent.incrementTotalCount(1);
} else {
absent = invoke;
}
if (backlogValue > apiControlProps.getBacklogThreshold()) {
// 开启过载保护
cacheControl.turnOnOverloadProtection();
ApiStatistics apiStatistics = rateValve.get(key);
if (apiStatistics != null && apiStatistics.getAvgDuration() > apiControlProps.getDurationThreshold()) {
if (!apiControlProps.getNoneBlockingMethods().contains(methodName)) {
enterCount.decrementAndGet();
absent.incrementReject(1);
response.setStatus(429);
StringBuffer url = request.getRequestURL();
if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) {
url.append("?").append(request.getQueryString());
}
log.info("用户请求存在积压({}),此接口({})请求耗时较长({}),拒绝请求", backlogValue, request.getRequestURI(), apiStatistics.getAvgDuration());
PrintWriter out = response.getWriter();
out.print("Blocked by backlog (flow limiting)");
out.flush();
out.close();
return false;
}
}
} else {
// 关闭过载保护
cacheControl.turnOffOverloadProtection();
}
absent.incrementAccept(1);
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
this.statisticsApiCall(request, response, handler);
}
private void statisticsApiCall(HttpServletRequest request, HttpServletResponse response, Object handler) {
enterCount.decrementAndGet();
long duration = 0;
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
Long startTime = invokeTimeTrace.remove(new HandlerMethodKey(hm));
if (startTime != null) {
duration = System.currentTimeMillis() - startTime;
String methodName = hm.getMethod().getDeclaringClass().getName() + "." + hm.getMethod().getName();
String parameterTypes = Arrays.stream(hm.getMethod().getParameterTypes()).map(Class::getName).collect(Collectors.joining(","));
String key = methodName + "(" + parameterTypes + ")";
ApiStatistics value = invokeDuration.get(key);
if (value != null) {
value.incrementDuration(duration);
}
}
}
log.debug("{} {}, duration={}ms, {}", request.getMethod(), request.getRequestURI(), duration, response.getStatus());
}
}
1. 类的成员变量
private int requestCount = 0; // 累计处理的请求数
private final AtomicInteger enterCount = new AtomicInteger(0); // 当前处理中的请求数
private ApiManager apiManager; // API 管理器实例,用于处理 API 统计相关功能
private final ApiControlProperties apiControlProps; // API 控制属性配置
private final CacheControl cacheControl; // 缓存控制,用于管理过载保护等功能
private final ApplicationContext applicationContext; // Spring 应用上下文,用于获取 Bean
private final Set<String> handlerMethods = new HashSet<>(512); // 记录被调用的 handler 方法
private final Map<HandlerMethodKey, Long> invokeTimeTrace = new ConcurrentHashMap<>(1024); // 存储请求的开始时间
private final Map<String, ApiStatistics> invokeDuration = new ConcurrentHashMap<>(1024); // 存储每个 API 的统计信息
private Map<String, ApiStatistics> rateValve = new HashMap<>(1024); // 存储从 Redis 获取到的统计信息
private volatile long loggingBacklogTime = 0; // 记录上一次记录积压日志的时间戳
- requestCount: 用于累计处理的请求数,定期用于控制某些统计功能的触发。
- enterCount: 原子整数,用于记录当前处理中的请求数,用来判断是否开启过载保护等功能。
- apiManager: 负责 API 统计数据的管理和 Redis 操作。
- apiControlProps: 配置类,用于读取 API 控制相关的配置项,例如是否开启统计、积压阈值等。
- cacheControl: 用于管理缓存以及系统的过载保护。
- handlerMethods: 存储被调用的 API 方法,帮助识别活跃的 API。
- invokeTimeTrace: 存储 API 请求的开始时间,帮助计算 API 的请求时长。
- invokeDuration: 记录每个 API 请求的统计数据,包括总调用次数、耗时、接受和拒绝的请求数量。
2. 构造方法
public ApiControlInterceptor(ApiControlProperties apiControlProps, CacheControl cacheControl, ApplicationContext applicationContext) {
this.apiControlProps = apiControlProps;
this.cacheControl = cacheControl;
this.applicationContext = applicationContext;
this.apiControlProps.initModuleIsEnabled();
}
- 构造方法:初始化 ApiControlInterceptor 实例,并接收一些配置类和上下文。该类依赖 ApiManager 和 ApiControlProperties 来执行 API 管理任务。
3. preHandle 方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
requestCount++; // 记录总请求次数
long startTime = System.currentTimeMillis(); // 记录请求的开始时间
// 获取 ApiManager 实例,如果还没有初始化则从应用上下文中获取
if (this.apiManager == null) {
this.apiManager = applicationContext.getBean(ApiManager.class);
}
// 如果请求来自 'gateway' 且启用统计权重,将请求信息存储到 Redis
String header = request.getHeader("X-Request-From");
if (Objects.equals(header, "gateway")) {
if (apiControlProps.isStatisticalWeight()) {
this.apiManager.requestToRedis(
request.getHeader("deviceId"),
request.getHeader("Host"),
request.getHeader("client-type")
);
}
}
// 定期标记已经使用的 API
if (requestCount % apiControlProps.getReportUnusedRate() == 0) {
this.apiManager.markAsUsed(handlerMethods);
}
// 如果 handler 是一个方法,记录其信息
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
handlerMethods.add(method.getDeclaringClass().getName() + "." + method.getName());
}
int backlogValue = this.enterCount.incrementAndGet(); // 增加正在处理的请求数
// 如果请求积压超过阈值并且距离上次记录积压日志的时间超过5秒,记录积压日志
if (backlogValue > apiControlProps.getLoggingBacklogThreshold() && startTime - loggingBacklogTime > 5000) {
loggingBacklogTime = startTime;
logBacklogRequests();
}
// 如果启用了 API 控制
if (apiControlProps.isEnabledApiControl()) {
// 定期推送和获取 API 统计信息
if (requestCount % apiControlProps.getReportStatisticsRate() == 0) {
this.rateValve = this.apiManager.pushAndFetchApiStatistics(invokeDuration);
}
// 如果 handler 是一个方法,进行统计
if (handler instanceof HandlerMethod) {
return handleApiRequest(request, response, handler, startTime, backlogValue);
}
}
return true;
}
- 统计请求数:每次处理请求时增加计数requestCount,用于定期执行一些操作(如标记API、获取统计信息)。
- 积压日志:当积压请求数超过阈值时,记录日志,输出当前积压的API请求及其处理时长。
- Redis 存储请求信息:对于来自特定来源的请求,将它们的信息(如deviceId、Host、client-type)存储到 Redis 以进行统计分析。
- API 控制逻辑:如果 API 控制功能开启,则根据配置定期从 Redis 获取最新的统计信息。
4. 处理API请求的方法
private boolean handleApiRequest(HttpServletRequest request, HttpServletResponse response, Object handler, long startTime, int backlogValue) throws IOException {
HandlerMethod hm = (HandlerMethod) handler;
HandlerMethodKey hmk = new HandlerMethodKey(hm);
// 记录请求开始时间
invokeTimeTrace.put(hmk, startTime);
String methodName = hm.getMethod().getDeclaringClass().getName() + "." + hm.getMethod().getName();
String parameterTypes = Arrays.stream(hm.getMethod().getParameterTypes()).map(Class::getName).collect(Collectors.joining(","));
String key = methodName + "(" + parameterTypes + ")";
// 初始化或更新统计信息
ApiStatistics invoke = new ApiStatistics();
invoke.setTotalCount(1);
ApiStatistics absent = invokeDuration.putIfAbsent(key, invoke);
if (absent != null) {
absent.incrementTotalCount(1);
} else {
absent = invoke;
}
// 如果请求积压超过阈值,启动过载保护
if (backlogValue > apiControlProps.getBacklogThreshold()) {
cacheControl.turnOnOverloadProtection();
ApiStatistics apiStatistics = rateValve.get(key);
// 如果请求的平均处理时长超过阈值,且该方法不是非阻塞的,拒绝请求
if (apiStatistics != null && apiStatistics.getAvgDuration() > apiControlProps.getDurationThreshold()) {
if (!apiControlProps.getNoneBlockingMethods().contains(methodName)) {
enterCount.decrementAndGet();
absent.incrementReject(1);
rejectRequest(response, request, backlogValue, apiStatistics);
return false;
}
}
} else {
// 关闭过载保护
cacheControl.turnOffOverloadProtection();
}
absent.incrementAccept(1); // 统计接受请求
return true;
}
- 记录请求的开始时间:用于后续统计请求处理时长。
- 统计信息的初始化和更新:每个API请求都会初始化或更新它的统计信息,包括请求的总数、接受或拒绝请求等。
- 过载保护:如果积压的请求数超过阈值,则开启过载保护。对于处理时长过长的API,会拒绝请求,并返回HTTP状态码429。
5. 统计请求的结束时间
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
this.statisticsApiCall(request, response, handler); // 请求完成后记录统计数据
}
private void statisticsApiCall(HttpServletRequest request, HttpServletResponse response, Object handler) {
enterCount.decrementAndGet();
long duration = 0;
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
Long startTime = invokeTimeTrace.remove(new HandlerMethodKey(hm));
if (startTime != null) {
duration = System.currentTimeMillis() - startTime;
String methodName = hm.getMethod().getDeclaringClass().getName() + "." + hm.getMethod().getName();
String parameterTypes = Arrays.stream(hm.getMethod().getParameterTypes()).map(Class::getName).collect(Collectors.joining(","));
String key = methodName + "(" + parameterTypes + ")";
ApiStatistics value = invokeDuration.get(key);
if (value != null) {
value.incrementDuration(duration);
}
}
}
log.debug("{} {}, duration={}ms, {}", request.getMethod(), request.getRequestURI(), duration, response.getStatus());
}
- 在 afterCompletion 方法中,系统会记录请求的处理时长,并将这些统计信息记录到 invokeDuration 中。