

در این مقاله نحوهٔ پیکربندی بازتلاش (retry) برای درخواستهای HTTP هنگام استفاده از OkHttp در پروژههای وب اسکریپینگ را بررسی میکنیم. هدف این است که پس از خواندن مقاله، بتوانید با دو رویکرد رایج — استفاده از یک کتابخانهٔ آماده (Retry4j) و نوشتن یک wrapper سفارشی — رفتار بازتلاش را پیادهسازی، پیکربندی و بهینه کنید. همچنین نکات عملی دربارهٔ کنترل تعداد تلاشها، backoff، تشخیص صفحات بن (ban) و نکات امنیتی و performance را پوشش میدهیم.
ایدهٔ کلی: به جای نوشتن منطق بازتلاش از صفر، از یک کتابخانهٔ تخصصی مثل retry4j استفاده میکنیم تا سیاستهای بازتلاش، listenerها و تابع backoff را بهسادگی تعریف کنیم. Retry4j امکان تعریف تعداد تکرار، تاخیر بین تلاشها و exponential backoff را فراهم میکند.
مثال زیر یک پیادهسازی ساده را نشان میدهد. ورودی: هیچ پارامتری؛ خروجی: متن بدنهٔ پاسخ در صورت موفقیت. اگر پاسخ دارای وضعیتهایی مثل 429 یا 5xx باشد، exception پرتاب میشود تا retry فعال شود.
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import com.evanlennick.retry4j.CallExecutor;
import com.evanlennick.retry4j.CallExecutorBuilder;
import com.evanlennick.retry4j.Status;
import com.evanlennick.retry4j.config.RetryConfig;
import com.evanlennick.retry4j.config.RetryConfigBuilder;
public class RetryWithRetry4j {
public static List badStatusCodes = Arrays.asList(429, 500, 502, 503, 504);
public static void main(String[] args) throws Exception {
OkHttpClient client = new OkHttpClient();
Callable makeRequest = () -> {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
try (Response response = client.newCall(request).execute()) {
int statusCode = response.code();
if (badStatusCodes.contains(statusCode)) {
throw new Exception("Bad status code: " + statusCode);
}
String body = response.body().string();
System.out.println("Response body length: " + body.length());
}
return null;
};
RetryConfig config = new RetryConfigBuilder()
.retryOnAnyException()
.withMaxNumberOfTries(5)
.withDelayBetweenTries(10, ChronoUnit.SECONDS)
.withExponentialBackoff()
.build();
CallExecutor callExecutor = new CallExecutorBuilder()
.config(config)
.onFailureListener((Status s) -> System.out.println("Maximum number of retries reached."))
.afterFailedTryListener((Status s) -> System.out.println("Total tries: " + s.getTotalTries()))
.build();
callExecutor.execute(makeRequest);
}
}
توضیح بخشها:
نکات عملی و محدودیتها:
ایدهٔ کلی: خودتان حلقهٔ retry را کنترل میکنید تا رفتار دقیقتری داشته باشید؛ مثلاً فقط روی خطاهای اتصال تلاش کنید، یا پس از مشاهده HTML مشخصی (مثلاً صفحهٔ بن) دوباره تلاش کنید. این رویکرد کنترل کامل بر جریان، گزارش و handling خطا را میدهد.
نمونهٔ سادهٔ wrapper با تعداد ثابت تلاشها:
import java.net.ConnectException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class CustomRetryLogic {
final public static int NUM_RETRIES = 3;
public static void main(String[] args) throws Exception {
Response response = null;
OkHttpClient client = new OkHttpClient();
for (int i = 0; i < NUM_RETRIES; i++) {
try {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
response = client.newCall(request).execute();
int status = response.code();
if (status == 200 || status == 404) {
// خروج از حلقه در صورت موفقیت یا صفحهٔ پیدا نشد
break;
}
} catch (Exception e) {
boolean connectionError = e instanceof ConnectException;
if (connectionError) {
// لاگ، متریک یا backoff ساده
}
} finally {
System.out.println("Total tries: " + (i + 1));
}
// مثال: تاخیر افزایشی ساده بین تلاشها
Thread.sleep(2000L * (i + 1));
}
if (response != null && response.code() == 200) {
System.out.println("Response body: " + response.body().string());
} else {
System.out.println("No valid response after maximum number of tries");
}
}
}
توضیح مختصر:
گاهی سرور وضعیت 200 برمیگرداند اما محتوا صفحهٔ بن یا کپچا است. در این حالت بهتر است بعد از دریافت HTML، محتوای آن را بررسی کرده و در صورت پیدا شدن الگوهای بن، درخواست را مجدداً تلاش کنید.
import java.net.ConnectException;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.Jsoup;
public class CustomRetryWithContentCheck {
final public static int NUM_RETRIES = 3;
public static void main(String[] args) throws Exception {
Response response = null;
String responseBodyText = null;
boolean validResponse = false;
OkHttpClient client = new OkHttpClient();
for (int i = 0; i < NUM_RETRIES; i++) {
try {
Request request = new Request.Builder()
.url("https://quotes.toscrape.com")
.build();
response = client.newCall(request).execute();
responseBodyText = response.body().string();
String pageTitle = Jsoup.parse(responseBodyText).title();
int status = response.code();
boolean validStatus = status == 200 || status == 404;
if (validStatus && !pageTitle.contains("Robot or human?")) {
validResponse = true;
break;
}
} catch (Exception e) {
boolean connectionError = e instanceof ConnectException;
if (connectionError) {
// لاگ یا هشدار
}
} finally {
System.out.println("Total tries: " + (i + 1));
}
// exponential-ish backoff ساده
Thread.sleep(1000L * (long) Math.pow(2, i));
}
if (response != null && validResponse && response.code() == 200) {
System.out.println("Response body: " + responseBodyText);
} else {
System.out.println("No valid response after maximum number of tries");
}
}
}
نکات مهم:
برای افزایش مقاومت سیستم وب اسکریپینگ در برابر خطاهای شبکه و محدودیتهای سروری، بازتلاش هوشمند یکی از ابزارهای کلیدی است. اگر به دنبال پیادهسازی سریع و قابلپیکربندی هستید، استفاده از کتابخانهای مثل retry4j مناسب است؛ اما اگر نیاز به قواعد ویژهٔ تشخیص محتوا و کنترل کامل دارید، یک wrapper سفارشی بهتر پاسخگو خواهد بود. در هر دو حالت رعایت قوانین مربوط به backoff، مدیریت timeouts، کنترل لاگ و محدود کردن نرخ درخواستها ضروری است.


