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 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 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