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

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(); }
}