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 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 task) { return retry(task, MAX_RETRY); } /** * 选做思考题优化: * 如果某次失败是"永久失败"(如 404),则不应继续重试。 * 通过 NonRetryableException 标记不可重试异常, * 捕获到后立即终止重试循环。 */ public static boolean retryWithFastFail(Supplier 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 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 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(); } }