Apache HttpClient/Java:请求限流(429)及Token刷新方案咨询
解决Apache HttpComponents HttpClient 4.5.5限流及重试时Token刷新问题
一、处理Rate Limit(429响应)
针对电商API返回的限流响应,核心思路是根据响应中的恢复时间等待后重试,结合HttpClient的重试逻辑实现可控的重试策略。
1. 自定义限流重试处理器
继承DefaultHttpRequestRetryHandler,扩展对429响应的处理逻辑,根据API返回的恢复时间计算等待时长:
import org.apache.http.HttpResponse; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.protocol.HttpContext; import java.io.IOException; import java.util.concurrent.TimeUnit; public class RateLimitRetryHandler implements HttpRequestRetryHandler { private final int maxRetries; public RateLimitRetryHandler(int maxRetries) { this.maxRetries = maxRetries; } // 处理IO异常的基础重试逻辑 @Override public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { return executionCount < maxRetries; } // 针对429响应的重试判断与等待逻辑 public boolean shouldRetryOn429(HttpResponse response, int executionCount) { if (executionCount >= maxRetries) { return false; } // 从响应头获取恢复时间(根据电商API实际返回的头字段调整,比如Retry-After或X-RateLimit-Reset) String retryAfterHeader = response.getFirstHeader("Retry-After") != null ? response.getFirstHeader("Retry-After").getValue() : null; long waitSeconds = 30; // 默认等待30秒 if (retryAfterHeader != null) { try { // 若返回的是秒数,直接解析 waitSeconds = Long.parseLong(retryAfterHeader); } catch (NumberFormatException e) { // 若返回的是日期格式,需额外解析为时间差,此处简化处理 waitSeconds = 60; } } try { TimeUnit.SECONDS.sleep(waitSeconds); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } return true; } }
2. 结合请求执行逻辑实现重试
在发起请求时,判断响应状态码,触发429时调用重试处理器的等待逻辑,循环执行请求直到成功或达到最大重试次数:
CloseableHttpClient httpClient = HttpClients.custom() .setRetryHandler(new RateLimitRetryHandler(3)) // 最多重试3次 .build(); HttpGet inventoryRequest = new HttpGet("https://api.example.com/inventory"); inventoryRequest.setHeader("Authorization", "Bearer " + getCurrentToken()); CloseableHttpResponse response = null; int executionCount = 0; boolean requestSuccess = false; do { executionCount++; try { response = httpClient.execute(inventoryRequest); int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { // 处理库存数据 String inventoryData = EntityUtils.toString(response.getEntity()); // ... 业务逻辑处理 requestSuccess = true; } else if (statusCode == 429) { RateLimitRetryHandler retryHandler = (RateLimitRetryHandler) httpClient.getHttpRequestRetryHandler(); if (!retryHandler.shouldRetryOn429(response, executionCount)) { throw new RuntimeException("已达最大重试次数,请求仍被限流"); } // 此处先判断Token是否过期,再重试 if (isTokenExpired()) { refreshToken(); inventoryRequest.setHeader("Authorization", "Bearer " + getCurrentToken()); } } else { throw new RuntimeException("请求失败,状态码:" + statusCode); } } catch (IOException e) { e.printStackTrace(); } finally { if (response != null) { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } } while (!requestSuccess && executionCount <= 3);
二、重试前刷新Token
由于Token有效期仅5分钟,需在重试前判断Token状态,过期则刷新后再发起请求。
1. 维护Token状态
存储当前Token及过期时间戳,用于判断是否需要刷新:
private String currentToken; private long tokenExpiryTime; // 过期时间(毫秒级时间戳)
2. 实现Token过期判断与刷新逻辑
import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.json.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; // 判断Token是否过期 private boolean isTokenExpired() { return System.currentTimeMillis() >= tokenExpiryTime; } // 刷新Token的方法 private void refreshToken() throws IOException { CloseableHttpClient authClient = HttpClients.createDefault(); HttpPost authRequest = new HttpPost("https://api.example.com/auth"); // 设置认证参数(根据API要求调整,比如用户名密码、客户端凭证等) List<NameValuePair> authParams = new ArrayList<>(); authParams.add(new BasicNameValuePair("client_id", "your_client_id")); authParams.add(new BasicNameValuePair("client_secret", "your_client_secret")); authRequest.setEntity(new UrlEncodedFormEntity(authParams)); CloseableHttpResponse authResponse = authClient.execute(authRequest); if (authResponse.getStatusLine().getStatusCode() == 200) { // 解析响应获取新Token及有效期(假设返回JSON格式) String authResponseBody = EntityUtils.toString(authResponse.getEntity()); JSONObject authJson = new JSONObject(authResponseBody); currentToken = authJson.getString("access_token"); int expiresInSeconds = authJson.getInt("expires_in"); // 通常返回秒数,此处为300秒(5分钟) tokenExpiryTime = System.currentTimeMillis() + (expiresInSeconds * 1000L); } else { throw new RuntimeException("刷新Token失败,状态码:" + authResponse.getStatusLine().getStatusCode()); } authResponse.close(); authClient.close(); }
3. 多线程环境注意事项
如果是多线程调用,需保证Token更新的线程安全:
- 使用
synchronized关键字修饰refreshToken()方法 - 采用双重检查锁定避免重复刷新Token
- 用原子类存储Token和过期时间(比如
AtomicReference)
内容的提问来源于stack exchange,提问作者Michael
相关产品推荐
相关产品推荐

