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
Manage your data invogue nexus web design. If you can make it go viral on social media platforms, it can generate lots of backlinks and traffic to your site.