ApiControlInterceptor 类分析

Truman - Sep 7 - - Dev Community

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 中。
. . . . . . . . .
Terabox Video Player