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