2 changed files with 284 additions and 0 deletions
@ -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<ParseStrategy> 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<ParseStrategy> getAll() { return List.copyOf(strategies); } |
||||
|
} |
||||
@ -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<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(); } |
||||
|
} |
||||
Loading…
Reference in new issue