首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java 外汇行情接口接入:认证失败、超时与脏数据的处理路径

Java 外汇行情接口接入:认证失败、超时与脏数据的处理路径

原创
作者头像
小张的云上实验室
修改于 2026-11-04 11:02:02
修改于 2026-11-04 11:02:02
110
举报

Java 外汇行情接口接入:认证失败、超时与脏数据的处理路径

前段时间接入一个外汇行情接口,用于获取实时报价。第一版代码很短:构建请求、带上认证头、拿到 JSON、解析出价格,本地跑通后就直接放进了采集流程。结果上线后不久,采集线程卡了 8 秒,日志里只留下一条被统一 catch 吞掉的异常,既看不出是认证问题、限流还是网络抖动,也无法判断拿到的价格是否已经过期。

这次排查之后,我把接入层重新整理了一遍。下面记录三个改动:超时与重试的边界、认证失败与限流的分类处理、解析阶段的字段校验。示例中的地址、请求头和字段名均为通用占位,实际接入时以所用接口的说明为准。

第一版代码的问题

最初的调用逻辑大致如下:

代码语言:java
复制
HttpClient client = HttpClient.newHttpClient();// 示例基于 DeepTick 接口的返回结构做了脱敏改写,端点与字段均为占位

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(ENDPOINT + "?symbol=EURUSD"))
        .header("X-API-Key", TOKEN)
        .header("Accept", "application/json")
        .build();

HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());

这段代码能跑,但有几个明显问题:

  1. 没有设置超时。HttpClient.newHttpClient() 使用默认配置,连接不响应时,采集线程会一直等下去。
  2. 异常被混在一起。认证失败、限流、网络超时都落到同一个分支,排查时只能靠猜。
  3. 返回内容直接打印。没有校验字段是否存在、时间戳是否过期,脏数据会继续流向下游。
  4. 认证信息硬编码。早期为了调试方便,Key 直接写在常量里,后来才改为从环境变量读取并在启动时校验。

改动一:给请求设置明确的超时边界

外汇接口调用不能假设对方永远及时响应。我给 HttpClient 设置了连接超时,并给单次请求设置了总超时:

代码语言:java
复制
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .build();

HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(ENDPOINT + "?symbol=EURUSD"))
        .timeout(Duration.ofSeconds(5))
        .header("X-API-Key", TOKEN)//  替换成:DeepTick的token
        .header("Accept", "application/json")
        .build();

这里的取舍是:连接超时设得比请求总超时短,避免在 TCP 阶段消耗掉整个请求预算;请求总超时则根据采集周期来定,必须小于下游消费允许的最大延迟。如果采集周期是 2 秒,请求超时设成 5 秒本身就不合理,因为一次慢请求会直接拖过多个采集窗口。

重试没有直接加在 send() 外面。原因是行情请求对时效敏感,盲目重试可能拿到更晚的数据,也可能在对方限流时加重问题。当前的做法是:只对连接超时和可恢复的服务端错误做有限重试,认证失败和参数错误不重试。重试次数和退避时间放在采集层统一控制,而不是塞在单次请求里。

改动二:把认证失败和限流分开处理

状态码不同,处理方式完全不同。认证失败通常意味着凭据无效或请求头缺失,重试没有意义;限流则需要退避;服务端错误才值得有限重试。调整后的分支大致如下:

代码语言:java
复制
import java.io.IOException;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;

try {
    HttpResponse<String> response =
            client.send(request, HttpResponse.BodyHandlers.ofString());

    int status = response.statusCode();

    if (status == 200) {
        handleBody(response.body());
    } else if (status == 401 || status == 403) {
        // 凭据问题:记录并停止当前采集,避免反复无效请求
        log.error("认证失败,status={},请检查凭据与请求头", status);
    } else if (status == 429) {
        // 限流:交给采集层退避,不在单次请求内循环重试
        log.warn("触发限流,status=429");
    } else if (status >= 500) {
        log.warn("服务端错误,status={}", status);
    } else {
        log.warn("未预期状态码,status={},body={}", status, response.body());
    }
} catch (HttpTimeoutException e) {
    log.warn("请求超时", e);
} catch (IOException e) {
    log.warn("网络异常", e);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    log.warn("采集线程被中断", e);
}

