72 changed files with 1694 additions and 6 deletions
@ -0,0 +1,135 @@ |
|||
# AI 架构审计:策略模式解耦与封装审查 |
|||
|
|||
## 一、类签名汇总 |
|||
|
|||
### 1. Article(数据模型) |
|||
```java |
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
private long timestamp; |
|||
|
|||
public Article(String title, String url) // 防御性参数校验 |
|||
public void setTitle(String title) // 防御性参数校验 |
|||
public void setUrl(String url) // 防御性参数校验 |
|||
// Getter方法 |
|||
} |
|||
``` |
|||
|
|||
### 2. ArticleRepository(仓储层) |
|||
```java |
|||
public class ArticleRepository { |
|||
private final List<Article> articles; |
|||
|
|||
public void add(Article article) // 防御 null |
|||
public void addAll(Collection<Article>) // 防御 null 和空集合 |
|||
public List<Article> getAll() // 返回不可变视图 |
|||
public Article findByTitle(String) // 防御 null |
|||
public int count() |
|||
public void clear() |
|||
public boolean contains(Article) // 防御 null |
|||
} |
|||
``` |
|||
|
|||
### 3. ArticleAnalyzer(策略接口) |
|||
```java |
|||
public interface ArticleAnalyzer { |
|||
Map<String, Object> analyze(List<Article> articles); |
|||
String getName(); |
|||
} |
|||
``` |
|||
|
|||
### 4. ArticleCountAnalyzer(具体策略1) |
|||
```java |
|||
public class ArticleCountAnalyzer implements ArticleAnalyzer { |
|||
public Map<String, Object> analyze(List<Article>); |
|||
public String getName(); |
|||
} |
|||
``` |
|||
|
|||
### 5. TitleLengthAnalyzer(具体策略2) |
|||
```java |
|||
public class TitleLengthAnalyzer implements ArticleAnalyzer { |
|||
public Map<String, Object> analyze(List<Article>); |
|||
public String getName(); |
|||
} |
|||
``` |
|||
|
|||
### 6. AnalyzeCommand(命令类) |
|||
```java |
|||
public class AnalyzeCommand { |
|||
private final List<Article> articles; |
|||
private final List<ArticleAnalyzer> analyzers; |
|||
|
|||
public AnalyzeCommand(List<Article>, List<ArticleAnalyzer>); |
|||
public void execute(); // 复用策略解析,不存储结果 |
|||
public void addAnalyzer(ArticleAnalyzer); |
|||
} |
|||
``` |
|||
|
|||
## 二、AI 架构审计请求 |
|||
|
|||
> 作为 Java 架构师,请审查我的代码设计: |
|||
> |
|||
> 1. **策略解耦**: |
|||
> - ArticleAnalyzer 接口是否定义清晰? |
|||
> - 具体策略(ArticleCountAnalyzer、TitleLengthAnalyzer)是否与上下文解耦? |
|||
> - 添加新策略是否无需修改现有代码? |
|||
> |
|||
> 2. **封装性**: |
|||
> - ArticleRepository 的内部状态是否得到保护? |
|||
> - getAll() 返回不可变视图是否正确? |
|||
> - 防御性编程是否过度或不足? |
|||
> |
|||
> 3. **单一职责**: |
|||
> - AnalyzeCommand 是否只负责协调执行,不承担存储职责? |
|||
> - 各分析器是否只关注单一分析维度? |
|||
> |
|||
> 4. **依赖注入**: |
|||
> - 依赖是否通过构造函数注入? |
|||
> - 是否符合依赖倒置原则? |
|||
> |
|||
> 请给出改进建议和优化方案。 |
|||
|
|||
## 三、设计原则应用 |
|||
|
|||
### 已应用的设计原则 |
|||
|
|||
| 原则 | 应用位置 | 说明 | |
|||
|------|---------|------| |
|||
| 单一职责 | ArticleAnalyzer 接口 | 每个分析器只负责一种分析 | |
|||
| 开闭原则 | 策略模式 | 添加新分析器无需修改现有代码 | |
|||
| 依赖倒置 | AnalyzeCommand | 依赖抽象接口而非具体实现 | |
|||
| 里氏替换 | 策略实现 | 所有分析器可互换使用 | |
|||
|
|||
### 防御性编程 |
|||
|
|||
| 防御点 | 位置 | 方式 | |
|||
|--------|------|------| |
|||
| null检查 | Article构造器 | IllegalArgumentException | |
|||
| null检查 | Repository.add() | Objects.requireNonNull | |
|||
| null检查 | Repository.addAll() | 遍历检查每个元素 | |
|||
| 不可变返回 | Repository.getAll() | Collections.unmodifiableList | |
|||
|
|||
## 四、文件列表 |
|||
|
|||
- [Article.java](file:///D:\嘻嘻哈哈\Git\java\w10\Article.java) |
|||
- [ArticleRepository.java](file:///D:\嘻嘻哈哈\Git\java\w10\ArticleRepository.java) |
|||
- [ArticleAnalyzer.java](file:///D:\嘻嘻哈哈\Git\java\w10\ArticleAnalyzer.java) |
|||
- [ArticleCountAnalyzer.java](file:///D:\嘻嘻哈哈\Git\java\w10\ArticleCountAnalyzer.java) |
|||
- [TitleLengthAnalyzer.java](file:///D:\嘻嘻哈哈\Git\java\w10\TitleLengthAnalyzer.java) |
|||
- [AnalyzeCommand.java](file:///D:\嘻嘻哈哈\Git\java\w10\AnalyzeCommand.java) |
|||
- [TestMain.java](file:///D:\嘻嘻哈哈\Git\java\w10\TestMain.java) |
|||
|
|||
## 五、运行方式 |
|||
|
|||
```bash |
|||
# 编译 |
|||
javac Article.java ArticleRepository.java ArticleAnalyzer.java \ |
|||
ArticleCountAnalyzer.java TitleLengthAnalyzer.java \ |
|||
AnalyzeCommand.java TestMain.java |
|||
|
|||
# 运行 |
|||
java TestMain |
|||
``` |
|||
Binary file not shown.
@ -0,0 +1,85 @@ |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
|
|||
/** |
|||
* 分析命令类 |
|||
* 复用策略解析但不存储,只输出统计信息 |
|||
*/ |
|||
public class AnalyzeCommand { |
|||
private final List<Article> articles; |
|||
private final List<ArticleAnalyzer> analyzers; |
|||
|
|||
/** |
|||
* 构造函数,接收文章列表和分析器列表 |
|||
* @param articles 要分析的文章列表 |
|||
* @param analyzers 分析器列表 |
|||
*/ |
|||
public AnalyzeCommand(List<Article> articles, List<ArticleAnalyzer> analyzers) { |
|||
Objects.requireNonNull(articles, "文章列表不能为null"); |
|||
Objects.requireNonNull(analyzers, "分析器列表不能为null"); |
|||
|
|||
this.articles = new ArrayList<>(articles); |
|||
this.analyzers = new ArrayList<>(analyzers); |
|||
} |
|||
|
|||
/** |
|||
* 执行分析并输出结果(不存储) |
|||
*/ |
|||
public void execute() { |
|||
System.out.println("================================================"); |
|||
System.out.println(" 文章分析报告"); |
|||
System.out.println("================================================"); |
|||
System.out.println("分析文章数量: " + articles.size()); |
|||
System.out.println("分析器数量: " + analyzers.size()); |
|||
System.out.println("------------------------------------------------"); |
|||
|
|||
for (ArticleAnalyzer analyzer : analyzers) { |
|||
System.out.println("\n【" + analyzer.getName() + "】"); |
|||
Map<String, Object> result = analyzer.analyze(articles); |
|||
|
|||
// 输出分析结果(不存储)
|
|||
for (Map.Entry<String, Object> entry : result.entrySet()) { |
|||
if (!"analyzer".equals(entry.getKey())) { |
|||
System.out.println(" " + formatKey(entry.getKey()) + ": " + formatValue(entry.getValue())); |
|||
} |
|||
} |
|||
} |
|||
|
|||
System.out.println("\n================================================"); |
|||
System.out.println(" 分析报告结束"); |
|||
System.out.println("================================================"); |
|||
} |
|||
|
|||
/** |
|||
* 格式化键名 |
|||
*/ |
|||
private String formatKey(String key) { |
|||
switch (key) { |
|||
case "totalCount": return "文章总数"; |
|||
case "averageLength": return "平均标题长度"; |
|||
case "maxLength": return "最长标题长度"; |
|||
case "minLength": return "最短标题长度"; |
|||
default: return key; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 格式化值 |
|||
*/ |
|||
private String formatValue(Object value) { |
|||
if (value instanceof Double) { |
|||
return String.format("%.2f", value); |
|||
} |
|||
return value.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 添加分析器 |
|||
*/ |
|||
public void addAnalyzer(ArticleAnalyzer analyzer) { |
|||
Objects.requireNonNull(analyzer, "分析器不能为null"); |
|||
analyzers.add(analyzer); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,65 @@ |
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
private long timestamp; |
|||
|
|||
public Article(String title, String url) { |
|||
if (title == null || title.trim().isEmpty()) { |
|||
throw new IllegalArgumentException("标题不能为空"); |
|||
} |
|||
if (url == null || url.trim().isEmpty()) { |
|||
throw new IllegalArgumentException("URL不能为空"); |
|||
} |
|||
this.title = title; |
|||
this.url = url; |
|||
this.timestamp = System.currentTimeMillis(); |
|||
} |
|||
|
|||
public String getTitle() { |
|||
return title; |
|||
} |
|||
|
|||
public void setTitle(String title) { |
|||
if (title == null || title.trim().isEmpty()) { |
|||
throw new IllegalArgumentException("标题不能为空"); |
|||
} |
|||
this.title = title; |
|||
} |
|||
|
|||
public String getUrl() { |
|||
return url; |
|||
} |
|||
|
|||
public void setUrl(String url) { |
|||
if (url == null || url.trim().isEmpty()) { |
|||
throw new IllegalArgumentException("URL不能为空"); |
|||
} |
|||
this.url = url; |
|||
} |
|||
|
|||
public String getContent() { |
|||
return content; |
|||
} |
|||
|
|||
public void setContent(String content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
public long getTimestamp() { |
|||
return timestamp; |
|||
} |
|||
|
|||
public void setTimestamp(long timestamp) { |
|||
this.timestamp = timestamp; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Article{" + |
|||
"title='" + title + '\'' + |
|||
", url='" + url + '\'' + |
|||
", timestamp=" + timestamp + |
|||
'}'; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,21 @@ |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 文章分析策略接口 |
|||
* 策略模式:定义分析算法的接口,不同的分析算法可以独立变化 |
|||
*/ |
|||
public interface ArticleAnalyzer { |
|||
/** |
|||
* 分析文章并返回统计结果 |
|||
* @param articles 文章列表 |
|||
* @return 统计结果映射 |
|||
*/ |
|||
Map<String, Object> analyze(List<Article> articles); |
|||
|
|||
/** |
|||
* 获取分析器名称 |
|||
* @return 分析器名称 |
|||
*/ |
|||
String getName(); |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,18 @@ |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class ArticleCountAnalyzer implements ArticleAnalyzer { |
|||
@Override |
|||
public Map<String, Object> analyze(List<Article> articles) { |
|||
Map<String, Object> result = new HashMap<>(); |
|||
result.put("analyzer", getName()); |
|||
result.put("totalCount", articles.size()); |
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "文章数量分析器"; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,73 @@ |
|||
import java.util.ArrayList; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
|
|||
public class ArticleRepository { |
|||
private final List<Article> articles; |
|||
|
|||
public ArticleRepository() { |
|||
this.articles = new ArrayList<>(); |
|||
} |
|||
|
|||
/** |
|||
* 添加单个文章,防御 null |
|||
*/ |
|||
public void add(Article article) { |
|||
Objects.requireNonNull(article, "文章不能为null"); |
|||
articles.add(article); |
|||
} |
|||
|
|||
/** |
|||
* 添加多个文章,防御 null 和空集合 |
|||
*/ |
|||
public void addAll(Collection<Article> articlesToAdd) { |
|||
Objects.requireNonNull(articlesToAdd, "文章集合不能为null"); |
|||
|
|||
for (Article article : articlesToAdd) { |
|||
Objects.requireNonNull(article, "集合中包含null文章"); |
|||
articles.add(article); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取所有文章的不可变视图 |
|||
*/ |
|||
public List<Article> getAll() { |
|||
return Collections.unmodifiableList(articles); |
|||
} |
|||
|
|||
/** |
|||
* 根据标题查找文章 |
|||
*/ |
|||
public Article findByTitle(String title) { |
|||
Objects.requireNonNull(title, "标题不能为null"); |
|||
return articles.stream() |
|||
.filter(a -> title.equals(a.getTitle())) |
|||
.findFirst() |
|||
.orElse(null); |
|||
} |
|||
|
|||
/** |
|||
* 获取文章数量 |
|||
*/ |
|||
public int count() { |
|||
return articles.size(); |
|||
} |
|||
|
|||
/** |
|||
* 清空所有文章 |
|||
*/ |
|||
public void clear() { |
|||
articles.clear(); |
|||
} |
|||
|
|||
/** |
|||
* 检查是否包含指定文章 |
|||
*/ |
|||
public boolean contains(Article article) { |
|||
Objects.requireNonNull(article, "文章不能为null"); |
|||
return articles.contains(article); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,95 @@ |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
|
|||
public class TestMain { |
|||
public static void main(String[] args) { |
|||
System.out.println("===== 测试 ArticleRepository ====="); |
|||
testRepository(); |
|||
|
|||
System.out.println("\n===== 测试 AnalyzeCommand ====="); |
|||
testAnalyzeCommand(); |
|||
|
|||
System.out.println("\n===== 测试防御性编程 ====="); |
|||
testDefensiveProgramming(); |
|||
} |
|||
|
|||
private static void testRepository() { |
|||
ArticleRepository repo = new ArticleRepository(); |
|||
|
|||
Article article1 = new Article("Java编程入门", "https://example.com/java"); |
|||
Article article2 = new Article("Python数据分析", "https://example.com/python"); |
|||
Article article3 = new Article("JavaScript高级", "https://example.com/js"); |
|||
|
|||
// 测试添加单个文章
|
|||
repo.add(article1); |
|||
repo.add(article2); |
|||
System.out.println("添加2篇文章后数量: " + repo.count()); |
|||
|
|||
// 测试添加多个文章
|
|||
repo.addAll(Arrays.asList(article3)); |
|||
System.out.println("批量添加后数量: " + repo.count()); |
|||
|
|||
// 测试查找
|
|||
Article found = repo.findByTitle("Java编程入门"); |
|||
System.out.println("查找结果: " + (found != null ? found.getTitle() : "未找到")); |
|||
|
|||
// 测试获取所有文章(不可变视图)
|
|||
List<Article> all = repo.getAll(); |
|||
System.out.println("获取文章列表大小: " + all.size()); |
|||
} |
|||
|
|||
private static void testAnalyzeCommand() { |
|||
// 创建示例文章
|
|||
List<Article> articles = new ArrayList<>(); |
|||
articles.add(new Article("Java编程入门教程", "https://example.com/java")); |
|||
articles.add(new Article("Python数据分析实战", "https://example.com/python")); |
|||
articles.add(new Article("JavaScript高级编程", "https://example.com/js")); |
|||
articles.add(new Article("Spring Boot指南", "https://example.com/spring")); |
|||
articles.add(new Article("Docker容器化部署", "https://example.com/docker")); |
|||
|
|||
// 创建分析器列表
|
|||
List<ArticleAnalyzer> analyzers = new ArrayList<>(); |
|||
analyzers.add(new ArticleCountAnalyzer()); |
|||
analyzers.add(new TitleLengthAnalyzer()); |
|||
|
|||
// 创建并执行分析命令
|
|||
AnalyzeCommand command = new AnalyzeCommand(articles, analyzers); |
|||
command.execute(); |
|||
} |
|||
|
|||
private static void testDefensiveProgramming() { |
|||
ArticleRepository repo = new ArticleRepository(); |
|||
|
|||
try { |
|||
repo.add(null); |
|||
System.out.println("ERROR: 应该抛出异常!"); |
|||
} catch (NullPointerException e) { |
|||
System.out.println("正确: 成功捕获 null 文章异常"); |
|||
} |
|||
|
|||
try { |
|||
repo.addAll(null); |
|||
System.out.println("ERROR: 应该抛出异常!"); |
|||
} catch (NullPointerException e) { |
|||
System.out.println("正确: 成功捕获 null 集合异常"); |
|||
} |
|||
|
|||
try { |
|||
List<Article> listWithNull = new ArrayList<>(); |
|||
listWithNull.add(new Article("Test", "https://test.com")); |
|||
listWithNull.add(null); |
|||
repo.addAll(listWithNull); |
|||
System.out.println("ERROR: 应该抛出异常!"); |
|||
} catch (NullPointerException e) { |
|||
System.out.println("正确: 成功捕获集合中包含 null 异常"); |
|||
} |
|||
|
|||
try { |
|||
new Article(null, "https://test.com"); |
|||
System.out.println("ERROR: 应该抛出异常!"); |
|||
} catch (IllegalArgumentException e) { |
|||
System.out.println("正确: 成功捕获空标题异常"); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,44 @@ |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class TitleLengthAnalyzer implements ArticleAnalyzer { |
|||
@Override |
|||
public Map<String, Object> analyze(List<Article> articles) { |
|||
Map<String, Object> result = new HashMap<>(); |
|||
|
|||
if (articles.isEmpty()) { |
|||
result.put("analyzer", getName()); |
|||
result.put("averageLength", 0); |
|||
result.put("maxLength", 0); |
|||
result.put("minLength", 0); |
|||
return result; |
|||
} |
|||
|
|||
int totalLength = articles.stream() |
|||
.mapToInt(a -> a.getTitle().length()) |
|||
.sum(); |
|||
|
|||
int maxLength = articles.stream() |
|||
.mapToInt(a -> a.getTitle().length()) |
|||
.max() |
|||
.orElse(0); |
|||
|
|||
int minLength = articles.stream() |
|||
.mapToInt(a -> a.getTitle().length()) |
|||
.min() |
|||
.orElse(0); |
|||
|
|||
result.put("analyzer", getName()); |
|||
result.put("averageLength", (double) totalLength / articles.size()); |
|||
result.put("maxLength", maxLength); |
|||
result.put("minLength", minLength); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "标题长度分析器"; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,54 @@ |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
public class ExceptionDemo { |
|||
public static void main(String[] args) { |
|||
System.out.println("===== 测试 URL 校验测试 ====="); |
|||
testUrlValidation(); |
|||
|
|||
System.out.println("\n===== 测试重试机制测试 ====="); |
|||
testRetryMechanism(); |
|||
} |
|||
|
|||
private static void testUrlValidation() { |
|||
String[] testUrls = { |
|||
"https://www.example.com", |
|||
"invalid-url", |
|||
null |
|||
}; |
|||
|
|||
for (String url : testUrls) { |
|||
try { |
|||
System.out.println("正在校验 URL: " + url); |
|||
UrlValidator.validateUrl(url); |
|||
System.out.println("URL 格式有效!"); |
|||
} catch (UrlFormatException e) { |
|||
System.err.println("错误: " + e.getMessage()); |
|||
System.err.println("无效的 URL: " + e.getInvalidUrl()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void testRetryMechanism() { |
|||
// 模拟一个会失败几次的操作
|
|||
AtomicInteger attemptCounter = new AtomicInteger(0); |
|||
|
|||
try { |
|||
String result = RetryUtils.executeWithRetry(() -> { |
|||
int currentAttempt = attemptCounter.incrementAndGet(); |
|||
System.out.println("执行操作... (尝试次数: " + currentAttempt + ")"); |
|||
|
|||
// 模拟前3次失败,第4次成功
|
|||
if (currentAttempt < 4) { |
|||
throw new RuntimeException("模拟失败"); |
|||
} |
|||
|
|||
return "操作成功完成!"; |
|||
}); |
|||
|
|||
System.out.println("结果: " + result); |
|||
} catch (NetworkException e) { |
|||
System.err.println("错误: " + e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,9 @@ |
|||
public class NetworkException extends Exception { |
|||
public NetworkException(String message) { |
|||
super(message); |
|||
} |
|||
|
|||
public NetworkException(String message, Throwable cause) { |
|||
super(message, cause); |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
# Java 异常体系设计文档 |
|||
|
|||
## 1. 异常层次设计 |
|||
|
|||
### 1.1 未检查异常 (RuntimeException) |
|||
- **UrlFormatException**:继承自 RuntimeException,用于处理 URL 格式校验失败 |
|||
- 包含无效 URL 的信息 |
|||
- 提供有意义的错误消息 |
|||
|
|||
### 1.2 已检查异常 (Exception) |
|||
- **NetworkException**:继承自 Exception,用于处理网络相关问题 |
|||
- 支持链式异常 (cause) |
|||
- 提供网络操作的上下文信息 |
|||
|
|||
## 2. 异常设计原则 |
|||
|
|||
### 2.1 Checked/Unchecked 选择 |
|||
- **UrlFormatException (Unchecked)**:因为 URL 格式校验是编程错误,可以通过预检查避免,使用 Unchecked 异常更合理 |
|||
- **NetworkException (Checked)**:因为网络问题是不可预测的外部因素,使用 Checked 异常强制调用者处理 |
|||
|
|||
### 2.2 异常包装 |
|||
- 所有异常都保留原始异常信息 (cause) |
|||
- 使用链式异常避免根因丢失 |
|||
- 提供构造函数支持 cause 链 |
|||
|
|||
### 2.3 上下文信息 |
|||
- UrlFormatException 保存无效的 URL |
|||
- NetworkException 支持传递详细的错误信息 |
|||
- 异常消息包含足够的上下文帮助调试 |
|||
|
|||
## 3. 重试机制 |
|||
|
|||
### 3.1 指数退避策略 |
|||
- 基础等待时间:500ms |
|||
- 计算公式:`wait = base * 2^attempt` |
|||
- 最大重试次数:5次 |
|||
- 等待时间示例:500ms, 1000ms, 2000ms, 4000ms, 8000ms |
|||
|
|||
## 4. AI 架构审计请求 |
|||
|
|||
> 作为Java架构师,请审查我的异常层次设计:Checked/Unchecked选择是否合理?异常包装是否丢失根因?日志是否包含足够上下文? |
|||
|
|||
## 5. 文件列表 |
|||
- [UrlFormatException.java](file:///D:\嘻嘻哈哈\Git\java\w11\UrlFormatException.java) |
|||
- [NetworkException.java](file:///D:\嘻嘻哈哈\Git\java\w11\NetworkException.java) |
|||
- [RetryUtils.java](file:///D:\嘻嘻哈哈\Git\java\w11\RetryUtils.java) |
|||
- [UrlValidator.java](file:///D:\嘻嘻哈哈\Git\java\w11\UrlValidator.java) |
|||
- [ExceptionDemo.java](file:///D:\嘻嘻哈哈\Git\java\w11\ExceptionDemo.java) |
|||
Binary file not shown.
@ -0,0 +1,36 @@ |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.function.Supplier; |
|||
|
|||
public class RetryUtils { |
|||
private static final long BASE_WAIT_MS = 500; |
|||
private static final int MAX_ATTEMPTS = 5; |
|||
|
|||
public static <T> T executeWithRetry(Supplier<T> supplier) throws NetworkException { |
|||
int attempt = 0; |
|||
while (true) { |
|||
try { |
|||
return supplier.get(); |
|||
} catch (Exception e) { |
|||
attempt++; |
|||
if (attempt >= MAX_ATTEMPTS) { |
|||
throw new NetworkException("重试失败,已达最大重试次数", e); |
|||
} |
|||
|
|||
long waitTime = calculateWaitTime(attempt); |
|||
System.out.println("重试失败,等待 " + waitTime + "ms 后重试... (第 " + attempt + " 次尝试)"); |
|||
|
|||
try { |
|||
TimeUnit.MILLISECONDS.sleep(waitTime); |
|||
} catch (InterruptedException ie) { |
|||
Thread.currentThread().interrupt(); |
|||
throw new NetworkException("重试被中断", ie); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static long calculateWaitTime(int attempt) { |
|||
// 指数退避公式:wait = base * 2^attempt
|
|||
return BASE_WAIT_MS * (long) Math.pow(2, attempt); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,17 @@ |
|||
public class UrlFormatException extends RuntimeException { |
|||
private String invalidUrl; |
|||
|
|||
public UrlFormatException(String message, String invalidUrl) { |
|||
super(message); |
|||
this.invalidUrl = invalidUrl; |
|||
} |
|||
|
|||
public UrlFormatException(String message, Throwable cause, String invalidUrl) { |
|||
super(message, cause); |
|||
this.invalidUrl = invalidUrl; |
|||
} |
|||
|
|||
public String getInvalidUrl() { |
|||
return invalidUrl; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,16 @@ |
|||
import java.net.MalformedURLException; |
|||
import java.net.URL; |
|||
|
|||
public class UrlValidator { |
|||
public static void validateUrl(String urlStr) { |
|||
if (urlStr == null || urlStr.trim().isEmpty()) { |
|||
throw new UrlFormatException("URL不能为空", urlStr); |
|||
} |
|||
|
|||
try { |
|||
new URL(urlStr); |
|||
} catch (MalformedURLException e) { |
|||
throw new UrlFormatException("URL格式无效", e, urlStr); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||
public class Circle extends Shape { |
|||
@Override |
|||
public void draw() { |
|||
System.out.println("绘制圆形"); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||
public class Rectangle extends Shape { |
|||
@Override |
|||
public void draw() { |
|||
System.out.println("绘制矩形"); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,5 @@ |
|||
public class Shape { |
|||
public void draw() { |
|||
System.out.println("绘制形状"); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -1,4 +1,22 @@ |
|||
package PACKAGE_NAME; |
|||
|
|||
public class ShapeTest { |
|||
} |
|||
public static void drawShape(Shape s) { |
|||
s.draw(); |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
// 创建形状对象
|
|||
Shape myShape = new Shape(); |
|||
Shape myCircle = new Circle(); |
|||
Shape myRectangle = new Rectangle(); |
|||
|
|||
// 测试 drawShape 方法
|
|||
System.out.println("测试 Shape 基类:"); |
|||
drawShape(myShape); |
|||
|
|||
System.out.println("\n测试 Circle 子类:"); |
|||
drawShape(myCircle); |
|||
|
|||
System.out.println("\n测试 Rectangle 子类:"); |
|||
drawShape(myRectangle); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||
public abstract class Animal { |
|||
public abstract void makeSound(); |
|||
} |
|||
Binary file not shown.
@ -1,4 +1,25 @@ |
|||
package PACKAGE_NAME; |
|||
|
|||
public class AnimalTest { |
|||
} |
|||
public static void main(String[] args) { |
|||
// 多态:使用 Animal 类型引用指向子类对象
|
|||
Animal dog = new Dog(); |
|||
Animal cat = new Cat(); |
|||
|
|||
System.out.println("===== 测试动物叫声 ====="); |
|||
dog.makeSound(); |
|||
cat.makeSound(); |
|||
|
|||
System.out.println("\n===== 测试接口能力 ====="); |
|||
// 使用 instanceof 检查对象是否实现了 Swimmable 接口
|
|||
if (dog instanceof Swimmable) { |
|||
((Swimmable) dog).swim(); |
|||
} else { |
|||
System.out.println("这个动物不会游泳"); |
|||
} |
|||
|
|||
if (cat instanceof Swimmable) { |
|||
((Swimmable) cat).swim(); |
|||
} else { |
|||
System.out.println("这个动物不会游泳"); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||
public class Cat extends Animal { |
|||
@Override |
|||
public void makeSound() { |
|||
System.out.println("猫叫:喵喵喵!"); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,11 @@ |
|||
public class Dog extends Animal implements Swimmable { |
|||
@Override |
|||
public void makeSound() { |
|||
System.out.println("狗叫:汪汪汪!"); |
|||
} |
|||
|
|||
@Override |
|||
public void swim() { |
|||
System.out.println("狗在游泳"); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||
public interface Swimmable { |
|||
void swim(); |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,19 @@ |
|||
public class Account { |
|||
private double balance; |
|||
|
|||
public Account(double initialBalance) { |
|||
this.balance = initialBalance; |
|||
} |
|||
|
|||
public void withdraw(double amount) throws InsufficientFundsException { |
|||
if (amount > balance) { |
|||
throw new InsufficientFundsException(balance); |
|||
} |
|||
balance -= amount; |
|||
System.out.println("取款成功!当前余额:" + balance); |
|||
} |
|||
|
|||
public double getBalance() { |
|||
return balance; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,24 @@ |
|||
public class AccountTest { |
|||
public static void main(String[] args) { |
|||
// 创建账户,初始余额 1000 元
|
|||
Account account = new Account(1000.0); |
|||
System.out.println("当前账户余额:" + account.getBalance()); |
|||
|
|||
// 测试取款功能
|
|||
System.out.println("\n===== 尝试取款 500 元 ====="); |
|||
try { |
|||
account.withdraw(500.0); |
|||
} catch (InsufficientFundsException e) { |
|||
System.out.println("错误:" + e.getMessage()); |
|||
System.out.println("当前余额:" + e.getBalance()); |
|||
} |
|||
|
|||
System.out.println("\n===== 尝试取款 700 元 ====="); |
|||
try { |
|||
account.withdraw(700.0); |
|||
} catch (InsufficientFundsException e) { |
|||
System.out.println("错误:" + e.getMessage()); |
|||
System.out.println("当前余额:" + e.getBalance()); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,12 @@ |
|||
public class InsufficientFundsException extends Exception { |
|||
private double balance; |
|||
|
|||
public InsufficientFundsException(double balance) { |
|||
super("余额不足,当前余额为:" + balance); |
|||
this.balance = balance; |
|||
} |
|||
|
|||
public double getBalance() { |
|||
return balance; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,26 @@ |
|||
public class Box<T> { |
|||
private T content; |
|||
|
|||
public Box() {} |
|||
|
|||
public Box(T content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
public T getContent() { |
|||
return content; |
|||
} |
|||
|
|||
public void setContent(T content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
public boolean isEmpty() { |
|||
return content == null; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Box{" + "content=" + content + '}'; |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,51 @@ |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class GenericDemo { |
|||
public static void main(String[] args) { |
|||
// 测试 Box 泛型类
|
|||
System.out.println("===== 测试 Box 泛型类 ====="); |
|||
Box<String> stringBox = new Box<>("Hello, Generics!"); |
|||
System.out.println(stringBox); |
|||
|
|||
Box<Integer> integerBox = new Box<>(100); |
|||
System.out.println(integerBox); |
|||
|
|||
integerBox.setContent(200); |
|||
System.out.println("修改后:" + integerBox); |
|||
|
|||
Box<Double> doubleBox = new Box<>(); |
|||
System.out.println("空盒子:" + doubleBox); |
|||
System.out.println("是否为空:" + doubleBox.isEmpty()); |
|||
|
|||
// 测试 Pair 泛型类
|
|||
System.out.println("\n===== 测试 Pair 泛型类 ====="); |
|||
Pair<String, Integer> nameAgePair = new Pair<>("张三", 25); |
|||
System.out.println(nameAgePair); |
|||
System.out.println("姓名:" + nameAgePair.getKey()); |
|||
System.out.println("年龄:" + nameAgePair.getValue()); |
|||
|
|||
Pair<String, String> countryCapitalPair = new Pair<>("中国", "北京"); |
|||
System.out.println(countryCapitalPair); |
|||
|
|||
// 测试泛型方法
|
|||
System.out.println("\n===== 测试泛型方法 ====="); |
|||
String[] strArray = {"Java", "Python", "C++", "JavaScript"}; |
|||
System.out.print("字符串数组:"); |
|||
GenericUtils.printArray(strArray); |
|||
|
|||
Integer[] intArray = {1, 2, 3, 4, 5}; |
|||
System.out.print("原始数组:"); |
|||
GenericUtils.printArray(intArray); |
|||
|
|||
GenericUtils.swap(intArray, 0, 4); |
|||
System.out.print("交换后数组:"); |
|||
GenericUtils.printArray(intArray); |
|||
|
|||
List<Double> doubleList = new ArrayList<>(); |
|||
doubleList.add(1.5); |
|||
doubleList.add(2.5); |
|||
doubleList.add(3.5); |
|||
System.out.println("列表元素数量:" + GenericUtils.countListElements(doubleList)); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,24 @@ |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class GenericUtils { |
|||
// 泛型方法:打印数组元素
|
|||
public static <E> void printArray(E[] array) { |
|||
for (E element : array) { |
|||
System.out.print(element + " "); |
|||
} |
|||
System.out.println(); |
|||
} |
|||
|
|||
// 泛型方法:交换数组中的两个元素
|
|||
public static <T> void swap(T[] array, int i, int j) { |
|||
T temp = array[i]; |
|||
array[i] = array[j]; |
|||
array[j] = temp; |
|||
} |
|||
|
|||
// 泛型方法:计算列表中的元素数量
|
|||
public static <E> int countListElements(List<E> list) { |
|||
return list.size(); |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,30 @@ |
|||
public class Pair<K, V> { |
|||
private K key; |
|||
private V value; |
|||
|
|||
public Pair(K key, V value) { |
|||
this.key = key; |
|||
this.value = value; |
|||
} |
|||
|
|||
public K getKey() { |
|||
return key; |
|||
} |
|||
|
|||
public void setKey(K key) { |
|||
this.key = key; |
|||
} |
|||
|
|||
public V getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public void setValue(V value) { |
|||
this.value = value; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Pair{" + "key=" + key + ", value=" + value + '}'; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
# Java 泛型类练习 |
|||
|
|||
本项目包含几个典型的 Java 泛型类练习,展示了泛型的使用方法。 |
|||
|
|||
## 文件结构 |
|||
|
|||
1. [Box.java](file:///D:\嘻嘻哈哈\Git\java\w8\Box.java) - 简单的泛型容器类 |
|||
2. [Pair.java](file:///D:\嘻嘻哈哈\Git\java\w8\Pair.java) - 存储键值对的泛型类 |
|||
3. [GenericUtils.java](file:///D:\嘻嘻哈哈\Git\java\w8\GenericUtils.java) - 包含泛型方法的工具类 |
|||
4. [GenericDemo.java](file:///D:\嘻嘻哈哈\Git\java\w8\GenericDemo.java) - 演示程序 |
|||
5. README.md - 本文档 |
|||
|
|||
## 泛型类概述 |
|||
|
|||
### Box<T> |
|||
一个简单的泛型容器类,可以存储任意类型的对象。 |
|||
- 使用单个类型参数 T |
|||
- 提供构造函数、getter、setter 方法 |
|||
- 判断是否为空的方法 |
|||
|
|||
### Pair<K, V> |
|||
一个存储键值对的泛型类,可以存储两个不同类型的对象。 |
|||
- 使用两个类型参数 K 和 V |
|||
- 提供构造函数和 getter/setter 方法 |
|||
|
|||
### GenericUtils |
|||
一个包含泛型方法的工具类,展示了如何创建和使用泛型方法。 |
|||
- printArray: 打印任意类型数组的元素 |
|||
- swap: 交换数组中的任意两个元素 |
|||
- countListElements: 计算列表中的元素数量 |
|||
|
|||
## 编译运行 |
|||
|
|||
```bash |
|||
# 编译所有 Java 文件 |
|||
javac Box.java Pair.java GenericUtils.java GenericDemo.java |
|||
|
|||
# 运行演示程序 |
|||
java GenericDemo |
|||
``` |
|||
|
|||
## 运行结果 |
|||
|
|||
程序会展示泛型类的各种用法,包括: |
|||
- Box 类存储不同类型的数据 |
|||
- Pair 类存储键值对 |
|||
- GenericUtils 的泛型方法的使用 |
|||
|
|||
## 泛型的主要优点 |
|||
|
|||
1. **类型安全**:编译时检查,避免运行时类型错误 |
|||
2. **代码复用**:一套代码支持多种数据类型 |
|||
3. **可读性**:更清晰的代码,更易理解 |
|||
4. **避免强制类型转换**:减少代码中的类型转换 |
|||
@ -0,0 +1,127 @@ |
|||
# 文章爬虫系统 |
|||
|
|||
这是一个使用 Maven 创建的 Java 项目,实现了命令行界面的文章爬虫系统。 |
|||
|
|||
## 项目结构 |
|||
|
|||
``` |
|||
w9/ |
|||
├── pom.xml # Maven 配置文件 |
|||
├── README.md # 项目文档 |
|||
└── src/ |
|||
└── main/ |
|||
└── java/ |
|||
└── com/ |
|||
└── crawler/ |
|||
├── Main.java # 主程序入口 |
|||
├── model/ |
|||
│ └── Article.java # 文章数据模型 |
|||
├── view/ |
|||
│ └── ConsoleView.java # 控制台视图 |
|||
├── command/ |
|||
│ ├── Command.java # 命令接口 |
|||
│ ├── HelpCommand.java # 帮助命令 |
|||
│ ├── ListCommand.java # 列表命令 |
|||
│ ├── CrawlCommand.java # 爬取命令 |
|||
│ └── ExitCommand.java # 退出命令 |
|||
└── controller/ |
|||
└── CommandController.java # 命令控制器 |
|||
``` |
|||
|
|||
## 功能特性 |
|||
|
|||
### 已实现的命令 |
|||
|
|||
1. **help** - 显示帮助信息 |
|||
2. **list** - 显示已抓取的文章列表 |
|||
3. **crawl** - 开始抓取文章(目前为存根实现) |
|||
4. **exit** - 退出程序 |
|||
|
|||
### 项目架构 |
|||
|
|||
- **model**: 数据模型层 - 定义文章数据结构 |
|||
- **view**: 视图层 - 处理控制台输出 |
|||
- **command**: 命令层 - 实现各种命令 |
|||
- **controller**: 控制层 - 管理命令执行 |
|||
|
|||
## 编译运行 |
|||
|
|||
### 编译项目 |
|||
|
|||
```bash |
|||
cd w9 |
|||
mvn clean compile |
|||
``` |
|||
|
|||
### 运行项目 |
|||
|
|||
```bash |
|||
mvn exec:java |
|||
``` |
|||
|
|||
或者先打包再运行: |
|||
|
|||
```bash |
|||
mvn package |
|||
java -jar target/crawler-app-1.0-SNAPSHOT.jar |
|||
``` |
|||
|
|||
## 使用示例 |
|||
|
|||
``` |
|||
================================================ |
|||
欢迎使用文章爬虫系统 |
|||
================================================ |
|||
|
|||
可用命令: |
|||
help - 显示帮助信息 |
|||
list - 显示已抓取的文章列表 |
|||
crawl - 开始抓取文章 |
|||
exit - 退出程序 |
|||
|
|||
请输入命令 > help |
|||
|
|||
可用命令: |
|||
help - 显示帮助信息 |
|||
list - 显示已抓取的文章列表 |
|||
crawl - 开始抓取文章 |
|||
exit - 退出程序 |
|||
|
|||
请输入命令 > crawl |
|||
|
|||
正在开始抓取文章... |
|||
抓取完成,共抓取了 3 篇文章 |
|||
|
|||
请输入命令 > list |
|||
|
|||
已抓取的文章列表: |
|||
---------------------------------------- |
|||
1. 示例文章1 |
|||
URL: https://example.com/article1 |
|||
---------------------------------------- |
|||
2. 示例文章2 |
|||
URL: https://example.com/article2 |
|||
---------------------------------------- |
|||
3. 示例文章3 |
|||
URL: https://example.com/article3 |
|||
---------------------------------------- |
|||
共 3 篇文章 |
|||
|
|||
请输入命令 > exit |
|||
|
|||
感谢使用,再见! |
|||
``` |
|||
|
|||
## 技术栈 |
|||
|
|||
- Java 8 |
|||
- Maven 3.x |
|||
|
|||
## 扩展功能 |
|||
|
|||
当前 crawl 命令是存根实现,可以扩展为: |
|||
|
|||
1. 连接实际的文章网站 |
|||
2. 解析 HTML 内容 |
|||
3. 提取文章标题、URL、内容等 |
|||
4. 保存到数据库或文件 |
|||
@ -0,0 +1,167 @@ |
|||
# Maven 项目运行说明 |
|||
|
|||
## 项目已成功创建并测试完成 |
|||
|
|||
### 项目信息 |
|||
|
|||
- **项目类型**: Maven Java 项目 |
|||
- **包结构**: com.crawler |
|||
- **功能**: 命令行文章爬虫系统 |
|||
|
|||
### 已实现的包结构 |
|||
|
|||
``` |
|||
w9/ |
|||
├── pom.xml # Maven 配置文件 |
|||
├── README.md # 详细项目文档 |
|||
├── test.bat # Windows 测试脚本 |
|||
└── src/ |
|||
└── main/ |
|||
└── java/ |
|||
└── com/ |
|||
└── crawler/ |
|||
├── Main.java # 主程序入口,包含命令循环 |
|||
├── model/ |
|||
│ └── Article.java # 文章数据模型 |
|||
├── view/ |
|||
│ └── ConsoleView.java # 控制台视图(用户界面) |
|||
├── command/ |
|||
│ ├── Command.java # 命令接口 |
|||
│ ├── HelpCommand.java # help 命令实现 |
|||
│ ├── ListCommand.java # list 命令实现 |
|||
│ ├── CrawlCommand.java # crawl 命令实现 |
|||
│ └── ExitCommand.java # exit 命令实现 |
|||
└── controller/ |
|||
└── CommandController.java # 命令控制器 |
|||
``` |
|||
|
|||
### 已实现的 4 个命令 |
|||
|
|||
1. **help** - 显示所有可用命令的帮助信息 |
|||
2. **list** - 显示已抓取的文章列表(目前为存根) |
|||
3. **crawl** - 开始抓取文章(目前为存根实现,添加3篇示例文章) |
|||
4. **exit** - 退出程序 |
|||
|
|||
### 运行方法 |
|||
|
|||
#### 方法一:使用 Java 编译器直接运行(已测试) |
|||
|
|||
```bash |
|||
# 1. 编译项目 |
|||
cd D:\嘻嘻哈哈\Git\java\w9\src\main\java |
|||
javac -d ../../../../target/classes -cp . com/crawler/model/Article.java com/crawler/view/ConsoleView.java com/crawler/command/*.java com/crawler/controller/*.java com/crawler/Main.java |
|||
|
|||
# 2. 运行程序 |
|||
java -cp ../../../../target/classes com.crawler.Main |
|||
``` |
|||
|
|||
#### 方法二:使用 Maven 运行(需要配置 JAVA_HOME) |
|||
|
|||
```bash |
|||
# 设置 JAVA_HOME |
|||
set JAVA_HOME=你的Java安装路径 |
|||
|
|||
# 编译项目 |
|||
cd w9 |
|||
mvn clean compile |
|||
|
|||
# 运行程序 |
|||
mvn exec:java |
|||
``` |
|||
|
|||
### 测试结果 |
|||
|
|||
程序已成功运行,所有命令都正常工作: |
|||
|
|||
``` |
|||
================================================ |
|||
欢迎使用文章爬虫系统 |
|||
================================================ |
|||
|
|||
可用命令: |
|||
help - 显示帮助信息 |
|||
list - 显示已抓取的文章列表 |
|||
crawl - 开始抓取文章 |
|||
exit - 退出程序 |
|||
|
|||
请输入命令 > help |
|||
可用命令: |
|||
help - 显示帮助信息 |
|||
list - 显示已抓取的文章列表 |
|||
crawl - 开始抓取文章 |
|||
exit - 退出程序 |
|||
|
|||
请输入命令 > list |
|||
已抓取的文章列表: |
|||
---------------------------------------- |
|||
暂无已抓取的文章 |
|||
共 0 篇文章 |
|||
|
|||
请输入命令 > crawl |
|||
正在开始抓取文章... |
|||
抓取完成,共抓取了 3 篇文章 |
|||
|
|||
请输入命令 > list |
|||
已抓取的文章列表: |
|||
---------------------------------------- |
|||
1. 示例文章1 |
|||
URL: https://example.com/article1 |
|||
---------------------------------------- |
|||
2. 示例文章2 |
|||
URL: https://example.com/article2 |
|||
---------------------------------------- |
|||
3. 示例文章3 |
|||
URL: https://example.com/article3 |
|||
---------------------------------------- |
|||
共 3 篇文章 |
|||
|
|||
请输入命令 > exit |
|||
感谢使用,再见! |
|||
``` |
|||
|
|||
### 项目架构说明 |
|||
|
|||
#### MVC 架构模式 |
|||
|
|||
- **Model (模型层)**: `model/Article.java` - 数据模型 |
|||
- **View (视图层)**: `view/ConsoleView.java` - 用户界面输出 |
|||
- **Controller (控制层)**: `controller/CommandController.java` - 命令调度 |
|||
|
|||
#### 命令模式 |
|||
|
|||
- **Command 接口**: 定义命令的基本结构 |
|||
- **具体命令类**: HelpCommand, ListCommand, CrawlCommand, ExitCommand |
|||
- **命令控制器**: 管理命令的注册和执行 |
|||
|
|||
### 扩展建议 |
|||
|
|||
当前 crawl 命令是存根实现,可以扩展为: |
|||
|
|||
1. **网络爬虫**: 添加 Jsoup 依赖,实现真实的网页爬取 |
|||
2. **数据存储**: 将抓取的文章保存到文件或数据库 |
|||
3. **多线程**: 使用 ExecutorService 实现并行爬取 |
|||
4. **配置管理**: 添加配置文件管理爬取规则 |
|||
5. **日志系统**: 使用 SLF4J 添加日志记录 |
|||
|
|||
### Maven 依赖配置 |
|||
|
|||
如需添加 Jsoup 依赖,可以在 pom.xml 中添加: |
|||
|
|||
```xml |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.jsoup</groupId> |
|||
<artifactId>jsoup</artifactId> |
|||
<version>1.17.2</version> |
|||
</dependency> |
|||
</dependencies> |
|||
``` |
|||
|
|||
### 总结 |
|||
|
|||
✅ Maven 项目结构已创建 |
|||
✅ 完整的包结构 (model/view/command/controller) 已实现 |
|||
✅ 4 个命令已全部实现并测试通过 |
|||
✅ list 命令可展示已抓取的文章 |
|||
✅ 命令循环运行正常 |
|||
✅ 所有文件已保存在 w9 目录中 |
|||
@ -0,0 +1,40 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 |
|||
http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<groupId>com.example</groupId> |
|||
<artifactId>crawler-app</artifactId> |
|||
<version>1.0-SNAPSHOT</version> |
|||
<packaging>jar</packaging> |
|||
|
|||
<properties> |
|||
<maven.compiler.source>1.8</maven.compiler.source> |
|||
<maven.compiler.target>1.8</maven.compiler.target> |
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
</properties> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>3.8.1</version> |
|||
<configuration> |
|||
<source>1.8</source> |
|||
<target>1.8</target> |
|||
</configuration> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.codehaus.mojo</groupId> |
|||
<artifactId>exec-maven-plugin</artifactId> |
|||
<version>1.6.0</version> |
|||
<configuration> |
|||
<mainClass>com.crawler.Main</mainClass> |
|||
</configuration> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
|||
@ -0,0 +1,43 @@ |
|||
package com.crawler; |
|||
|
|||
import com.crawler.command.*; |
|||
import com.crawler.controller.CommandController; |
|||
import com.crawler.model.Article; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Scanner; |
|||
|
|||
public class Main { |
|||
public static void main(String[] args) { |
|||
// 初始化组件
|
|||
ConsoleView view = new ConsoleView(); |
|||
List<Article> articles = new ArrayList<>(); |
|||
boolean[] running = {true}; |
|||
|
|||
// 创建控制器
|
|||
CommandController controller = new CommandController(view); |
|||
|
|||
// 注册命令
|
|||
controller.registerCommand(new HelpCommand(view)); |
|||
controller.registerCommand(new ListCommand(view, articles)); |
|||
controller.registerCommand(new CrawlCommand(view, articles)); |
|||
controller.registerCommand(new ExitCommand(view, running)); |
|||
|
|||
// 显示欢迎信息
|
|||
view.showWelcome(); |
|||
view.showHelp(); |
|||
|
|||
// 创建扫描器读取用户输入
|
|||
Scanner scanner = new Scanner(System.in); |
|||
|
|||
// 命令循环
|
|||
while (running[0]) { |
|||
view.showPrompt(); |
|||
String input = scanner.nextLine(); |
|||
controller.executeCommand(input); |
|||
} |
|||
|
|||
scanner.close(); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
package com.crawler.command; |
|||
|
|||
public interface Command { |
|||
String getName(); |
|||
void execute(); |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.model.Article; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class CrawlCommand implements Command { |
|||
private ConsoleView view; |
|||
private List<Article> articles; |
|||
|
|||
public CrawlCommand(ConsoleView view, List<Article> articles) { |
|||
this.view = view; |
|||
this.articles = articles; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "crawl"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute() { |
|||
view.showCrawlStart(); |
|||
|
|||
// 这里是存根实现,实际可以在这里调用爬虫逻辑
|
|||
Article article1 = new Article("示例文章1", "https://example.com/article1"); |
|||
Article article2 = new Article("示例文章2", "https://example.com/article2"); |
|||
Article article3 = new Article("示例文章3", "https://example.com/article3"); |
|||
|
|||
articles.add(article1); |
|||
articles.add(article2); |
|||
articles.add(article3); |
|||
|
|||
view.showCrawlComplete(articles.size()); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.view.ConsoleView; |
|||
|
|||
public class ExitCommand implements Command { |
|||
private ConsoleView view; |
|||
private boolean[] running; |
|||
|
|||
public ExitCommand(ConsoleView view, boolean[] running) { |
|||
this.view = view; |
|||
this.running = running; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "exit"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute() { |
|||
view.showGoodbye(); |
|||
running[0] = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.view.ConsoleView; |
|||
|
|||
public class HelpCommand implements Command { |
|||
private ConsoleView view; |
|||
|
|||
public HelpCommand(ConsoleView view) { |
|||
this.view = view; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "help"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute() { |
|||
view.showHelp(); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.model.Article; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class ListCommand implements Command { |
|||
private ConsoleView view; |
|||
private List<Article> articles; |
|||
|
|||
public ListCommand(ConsoleView view, List<Article> articles) { |
|||
this.view = view; |
|||
this.articles = articles; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "list"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute() { |
|||
view.showArticles(articles); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
package com.crawler.controller; |
|||
|
|||
import com.crawler.command.Command; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
public class CommandController { |
|||
private Map<String, Command> commands; |
|||
private ConsoleView view; |
|||
|
|||
public CommandController(ConsoleView view) { |
|||
this.view = view; |
|||
this.commands = new HashMap<>(); |
|||
} |
|||
|
|||
public void registerCommand(Command command) { |
|||
commands.put(command.getName(), command); |
|||
} |
|||
|
|||
public void executeCommand(String input) { |
|||
String commandName = input.trim().toLowerCase(); |
|||
|
|||
if (commandName.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
Command command = commands.get(commandName); |
|||
if (command != null) { |
|||
command.execute(); |
|||
} else { |
|||
view.showUnknownCommand(commandName); |
|||
} |
|||
} |
|||
|
|||
public boolean hasCommand(String commandName) { |
|||
return commands.containsKey(commandName); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
package com.crawler.model; |
|||
|
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
private long timestamp; |
|||
|
|||
public Article(String title, String url) { |
|||
this.title = title; |
|||
this.url = url; |
|||
this.timestamp = System.currentTimeMillis(); |
|||
} |
|||
|
|||
public String getTitle() { |
|||
return title; |
|||
} |
|||
|
|||
public void setTitle(String title) { |
|||
this.title = title; |
|||
} |
|||
|
|||
public String getUrl() { |
|||
return url; |
|||
} |
|||
|
|||
public void setUrl(String url) { |
|||
this.url = url; |
|||
} |
|||
|
|||
public String getContent() { |
|||
return content; |
|||
} |
|||
|
|||
public void setContent(String content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
public long getTimestamp() { |
|||
return timestamp; |
|||
} |
|||
|
|||
public void setTimestamp(long timestamp) { |
|||
this.timestamp = timestamp; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Article{" + |
|||
"title='" + title + '\'' + |
|||
", url='" + url + '\'' + |
|||
", timestamp=" + timestamp + |
|||
'}'; |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
package com.crawler.view; |
|||
|
|||
import com.crawler.model.Article; |
|||
import java.util.List; |
|||
|
|||
public class ConsoleView { |
|||
public void showWelcome() { |
|||
System.out.println("================================================"); |
|||
System.out.println(" 欢迎使用文章爬虫系统"); |
|||
System.out.println("================================================"); |
|||
System.out.println(); |
|||
} |
|||
|
|||
public void showPrompt() { |
|||
System.out.print("请输入命令 > "); |
|||
} |
|||
|
|||
public void showHelp() { |
|||
System.out.println("\n可用命令:"); |
|||
System.out.println(" help - 显示帮助信息"); |
|||
System.out.println(" list - 显示已抓取的文章列表"); |
|||
System.out.println(" crawl - 开始抓取文章"); |
|||
System.out.println(" exit - 退出程序"); |
|||
System.out.println(); |
|||
} |
|||
|
|||
public void showArticles(List<Article> articles) { |
|||
System.out.println("\n已抓取的文章列表:"); |
|||
System.out.println("----------------------------------------"); |
|||
|
|||
if (articles.isEmpty()) { |
|||
System.out.println("暂无已抓取的文章"); |
|||
} else { |
|||
for (int i = 0; i < articles.size(); i++) { |
|||
Article article = articles.get(i); |
|||
System.out.println((i + 1) + ". " + article.getTitle()); |
|||
System.out.println(" URL: " + article.getUrl()); |
|||
System.out.println("----------------------------------------"); |
|||
} |
|||
} |
|||
System.out.println("共 " + articles.size() + " 篇文章\n"); |
|||
} |
|||
|
|||
public void showCrawlStart() { |
|||
System.out.println("\n正在开始抓取文章..."); |
|||
} |
|||
|
|||
public void showCrawlComplete(int count) { |
|||
System.out.println("抓取完成,共抓取了 " + count + " 篇文章\n"); |
|||
} |
|||
|
|||
public void showGoodbye() { |
|||
System.out.println("\n感谢使用,再见!"); |
|||
} |
|||
|
|||
public void showUnknownCommand(String command) { |
|||
System.out.println("未知命令: " + command); |
|||
System.out.println("输入 'help' 查看可用命令\n"); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
@echo off |
|||
cd /d "%~dp0src\main\java" |
|||
echo help > input.txt |
|||
echo list >> input.txt |
|||
echo crawl >> input.txt |
|||
echo list >> input.txt |
|||
echo exit >> input.txt |
|||
|
|||
java -cp ..\..\..\..\target\classes com.crawler.Main < input.txt |
|||
del input.txt |
|||
Loading…
Reference in new issue