You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
8.2 KiB
202 lines
8.2 KiB
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.function.Supplier;
|
|
import java.util.logging.Logger;
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 必做1:UrlFormatException(Unchecked / Runtime)
|
|
// 选择 RuntimeException 理由:
|
|
// URL 格式错误属于调用方编程错误(非法参数),
|
|
// 不应强迫上层 catch,符合 Unchecked 语义。
|
|
// ─────────────────────────────────────────────
|
|
class UrlFormatException extends RuntimeException {
|
|
private final String invalidUrl;
|
|
|
|
public UrlFormatException(String url) {
|
|
// 保留根因信息,避免"异常包装丢失根因"问题
|
|
super("URL 格式不合法,必须以 http:// 或 https:// 开头:" + url);
|
|
this.invalidUrl = url;
|
|
}
|
|
|
|
/** 构造器2:包装底层异常,保留完整调用链 */
|
|
public UrlFormatException(String url, Throwable cause) {
|
|
super("URL 格式不合法:" + url, cause);
|
|
this.invalidUrl = url;
|
|
}
|
|
|
|
public String getInvalidUrl() { return invalidUrl; }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 必做2:RetryUtils —— 指数退避重试
|
|
// wait = base * 2^attempt (base = 500ms)
|
|
// ─────────────────────────────────────────────
|
|
class RetryUtils {
|
|
private static final Logger log = Logger.getLogger(RetryUtils.class.getName());
|
|
|
|
private static final long BASE_MS = 500; // 500ms 基准
|
|
private static final int MAX_RETRY = 4; // 最多重试次数
|
|
|
|
/**
|
|
* 执行带指数退避重试的操作
|
|
* @param task 要执行的任务,返回 true 表示成功
|
|
* @param maxRetry 最大重试次数
|
|
* @return true = 最终成功,false = 全部失败
|
|
*/
|
|
public static boolean retry(Supplier<Boolean> task, int maxRetry) {
|
|
for (int attempt = 0; attempt <= maxRetry; attempt++) {
|
|
if (attempt > 0) {
|
|
long waitMs = BASE_MS * (1L << attempt); // base * 2^attempt
|
|
log.info(String.format("第 %d 次重试,等待 %d ms ...", attempt, waitMs));
|
|
try {
|
|
Thread.sleep(waitMs);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
log.warning("重试等待被中断");
|
|
return false;
|
|
}
|
|
}
|
|
try {
|
|
if (task.get()) {
|
|
if (attempt > 0) log.info("第 " + attempt + " 次重试成功");
|
|
return true;
|
|
}
|
|
log.warning("第 " + attempt + " 次尝试失败(任务返回 false)");
|
|
} catch (Exception e) {
|
|
log.warning("第 " + attempt + " 次尝试抛出异常:" + e.getMessage());
|
|
}
|
|
}
|
|
log.severe("已达最大重试次数 " + maxRetry + ",放弃。");
|
|
return false;
|
|
}
|
|
|
|
/** 使用默认最大重试次数 */
|
|
public static boolean retry(Supplier<Boolean> task) {
|
|
return retry(task, MAX_RETRY);
|
|
}
|
|
|
|
/**
|
|
* 选做思考题优化:
|
|
* 如果某次失败是"永久失败"(如 404),则不应继续重试。
|
|
* 通过 NonRetryableException 标记不可重试异常,
|
|
* 捕获到后立即终止重试循环。
|
|
*/
|
|
public static boolean retryWithFastFail(Supplier<Boolean> task, int maxRetry) {
|
|
for (int attempt = 0; attempt <= maxRetry; attempt++) {
|
|
if (attempt > 0) {
|
|
long waitMs = BASE_MS * (1L << attempt);
|
|
try { Thread.sleep(waitMs); }
|
|
catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
return false;
|
|
}
|
|
}
|
|
try {
|
|
if (task.get()) return true;
|
|
} catch (NonRetryableException e) {
|
|
// 选做思考题:404 / 永久失败 → 立即放弃,不再等待
|
|
log.severe("不可重试异常,立即终止:" + e.getMessage());
|
|
return false;
|
|
} catch (Exception e) {
|
|
log.warning("attempt " + attempt + " failed: " + e.getMessage());
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 选做思考题辅助:不可重试异常(如 HTTP 404)
|
|
// ─────────────────────────────────────────────
|
|
class NonRetryableException extends RuntimeException {
|
|
private final int httpStatus;
|
|
|
|
public NonRetryableException(int httpStatus, String url) {
|
|
super("HTTP " + httpStatus + " 永久失败,不应重试:" + url);
|
|
this.httpStatus = httpStatus;
|
|
}
|
|
|
|
public int getHttpStatus() { return httpStatus; }
|
|
}
|
|
|
|
// ─────────────────────────────────────────────
|
|
// 选做:CircuitBreaker(断路器)
|
|
// 状态机:CLOSED → OPEN(连续失败 N 次)→ HALF_OPEN(冷却后)→ CLOSED
|
|
// ─────────────────────────────────────────────
|
|
class CircuitBreaker {
|
|
private static final Logger log = Logger.getLogger(CircuitBreaker.class.getName());
|
|
|
|
public enum State { CLOSED, OPEN, HALF_OPEN }
|
|
|
|
private final int failureThreshold; // 连续失败多少次触发熔断
|
|
private final long cooldownMs; // 熔断后冷却时间
|
|
|
|
private volatile State state = State.CLOSED;
|
|
private final AtomicInteger failureCount = new AtomicInteger(0);
|
|
private volatile long openedAt = 0;
|
|
|
|
public CircuitBreaker(int failureThreshold, long cooldownMs) {
|
|
this.failureThreshold = failureThreshold;
|
|
this.cooldownMs = cooldownMs;
|
|
}
|
|
|
|
/**
|
|
* 执行一次操作,断路器保护:
|
|
* - OPEN 状态直接拒绝(不发请求)
|
|
* - 成功则重置计数器;失败则累加,超阈值触发熔断
|
|
*/
|
|
public boolean call(Supplier<Boolean> task) {
|
|
switch (getEffectiveState()) {
|
|
case OPEN:
|
|
log.warning("断路器 OPEN,请求被拒绝(冷却剩余 "
|
|
+ (cooldownMs - (System.currentTimeMillis() - openedAt)) + "ms)");
|
|
return false;
|
|
|
|
case HALF_OPEN:
|
|
// 半开状态:放一个试探请求
|
|
log.info("断路器 HALF_OPEN,尝试探测...");
|
|
if (execute(task)) {
|
|
reset(); // 探测成功,恢复 CLOSED
|
|
return true;
|
|
}
|
|
trip(); // 探测失败,重新熔断
|
|
return false;
|
|
|
|
default: // CLOSED
|
|
if (execute(task)) {
|
|
failureCount.set(0);
|
|
return true;
|
|
}
|
|
if (failureCount.incrementAndGet() >= failureThreshold) {
|
|
trip();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean execute(Supplier<Boolean> task) {
|
|
try { return Boolean.TRUE.equals(task.get()); }
|
|
catch (Exception e) { return false; }
|
|
}
|
|
|
|
private State getEffectiveState() {
|
|
if (state == State.OPEN
|
|
&& System.currentTimeMillis() - openedAt >= cooldownMs) {
|
|
state = State.HALF_OPEN;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
private void trip() {
|
|
state = State.OPEN;
|
|
openedAt = System.currentTimeMillis();
|
|
log.severe("断路器已熔断!连续失败 " + failureCount.get() + " 次。");
|
|
}
|
|
|
|
private void reset() {
|
|
state = State.CLOSED;
|
|
failureCount.set(0);
|
|
log.info("断路器已恢复 CLOSED。");
|
|
}
|
|
|
|
public State getState() { return getEffectiveState(); }
|
|
}
|
|
|