InterruptedException 单独处理并恢复中断标志,这一点容易被忽略。如果只打印栈而不调用 Thread.currentThread().interrupt(),上层就无法感知线程已被中断,关闭采集流程时可能继续空转。

认证信息也改为从环境变量读取,并在启动阶段做一次 fail-fast 校验:

代码语言:java
复制
private static final String ENDPOINT = System.getenv("QUOTE_ENDPOINT");
private static final String TOKEN = System.getenv("QUOTE_TOKEN");

static {
    if (ENDPOINT == null || ENDPOINT.isBlank()) {
        throw new IllegalStateException("缺少 QUOTE_ENDPOINT");
    }
    if (TOKEN == null || TOKEN.isBlank()) {
        throw new IllegalStateException("缺少 QUOTE_TOKEN");
    }
}

这样做的直接好处是:容器环境变量注入顺序和本地不同时,问题会在启动阶段暴露,而不是等到第一次请求失败后才发现。

改动三:解析阶段先校验字段,再取价格

行情接口返回 JSON,解析本身不难,难的是不让缺失字段或异常值流向下游。使用 Jackson 时,path() 对不存在的字段返回 MissingNode,直接 asDouble() 会得到 0.0,很容易把“没有价格”误判成“价格为零”。

当前解析逻辑会先检查关键字段,再校验时间戳:

代码语言:java
复制
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Instant;

ObjectMapper mapper = new ObjectMapper();

void handleBody(String body) {
    try {
        JsonNode root = mapper.readTree(body);

        JsonNode symbolNode = root.path("symbol");
        JsonNode priceNode = root.path("price");
        JsonNode tsNode = root.path("timestamp");

        if (symbolNode.isMissingNode()
                || priceNode.isMissingNode()
                || tsNode.isMissingNode()) {
            log.warn("返回字段缺失,body={}", body);
            return;
        }

        double price = priceNode.asDouble();
        long timestamp = tsNode.asLong();

        if (price <= 0) {
            log.warn("价格无效,price={}", price);
            return;
        }

        long now = Instant.now().getEpochSecond();
        if (Math.abs(now - timestamp) > 30) {
            log.warn("数据时间戳偏差过大,timestamp={},now={}", timestamp, now);
            return;
        }

        // 通过校验后再交给下游
        publish(symbolNode.asText(), price, timestamp);

    } catch (Exception e) {
        log.warn("解析失败,body={}", body, e);
    }
}

时间戳偏差阈值按业务容忍度设定。行情数据对时效敏感,但也不能假设本地时钟和对方完全一致,所以这里用绝对偏差而不是“必须晚于本地时间”。字段名和返回结构在实际接入时要以对应接口的说明为准,不同标的的字段可能并不完全一致。

当前状态与下一步

改造后,采集线程不再因为单次请求无限阻塞,认证失败和限流在日志里可以区分,字段缺失和过期数据也不会直接进入下游。仍有两个问题没有完全解决:

  1. 同一标的的重复请求。当前每次采集都直接请求,尚未加入本地短缓存。下一步会按标的设置一个很短的缓存窗口,减少同一秒内的重复调用。
  2. 连接复用与采集频率的配合。HttpClient 默认支持连接复用,但采集频率、超时和重试退避之间还没有形成统一配置,需要在压测中确认边界。

这次接入最大的体会是:外部行情接口的难点不在“把请求发出去”,而在把失败分类清楚、把脏数据挡在解析层、把超时和重试放在合适的层级。这些基础环节做扎实之后,再扩展标的数量或增加其他数据通道,改动成本会低很多。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Java 外汇行情接口接入:认证失败、超时与脏数据的处理路径
    • 第一版代码的问题
    • 改动一:给请求设置明确的超时边界
    • 改动二:把认证失败和限流分开处理
    • 改动三:解析阶段先校验字段,再取价格
    • 当前状态与下一步
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档