diff --git a/w11/ParseStrategies.java b/w11/ParseStrategies.java new file mode 100644 index 0000000..6435e14 --- /dev/null +++ b/w11/ParseStrategies.java @@ -0,0 +1,82 @@ +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; + +// ───────────────────────────────────────────── +// 策略接口 +// ───────────────────────────────────────────── +interface ParseStrategy { + boolean supports(String url); + Article parse(String url); + String getName(); + default int getPriority() { return 100; } +} + +// ───────────────────────────────────────────── +// 策略实现1:GitHub 专属(priority=10) +// ───────────────────────────────────────────── +class GithubParseStrategy implements ParseStrategy { + @Override + public boolean supports(String url) { + return url != null && url.matches("^https?://(www\\.)?github\\.com/.*"); + } + + @Override + public Article parse(String url) { + String path = url.replaceFirst("https?://(www\\.)?github\\.com/", ""); + String[] parts = path.split("/"); + String owner = parts.length > 0 ? parts[0] : "unknown"; + String repo = parts.length > 1 ? parts[1] : "repo"; + return new Article("[GitHub] " + owner + "/" + repo, url, + "(GitHub stub)", owner, LocalDate.now()); + } + + @Override public String getName() { return "GithubParseStrategy"; } + @Override public int getPriority(){ return 10; } +} + +// ───────────────────────────────────────────── +// 策略实现2:通用默认兜底(priority=999) +// ───────────────────────────────────────────── +class DefaultParseStrategy implements ParseStrategy { + private static final Pattern URL_PATTERN = + Pattern.compile("^https?://[^\\s/$.?#].[^\\s]*$"); + + @Override + public boolean supports(String url) { + return url != null && URL_PATTERN.matcher(url).matches(); + } + + @Override + public Article parse(String url) { + String domain = url.replaceFirst("https?://", "").replaceAll("/.*", ""); + return new Article("文章来自 " + domain, url, + "(default stub)", "unknown", LocalDate.now()); + } + + @Override public String getName() { return "DefaultParseStrategy"; } + @Override public int getPriority(){ return 999; } +} + +// ───────────────────────────────────────────── +// 策略选择器 +// ───────────────────────────────────────────── +class StrategySelector { + private final List strategies = new ArrayList<>(); + + public StrategySelector() { + strategies.add(new GithubParseStrategy()); + strategies.add(new DefaultParseStrategy()); + strategies.sort(Comparator.comparingInt(ParseStrategy::getPriority)); + } + + public ParseStrategy select(String url) { + return strategies.stream() + .filter(s -> s.supports(url)) + .findFirst().orElse(null); + } + + public List getAll() { return List.copyOf(strategies); } +} diff --git a/w11/Utils.java b/w11/Utils.java new file mode 100644 index 0000000..e12822d --- /dev/null +++ b/w11/Utils.java @@ -0,0 +1,202 @@ +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(); } +}