ApiControlInterceptor 类分析

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()); }
} 
Enter fullscreen mode
Exit fullscreen mode

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; // 记录上一次记录积压日志的时间戳
Enter fullscreen mode
Exit fullscreen mode
  • 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();
}
Enter fullscreen mode
Exit fullscreen mode
  • 构造方法:初始化 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;
} 
Enter fullscreen mode
Exit fullscreen mode
  • 统计请求数:每次处理请求时增加计数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;
} 
Enter fullscreen mode
Exit fullscreen mode
  • 记录请求的开始时间:用于后续统计请求处理时长。
  • 统计信息的初始化和更新:每个API请求都会初始化或更新它的统计信息,包括请求的总数、接受或拒绝请求等。
  • 过载保护:如果积压的请求数超过阈值,则开启过载保护。对于处理时长过长的API,会拒绝请求,并返回HTTP状态码429。

5. 统计请求的结束时间

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { this.statisticsApiCall(request, response, handler); // 请求完成后记录统计数据
} 
Enter fullscreen mode
Exit fullscreen mode
 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()); }
Enter fullscreen mode
Exit fullscreen mode
  • 在 afterCompletion 方法中,系统会记录请求的处理时长,并将这些统计信息记录到 invokeDuration 中。

Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top
Pip3 install docker compose. Cart urban safari pet.