diff --git a/README.md b/README.md
index e1278f7..f74c5fb 100644
--- a/README.md
+++ b/README.md
@@ -14,35 +14,23 @@
### :star: Java ###
- [深入详解ThreadLocal](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486776&idx=1&sn=f4425cb88bc5393e4d5125f5fd08ed68&chksm=cf847efdf8f3f7ebc79c5bcd3c47f1fc2f83abf119c2b22782cc90a1c69f606a95a4051dab53#rd)
-
- [使用Optional优雅避免空指针异常](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486914&idx=1&sn=b2b0f2c41b8168fbfcf1df21a3e00acb&chksm=cf847e07f8f3f711de06cb9269ba41541ec9399a56963768add081031566bf7fa49cbb6f7fa0#rd)
-
- [我画了35张图就是为了让你深入 AQS](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486172&idx=1&sn=b39cccd87dcd21176597dce0b15f7232&chksm=cf847919f8f3f00f86219d44cd95badee969d754aec89e644992437f2e8e0f7ad784695b4d90#rd)
-
- [一个 static 还能难得住我?](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486175&idx=1&sn=041c85c052c11d2d15243994bc46d90a&chksm=cf84791af8f3f00c90a18b29d1fa47c9bcd713651514fc5ce4a9f82d656fe637bb21d45c42be#rd)
-
- [原来这才是动态代理!!!](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486178&idx=1&sn=9610c1a0fa1df4c69558408ab2a3fcae&chksm=cf847927f8f3f0315b0c86f9b577926820c3d264d605149f850b597fcd17fafe432d82aaffcf#rd)
-
- [synchronized 的超多干货!](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486181&idx=1&sn=4cb9340ba2f19ccb19ccec0c54d61b86&chksm=cf847920f8f3f036cd752455290a97f6584f8a4ce9662d1102515dd5ed967c94e14cec7a767d#rd)
-
- [ExecutorCompletionService详解](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487958&idx=1&sn=2ace7ac53d596cd909d1d1c7e96fbff2&chksm=cf846213f8f3eb05c9de1fab2c609f4774ca86497ad5542a26aae5928efd808bfd865738aa4f#rd)
-
- [CompletableFuture深度解析](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247488046&idx=1&sn=2bb0b6dc4576278ff2e7f9b917cb6fe8&chksm=cf8461ebf8f3e8fd013d08c5028d41281444b1ac1d60f1706c841c4b444a4235d82c84644b9b#rd)
+- [面试官:响应式编程和虚拟线程怎么选?看完这篇不再被问倒](https://mp.weixin.qq.com/s/V7H_hyjycT3n1Rr7FBOrYQ)
### :page_facing_up: JVM ###
- [面试官:JVM是如何判定对象已死的?学JVM必会的知识!](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486087&idx=1&sn=c6f1a9932961095ffdf2aef8a789e115&chksm=cf847942f8f3f0549c798671fe804c93378586b4fc547cce14db2359852ff0723a3aab64a187#rd)
-
- [GC的前置工作,聊聊GC是如何快速枚举根节点的](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486168&idx=1&sn=9eef35ec701b5c2f8097641b7e69ae71&chksm=cf84791df8f3f00b1e85039f31b17e00bf9cb624bbee638efeca110e51df6c6b6ba6363705ee#rd)
-
- [GC面临的困境,JVM是如何解决跨代引用的?](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486242&idx=1&sn=83d4ace26fea86b0f16e93e25b3cdadf&chksm=cf8478e7f8f3f1f17a65a7fc0d25237e8f25b90f300085bb5a7e8128f7d80f5ba1a02e5a6c2f#rd)
-
- [昨晚做梦面试官问我三色标记算法](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486265&idx=1&sn=1464f25915c2c09ef65b784985b76fa3&chksm=cf8478fcf8f3f1ea80715ae949c1b4aec988368ead269c746d38244ae62028948a199f099d14#rd)
-
- [深入解析CMS垃圾回收器](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486628&idx=1&sn=984b273af7d1d0398517a2f5442ffb38&chksm=cf847f61f8f3f677372a5ebc9f81403a8324be1bed49bf92e763882715c943324de4f1b0139a#rd)
-
- [深入解析G1垃圾回收器](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486736&idx=1&sn=5e0710485783c3bcc4854a10412b9a40&chksm=cf847ed5f8f3f7c3826fa8c67bc76ce8dd218a725ee04f54cdafa27e14d190f5c92332589ae2#rd)
-
- [深入解析ZGC垃圾回收器](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486746&idx=1&sn=7257ecf8c36509d06be359e3889400f2&chksm=cf847edff8f3f7c96edc667051d9ef70537000202c1ec77699fa5e30e46c2c8ddabd122297f3#rd)
### :hammer: MySQL ###
@@ -123,6 +111,7 @@
### :date: 框架 ###
- [本地缓存无冕之王Caffeine Cache](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247486885&idx=1&sn=37c7a9461402bd97822295cf51361777&chksm=cf847e60f8f3f776eb3b477decfbac55dc8b7ae1cf607ef68fbee89dbe02d40a800a92fabec7#rd)
+- [响应式编程不只有概念!万字长文 + 代码示例,手把手带你玩转 RxJava](https://mp.weixin.qq.com/s/r0DJiOxR8wnZZ6tIKrSPzg)
### :fire: 架构设计 ###
@@ -141,8 +130,8 @@
### :dash: 编程语言 ###
- [Scala语言入门:初学者的基础语法指南](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487245&idx=1&sn=d089e22890f1f7449b7cf34e3cf2f6ed&chksm=cf847cc8f8f3f5deb39556f4229bafb6f1498906dc1d75040f90817bf0396117a7c2cdb498f9#rd)
-
- [Groovy 初学者指南](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487066&idx=1&sn=da9e3a9aff377d383e34e537e2f55666&chksm=cf847d9ff8f3f489011f26a784302ee68b9c1d7d57d52bc2c924a7c9b1a5f528ef2a417114c0#rd)
+- [自研 DSL 神器:万字拆解 ANTLR 4 核心原理与高级应用](https://mp.weixin.qq.com/s/nFiEqhi1B_SxrZGCAqLgLw)
### :satellite: 设计模式 ###
@@ -172,13 +161,19 @@
### :eyes: 大数据 ###
- [Spark入门指南:从基础概念到实践应用全解析](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487398&idx=1&sn=077859e1109e07b1469d242ec2b8091a&chksm=cf847c63f8f3f575e50012ef3667d9724998f07e32ebd27b6e3a37c5bdf2251d02e89030cff0#rd)
-
+
- [HBase入门指南](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487105&idx=1&sn=2ee82c9b239aa502bd3dffcf320b3f93&chksm=cf847d44f8f3f452e1b8ac83b9f62f380e349615b67da92343539d4014077c2ad9e787e256cc#rd)
-
+
- [全网最详细4W字Flink全面解析与实践(上)](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487459&idx=1&sn=a1826b2d592fff29b5e11a374468796a&chksm=cf847c26f8f3f53073cc24584264fa2752a26c98bbd31c86bcf519296789eff05d72904d27ac#rd)
-
+
- [全网最详细4W字Flink全面解析与实践(下)](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247487535&idx=1&sn=736f1adda56cc550191f17e7111598b5&chksm=cf8463eaf8f3eafc38819e342705df1884683e03d5d39e9df876834ab0a84f61cc55923a5a03#rd)
+### :watch:AI
+
+- [深度解析Skills:从Prompt到能力复用的技术革命](https://mp.weixin.qq.com/s/Se6_L1PbhlEUGaBSY8sZsQ)
+- [为什么ChatGPT能听懂你说的话?Embedding技术揭秘](https://mp.weixin.qq.com/s/CoHcpXIaamdfmXCf-3qlgw)
+- [RAG详解:让大模型看见你的私有知识](https://mp.weixin.qq.com/s/mAC3DeqPLM41LyfGh2QjUw)
+
### :jack_o_lantern: 其他 ###
@@ -186,6 +181,7 @@
- [实战Arthas:常见命令与最佳实践](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247488559&idx=1&sn=4b5003cb33446ab4a6173285fe9d83d3&chksm=cf8467eaf8f3eefc033de8f63cba9f0d7b2b5eb0ccfb5209f458a9ab447367b34954f296638b#rd)
- [Maven实战](https://mp.weixin.qq.com/s/ErtWrRNzjJcR2ettUhAxsQ)
- [不用Mockito写单元测试?你可能在浪费一半时间](https://mp.weixin.qq.com/s/NICubD9Yq0pn6qwpVIznfg)
+- [用好PowerMock,轻松搞定那些让你头疼的单元测试](https://mp.weixin.qq.com/s/rWIjqJKBQOe72RWW6qyJmA)
### :bulb: 资源 ###
diff --git "a/docs/md/AI/RAG\350\257\246\350\247\243\357\274\232\350\256\251\345\244\247\346\250\241\345\236\213\347\234\213\350\247\201\344\275\240\347\232\204\347\247\201\346\234\211\347\237\245\350\257\206.md" "b/docs/md/AI/RAG\350\257\246\350\247\243\357\274\232\350\256\251\345\244\247\346\250\241\345\236\213\347\234\213\350\247\201\344\275\240\347\232\204\347\247\201\346\234\211\347\237\245\350\257\206.md"
new file mode 100644
index 0000000..2204101
--- /dev/null
+++ "b/docs/md/AI/RAG\350\257\246\350\247\243\357\274\232\350\256\251\345\244\247\346\250\241\345\236\213\347\234\213\350\247\201\344\275\240\347\232\204\347\247\201\346\234\211\347\237\245\350\257\206.md"
@@ -0,0 +1,223 @@
+在把大语言模型用到实际业务时,开发者很快会遇到一个问题:通用模型很难满足特定场景的需求。
+
+主要卡在三个地方:
+
+- **知识过时**:模型的训练数据有截止日期,问它最近发生的事基本白问。
+
+- **幻觉严重**:模型经常一本正经地胡说八道,在需要准确性的场景这是致命的。
+
+- **数据安全**:企业不可能把内部文档上传给第三方,但不上传模型又不会。
+
+
+
+基于这个问题,**RAG(检索增强生成)** 出现了。它的思路很简单:不把数据交给模型,而是让模型"看到"它需要的部分。
+
+用户提问时,系统从私有知识库中检索相关片段,把这些片段和问题一起发给大模型。模型结合真实信息来回答,而不是靠"记忆"里不知道哪来的东西生成。
+
+## 为什么需要 RAG
+
+### 知识的局限性
+
+大语言模型的知识完全来自训练数据。GPT-4、文心一言、通义千问这些主流模型,训练数据主要来自网络公开数据。这带来两个问题:
+
+- **时效性**:模型的"知识"被定格在训练截止时间点。GPT-4 的知识库可能停在 2023 年 12 月,之后的新事件、新政策、新技术,它无法直接给出准确答案。
+- **私有领域缺失**:企业的产品规格文档、内部流程规范、医疗机构的诊断指南、法律机构的判例汇编——这些数据从没出现在公开网络上,通用大模型对此一无所知。不借助外部手段,模型在这些领域的回答质量会大打折扣。
+
+### 幻觉问题
+
+大语言模型在生成文本时,实际上是在计算下一个 token 出现的概率分布。这种机制导致模型在面对不确定性问题时,经常编造看似合理实则错误的答案——学名叫"幻觉"(Hallucination)。
+
+更麻烦的是,模型不具备某一方面知识时,它不会选择"不知道",而是倾向于根据训练数据中学习到的语言模式,自信满满地瞎编。在需要高度准确性的生产环境中,这绝对不可接受。
+
+### 数据安全
+
+对企业来说,数据安全是生死攸关的议题。没人愿意承担核心商业机密泄露的风险,因此几乎没有企业愿意将私有数据上传到第三方平台进行模型训练或推理。
+
+这意味着,完全依赖通用大模型自身能力的应用方案,不得不在**数据安全**和**应用效果**之间做取舍。传统方案要么保数据安全牺牲模型能力,要么提升模型能力却承担数据泄露风险。
+
+## RAG 的破局思路
+
+RAG 提供了第三条路:不把数据交给模型,而是让模型"看到"它需要的那部分数据。
+
+用户提问时,系统从私有知识库中检索相关片段,把这些片段作为上下文提供给大模型。大模型在回答时,既能"参考"检索到的真实信息,又能结合自身的语言理解能力生成流畅的回答。数据始终留在本地,模型获得了"感知"私有知识的能力。
+
+## RAG 的技术架构
+
+RAG 的完整工作流程分为两个阶段:**数据准备阶段(离线索引)** 和 **应用阶段(在线推理)**。
+
+
+
+### 数据准备阶段:构建知识的向量化索引
+
+数据准备是离线过程,目标是将私有数据转化为可高效检索的向量形式。流程包含四个环节:
+
+**数据提取**。从多种格式的原始文件中提取纯文本内容,包括 PDF、Word、HTML、数据库记录等。技术挑战在于处理各种格式解析、特殊字符清理、无用内容过滤。常用 LangChain 的 DocumentLoaders 来统一处理不同来源的数据。
+
+**文本分割(Chunking)**。直接影响检索质量。需要综合考虑两个因素:一是 embedding 模型对 token 长度的限制;二是语义完整性对检索效果的影响。
+
+固定长度分割实现简单,但容易切断语义边界,导致检索时丢失关键上下文。语义分割通过识别句子边界或段落结构来进行切分,能更好地保留语义完整性,但实现复杂度更高。业界常用策略是设置合适的 chunk size(如 512 tokens)和 overlap(如 50-100 tokens),在保证语义完整性的同时避免边界效应。
+
+**向量化(Embedding)**。将文本转化为高维向量,让语义相似的文本在向量空间中具有相近的位置关系。常见的 embedding 模型:
+
+| 模型名称 | 特点 | 适用场景 |
+|---------|------|---------|
+| OpenAI text-embedding-3 | API 调用,稳定可靠 | 通用场景 |
+| BGE (BAAI) | 开源、支持中英文 | 自部署场景 |
+| M3E (MokaAI) | 开源、多语言支持 | 中文场景 |
+| ERNIE-Embedding | 百度自研、中文优化 | 国产化需求 |
+
+**数据入库**。将向量及其关联的原始文本、元数据写入向量数据库。主流选择包括 FAISS、Chroma、Milvus、Weaviate 和 Qdrant 等。这些数据库采用近似最近邻(ANN)算法,能够在海量向量中快速找到与查询向量最相似的结果。
+
+### 应用阶段
+
+
+
+用户提出问题时,RAG 系统进入在线推理阶段,包含四个关键步骤:
+
+**查询向量化**。使用与索引阶段相同的 embedding 模型,将用户的自然语言问题转化为语义向量。这个向量的质量直接决定后续检索的准确性。
+
+**信息检索(Retrieval)**。通过计算查询向量与所有存储向量的相似度(如余弦相似度、欧氏距离),找出 top-k 个最相关的文档片段。现代向量数据库通常采用 HNSW(Hierarchical Navigable Small World)等算法,在保证检索精度的同时实现毫秒级响应。
+
+**上下文构建**。将检索到的相关片段与原始问题组装成完整的提示模板。一个典型的 RAG 提示模板:
+
+```
+【任务描述】
+假如你是一个专业的客服机器人,请参考【背景知识】中的内容,准确回答用户的问题。
+
+【背景知识】
+{检索到的相关文档片段}
+
+【用户问题】
+{用户原始问题}
+
+【回答要求】
+- 仅根据【背景知识】中的内容回答
+- 如果【背景知识】中没有相关信息,请明确告知
+- 回答要简洁、准确、有条理
+```
+
+**答案生成(Generation)**。大语言模型接收包含问题和上下文的提示后,结合检索到的真实信息生成最终答案。由于有明确的参考资料作为约束,模型产生幻觉的概率大大降低。
+
+## 高级 RAG 技术
+
+基础 RAG 流程能工作,但在实际应用中往往面临检索质量不足、生成效果不稳定等问题。业界发展出一系列高级 RAG 技术来优化系统性能。
+
+
+
+### 搜索索引的演进
+
+**平面索引(Flat Index)** 直接计算查询向量与所有存储向量的相似度。实现简单,但数据规模达到数万条时,线性扫描的计算开销变得不可接受。
+
+**向量索引** 采用近似最近邻算法来解决效率问题。FAISS 库提供了多种索引类型:
+
+- **IVF(Inverted File Index)**:通过聚类将向量分组,检索时只搜索最相关的聚类中心,显著减少计算量。
+- **HNSW(Hierarchical Navigable Small World)**:构建多层图结构,实现对数级别的检索复杂度。
+- **PQ(Product Quantization)**:对高维向量进行压缩,大幅降低内存占用。
+
+**分层索引** 应对大型知识库。系统维护两个层级的索引:一个由文档摘要组成,用于快速过滤;另一个由文档块组成,用于精确检索。这种设计在保证检索召回率的同时,大幅提升检索效率。
+
+### 混合搜索
+
+单纯的向量检索在处理精确术语匹配时往往表现不佳。比如用户搜索"如何重置密码",向量检索可能无法准确识别"重置"和"修改"之间的细微差别。
+
+**混合搜索** 结合传统关键词检索(如 BM25、TF-IDF)和现代语义向量检索。两种检索结果通过 **Reciprocal Rank Fusion(RRF)算法** 进行融合:对不同检索方法的结果按排名赋分,综合排名最高的文档被选中。
+
+关键词检索确保精确匹配的召回,语义检索捕捉同义词和语义关联。两者互补,能够应对更广泛的查询类型。
+
+### 内容增强
+
+**语句窗口检索器(Sentence Window Retriever)** 采用"小块索引、大窗口上下文"的策略。文档中的每个句子单独嵌入向量索引以保证检索精度,但检索后扩展上下文窗口,额外获取前后各 k 个句子,提供更完整的语义信息给大模型。
+
+**自动合并检索器(Parent Document Retriever)** 采用层级分割策略:文档被递归分割为较小的子块,每个子块与较大的父块存在引用关系。检索时获取相关子块,当多个相关子块指向同一父块时,自动升级为使用父块作为上下文。这种设计让系统同时获得精确检索和宏观语义。
+
+### HyDE:假设性答案增强检索
+
+**HyDE(Hypothetical Document Embeddings)** 是一种逆向思维的方法:不直接用问题检索,而是先用大模型根据问题生成一个假设性的答案,然后用这个假设答案去检索相关文档。
+
+前提假设是:假设答案与真实文档在语义空间中可能更接近,因为两者都是"回答性"的文本。HyDE 在处理抽象概念或模糊查询时表现尤为出色。
+
+### 查询转换
+
+用户的原始问题往往不够"检索友好"。**查询转换** 系列技术利用大模型的推理能力来优化查询:
+
+**查询分解(Query Decomposition)** 将复杂问题拆分为多个简单子问题。例如,"LangChain 和 LlamaIndex 哪个更适合做 RAG 开发?"这个问题难以直接检索,但可以拆分为"LangChain 做 RAG 开发的优缺点"和"LlamaIndex 做 RAG 开发的优缺点"两个子问题,分别检索后再综合回答。
+
+**Step-back Prompting** 生成更通用的查询来获取高层次上下文,与原始查询的检索结果一起输入模型,实现"由面到点"的推理。
+
+**查询重写(Query Rewriting)** 使用大模型改写问题表达,尝试用不同的表述方式检索,提高召回率。
+
+### 重排与过滤
+
+检索返回的 top-k 结果可能存在质量问题:相关性参差不齐、信息冗余、噪声干扰。**重排(Reranking)** 技术应运而生。
+
+**交叉编码器重排** 将查询和每个候选文档一起输入专门的交叉编码模型,输出精细化的相关性评分。这种方法比向量相似度计算更准确,但计算开销更大,通常用于对初始检索结果的二次筛选。
+
+**基于元数据的过滤** 利用文档的元数据(时间、来源、类别等)进行条件筛选,快速排除不相关的结果。
+
+## RAG 融合
+
+**RAG 融合(RAG Fusion)** 通过 LLM 生成多个变体查询来增强检索效果。单一查询可能无法覆盖用户问题的所有方面,通过让 LLM 生成多个不同角度的查询,可以从知识库中召回更丰富、更多样化的相关信息。
+
+
+
+### 技术流程
+
+1. **多查询生成**:使用 LLM 根据用户原始问题生成 n 个相关查询。
+2. **并行向量搜索**:用所有生成的查询分别进行向量检索。
+3. **结果融合排序**:应用 RRF 算法对所有检索结果进行综合排名。
+4. **上下文注入**:将融合后的相关文档注入提示模板。
+5. **答案生成**:LLM 基于融合后的上下文生成最终答案。
+
+### 优势
+
+**多样性增强**:不同查询从不同角度切入问题,最终结果涵盖更广泛的视角,减少单一视角带来的偏差。
+
+**鲁棒性提升**:某个查询因表述偏差导致检索不佳时,其他查询可以弥补缺陷,提升整体系统的稳定性。
+
+**语义纠偏**:LLM 生成的查询往往是对原始问题的语义扩展,能够捕捉隐含的语义关联。
+
+### 注意事项
+
+**延迟增加**:额外的 LLM 调用会引入额外延迟,在延迟敏感的场景中需要权衡。
+
+**专业术语处理**:如果知识库包含大量内部术语或行话,LLM 可能因不了解这些术语而产生无关查询,此时需要针对性优化提示词。
+
+**成本考量**:额外的 LLM 调用意味着额外的 token 消耗,需要评估 ROI。
+
+## 评估体系
+
+RAG 系统的质量评估是工程实践中的重要环节。业界通常采用 **RAG 三元组** 评估框架:
+
+- 检索内容相关性(Context Relevance):评估检索到的文档与用户问题的相关程度。高相关性意味着检索阶段工作良好,能够召回真正有用的信息。常用指标包括召回率(Recall)和精确率(Precision)。
+- 答案基于性(Answer Groundedness):衡量 LLM 的回答是否基于检索到的上下文,而非依赖自身知识或产生幻觉。这一指标直接反映 RAG 机制的有效性。
+- 答案相关性(Answer Relevance):评估生成的回答是否有效解决了用户的问题。高相关性意味着即使检索和基于性都良好,最终答案也能真正满足用户需求。
+
+
+
+### 评估框架与工具
+
+**RAGAs** 是当前流行的 RAG 评估框架,提供标准化的评估流程和指标计算方法。
+
+**LangSmith** 是 LangChain 官方提供的评估平台,支持自定义评估器、运行时监控和调试追踪。
+
+**Truelens** 由 LlamaIndex 团队推出,专注于 RAG 系统的可观测性和评估。
+
+## 发展趋势
+
+RAG 技术正在快速演进,几个方向值得关注:
+
+**端到端优化**。传统 RAG 将检索和生成视为独立环节,但最新研究开始探索联合优化的可能性。Meta AI 提出的 **RA-DIT** 技术同时微调 LLM 和 Retriever,让两个组件在学习过程中相互适应,在知识密集型任务上取得了显著提升。
+
+**多模态 RAG**。随着多模态大模型的发展,RAG 的边界正在从纯文本扩展到图像、视频、音频等多种模态。未来的 RAG 系统需要能够处理跨模态的知识检索和生成。
+
+**主动学习与持续优化**。结合用户反馈和评估结果,构建自适应优化机制,让 RAG 系统能够从实际使用中持续学习和改进。
+
+**轻量化与边缘部署**。随着模型压缩技术的发展,更小、更快的 LLM 将成为 RAG 系统的新选择。Mistral Mixtral、Microsoft Phi-2 等小参数模型的崛起,为 RAG 在边缘设备上的部署开辟了新的可能性。
+
+## 结语
+
+RAG 技术通过将检索能力与大语言模型的生成能力相结合,为 LLM 的实际应用提供了一条切实可行的路。它解决了知识时效性、私有数据访问和幻觉抑制等核心问题。
+
+当然,RAG 不是万能药。检索质量、响应延迟、系统复杂度等挑战依然存在。开发者需要根据具体场景权衡利弊,选择合适的技术组合。
+
+至于未来会怎样,让时间来检验。
diff --git "a/docs/md/AI/\344\270\272\344\273\200\344\271\210ChatGPT\350\203\275\345\220\254\346\207\202\344\275\240\350\257\264\347\232\204\350\257\235\357\274\237Embedding\346\212\200\346\234\257\346\217\255\347\247\230.md" "b/docs/md/AI/\344\270\272\344\273\200\344\271\210ChatGPT\350\203\275\345\220\254\346\207\202\344\275\240\350\257\264\347\232\204\350\257\235\357\274\237Embedding\346\212\200\346\234\257\346\217\255\347\247\230.md"
new file mode 100644
index 0000000..f14955d
--- /dev/null
+++ "b/docs/md/AI/\344\270\272\344\273\200\344\271\210ChatGPT\350\203\275\345\220\254\346\207\202\344\275\240\350\257\264\347\232\204\350\257\235\357\274\237Embedding\346\212\200\346\234\257\346\217\255\347\247\230.md"
@@ -0,0 +1,196 @@
+ChatGPT、Claude这些AI助手能理解我们说的话,还能给出像样的回答。做到这点,靠的是Embedding技术。
+
+没有它,大语言模型根本没法处理文字输入。Embedding把人类语言变成数字,让机器能"读懂"。
+
+## 什么是Embedding
+
+Embedding就是把词语、句子变成一串数字。听起来简单,但背后的想法很有意思。
+
+我们说"北京"这个词时,脑子里会想到:城市、首都、政治中心、文化古都。这些概念连在一起,构成我们对"北京"的理解。Embedding做的,就是把这种理解映射到数学空间里。
+
+每个词变成一个向量——一组数字。有意思的是,语义相近的词,向量也靠得近。"北京"和"上海"的向量距离很近,都是城市。但"北京"和"苹果"就离得远,语义完全不同。
+
+
+
+这就让机器能通过距离来理解语义,不是简单匹配关键词。
+
+## 怎么把词变成数字
+
+举个通俗的例子。描述几个水果:
+
+- 苹果:红色的、中等大小、甜度适中、脆的、圆形的。
+- 香蕉:黄色的、细长的、很甜、软的、弯曲的。
+- 西瓜:绿色的、很大的、甜度中等、多汁的、圆形的。
+
+用数字表示这些特征,比如给每个特征打分(0到1):
+
+- 苹果:[0.9(红色),0.5(大小),0.7(甜度),0.8(脆度),1.0(圆润)]。
+- 香蕉:[0.1(红色), 0.3(大小),0.9(甜度),0.2(脆度), 0.2(圆润)]。
+- 西瓜:[0.1(红色),0.95(大小),0.6(甜度),0.3(脆度),1.0(圆润)]。
+
+真实的Embedding要复杂得多,通常是几百到几千维。但思路就是这样:用数字刻画特征。
+
+模型怎么学会这些特征?它看上下文。就像通过朋友圈了解一个人,模型通过观察一个词周围经常出现什么词,理解这个词的含义。
+
+比如"银行":
+- "我去银行存钱"——周围是"存钱",指金融机构。
+- "他坐在银行边钓鱼"——周围是"钓鱼",指河岸。
+
+模型通过阅读海量文本,学会根据上下文判断词义。语义相近的词,向量也接近。
+
+## 怎么衡量向量的相似度
+
+用余弦相似度。计算两个向量的夹角:
+
+- 夹角小,相似度接近1,语义相似。
+- 夹角大,相似度接近0,语义不相关。
+
+余弦相似度关注向量的方向,不是长度。在自然语言里,概念的相似性更多体现在"方向"上。
+
+
+
+## 向量和张量的关系
+
+机器学习里有个更广的概念——张量(Tensor),就是N维数组:
+
+- 0维张量是标量(单个数字)。
+- 1维张量是向量(一维数组)。
+- 2维张量是矩阵(二维数组)。
+- 更高维张量表示更复杂的数据结构。
+
+Embedding向量是一维张量。在大语言模型里,虽然每个词的Embedding是向量,但批量处理时会组织成矩阵或更高维张量。
+
+## 技术演进
+
+Embedding技术从简单到复杂,走了一段路。
+
+### Word2Vec:早期的尝试
+
+Word2Vec是早期代表,关注单个词的向量化。思路是:通过上下文学习词的语义。
+
+模型观察大量文本,学习哪些词经常出现在相似的语境中。"猫"和"老虎"都会出现在"动物园"、"宠物"这些上下文里。通过统计学习,这些词在向量空间中位置靠近。
+
+这揭示了一个特性:语义相似性可以通过上下文分布的相似性来捕捉。
+
+但Word2Vec有局限:每个词只有固定向量,处理不了一词多义。"苹果"可以指水果,也可以指科技公司,Word2Vec把它们映射到同一个向量。
+
+### 自注意力机制:突破
+
+Transformer引入的自注意力机制(Self-Attention)是重大突破。模型生成某个词的向量时,能同时考虑句子中所有其他词。
+
+两个优势:
+
+- **长距离依赖**:传统序列模型里,词与词的依赖关系随距离增加而减弱。自注意力机制能直接计算句子中任意两个词的关联强度,不管它们离多远。这帮助模型理解复杂的句法和语义。
+- **动态上下文表示**:Word2Vec给每个词分配固定向量,自注意力机制根据上下文生成不同向量。"我吃了一个苹果"和"苹果公司发布了新产品",两个"苹果"向量完全不同。
+
+### BERT:双向理解
+
+BERT(Bidirectional Encoder Representations from Transformers)实现了双向上下文理解。预训练时同时考虑一个词左右两侧的所有词。
+
+以"很长"这个词组为例:
+- "这条河很长"——指河流长度。
+- "他当了很长时间的厂长"——指时间持续。
+
+BERT根据不同上下文为"很长"生成不同向量。Embedding技术从静态表示迈向动态、上下文感知的语义理解。
+
+
+
+## Embedding在LLM里的作用
+
+Embedding是大语言模型(LLM)运转的基石。
+
+### LLM内部怎么工作
+
+用户向ChatGPT输入问题时,系统内部经历几个步骤:
+
+- **第一步:Tokenization(分词)**:分词器把文本拆成token。一个token可能是一个词、一个字,也可能是一个词组。"请写一首关于秋天的诗"会被拆为["请"、"写"、"一首"、"关于"、"秋天"、"的"、"诗"]。
+- **第二步:Embedding Lookup(向量查询)**:每个token有唯一ID。LLM内部维护巨大Embedding矩阵,类似字典。模型看到token ID,在矩阵中查找对应向量。
+- **第三步:Position Encoding(位置编码)**。模型要知道每个token在句子中的位置。给每个token加上位置编码向量,保留顺序信息。
+- **第四步:向量处理与生成**:语义向量和位置编码结合,形成最终输入。这些向量经过Transformer多层网络计算,生成输出。
+
+Embedding把人类可读的语言变成机器可计算的数字。没有这一步,推理、理解、生成都无从谈起。
+
+
+
+### 理解和推理的数学基础
+
+Embedding的重要性不只体现在输入阶段。在向量空间里,复杂语义操作通过数学运算实现:
+
+- 语义相似性:向量余弦相似度度量。
+- 语义关系:向量运算捕捉("国王" - "男人" + "女人" ≈ "女王")。
+- 语义组合:向量加权求和。
+
+这些数学操作让LLM进行推理和生成,不是简单模式匹配。
+
+## RAG框架里的Embedding
+
+除了在LLM内部,Embedding在实际应用中也很重要,特别是在RAG(Retrieval-Augmented Generation,检索增强生成)框架中。
+
+### RAG是什么
+
+RAG把大语言模型和可搜索的外部知识库结合。核心想法:让模型访问训练时没见过的新信息,提升回答准确性和时效性。
+
+传统LLM应用里,模型知识来自训练数据。GPT-4的训练数据截止到2023年,之后发生的事它不知道。RAG通过连接外部知识库,让模型实时获取最新信息。
+
+### Embedding在RAG里的作用
+
+在RAG框架中,Embedding连接外部知识库与大语言模型。工作流程:
+
+- **知识库准备**:把外部文档(PDF、网页等)分割成文本块。用Embedding模型把每个文本块转换为向量,存储在向量数据库。文本内容变成可计算的数学表示。
+- **查询与检索**:用户提问时,系统把查询语句转换为查询向量。在向量数据库中通过余弦相似度计算,找出最相似的top-k个文本块。这是语义检索,不是关键词匹配。
+- **生成**:检索出的文本块和用户查询一起传给大语言模型。模型基于这些信息生成回答。
+
+
+
+### 模型一致性原则
+
+RAG里有个原则必须遵守:导入数据和查询时,必须用同一个Embedding模型。
+
+不同模型把相同文本映射到不同向量空间。导入和查询用不同模型,就像用英语语法规则理解中文句子,匹配会失败。保持模型一致,检索才准确。
+
+### Embedding质量影响RAG效果
+
+Embedding模型性能直接决定RAG效果。高质量模型能准确捕捉文本语义,检索出最相关的信息。模型性能不佳会:
+
+- 检索不准确:返回的内容相关性不高。
+- 遗漏关键信息:没检索到有用的信息。
+- 引入噪音:检索出不相关内容,干扰模型判断。
+
+选合适的Embedding模型,是RAG系统成功的关键。
+
+## 向量数据库
+
+Embedding技术广泛应用后,专门存储和检索高维向量的向量数据库出现了。这类数据库的核心能力是相似性搜索,根据向量距离查找最相似的向量。
+
+### 两类向量数据库
+
+- **专用向量数据库**:完全为向量检索构建,采用高级索引算法(HNSW、IVF)在海量数据中实现毫秒级查询。代表产品有Pinecone、Milvus、Weaviate。优势是检索快、性能优化好,适合大规模向量检索。
+- **集成向量检索功能的通用数据库**:传统关系型或文档型数据库,通过插件或内置功能支持向量检索。代表产品有Elasticsearch(dense_vector字段)、PostgreSQL(pgvector插件)、Redis。优势是同时处理结构化数据和向量数据,适合混合检索场景。
+
+
+
+### Elasticsearch的语义检索
+
+Elasticsearch通过dense_vector字段和kNN(最近邻)搜索功能,把Embedding转换、存储和检索封装在一起。用户可以直接把Elasticsearch作为RAG框架的向量存储:
+
+- 导入文档时,配置处理管道让Elasticsearch自动调用模型把文本转换为向量并存储。
+- 查询时,系统自动把查询转换为向量,执行相似性搜索。
+
+这降低了技术门槛,开发者不用单独部署向量数据库,就能实现语义检索。
+
+## Embedding的价值
+
+Embedding技术把语言变成数学,让计算机能"理解"人类语言。
+
+从技术演进看,Embedding从简单的词向量发展到上下文感知的动态表示。从Word2Vec到BERT,再到如今的大语言模型,每次技术突破都伴随着Embedding能力提升。
+
+从应用看,Embedding在LLM内部把自然语言转化为数学表示。在RAG等应用中,Embedding实现从关键词匹配到语义检索,大幅提升信息检索准确性。
+
+未来,Embedding还会承担更多:
+
+- 多模态融合:把文本、图像、音频映射到统一的向量空间,实现跨模态理解和生成。
+- 知识图谱构建:通过向量表示构建大规模知识网络,支持复杂推理和决策。
+- 个性化推荐:基于用户行为和偏好的向量表示,实现精准个性化服务。
+- 隐私保护计算:在向量空间进行加密计算,保护数据隐私同时实现智能分析。
+
+理解Embedding的原理和应用,有助于更好地使用AI工具,也为探索AI技术未来提供视角。在AI时代,Embedding将继续连接人类智慧与机器能力。
\ No newline at end of file
diff --git "a/docs/md/AI/\346\267\261\345\272\246\350\247\243\346\236\220Skills\357\274\232\344\273\216Prompt\345\210\260\350\203\275\345\212\233\345\244\215\347\224\250\347\232\204\346\212\200\346\234\257\351\235\251\345\221\275.md" "b/docs/md/AI/\346\267\261\345\272\246\350\247\243\346\236\220Skills\357\274\232\344\273\216Prompt\345\210\260\350\203\275\345\212\233\345\244\215\347\224\250\347\232\204\346\212\200\346\234\257\351\235\251\345\221\275.md"
new file mode 100644
index 0000000..8ac1184
--- /dev/null
+++ "b/docs/md/AI/\346\267\261\345\272\246\350\247\243\346\236\220Skills\357\274\232\344\273\216Prompt\345\210\260\350\203\275\345\212\233\345\244\215\347\224\250\347\232\204\346\212\200\346\234\257\351\235\251\345\221\275.md"
@@ -0,0 +1,428 @@
+## 从Prompt到Skills的转变
+
+2023年到2024年是"Prompt工程"的黄金时期。到了2025年底,AI圈开始频繁讨论一个新概念——**Skills(技能)**。
+
+GitHub上Skills相关仓库获得上万star,各行各业的专业人士开始分享自己封装的Skills。Skills到底是什么?它为什么能引发如此关注?
+
+### Skills的本质:模块化能力包
+
+**Agent Skills是模块化的能力包,包含指令、元数据和可选资源(脚本、模板),让AI Agent在需要时自动加载和使用**。
+
+Skills就像AI助手的"工作手册库"。它不是每次对话都要重新输入的临时指令,而是一套可以长期保存、随时调用的能力模块。
+
+### 从"带新人"到"给手册"
+
+要理解Skills,先看传统AI交互的问题。
+
+想象你在公司带一个新人。他聪明、理解能力强,但不熟悉规矩。
+
+- Prompt方式就像你每次都口头交代任务:"今天写一段公众号开头"、"把这个语气改得更克制"、"按我的结构写一页PPT"。这适合一次性指令,但一旦关闭对话,所有指令就消失,下次得从头教。
+- Rules或记忆机制相当于在工位贴一张"公司行为守则",只能管态度和格式这类宽泛要求。
+- MCP和工具调用更像是给他的电脑装一堆软件和API,他能调用外部工具,但不知道什么时候该用、怎么组合。
+
+**Skills**改变了这一局面。它就像给新人一本完整的公司内部SOP手册——不是长到让人窒息的Word文档,而是一个知识库文件夹,里面有规范、脚本、模板、参考资料。AI会在需要时自己翻阅,按需加载。
+
+### Skills的物理形态
+
+很多人问:"这不就是Prompt吗?"实际上两者在形态上有本质区别:
+
+- **Prompt**:一段文本(通常是Markdown格式)。
+- **Skills**:一个文件夹结构,包含多种资源。
+
+一个标准的Skill目录:
+
+```
+skill-name/
+├── SKILL.md # 核心指令文件(必需)
+├── scripts/ # 可执行脚本(可选)
+├── references/ # 参考文档(可选)
+├── templates/ # 模板文件(可选)
+└── assets/ # 其他资源(可选)
+```
+
+
+
+**SKILL.md是唯一必需的文件**,它采用YAML前导格式(类似简历开头的个人信息区),包含元数据和详细指令。这种设计让Skills不仅能承载知识,还能承载工具和流程。
+
+## 渐进式披露架构
+
+### 为什么"一次性塞进所有信息"行不通?
+
+Skills采用了**渐进式披露(Progressive Disclosure)**架构。这个概念在移动互联网时代曾是用户体验设计的核心原则之一。
+
+打开一个APP,如果它一次性把所有功能、设置、选项都堆在你面前,你会怎样?认知负荷爆炸,不知所措。
+
+人的瞬时记忆区非常有限,一瞬间只能接受最多7±2个信息块。AI也是如此——受限于Token窗口,对话越长,模型越"笨"。Token在Agent架构上寸土寸金。
+
+传统做法:每次对话都把完整指令塞进上下文。一个详细的PDF处理工作流可能需要3000+ tokens。如果同时处理Excel、写代码、生成报告,上下文窗口很快爆满。
+
+### 三层加载机制
+
+Skills通过三层渐进式加载解决这个问题:
+
+**第一层:元数据——目录索引**
+
+这是Skills的"封面",包含技能名称和一句话描述。
+
+- **加载时机**:每次对话开始时。
+- **Token消耗**:约100 tokens/Skill。
+- **作用**:让AI知道有哪些Skills可用,何时该用。
+
+你可以安装数十个Skills,几乎没有性能损失。AI就像看图书馆的目录,知道有哪些书,但不必都翻开。
+
+**第二层:指令——详细手册**
+
+当AI通过元数据判断某个任务需要特定Skill时,它会读取完整的SKILL.md文件。
+
+- **加载时机**:任务匹配时触发。
+- **Token消耗**:数千tokens(按实际文件大小)。
+- **作用**:提供详细的操作指南和最佳实践。
+
+用户说"帮我处理这个PDF",AI会判断匹配PDF Skill,然后加载详细的处理流程:先提取文本,再识别表单字段,最后填写并保存。
+
+**第三层:资源和代码——深度参考**
+
+这层包括参考文档、可执行脚本、模板文件等。
+
+- **加载时机**:SKILL.md中引用时。
+- **Token消耗**:按需加载。
+- **关键优势**:脚本执行不消耗上下文(仅结果消耗)。
+
+一个包含复杂Python脚本的Skill,脚本本身的代码不会进入上下文,只有执行结果会返回。这让Skills可以承载几乎无限的资源,而不必担心Token限制。
+
+
+
+### 一个真实的加载流程
+
+以PDF处理为例,看Skills如何工作:
+
+**阶段1:初始状态**
+
+```
+用户输入:"用PDF技能填写这份合同"
+系统提示 + 技能目录 + 用户消息
+Token消耗:约100 tokens
+```
+
+**阶段2:加载主手册**
+
+```
+AI判断:这个任务匹配PDF Skill
+执行:bash cat ~/.claude/skills/pdf/SKILL.md
+Token消耗:+3000 tokens
+```
+
+**阶段3:按需加载参考资料**
+
+```
+AI判断:需要表单填写规则
+执行:bash cat ~/.claude/skills/pdf/references/forms.md
+Token消耗:+500 tokens
+```
+
+**阶段4:执行脚本**
+
+```
+执行:python scripts/fill_form.py --input contract.pdf --output filled.pdf
+Token消耗:+200 tokens(仅输出结果)
+```
+
+**总Token消耗**:约3800 tokens。
+
+对比传统方式:一次性加载所有相关文档和脚本定义,可能需要10,000+ tokens。Skills节省了60-70%的上下文空间。
+
+
+
+## Skills vs MCP vs Prompt:互补关系
+
+### 三者的核心定位
+
+Skills、MCP、Prompt不是竞争关系,而是互补关系:
+
+| 维度 | Skills | MCP | Prompt |
+|------|--------|-----|--------|
+| **核心定位** | 工作流程指南(How) | 外部系统连接(What) | 临时指令 |
+| **解决问题** | 如何使用能力 | 提供什么数据/能力 | 当下做什么 |
+| **形象比喻** | 使用说明书 | 工具箱 | 口头指令 |
+| **Token效率** | 高(渐进加载) | 低(全量加载) | 中(每次重复) |
+| **复用性** | 强(文件系统) | 中(协议层面) | 弱(手动复制) |
+
+
+
+### Skills与MCP:工作手册 vs 门禁卡
+
+**Skills解决"怎么做"(方法论/工作流),MCP解决"连到哪儿"(连接外部系统)**。
+
+用职场类比:
+- **MCP**:给AI一张门禁卡,让它能进入公司的各个系统(数据库、API、外部工具)。
+- **Skills**:给AI一本工作手册,教它如何使用这些系统完成具体任务。
+
+一个组合场景:
+
+**生成销售报告**
+
+1. **MCP提供数据连接**
+ - 连接Salesforce获取客户数据。
+ - 连接PostgreSQL查询销售记录。
+ - 连接Google Sheets读取目标数据。
+
+2. **Skills提供工作流程**
+ - 数据提取顺序(先查哪个系统)。
+ - 计算逻辑(增长率、完成率)。
+ - 报告格式和模板。
+ - 异常处理规则。
+
+MCP解决"能访问什么数据",Skills解决"如何使用这些数据生成报告"。
+
+
+
+### Skills vs Prompt:从临时指令到持久能力
+
+**Skills不就是高级一点的Prompt吗?**
+
+答案既是肯定的,也是否定的。
+
+**相同点**:Skills的核心确实是自然语言指令,这与Prompt一致。
+
+**根本区别**:
+
+- **生命周期**:Prompt是对话级的,Skills是系统级的。
+- **复用方式**:Prompt需要手动复制粘贴,Skills自动匹配触发。
+- **承载能力**:Prompt只能承载文本,Skills可以承载脚本、模板、参考文档。
+- **Token效率**:Prompt每次都全量加载,Skills按需渐进加载。
+
+用一个实际例子:
+
+**没有Skills时**,每次都要说:
+
+```
+帮我总结这篇文章 → 翻译成英文 → 改成公众号风格 → 加标题 → 输出Markdown格式
+```
+
+**有了Skills后**,只需要一句:
+```
+使用「技术文章转公众号」Skill
+```
+
+AI会自动按照预设的完整流程执行。
+
+## 实际应用
+
+### 个人场景:把重复工作封装成能力包
+
+**案例1:AI选题系统**
+
+一个内容团队用Skills构建了自动化选题系统,包含:
+- 1个总控Agent。
+- 3个Skill(热点采集、选题生成、选题审核)。
+
+每天只需要一句:"开始今日选题生成",系统就会自动:
+1. 从多个平台采集全网热点。
+2. 筛选并生成TOP10选题(包含事件描述、核心角度、标题)。
+3. 按照内部方法论自动审核。
+4. 不通过时给出修改意见并迭代优化。
+
+过去需要2-3小时的工作,现在几分钟就能完成初筛。
+
+**案例2:整合包生成器**
+
+很多GitHub开源项目没有前端界面,环境配置复杂。有人用Skills做了一个"整合包生成器":
+
+提供一个GitHub链接,Skill就会:
+1. 分析项目结构。
+2. 自动生成前端界面。
+3. 编写启动脚本。
+4. 打包成开箱即用的整合包。
+
+解决了"想用但不会配置"的痛点。
+
+### 团队场景:知识资产沉淀与共享
+
+**传统方式的问题**:
+- 每个团队各自维护长Prompt。
+- 写法、风格不统一。
+- 复用靠复制粘贴。
+- 难以版本管理和评审。
+
+**Skills带来的改变**:
+- 把"怎么做好一件事"固化成SKILL.md + 脚本 + 参考文档。
+- 放入Git版本库,走标准开发流程。
+- 团队间共享、评审、复用。
+- 形成企业内部的"技能库"(Skill Library)。
+
+**组织架构示例**:
+```
+公司级Agent产品
+├── 市场部维护:品牌文案Skill
+├── 法务部维护:合同审阅Skill
+├── 财务部维护:报销审核Skill
+└── 技术部维护:代码审查Skill
+```
+
+所有技能装在同一个Agent身上,用户只跟一个界面打交道。
+
+### 行业场景:专业知识标准化
+
+**医疗诊断流程**:将诊断标准、注意事项、药物禁忌等封装成Skill,确保AI遵循医疗规范
+
+**法律文书审查**:将审查要点、风险识别、合规要求标准化,提高审查质量和一致性
+
+**代码审计规范**:将安全检查项、代码风格要求、最佳实践固化
+
+**ML实验配置**:将实验设计规范、参数推荐范围、结果记录模板封装
+
+这些领域知识需要结构化存储、团队共享、版本管理、跨平台使用——正是Skills的强项。
+
+## 技术实现
+
+### 最小可行Skill
+
+创建一个Skill只需要一个SKILL.md文件:
+
+```markdown
+---
+name: hello-skill
+description: A simple skill that greets users
+---
+
+# Hello Skill
+
+When user says hello, respond with a friendly greeting.
+```
+
+**必填字段**:
+- `name`:技能名称(小写字母、数字、连字符符)。
+- `description`:功能描述。
+
+**简单到人人可创建,强大到专业团队可用**。
+
+### 完整Skill:PDF处理案例
+
+```
+pdf-skill/
+├── SKILL.md
+├── scripts/
+│ ├── extract_text.py
+│ ├── fill_form.py
+│ └── merge_pdfs.py
+├── references/
+│ ├── FORMS.md
+│ └── API_REFERENCE.md
+└── templates/
+ └── report_template.md
+```
+
+**SKILL.md内容**:
+
+```markdown
+---
+name: pdf-processing
+description: Extract text and tables from PDF files, fill forms, merge documents.
+ Use when working with PDF files or when the user mentions PDFs.
+---
+
+# PDF Processing
+
+## Quick Start
+
+1. For text extraction, use `python {baseDir}/scripts/extract_text.py`
+2. For form filling, see [FORMS.md](references/FORMS.md)
+3. For merging PDFs, execute the merge script
+
+## Supported Operations
+
+- Text extraction from text-based PDFs
+- OCR for scanned PDFs (requires Tesseract)
+- Form field identification and filling
+- Multi-document merging
+
+## Best Practices
+
+- Always validate PDF integrity before processing
+- Use OCR only when necessary (higher token cost)
+- Keep extracted text under 10,000 tokens for best performance
+```
+
+**关键点**:
+
+- `{baseDir}`是自动替换变量,表示Skill的安装路径。
+- 可以引用其他文件(如FORMS.md),AI会在需要时加载。
+- 指令清晰、结构化,便于AI理解和执行。
+
+### 安装和使用
+
+**方法1:命令安装**
+
+```bash
+# 安装官方Skill
+claude skill install https://github.com/anthropics/skills/tree/main/skills/pdf
+
+# 或在对话中直接说
+"安装这个skill:https://github.com/xxx/skill-name"
+```
+
+**方法2:手动放置**
+
+将Skill文件夹放到对应目录:
+- Claude Code:`~/.claude/skills/`。
+- Cursor:`~/.cursor/skills/。`
+- OpenCode:`~/.config/opencode/skill/`。
+
+**使用方式**:
+
+直接对话:
+```
+用户:"帮我处理这个PDF"
+AI会自动识别并调用PDF Skill
+```
+
+或者显式指定:
+```
+用户:"使用PDF Skill提取这份文档的文本"
+```
+
+## 未来展望
+
+### 从工具到生态
+
+目前Skills还处于早期阶段,但已经有了生态雏形:
+
+- **官方Skills库**:Anthropic开源了官方Skills仓库,包含PDF、Excel、PPT、Word等常用技能。
+- **社区贡献**:GitHub上涌现大量社区贡献的Skills,涵盖数据分析、代码审查、文档生成等多个领域。
+- **工具支持**:Claude Code、Cursor、OpenCode等主流工具均已支持Skills。
+- **技能市场**:扣子等平台开始提供技能市场,支持搜索、安装、分享Skills。
+
+### 潜在挑战
+
+Skills也面临挑战:
+
+- **标准化问题**:不同平台、不同团队的Skills格式可能不统一,需要建立行业标准。
+- **安全与隐私**:Skills可以执行脚本,需要沙箱隔离和权限控制。
+- **质量参差**:开放的生态意味着质量良莠不齐,需要评价和筛选机制。
+- **学习曲线**:虽然创建简单,但要设计高质量的Skill仍需要经验。
+
+### 对AI发展的意义
+
+Skills代表一个重要趋势:**从让AI"理解"到让AI"执行"**。
+
+过去几年,我们主要关注如何让AI更好地理解自然语言、理解上下文、理解意图。这是必要的基础,但还不够。
+
+Skills的出现,标志着我们开始关注如何让AI系统地、可重复地、高质量地执行复杂任务。这不仅需要理解能力,还需要方法论、最佳实践、工具链的支持。
+
+**这是AI从"对话伙伴"进化为"工作伙伴"的关键一步。**
+
+## 今天就开始你的第一个Skill
+
+Skills的热度已不亚于当年的Prompts。但这不只是流行趋势,而是实实在在的生产力革命。
+
+如果你还在犹豫是否要尝试Skills,建议从最简单的开始:
+
+**今天**,安装一个官方Skill(比如skill-creator),感受一下"一个命令安装能力"的便捷。
+
+**明天**,把最常用的一个动作固化成Skill——比如选题筛热点、报错日志分析、链接摘要生成。
+
+**后天**,你会想把更多工作流程都搬进去。
+
+到那一步,你就进入了另一个状态:**自由,创造的状态**。
+
+Skills的核心价值,在于**复用**。当你把一次性的努力转化为可重复调用的能力,你就不再是每次都从零开始,而是站在前人的肩膀上持续前进。
diff --git "a/docs/md/java/\351\235\242\350\257\225\345\256\230\357\274\232\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\345\222\214\350\231\232\346\213\237\347\272\277\347\250\213\346\200\216\344\271\210\351\200\211\357\274\237\347\234\213\345\256\214\350\277\231\347\257\207\344\270\215\345\206\215\350\242\253\351\227\256\345\200\222.md" "b/docs/md/java/\351\235\242\350\257\225\345\256\230\357\274\232\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\345\222\214\350\231\232\346\213\237\347\272\277\347\250\213\346\200\216\344\271\210\351\200\211\357\274\237\347\234\213\345\256\214\350\277\231\347\257\207\344\270\215\345\206\215\350\242\253\351\227\256\345\200\222.md"
new file mode 100644
index 0000000..b9fdea0
--- /dev/null
+++ "b/docs/md/java/\351\235\242\350\257\225\345\256\230\357\274\232\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\345\222\214\350\231\232\346\213\237\347\272\277\347\250\213\346\200\216\344\271\210\351\200\211\357\274\237\347\234\213\345\256\214\350\277\231\347\257\207\344\270\215\345\206\215\350\242\253\351\227\256\345\200\222.md"
@@ -0,0 +1,291 @@
+Java的高并发问题由来已久。传统线程模型下,每个Java线程映射一个操作系统内核线程,而操作系统线程是昂贵资源——默认每个线程消耗约1MB栈内存,调度还要在内核态与用户态之间来回切换。这让Java在处理高并发IO密集型应用时,总被Go、Lua等支持协程的语言压一头。为突破这个瓶颈,Java生态先后涌现出响应式编程与虚拟线程两种方案。前者要求改变编程范式,后者在底层机制上动刀,保留传统编码习惯。这两条路线的竞争,关系到Java平台的演进方向。
+
+## 传统线程模型的瓶颈
+
+先看传统thread-per-request模型有什么问题。以Tomcat为例,其维护的线程池默认最大线程数为200,单进程同时处理的最大并发请求数被这个数字死死卡住。当请求涉及数据库查询、缓存访问、下游服务调用等IO操作时,处理线程会在IO等待期间被阻塞,看起来线程很多,真正干活的可能没几个。
+
+提升并发能力的传统方法是增加线程池大小,但会遇到三重限制:
+
+- **系统资源限制**:操作系统支持的内核线程数量有限,Java平台线程与内核线程1:1映射,扩展不了。实测4000个平台线程,总线程栈空间占用约8096MB。
+- **调度开销累积**:平台线程调度由内核调度器完成,线程多了,上下文切换就频繁,CPU资源消耗在调度上而不是业务处理上。
+- **IO阻塞的低效性**:线程在IO等待期间完全闲置,干不了别的事。典型企业应用里,线程大部分时间都在等——数据库查询、HTTP调用、文件读写,真正CPU干活的时间很短,大把时间耗在等待上。
+
+响应式编程就是在这种背景下出来的,想通过编程范式的变革绕过硬件限制。
+
+## 响应式编程:代价沉重的性能提升
+
+响应式编程的核心思想是"缓冲区+回调",通过非阻塞IO让少量线程一直忙。技术实现依赖三块:
+
+- **非阻塞IO基础设施**:JDK 7引入的NIO为非阻塞操作打开了门,Socket读写、文件操作、锁API都有非阻塞版本。Spring WebFlux基于Project Reactor构建,用`Mono`和`Flux`类型实现发布-订阅模式,解耦数据生产者与消费者。
+- **事件循环模型**:单个线程通过事件循环处理多个请求,IO操作期间不阻塞线程,而是注册回调函数,数据就绪后由事件循环触发处理。
+- **背压机制**:通过流量控制防止生产者压垮消费者,这是响应式流规范的核心特性。
+
+### 响应式代码的复杂性
+
+响应式编程的性能优势明显,但代价也不小。看一个电商购物车价格计算的例子,传统代码:
+
+```java
+public void addProductToCart(String productId, String cartId) {
+ Product product = repository.findById(productId)
+ .orElseThrow(() -> new IllegalArgumentException("not found!"));
+
+ Price price = product.basePrice();
+ if (product.category().isEligibleForDiscount()) {
+ BigDecimal discount = discountService.discountForProduct(productId);
+ price.setValue(price.getValue().subtract(discount));
+ }
+
+ var event = new ProductAddedToCartEvent(productId, price.getValue(), price.getCurrency(), cartId);
+ kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event);
+}
+```
+
+改造成响应式风格:
+
+```java
+void addProductToCart(String productId, String cartId) {
+ repository.findById(productId)
+ .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("not found!")))
+ .flatMap(this::computePrice)
+ .map(price -> new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId))
+ .subscribe(event -> kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event));
+}
+
+Mono computePrice(Product product) {
+ if (product.category().isEligibleForDiscount()) {
+ return discountService.discountForProduct(product.id())
+ .map(product.basePrice()::applyDiscount);
+ }
+ return Mono.just(product.basePrice());
+}
+```
+
+代码量增加不是最要命的。响应式编程真正的痛点在于:
+
+- **可读性崩溃**:回调嵌套形成"回调地狱",链式操作符(`flatMap`、`map`、`zip`)把业务逻辑碎片化,代码审查时很难快速理解执行流程。操作全封装成回调函数,回调里面再嵌回调,看着头疼。
+- **调试黑洞**:在回调函数里打断点,调用栈追溯不到业务入口。传统阻塞式编程通过栈帧能逐层定位调用方,响应式代码的调用链路被异步边界切断,异常堆栈常常变成一堆废话,给不出有效的定位信息。
+- **思维模式冲突**:大多数程序员习惯阻塞式思维,响应式编程要求从流处理、背压控制、异步编排的角度思考,认知成本高。
+- **生态兼容性割裂**:WebFlux要求全链路非阻塞,传统阻塞式API(JPA、JDBC、RestTemplate)没法直接用,得换成R2DBC、WebClient等响应式组件。遗留项目迁移成本巨大,而且响应式生态并不完备,有些场景得自己造轮子。
+
+### 响应式编程的性能边界
+
+响应式编程不是万能药,性能优势主要在IO密集型场景。对于计算密集型任务,响应式编程往往适得其反——线程在CPU密集计算期间释放不了,反而搭进去响应式框架的额外开销。
+
+压测数据显示,WebFlux在IO密集型场景下,用25个线程就能达到964 req/sec的吞吐量,远超传统线程池的388 req/sec(200线程)或975 req/sec(500线程)。但这要付出代码复杂度和维护成本的巨大代价。
+
+## 虚拟线程的技术实现
+
+Java 21引入的虚拟线程(Virtual Thread),不改变编程范式,却实现了响应式编程的性能目标。核心技术原理:
+
+**virtual thread = continuation + scheduler + runnable**
+
+### 虚拟线程的工作机制
+
+虚拟线程不与特定操作系统线程绑定,而是在平台线程(载体线程)上运行Java代码,但在代码整个生命周期内不独占平台线程。多个虚拟线程可以在同一个平台线程上运行,共享平台线程资源。
+
+**Continuation组件**是虚拟线程的核心,它既包装用户的真实任务,又提供虚拟线程任务暂停/继续的能力,还负责虚拟线程与平台线程之间的数据转移:
+
+- 任务需要阻塞挂起时(如IO操作、锁等待、sleep),调用Continuation的yield操作,虚拟线程从平台线程卸载(unmount)。
+- 任务解除阻塞继续执行时,调用Continuation的run方法,虚拟线程重新挂载(mount)到载体线程。
+
+具体实现细节:
+
+- **Mount操作**:虚拟线程挂载到平台线程,Continuation堆栈帧数据从堆内存拷贝到平台线程栈,是从堆到栈的复制过程。
+- **Unmount操作**:虚拟线程从平台线程卸载,Continuation栈数据帧留在堆内存中,载体线程被释放到调度器等待新任务。
+- **调度器设计**:JVM用FIFO模式的ForkJoinPool作为虚拟线程调度器,当平台线程对应的虚拟线程任务列表全部阻塞时,支持工作窃取(work-stealing),平台线程可以去窃取其他平台线程的虚拟线程执行。
+
+### 虚拟线程的内存优势
+
+虚拟线程的低成本让它可以大规模创建:
+
+**平台线程资源占用**:
+
+- 预留1MB线程栈空间,
+- 平台线程实例占据2000+字节。
+
+**虚拟线程资源占用**:
+
+- Continuation栈占用数百字节到数百KB,作为堆栈块对象存储在Java堆中。
+- 虚拟线程实例占据200-240字节。
+
+实测数据:4000个平台线程总内存占用超过8000MB,而4000个虚拟线程内存占用不到300MB。而且虚拟线程的堆栈在堆中存储,可以被GC回收,进一步降低内存压力。
+
+### 虚拟线程的自动卸载机制
+
+虚拟线程的核心价值在于遇到阻塞操作时自动卸载,释放载体线程。JVM对核心类库做了改造,当代码遇到IO操作时,自动切换到非阻塞版本:
+
+```java
+Thread.startVirtualThread(() -> {
+ // 阻塞调用,但不会阻塞载体线程
+ Product product = repository.findById(productId);
+ BigDecimal discount = discountService.discountForProduct(productId);
+ // ...业务逻辑
+});
+```
+
+虚拟线程执行到`repository.findById()`时,JVM检测到IO操作,触发Continuation.yield(),虚拟线程从载体线程卸载,载体线程转而去执行其他虚拟线程。等数据库返回数据后,虚拟线程重新挂载到载体线程(可能是另一个载体线程)继续执行。
+
+这种机制让开发者用传统的阻塞式编程思维,就能享受到响应式编程的性能优势。
+
+## 虚拟线程的局限
+
+虚拟线程不是银弹,有它的局限:
+
+### Pinned Thread问题
+
+虚拟线程执行以下操作时,无法进行yield操作,,载体线程会被阻塞:
+
+- **Native方法调用**:JNI调用或Foreign Function & Memory API无法卸载虚拟线程。
+- **synchronized代码块**:在synchronized修饰的方法或代码块中,虚拟线程会pin住载体线程。官方建议用ReentrantLock替代:
+
+```java
+// 错误:会导致载体线程阻塞
+synchronized(lock) {
+ // IO操作
+}
+
+// 正确:虚拟线程可正常卸载
+ReentrantLock lock = new ReentrantLock();
+lock.lock();
+try {
+ // IO操作
+} finally {
+ lock.unlock();
+}
+```
+
+### ThreadLocal陷阱
+
+虚拟线程支持ThreadLocal,但因为虚拟线程数量可能达到数百万,ThreadLocal中存储的线程变量会急剧增加,导致频繁GC影响性能。官方建议:
+
+- 尽量少用ThreadLocal。
+- 不要在虚拟线程的ThreadLocal中放大对象。
+- 使用ScopedLocal替代ThreadLocal。
+
+### 池化思维的误区
+
+虚拟线程占用资源极少,不需要池化。平台线程因为创建成本高需要池化共享,但虚拟线程应该"用时创建,用完即弃":
+
+```java
+// 错误:虚拟线程不需要池化
+ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();
+for(Task task : tasks) {
+ pool.submit(task);
+}
+
+// 正确:直接创建虚拟线程
+for(Task task : tasks) {
+ Thread.startVirtualThread(task);
+}
+```
+
+### 适用场景限定
+
+虚拟线程只适用于IO密集型应用,计算密集型场景发挥不了优势。对于CPU密集计算,虚拟线程在执行期间无法卸载,反而引入调度开销。
+
+## 技术选型决策
+
+基于上述分析,虚拟线程与响应式编程的选型可以遵循以下原则:
+
+### 优先选择虚拟线程的场景
+
+- **传统Web应用或REST API**:基于Spring MVC的应用,只需启用虚拟线程配置(`spring.threads.virtual.enabled=true`),就能获得显著的性能提升。
+- **遗留项目迁移**:虚拟线程与现有阻塞式API(JPA、JDBC、RestTemplate)完全兼容,迁移成本低。
+- **团队技术栈约束**:团队没有响应式编程经验,或者希望保持代码可读性和调试便利性。
+- **中高并发IO密集型场景**:包含大量数据库查询、HTTP调用、文件操作的应用。
+
+### 选择响应式编程的场景
+
+- **流数据处理**:实时数据流、事件流处理,WebFlux的背压机制可以防止生产者压垮消费者。
+- **长连接应用**:WebSocket、Server-Sent Events等需要维持大量长连接的场景,WebFlux的事件循环模型更高效。
+- **端到端非阻塞架构**:系统架构要求全链路非阻塞,从网关到服务到数据库都用响应式技术栈。
+- **全新项目且团队具备响应式经验**:启动全新项目,团队熟悉响应式编程,可以构建完全非阻塞的技术栈。
+
+### 不应选择响应式编程的场景
+
+- **计算密集型应用**:响应式编程无法提升CPU密集型任务性能,反而引入框架开销。
+- **遗留系统改造**:把现有Spring MVC应用改成WebFlux要重写大部分代码,风险不可控。
+- **团队响应式经验不足**:学习曲线陡,容易引入难以排查的并发问题,维护成本高。
+
+## Spring Boot 3.2+的虚拟线程实践
+
+Spring Boot 3.2提供了虚拟线程的原生支持,集成很简单:
+
+### 启用虚拟线程
+
+```properties
+# application.properties
+spring.threads.virtual.enabled=true
+```
+
+这个配置会自动:
+- Tomcat请求处理线程使用虚拟线程。
+- 异步任务执行器使用虚拟线程。
+- ScheduledExecutor使用虚拟线程。
+
+### 手动创建虚拟线程
+
+```java
+// 方式1:Thread API
+Thread vt = Thread.startVirtualThread(() -> {
+ // 业务逻辑
+});
+
+// 方式2:ExecutorService
+try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ executor.submit(() -> {
+ // 业务逻辑
+ return result;
+ });
+}
+
+// 方式3:StructuredTaskScope(Java 21预览特性)
+try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
+ Future user = scope.fork(() -> findUser());
+ Future order = scope.fork(() -> fetchOrder());
+
+ scope.join();
+ scope.throwIfFailed();
+
+ return new Response(user.resultNow(), order.resultNow());
+}
+```
+
+### 与传统代码的兼容性
+
+虚拟线程最大的优势是与现有阻塞式代码完全兼容:
+
+```java
+@RestController
+public class UserController {
+ @Autowired
+ private UserService userService; // 传统阻塞式Service
+
+ @GetMapping("/users/{id}")
+ public User getUser(@PathVariable Long id) {
+ // 在虚拟线程上执行,阻塞不会阻塞载体线程
+ return userService.findUserById(id);
+ }
+}
+```
+
+不需要修改Service层代码,不用引入响应式类型,不用学新API,性能提升直接见效。
+
+## 虚拟线程与响应式编程的本质
+
+从技术本质看,虚拟线程与响应式编程追求的是同一目标:让少量平台线程一直忙,别在IO等待期间闲着。差异在实现层次:
+
+- **响应式编程**:在应用层通过编程范式变革实现,要求开发者显式构建异步管道,使用非阻塞API,思维模式要完全转换。
+- **虚拟线程**:在JVM层通过运行时机制实现,开发者不用改变编程习惯,JVM自动处理阻塞与恢复,底层实现continuation机制。
+
+这就是虚拟线程能替代响应式编程的原因——用更低的学习成本、更少的代码改动、更好的可维护性,实现了相同的性能目标。响应式编程是个"中间产物",存在的价值是填补Java平台缺失轻量级线程的空白。当JVM原生支持虚拟线程后,响应式编程的复杂度成本就变得不可接受了。
+
+当然,响应式编程不会马上消失。WebFlux在流处理、长连接等特定场景还有优势,而且大量现有系统已经采用响应式架构。但对于新项目,尤其是传统Web应用和微服务,虚拟线程是更务实的选择。Tomcat 11.0、Jetty 12.0都已经支持虚拟线程,主流框架的集成让虚拟线程的使用门槛降到很低。
+
+## Java并发编程的未来
+
+虚拟线程的引入,改变了Java并发编程的格局。它不是响应式编程的简单替代,而是Java平台对轻量级并发的原生支持。
+
+响应式编程没有完全失去价值。在流处理、事件驱动架构、全链路非阻塞系统等领域,WebFlux还有其独特优势。但对于绝大多数企业应用,虚拟线程提供了性能与开发效率的最佳平衡点。
+
+技术演进的逻辑是降低复杂度。响应式编程以增加复杂度换取性能,虚拟线程通过底层机制革新,在不增加应用层复杂度的前提下实现性能提升。两个方案性能相当,选择成本更低的那个是自然的技术演进方向。
\ No newline at end of file
diff --git "a/docs/md/\345\205\266\344\273\226/\347\224\250\345\245\275PowerMock\357\274\214\350\275\273\346\235\276\346\220\236\345\256\232\351\202\243\344\272\233\350\256\251\344\275\240\345\244\264\347\226\274\347\232\204\345\215\225\345\205\203\346\265\213\350\257\225.md" "b/docs/md/\345\205\266\344\273\226/\347\224\250\345\245\275PowerMock\357\274\214\350\275\273\346\235\276\346\220\236\345\256\232\351\202\243\344\272\233\350\256\251\344\275\240\345\244\264\347\226\274\347\232\204\345\215\225\345\205\203\346\265\213\350\257\225.md"
new file mode 100644
index 0000000..e405c0d
--- /dev/null
+++ "b/docs/md/\345\205\266\344\273\226/\347\224\250\345\245\275PowerMock\357\274\214\350\275\273\346\235\276\346\220\236\345\256\232\351\202\243\344\272\233\350\256\251\344\275\240\345\244\264\347\226\274\347\232\204\345\215\225\345\205\203\346\265\213\350\257\225.md"
@@ -0,0 +1,451 @@
+> 面对无法Mock的静态方法、私有方法和final类,PowerMock为你打开一扇新的大门
+
+作为一名Java开发者,单元测试是我们保证代码质量的重要环节。但在实际工作中,我们经常会遇到一些难以测试的代码场景:静态工具类、final类、私有方法等。传统的Mockito框架对这些情况束手无策,而PowerMock的出现正好解决了这些痛点。
+
+# PowerMock是什么?为什么需要它?
+
+## PowerMock的核心定位
+
+PowerMock是一个强大的Java单元测试框架,它通过扩展现有的Mock框架(如Mockito和EasyMock),提供了更强大的Mock能力。**PowerMock的核心价值在于它能够Mock那些传统Mock工具无法处理的情况**,包括静态方法、final类和方法、私有方法、构造函数等。
+
+与普通Mock框架不同,PowerMock使用自定义的类加载器和字节码操作技术(基于Javassist和ASM库),在运行时修改类的行为,从而实现对这些"难以Mock"的场景的完全控制。
+
+## PowerMock与Mockito的关系和区别
+
+虽然PowerMock和Mockito都是用于单元测试的Mock框架,但它们在功能和定位上有着明显的区别:
+
+**Mockito**是一个轻量级、简单易用的Mock框架,适用于大多数日常测试场景。但它有明显的局限性:无法Mock静态方法、final类、私有方法和构造函数等。
+
+**PowerMock**则是对Mockito的增强,填补了Mockito的功能空白。它不是替代Mockito,而是与Mockito协同工作,共同构建完整的单元测试解决方案。
+
+两者核心区别体现在底层实现上:Mockito使用动态代理(CGLIB)技术,而PowerMock通过修改字节码来实现更强大的Mock能力。
+
+正因为这种根本差异,PowerMock可以解决Mockito无法解决的问题。
+
+## PowerMock解决的痛点
+
+在日常开发中,我们经常会遇到以下测试难题:
+
+- **静态工具类**:如各种Util类中的静态方法。
+- **final类和final方法**:特别是第三方库中的final类。
+- **私有方法**:需要直接测试的私有方法逻辑。
+- **构造函数依赖**:方法内部通过new创建的对象。
+- **静态代码块和系统类**:如System.currentTimeMillis()。
+
+这些问题使用传统Mock框架难以解决,而PowerMock为此提供了完整的解决方案
+
+# 环境配置与基本用法
+
+## 添加Maven依赖
+
+要开始使用PowerMock,首先需要在项目中添加相关依赖。由于PowerMock需要与Mockito协同工作,需要同时添加两个依赖:
+
+```xml
+
+
+ org.powermock
+ powermock-module-junit4
+ 2.0.9
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ 2.0.9
+ test
+
+```
+
+**版本兼容性注意**:确保PowerMock与Mockito/JUnit版本匹配,具体兼容性关系可参考官方文档。
+
+## 基本配置注解
+
+使用PowerMock需要在测试类上添加必要的注解:
+
+```java
+@RunWith(PowerMockRunner.class) // 必须使用PowerMockRunner
+@PrepareForTest({StaticUtils.class, User.class}) // 声明需增强的类
+@PowerMockIgnore("javax.management.*") // 解决类加载器冲突
+public class UserServiceTest {
+ // 测试内容
+}
+```
+
+- `@RunWith(PowerMockRunner.class)`:告诉JUnit使用PowerMock的测试运行器。
+- `@PrepareForTest`:指定需要被PowerMock修改的类(包含静态方法、final方法等的类)。
+- `@PowerMockIgnore`:解决使用PowerMock后可能出现的类加载器冲突问题。
+
+# PowerMock核心使用场景详解
+
+## 静态方法Mock
+
+静态方法是最常见的测试难点之一,让我们看看PowerMock如何解决这个问题。
+
+**场景示例**:假设我们有一个静态工具类,用于生成唯一ID:
+
+```java
+public class IdGenerator {
+ public static String generateUniqueId() {
+ // 实际业务中可能包含复杂的逻辑或外部依赖
+ return UUID.randomUUID().toString();
+ }
+}
+
+public class OrderService {
+ public String createOrder() {
+ String orderId = IdGenerator.generateUniqueId();
+ // 创建订单的逻辑
+ return "ORDER_" + orderId;
+ }
+}
+```
+
+**测试代码**:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({IdGenerator.class, OrderService.class})
+public class OrderServiceTest {
+
+ @Test
+ public void testCreateOrderWithStaticMock() {
+ // 1. 准备静态类的Mock
+ PowerMockito.mockStatic(IdGenerator.class);
+
+ // 2. 预设静态方法行为
+ PowerMockito.when(IdGenerator.generateUniqueId()).thenReturn("123e4567");
+
+ // 3. 创建被测试对象并调用被测方法
+ OrderService orderService = new OrderService();
+ String result = orderService.createOrder();
+
+ // 4. 验证结果
+ assertEquals("ORDER_123e4567", result);
+
+ // 5. 验证静态方法调用(必须调用)
+ PowerMockito.verifyStatic(IdGenerator.class);
+ IdGenerator.generateUniqueId();
+ }
+}
+```
+
+**关键点说明**:
+
+- `mockStatic()`方法用于告诉PowerMock要Mock哪个类的静态方法
+- 静态方法的Stubbing(定义行为)与普通Mockito语法类似
+- **必须调用**`verifyStatic()`来验证静态方法的调用,且需要在验证前调用一次
+
+**常见坑点**:忘记调用`verifyStatic()`会导致无法验证静态方法是否被正确调用。
+
+## 私有方法Mock
+
+测试私有方法一直存在争议,但在某些场景下(如复杂算法验证)确实有必要直接测试私有方法。
+
+**场景示例**:一个包含复杂校验逻辑的UserService:
+
+```java
+public class UserService {
+ public boolean validateUser(String username, String password) {
+ if (!isValidFormat(username) || !isValidFormat(password)) {
+ return false;
+ }
+ return internalComplexValidation(username, password);
+ }
+
+ private boolean isValidFormat(String input) {
+ // 复杂的格式校验逻辑
+ return input != null && input.length() >= 5;
+ }
+
+ private boolean internalComplexValidation(String username, String password) {
+ // 非常复杂的内部校验逻辑
+ // 可能涉及加密、数据库查询等
+ return true; // 简化示例
+ }
+}
+```
+
+**测试代码**:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(UserService.class)
+public class UserServiceTest {
+
+ @Test
+ public void testPrivateMethod() throws Exception {
+ // 1. 创建被测类的Spy对象(部分真实调用)
+ UserService userService = new UserService();
+ UserService spyService = PowerMockito.spy(userService);
+
+ // 2. Stubbing:预设私有方法行为
+ PowerMockito.doReturn(true).when(spyService, "isValidFormat", Mockito.anyString());
+
+ // 3. 调用被测方法
+ boolean result = spyService.validateUser("testuser", "testpass");
+
+ // 4. 验证结果
+ assertTrue(result);
+
+ // 5. 验证私有方法被调用(可选)
+ PowerMockito.verifyPrivate(spyService,Mockito.times(2))
+ .invoke("isValidFormat", Mockito.anyString());
+ }
+
+ @Test
+ public void testPrivateMethodWithArguments() throws Exception {
+ UserService userService = new UserService();
+ UserService spyService = PowerMockito.spy(userService);
+
+ // Mock有参数的私有方法
+ PowerMockito.doReturn(false)
+ .when(spyService, "internalComplexValidation", "user", "pass");
+
+ boolean result = spyService.validateUser("user", "pass");
+
+ assertFalse(result);
+ }
+}
+```
+
+**关键点说明**:
+
+- 使用`spy()`方法创建对象,这样未被Mock的方法会保持真实行为。
+- 使用`doReturn().when()`语法来Mock私有方法,需通过方法名字符串指定目标方法。
+- 可以通过`verifyPrivate()`验证私有方法的调用。
+
+**最佳实践**:优先通过公共方法测试私有逻辑,仅在复杂算法验证等特殊场景下直接测试私有方法。
+
+## final类与方法Mock
+
+final类和方法由于其不可继承性,在传统Mock框架中无法被Mock,但PowerMock完美解决了这个问题。
+
+**场景示例**:
+
+```java
+public final class FinalUtility {
+ public final String finalMethod() {
+ return "Final implementation";
+ }
+
+ public static final String staticFinalMethod() {
+ return "Static final implementation";
+ }
+}
+
+public class SomeService {
+ private FinalUtility utility = new FinalUtility();
+
+ public String useFinalClass() {
+ return utility.finalMethod() + "_processed";
+ }
+}
+```
+
+**测试代码**:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({FinalUtility.class, SomeService.class})
+public class SomeServiceTest {
+
+ @Test
+ public void testFinalClassAndMethod() {
+ // 1. 创建final类的Mock对象
+ FinalUtility mockUtility = PowerMockito.mock(FinalUtility.class);
+
+ // 2. 预设final方法行为
+ PowerMockito.when(mockUtility.finalMethod()).thenReturn("Mocked final");
+
+ // 3. 当创建真实对象时返回Mock对象
+ PowerMockito.whenNew(FinalUtility.class).withNoArguments().thenReturn(mockUtility);
+
+ // 4. 测试
+ SomeService service = new SomeService();
+ String result = service.useFinalClass();
+
+ assertEquals("Mocked final_processed", result);
+ }
+
+ @Test
+ public void testStaticFinalMethod() {
+ // Mock静态final方法
+ PowerMockito.mockStatic(FinalUtility.class);
+ PowerMockito.when(FinalUtility.staticFinalMethod()).thenReturn("Mocked static final");
+
+ assertEquals("Mocked static final", FinalUtility.staticFinalMethod());
+ }
+}
+```
+
+**底层原理**:PowerMock通过修改字节码,去除了final方法的final标识符,从而允许Mock操作。
+
+## 构造函数Mock
+
+当方法内部直接通过new创建对象时,传统Mock难以介入,PowerMock的构造函数Mock功能为此提供了解决方案。
+
+**场景示例**:
+
+```java
+public class DatabaseConnection {
+ private String connectionString;
+
+ public DatabaseConnection(String connectionString) {
+ this.connectionString = connectionString;
+ // 可能包含复杂的初始化逻辑
+ }
+
+ public boolean execute(String sql) {
+ // 执行SQL逻辑
+ return true;
+ }
+}
+
+public class UserRepository {
+ public boolean saveUser(String username) {
+ // 在方法内部直接创建依赖对象
+ DatabaseConnection connection = new DatabaseConnection("jdbc:mysql://localhost:3306/test");
+ return connection.execute("INSERT INTO users VALUES ('" + username + "')");
+ }
+}
+```
+
+**测试代码**:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(UserRepository.class)
+public class UserRepositoryTest {
+
+ @Test
+ public void testConstructorMock() throws Exception {
+ // 1. 创建Mock对象
+ DatabaseConnection mockConnection = PowerMockito.mock(DatabaseConnection.class);
+
+ // 2. 预设构造函数行为
+ PowerMockito.whenNew(DatabaseConnection.class)
+ .withParameterTypes(String.class)
+ .withArguments("jdbc:mysql://localhost:3306/test")
+ .thenReturn(mockConnection);
+
+ // 3. 预设方法行为
+ PowerMockito.when(mockConnection.execute(Mockito.anyString())).thenReturn(true);
+
+ // 4. 执行测试
+ UserRepository repository = new UserRepository();
+ boolean result = repository.saveUser("testuser");
+
+ // 5. 验证
+ assertTrue(result);
+ PowerMockito.verifyNew(DatabaseConnection.class)
+ .withArguments("jdbc:mysql://localhost:3306/test");
+ }
+}
+```
+
+**关键点说明**:
+
+- `whenNew()`用于拦截构造函数调用。
+- `withParameterTypes()`和`withArguments()`用于精确匹配构造函数。
+- 需要使用`verifyNew()`验证构造函数调用。
+
+**应用场景**:适用于测试遗留代码中在方法内部直接实例化依赖对象的情况。
+
+## 静态代码块处理
+
+静态代码块在类加载时执行,可能包含不愿在测试中运行的代码(如初始化昂贵资源),PowerMock可以抑制静态代码块的执行。
+
+**示例**:
+
+```java
+public class ConfigurationLoader {
+ static {
+ // 静态代码块,可能包含昂贵的初始化操作
+ loadConfigurationFromRemote();
+ }
+
+ private static void loadConfigurationFromRemote() {
+ // 模拟昂贵的初始化
+ throw new RuntimeException("不应该在测试中执行");
+ }
+
+ public static String getConfig(String key) {
+ return "value";
+ }
+}
+```
+
+**测试代码**:
+
+```java
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(ConfigurationLoader.class)
+public class ConfigurationLoaderTest {
+
+ @Test
+ public void testSuppressStaticInitializer() throws Exception {
+ // 抑制静态代码块执行
+ PowerMockito.suppress(PowerMockito.method(ConfigurationLoader.class, "loadConfigurationFromRemote"));
+
+ // 现在可以安全测试,静态代码块不会执行
+ assertNotNull(ConfigurationLoader.getConfig("testkey"));
+ }
+}
+```
+
+# PowerMock最佳实践与注意事项
+
+## 谨慎使用PowerMock
+
+虽然PowerMock功能强大,但过度使用可能是代码设计问题的信号。**以下是一些使用原则**:
+
+- **优先考虑重构**:如果代码中大量使用PowerMock,应该考虑重构代码以提高可测试性。例如,将静态方法改为实例方法,通过依赖注入解耦等。
+- **仅用于遗留代码**:在新项目中,优先通过良好设计避免使用PowerMock,仅在处理难以修改的遗留代码时大量使用。
+- **隔离使用**:将使用PowerMock的测试类单独放置,防止影响其他测试的执行效率。
+
+## 性能优化建议
+
+PowerMock由于使用自定义类加载器和字节码操作,会对测试执行时间产生显著影响。以下是一些优化建议:
+
+- **最小化@PrepareForTest**:只将确实需要Mock的类放入注解中,减少字节码操作的范围。
+- **合理使用Mockito**:对于常规Mock场景,仍然使用Mockito,仅在必要时使用PowerMock。
+- **避免过度Mock**:不要Mock系统类或简单值对象,这会给测试带来不必要的复杂性。
+
+## 版本选择与兼容性
+
+**版本兼容性**:PowerMock与Mockito、JUnit的版本兼容性非常重要。以下是推荐组合:
+
+- PowerMock 2.x + Mockito 2.x + JUnit 4.12+
+- 避免混合使用不兼容的版本
+
+**JUnit 5支持**:截至目前,PowerMock不支持JUnit 5,这是选择测试框架时需要考虑的因素。
+
+## 常见问题排查
+
+**类加载器冲突**:使用`@PowerMockIgnore`注解排除冲突的包。
+
+```java
+@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})
+```
+
+**版本冲突**:确保所有Mock相关库的版本兼容。
+
+**静态方法验证失败**:记住每次验证静态方法调用时都要先调用`verifyStatic()`。
+
+# 总结
+
+PowerMock解决了传统Mock框架无法处理的棘手问题。通过字节码操作技术,PowerMock能够Mock静态方法、final类、私有方法和构造函数等"不可Mock"的元素。
+
+**核心价值**:
+
+- 填补了Mockito的功能空白,完善了Java单元测试的工具链。
+- 特别适用于处理遗留代码和第三方库的测试问题。
+- 通过提高代码覆盖率来提升软件质量。
+
+**适用边界**:
+
+- 不是所有场景都适合使用PowerMock,新项目应优先考虑良好的代码设计。
+- 在测试性能和代码可维护性之间需要权衡。
+- 建议将使用范围控制在确实必要的复杂场景中。
+
+希望本文能帮助你在实际项目中更好地使用PowerMock。如果你有任何问题或经验分享,欢迎在评论区留言交流!
\ No newline at end of file
diff --git "a/docs/md/\346\241\206\346\236\266/\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\344\270\215\345\217\252\346\234\211\346\246\202\345\277\265\357\274\201\344\270\207\345\255\227\351\225\277\346\226\207 + \344\273\243\347\240\201\347\244\272\344\276\213\357\274\214\346\211\213\346\212\212\346\211\213\345\270\246\344\275\240\347\216\251\350\275\254 RxJava.md" "b/docs/md/\346\241\206\346\236\266/\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\344\270\215\345\217\252\346\234\211\346\246\202\345\277\265\357\274\201\344\270\207\345\255\227\351\225\277\346\226\207 + \344\273\243\347\240\201\347\244\272\344\276\213\357\274\214\346\211\213\346\212\212\346\211\213\345\270\246\344\275\240\347\216\251\350\275\254 RxJava.md"
new file mode 100644
index 0000000..465bc2f
--- /dev/null
+++ "b/docs/md/\346\241\206\346\236\266/\345\223\215\345\272\224\345\274\217\347\274\226\347\250\213\344\270\215\345\217\252\346\234\211\346\246\202\345\277\265\357\274\201\344\270\207\345\255\227\351\225\277\346\226\207 + \344\273\243\347\240\201\347\244\272\344\276\213\357\274\214\346\211\213\346\212\212\346\211\213\345\270\246\344\275\240\347\216\251\350\275\254 RxJava.md"
@@ -0,0 +1,1707 @@
+# Reactive Streams 介绍
+
+在聊 Reactive Streams 之前,先了解一下 Reactive Programming(反应式/响应式编程)。为了解决异步编程中出现的各种问题,程序员们提出了各种的思路去解决这些问题,这些解决问题的方式、方法,手段就可以叫做 Reactive Programming。
+
+Reactive Programming 是一种编程思想,类似面向对象,函数式编程。
+
+本质上是对数据流或某种变化做出的反应,这个变化什么时候触发是未知的,所以他是一种基于异步、回调的方式在处理问题。
+
+当越来越多的程序员,开始使用这种编程思想时,需要一些大佬来统一一个思想规范。所以国外的几个大佬公司启动了 Reactive Streams 项目。Netflix、Pivotal、Lightbend 联合来为异步流处理提供标准,规范。
+
+Reactive Streams 翻译过来就是响应式/反应式流。**其实是一种基于异步流处理的标准化规范,目的是在使用流处理时更加可靠,高效和响应式。**
+
+# Java 层面的 Reactive Streams
+
+基于这个规范的实现很多,比如三方库中比较出名的 RxJava,Reactor 等等。
+
+但是 JDK8 版本中,Java 已经有了 CompletableFuture 的支撑,我们可以将大量的异步任务做好编排。但是在 JDK8 版本中的 CompletableFuture 依然有很多特性无法支撑。所以在 JDK9,CompletableFuture 做了很多的更新,比如支持延迟,超时,子类化之类的功能。
+
+这时,咱们会发现,其实 CompletableFuture 已经可以去支撑做一些异步编程的操作了。但是为什么很多大公司依然还是使用 RxJava,Reactor 这种三方依赖库呢?
+
+问题在于,大多数的时候,咱们采用异步编程处理的任务并不是非常复杂的。这个时候,咱们确实不需要去使用 Reactive Streams 反应流的框架。如果系统越来越复杂,或者你处理的业务本身就是及其复杂的那种,你就要去写一个让人头皮发麻的代码了。随着时间的推移,这种代码会变成非常难以维护。
+
+其次 CompletableFuture 并不是真正的基于 Reactive Streams 去实现。CompletableFuture 描述的是单次执行的结果。尽管可以通过各种方法将异步任务之间构建成一串任务组成的流程图,本质上依然是单次的结果。
+
+反应式流,面向的是 Stream。 咱们 Java 中的 Stream API 更类似 Reactive Streams 的思想。Stream API 是同步阻塞的。
+
+最经典的就是 CompletableFuture 无法处理 Reactive Streams 中的一个核心概念,Back Pressure(背压,反压,回压),比如在上下游承载能力不同时,比如下游玩不转了,需要告知上游采取一些策略去解决。CompletableFuture 明显无法处理这种。
+
+其次还有 Java 中提供的回调,Future 机制在实现响应式编程中,问题和缺点都比较难处理。有个比较出名的概念叫做 Callback Hell(回调地狱)。简单来说就是回调里面套回调,虽然将子过程做到解耦,但是随着业务的负责,回调代码的可读性、复杂性就大大的增加,这个就是回调地狱。
+
+所以,咱们需要一套框架或者说类库来实现真正响应式流,大概需要几个特性:
+
+* 支持将异步任务做封装以及组装,需要 API 对异步任务进行包装,并且需要很多子任务来对异步操作进行链式组装,过程中包括过滤,异常处理,超时等等操作。
+* 减少异步任务的嵌套,减少代码的复杂性,增加可读性,避免 Callback Hell 这种及其复杂恶心的代码。
+* 支持背压 Back Pressure,也就需要有上游和下游的概念,可以做到协商处理数据流的速度。
+
+# Java 层面 Reactive Steams 的 API
+
+首先 Reactive Steams 响应流实现方式其实是基于观察者模式的扩展,同时也能看到发布订阅模式,责任链模式等等。
+
+整个 Reactive Steams 流程大致如下。
+
+
+
+直接在 JDK9 版本之上查看 Doug Lee 提供的 Flow 类。
+
+在 Flow 类中,提供了核心的四个接口:Publisher,Subscriber,Subscription,Processor
+
+
+
+Publisher:Publisher 是函数式接口,负责发布数据的。 Publisher 内部有一个方法 subscribe 方法,去和具体的订阅者绑定关系。
+
+
+
+Subscriber:Subscriber 是订阅者,负责订阅,消费数据。四个方法:
+
+- onSubscribe:订阅成功后触发,并且表明可以开始接收发布者的数据元素了。
+- onNext:每次获取到发布者的数据元素都会执行 onNext。
+- onError:接收数据元素时,出现异常等问题时,走 onError。
+- onComplete:当指定接收的元素个数搞定后,触发 onComplete。
+
+
+
+Subscription:发布者和订阅者是基于 Subscription 关联的。当建立了订阅的关系后,发布者会将 Subscription 传递给订阅者。订阅者指定获取元素的数量和取消订阅操作,都要基于 Subscription 去操作。提供了两个方法:
+
+- request:订阅者要获取的元素个数。
+- cancel:取消订阅,当前的订阅者不接收当前发布者的元素。
+
+
+
+Processor:Processor 继承了 Publisher 和 Subscriber,即是发布者也是订阅者。Processor 一般作为数据的中转,订阅者处理完数据元素,可以再次发给下一个订阅者。
+
+
+
+这四个接口很重要,是 Reactive Streams 的规范,但是可以明显的看到,内部没有具体的内容实现。
+
+这里就类似 JDBC 这种规范,规范在 JDK9 中提出来了,想实现,可以基于当前的这四个接口再做具体的逻辑处理以及实现的细节。
+
+# Java 层面 Reactive Steams 基本操作
+
+咱们测试 Java 中的 Flow 里提供的 API 时,就是走最基本的操作。
+
+其中 Processor 不需要重写,玩最基本的操作,不去做订阅者和发布者的转换。
+
+其次 Subscription 也不需要重写,这东西就是提供了订阅者指定订阅的消息个数,以及取消的操作。
+
+然后 Publisher 需要重写,但是 JDK 中已经提供了一个 Publisher 的实现,SubmissionPublisher,可以直接使用。
+
+最后,Subscriber 需要咱们自己重写,指定好订阅消息的个数,已经消费的一些逻辑
+
+```java
+import java.util.concurrent.Flow;
+
+
+public class MySubscriber implements Flow.Subscriber {
+
+ @Override
+ // 绑定好订阅关系后,就会触发这个方法
+ public void onSubscribe(Flow.Subscription subscription) {
+ subscription.request(10);
+ }
+
+ @Override
+ public void onNext(Integer item) {
+ System.out.println(Thread.currentThread().getName() + ":接收到数据流:" + item);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ System.out.println(Thread.currentThread().getName() + ":接收消息出现异常:" + throwable.getMessage());
+ }
+
+ @Override
+ public void onComplete() {
+ System.out.println(Thread.currentThread().getName() + ":当前订阅者要求接收的消息全部处理完毕。");
+ }
+}
+```
+
+直接使用 SubmissionPublisher 测试整体效果
+
+```java
+public static void main(String[] args) {
+ // 只有一个工作线程的线程池
+ ExecutorService executor = Executors.newFixedThreadPool(1);
+ // 指定缓冲区的大小
+ int maxBufferCapacity = 5;
+
+ // 需要指定两个参数
+ // 第一个参数需要传递一个线程池,指定订阅者使用的线程
+ // 第二个参数,需要指定一个缓冲区,发布者发布消息后,消息会扔到缓冲区里。
+ SubmissionPublisher publisher = new SubmissionPublisher<>(executor,maxBufferCapacity);
+
+ // 绑定订阅者
+ MySubscriber subscriber = new MySubscriber();
+ publisher.subscribe(subscriber);
+
+ // 发布消息
+ for (int i = 0; i < 10; i++) {
+ System.out.println(Thread.currentThread().getName() + ":发布消息:" + i);
+ publisher.submit(i);
+ }
+
+ // 释放资源
+ publisher.close();
+ executor.shutdown();
+}
+```
+
+结果输出如下:
+
+```
+main:发布消息:0
+main:发布消息:1
+main:发布消息:2
+main:发布消息:3
+main:发布消息:4
+main:发布消息:5
+main:发布消息:6
+main:发布消息:7
+main:发布消息:8
+main:发布消息:9
+pool-1-thread-1:接收到数据流:0
+pool-1-thread-1:接收到数据流:1
+pool-1-thread-1:接收到数据流:2
+pool-1-thread-1:接收到数据流:3
+pool-1-thread-1:接收到数据流:4
+pool-1-thread-1:接收到数据流:5
+pool-1-thread-1:接收到数据流:6
+pool-1-thread-1:接收到数据流:7
+pool-1-thread-1:接收到数据流:8
+pool-1-thread-1:接收到数据流:9
+pool-1-thread-1:当前订阅者要求接收的消息全部处理完毕。
+```
+
+- **缓冲区:** 缓冲区就是发布者和订阅者之间的一块内存,类似线程池中的阻塞队列,可以将消息扔到这个缓存区里。其次咱们设置的缓冲区大小是 5,但是发现 get 出来的时候,5 被替换为了 8。这是因为 SubmissionPublisher 为了更有效的使用内存,默认会基于 roundCapacity 方法将传递的缓冲区大小替换为 2 的 n 次幂。
+- **背压效果:** 当订阅者指定的消息已经全部处理完毕后,发布者最多只能发布缓冲区大小个数的消息,剩下的内容会基于背压的效果直接暂时不发送。
+- **onComplete:** 需要发布者做了close 操作,确认了发布者已经将消息全部发送,并且订阅者也已经将全部的消息处理完毕后,才会触发 onComplete。
+- **Subscription:** 订阅者可以在 onNext 或者其他方法中动态的使用 subscription 去指定后续需要几个消息订阅,以及是否需要取消订阅消息等操作。
+
+# Reactive Steams 落地体验
+
+## 回调地狱问题
+
+前面的方式大致了解了 JDK9 中更新的 Reactive Streams 的规范,咱们实现也仅仅是看到了发布订阅和回压的效果。并没有看到如何解决回调地狱的问题。咱们可以通过 Spring5 官网提供的一个例子,来体验一下 CallBack Hell 回调地狱带来的问题。后面再根据三方的实现来看一下基于 Reactive Streams 实现后效果如何。这里基本是根据伪代码走的。
+
+例子:在用户的 UI 页面上,展示当前用户最喜欢的 Top5 的商品详情。这里会根据用户的 ID 去查询当前用户 Top5 商品的ID,如果 ID 可以查询到之后再根据商品的 ID 去查询商品的详情。如果当前用户 ID 查询的结果不存在喜欢的 Top 商品,没有的话,通过推荐服务查询 Top5 的商品信息。展示给用户。
+
+当前例子需要三个服务的支撑:
+
+* 根据用户 ID 查询用户的 Top5 商品ID。
+* 根据 Top5 商品ID查询商品详情。
+* 调用推荐服务,获取5个商品详情。
+
+基于 Java 最原生的异步编程方式,实现上述操作,来看看到底什么是回调地狱。。。
+
+商品详情实体类:
+
+```java
+@Data
+public class Fav {
+
+ private String itemId;
+
+ private String itemName;
+
+ private String itemDetail;
+
+}
+```
+
+准备回调方法,拿到结果后触发
+
+```java
+public interface Callback {
+
+ void onSuccess(T t);
+
+ void onError(Throwable throwable);
+
+}
+```
+
+准备了访问三个服务的 Service 接口
+
+```java
+public interface UserService {
+
+ /**
+ * 根据用户Id查询用户的Top5商品Id
+ * @param userId
+ * @param list
+ */
+ void getFav(String userId, Callback> list);
+
+}
+
+public interface ItemService {
+
+ /**
+ * 根据商品Id查询商品的详情
+ *
+ * @param itemId
+ * @param callback
+ */
+ void getDetail(String itemId, Callback callback);
+}
+
+public interface SuggestionService {
+
+ /**
+ * 调用推荐服务,获取推荐商品
+ * @param favs
+ */
+ void getSuggestion(Callback> favs);
+}
+```
+
+准备了响应数据的 UI 线程工具以及响应方法
+
+```java
+public class UiUtils {
+
+ public static void submitOnUiThread(Runnable runnable){
+ // 线程池中的线程做响应的操作………………
+ }
+
+
+ public static void show(Object obj){
+ // 利用UI线程展示具体数据
+ }
+
+ public static void error(Object obj){
+ // 出现错误响应的内容
+ }
+
+}
+```
+
+完成了 Controller 中的异步编程效果
+
+```java
+@RestController
+public class CallBackHellController {
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private ItemService itemService;
+
+ @Autowired
+ private SuggestionService suggestionService;
+
+
+ @GetMapping("/callbackhell")
+ public void callbackHell(String userId){
+ //1、调用用户服务,查询Top5商品Id
+ userService.getFav(userId, new Callback>() {
+ @Override
+ public void onSuccess(List list) {
+ // 已经查询到商品Id,但是不知道是否有值
+ if (list.isEmpty()){
+ // 3、用户没有Top5商品Id,通过推荐服务查询推荐商品详情
+ suggestionService.getSuggestion(new Callback>(){
+ @Override
+ public void onSuccess(List favs) {
+ // 推荐服务查询到了商品详情,响应即可
+ UiUtils.submitOnUiThread(() -> {
+ favs.stream().limit(5).forEach(UiUtils::show);
+ });
+ }
+ @Override
+ public void onError(Throwable throwable) {
+ UiUtils.error(throwable);
+ }
+ });
+
+ }
+ else{
+ // 2、通过用户查询到了Top5商品Id,通过商品Id查询商品详情
+ list.stream().limit(5).forEach(itemId -> itemService.getDetail(itemId,new Callback(){
+
+ @Override
+ public void onSuccess(Fav fav) {
+ // 查询到了商品详情,利用UI线程,给客户端响应数据
+ UiUtils.submitOnUiThread(() -> UiUtils.show(fav));
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ // 出现异常了。
+ UiUtils.error(throwable);
+ }
+ }));
+ }
+ }
+ @Override
+ public void onError(Throwable throwable) {
+ // 出现异常了。
+ UiUtils.error(throwable);
+ }
+ });
+
+
+ }
+
+}
+```
+
+## 解决回调地狱问题
+
+这里为了解决回调地狱问题,需要一个 Reactor 的依赖来帮助咱们实现异步编程。
+
+需要导入依赖
+
+```xml
+
+
+ io.projectreactor
+ reactor-core
+ 3.7.7
+
+```
+
+不能再使用之前的 Callback 方式了。需要使用 reactor 提供的 Flux,并且这种链式操作会更直观,也更好维护。就只需要修改三个服务对应的 Service。
+
+```java
+public interface UserService {
+
+ /**
+ * 根据用户ID查询Top5商品ID
+ * @param userId
+ * @return
+ */
+ Flux> getFav(String userId);
+
+}
+
+public interface ItemService {
+
+
+ /**
+ * 根据商品ID查询商品详情
+ * @param itemId
+ * @return
+ */
+ Flux getDetail(String itemId);
+
+}
+
+public interface SuggestionService {
+
+ /**
+ * 获取推荐的商品详情
+ * @return
+ */
+ Flux> getSuggestion();
+
+}
+```
+
+然后就可以利用 Flux 提供的 API 来解决之前回调地狱的问题。
+
+```java
+@RestController
+public class ReactorCallbackController {
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private ItemService itemService;
+
+ @Autowired
+ private SuggestionService suggestionService;
+
+
+ @GetMapping("reactorcallback")
+ public void reactorCallback(String userId){
+ userService
+ .getFav(userId) // 根据用户Id查询Top5商品Id
+ .flatMap(itemService::getDetail) // 根据商品ID查询商品详情
+ .switchIfEmpty(suggestionService.getSuggestion()) // 如果前面为null,这里通过推荐服务查询商品详情
+ .take(5) // 获取前5个数据
+ .publishOn(UiUtils.reactorOnUiThread()) // 使用Ui线程
+ .subscribe(UiUtils::show,UiUtils::error); // 成功走show,失败走error
+ }
+
+}
+```
+
+## CompletableFuture 的异步编程
+
+Future 的形式相比 Callback Hell 效果要好一些,虽然 JDK8 和 9 都对 CompletableFuture 做了各种优化,但是他的表现还是不太好。多个Future在嵌套时,可读性还是比较差的。并且 CompletableFuture 不存在什么回压,或者是延迟调用的功能。
+
+现在借助 CompletableFuture 来实现一个场景。
+
+1. 获取一个用户ID的列表。
+2. 通过用户ID分别获取他的名字以及统计信息。(希望这两个操作是并行执行的)
+3. 当两个信息都获取到之后,封装成一个普通字符串即可。
+4. 响应数据,最后拿到结果(输出一下)。
+
+实现代码
+
+```java
+public class GetNameAndStatTestByCF {
+
+ public static void main(String[] args) {
+ // 1、获取一组用户ID列表
+ CompletableFuture> idList = getID();
+ CompletableFuture> dataCompletableFuture = idList.thenComposeAsync(ids -> {
+ Stream> resultStream = ids.stream().map(id -> {
+ // 2、并行基于ID查询名称信息
+ CompletableFuture nameTask = getName();
+ // 2、并行基于ID查询统计信息
+ CompletableFuture statTask = getStat();
+ // 让两个查询名称信息和查询统计信息操作并行执行
+ return nameTask.thenCombineAsync(statTask, (name, stat) -> {
+ // 3、拿到信息组装
+ return "Name:" + name + ",Stat:" + stat;
+ });
+ });
+ // 将resultStream转换成一个数组
+ List> resultList = resultStream.toList();
+ // 全部的任务封装起来
+ CompletableFuture allDone = CompletableFuture.allOf(resultList.toArray(new CompletableFuture[]{}));
+ // 执行全部任务
+ return allDone.thenApplyAsync(v -> resultList.stream()
+ .map(CompletableFuture::join)
+ .collect(Collectors.toList()));
+ });
+
+ // 4、获取全部的组件信息后响应客户端(输出)
+ List data = dataCompletableFuture.join();
+ System.out.println(data);
+ }
+
+ // 模拟zz服务获取统计信息
+ private static CompletableFuture getStat() {
+ return CompletableFuture.supplyAsync(() -> 666);
+ }
+
+ // 模拟yy服务获取名称信息
+ private static CompletableFuture getName() {
+ return CompletableFuture.supplyAsync(() -> "张三");
+ }
+
+ // 模拟xx服务,获取一组用户ID
+ private static CompletableFuture> getID() {
+ return CompletableFuture.supplyAsync(() -> {
+ // 模拟查询三方服务
+ List list = new ArrayList<>();
+ list.add("1");
+ list.add("2");
+ list.add("3");
+ return list;
+ });
+ }
+
+}
+```
+
+## 解决 CompletableFuture 的问题
+
+CompletableFuture 可以实现一些简单的异步编程,但是可看性和维护性以后后期的扩展都需要对整体代码做比较大成本的维护。依然采用 Reactor 来实现一个一模一样的逻辑,再看代码效果。
+
+```java
+public class GetNameAndStatByReactor {
+
+ public static void main(String[] args) {
+ // 1、获取一组用户ID列表
+ Flux idFlux = getId();
+
+ Flux result = idFlux.flatMap(id -> {
+ // 2、并行基于ID查询名称信息
+ Flux nameFlux = getName(id);
+ // 2、并行基于ID查询统计信息
+ Flux statFlux = getStat(id);
+ // 俩任务并行处理完毕,触发3
+ return nameFlux.zipWith(statFlux, (name, stat) -> {
+ // 3、拿到信息组装
+ return "Name:" + name + ",Stat:" + stat;
+ });
+ });
+ Mono> listMono = result.collectList();
+ List info = listMono.block();
+ // 4、获取全部的组件信息后响应客户端(输出)
+ System.out.println(info);
+ }
+
+ private static Flux getStat(String id) {
+ // 会查询三方服务,然后封装结果
+ return Flux.just(888);
+ }
+
+ private static Flux getName(String id) {
+ // 会查询三方服务,然后封装结果
+ return Flux.just("张三");
+ }
+
+
+ private static Flux getId() {
+ // 会查询三方服务,然后封装结果
+ return Flux.just("1","2","3");
+ }
+}
+```
+
+# RxJava2 实现异步编程
+
+RxJava 是一个小框架,或者是依赖库。在 RxJava 的1.x版本中,它并不基于 Reactive Streams 去实现的。没有关系,因为 RxJava 的 2 版本,就是基于Reactive Streams 实现的了。
+
+使用 RxJava 巨简单,因为作者想将 RxJava 尽量做到轻量级,就一个依赖。
+
+```xml
+
+
+ io.reactivex.rxjava2
+ rxjava
+ 2.2.21
+
+```
+
+## RxJava2 的入门操作
+
+获取一个 Person 对象的集合,将 Person 集合中的所有年龄大于10岁的 Person 对象筛选出来,并输出他的名字。
+
+采用 RxJava 来实现一下:
+
+```java
+public class Demo {
+
+ public static void main(String[] args) {
+ //1、获取Person对象集合
+ List personList = getPersonList();
+
+ //2、完成上面要求的操作
+ //2.1、将person集合转换为RxJava的流
+ Flowable.fromArray(personList.toArray(new Person[]{}))
+ //2.2 过滤年龄大于10岁的
+ .filter(person -> person.getAge() > 10)
+ //2.3 获取筛选后的Person名称
+ .map(person -> person.getName())
+ //2.4 输出Name
+ .subscribe(System.out::println);
+ }
+
+ private static List getPersonList() {
+ List personList = new ArrayList<>();
+ personList.add(new Person("大娃",5));
+ personList.add(new Person("二娃",7));
+ personList.add(new Person("三娃",9));
+ personList.add(new Person("四娃",11));
+ personList.add(new Person("五娃",13));
+ return personList;
+ }
+}
+```
+
+## RxJava2 的基础处理流程
+
+在 RxJava 中有三个核心的角色
+
+* 被观察者(Observable)
+* 观察者(Observer)
+* 订阅(Subscribe)
+
+```java
+public class Demo2 {
+
+ public static void main(String[] args) {
+ //1、构建Observable
+ Observable observable = Observable.create(emitter -> {
+ emitter.onNext("Hello");
+ emitter.onNext("World");
+ emitter.onComplete();
+ });
+
+ //2、构建Observer
+ Observer observer = new Observer<>() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ System.out.println("开始订阅");
+ }
+
+ @Override
+ public void onNext(String s) {
+ System.out.println("观察者:" + s);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ e.printStackTrace();
+ }
+
+ @Override
+ public void onComplete() {
+ System.out.println("订阅结束");
+ }
+ };
+
+ //3、订阅
+ observable.subscribe(observer);
+ }
+
+}
+```
+
+## 创建操作符
+
+### create
+
+Observable.create() 是手动创建 Observable 的方法,允许完全控制数据的发射、完成和错误处理。
+
+```java
+Observable observable = Observable.create(emitter -> {
+ emitter.onNext("Hello");
+ emitter.onNext("World");
+ emitter.onComplete();
+});
+
+observable.subscribe(
+ item -> System.out.println("收到: " + item),
+ error -> System.err.println("错误: " + error),
+ () -> System.out.println("完成")
+);
+
+// 输出:
+// 收到: Hello
+// 收到: World
+// 完成
+```
+
+### just
+
+just 用于创建一个发射固定数据的 Observable,数据是预定义的,发射后立即完成。
+
+```java
+Observable.just("Hello")
+ .subscribe(item ->
+ System.out.println("收到: " + item)
+ );
+
+// 输出:
+// 收到: Hello
+```
+
+### fromArray
+
+fromArray 用于从数组创建一个 Observable,按数组顺序发射所有元素。
+
+```java
+String[] fruits = {"Apple", "Banana", "Cherry", "Date"};
+Observable.fromArray(fruits)
+ .subscribe(fruit ->
+ System.out.print(fruit + " ")
+ );
+
+// 输出:
+// Apple Banana Cherry Date
+```
+
+### fromCallable
+
+fromCallable 用于从 Callable 创建 Observable,Callable 的返回值会被包装成 Observable 发射。
+
+```java
+ public static void main(String[] args) {
+ Observable.fromCallable(() -> "计算结果: " + System.currentTimeMillis())
+ .subscribe(System.out::println);
+
+ // 输出:
+ // 计算结果: 1620000000000
+ }
+```
+
+### timer
+
+timer 用于创建一个延迟指定时间后发射单个数据的 Observable,通常是 0L,然后结束。
+
+```java
+ public static void main(String[] args) throws InterruptedException {
+ System.out.println("开始时间: " + System.currentTimeMillis());
+
+ Observable.timer(2, TimeUnit.SECONDS)
+ .subscribe(tick ->
+ System.out.println("触发时间: " + System.currentTimeMillis() + ",值: " + tick)
+ );
+
+ Thread.sleep(3000);
+ }
+```
+
+默认使用 `Schedulers.computation()`线程池计算,线程池中的线程是守护线程,如果主线程结束守护线程也会随之终止。
+
+### interval
+
+interval() 方法用于创建一个 周期性定时发射的 Observable,它会按照指定的时间间隔无限期地发射递增的数字序列(从0开始)。
+
+```java
+ public static void main(String[] args) throws IOException {
+ Observable.interval(2, TimeUnit.SECONDS)
+ .subscribe(aLong -> System.out.println(Thread.currentThread().getName() + ":" + aLong));
+
+ System.in.read();
+ }
+```
+
+### intervalRange
+
+intervalRange() 是 interval() 的增强版本,用于创建一个有限次数的周期性发射的 Observable。它允许你指定起始值、发射次数、初始延迟和间隔时间。
+
+```java
+ public static void main(String[] args) throws IOException {
+ Observable.intervalRange(100, 4, 0, 2, TimeUnit.SECONDS)
+ .subscribe(aLong -> System.out.println(Thread.currentThread().getName() + ":" + aLong));
+ System.in.read();
+ }
+```
+
+### range & rangeLong
+
+这两个方法用于创建一个发射连续整数序列的 Observable:
+
+- `range(start, count)`:发射 Integer 类型的连续整数。
+- `rangeLong(start, count)`:发射 Long 类型的连续整数。
+
+```java
+ public static void main(String[] args) throws IOException {
+ Observable.range(0, 10)
+ .subscribe(integer -> System.out.println(Thread.currentThread().getName() + ":" + integer));
+ System.in.read();
+ }
+```
+
+### never、error、empty
+
+这三个方法都是创建特殊的 Observable 的工厂方法,用于特定的场景。
+
+* never:创建一个永远不会发射任何数据,也不会终止的 Observable。
+* error:创建一个立即发射错误的 Observable。
+* empty:创建一个立即完成但不发射任何数据的 Observable。
+
+```java
+ public static void main(String[] args) {
+ // Observable.never()
+ // Observable.error(new RuntimeException("error事件"))
+ Observable.empty()
+ .subscribe(new Observer() {
+ @Override
+ public void onSubscribe(Disposable d) {
+ System.out.println("开始订阅");
+ }
+
+ @Override
+ public void onNext(Object s) {
+ System.out.println("观察者:" + s);
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ System.out.println("出现异常:" + e.getMessage());
+ }
+
+ @Override
+ public void onComplete() {
+ System.out.println("订阅结束");
+ }
+ });
+ }
+```
+
+## 转换操作符
+
+转换操作符是 RxJava 中用于对 Observable 发射的数据进行变换、处理、组合的操作符,用于实现数据流的各种转换逻辑。
+
+### map
+
+map() 是 RxJava 中最基本、最常用的转换操作符,用于对 Observable 发射的每个元素进行一对一转换。
+
+简单来说:输入一个值,输出另一个值(1进1出)。
+
+```java
+ public static void main(String[] args) {
+ Observable.just(1, 2, 3, 4, 5)
+ .map(number -> "数字: " + number)
+ .subscribe(text -> System.out.println(text));
+ }
+```
+
+### flatMap
+
+flatMap() 用于将每个发射项转换为 Observable,然后"扁平化"合并成一个 Observable。
+简单来说:输入一个值,可以输出任意多个值(1进N出)。
+
+```java
+ public static void main(String[] args) {
+ Observable.just("A", "B")
+ .map(letter -> letter + "1")
+ .subscribe(result -> System.out.print(result + " "));
+ // 输出: A1 B1
+
+ // flatMap: 1对多转换
+ Observable.just("A", "B")
+ .flatMap(letter ->
+ Observable.just(letter + "1", letter + "2", letter + "3")
+ )
+ .subscribe(result -> System.out.print(result + " "));
+ // 输出: A1 A2 A3 B1 B2 B3
+ }
+```
+
+### concatMap
+
+concatMap() 是 flatMap() 的顺序保持版本,它会严格按照原始顺序依次处理每个元素,只有前一个元素的 Observable 完成之后,才会处理下一个。
+简单来说:flatMap是并发处理,concatMap是串行处理。
+
+```java
+ public static void main(String[] args) throws IOException {
+ Observable.just(3, 1, 2) // 注意顺序:3, 1, 2
+ .flatMap(num ->
+ Observable.just(num)
+ .delay(num, TimeUnit.SECONDS) // 延迟对应的秒数
+ )
+ .subscribe(num -> System.out.println("flatMap: " + num));
+ // 输出顺序:1 2 3(谁先完成谁先输出)
+
+ Observable.just(3, 1, 2)
+ .concatMap(num ->
+ Observable.just(num)
+ .delay(num, TimeUnit.SECONDS)
+ )
+ .subscribe(num -> System.out.println("concatMap: " + num));
+ // 输出顺序:3 1 2(严格保持原始顺序)
+ System.in.read();
+ }
+```
+
+### buffer
+
+buffer() 用于将 Observable 发射的数据项收集到集合中,然后一次性发射这些集合,而不是单个发射。
+简单来说:把多个单独的数据"打包"成一批一起发射。
+
+```java
+ public static void main(String[] args) {
+ Observable.range(1, 10) // 发射1-10
+ .buffer(3) // 每3个一批
+ .subscribe(batch ->
+ System.out.println("批次: " + batch)
+ );
+
+ // 输出:
+ // 批次: [1, 2, 3]
+ // 批次: [2, 4, 6]
+ // 批次: [7, 8, 9]
+ // 批次: [10] ← 最后一批不足3个
+ }
+```
+
+### scan
+
+scan() 用于对 Observable 发射的数据进行累积计算,并发射每个中间结果。
+简单来说:像 Excel 里的累计求和,每来一个新数据,就与前一个结果计算,并输出当前累计值。
+
+```java
+ public static void main(String[] args) {
+ Observable.just(1, 2, 3, 4, 5)
+ .scan(Integer::sum)
+ .subscribe(result -> System.out.print(result + " "));
+
+ // 输出:
+ // 1 3 6 10 15
+
+ // 计算过程:
+ // 初始:无种子值,第一次直接发射1
+ // 1 + 2 = 3
+ // 3 + 3 = 6
+ // 6 + 4 = 10
+ // 10 + 5 = 15
+ }
+```
+
+### window
+
+window() 用于将 Observable 发射的数据分组到多个子 Observable 中,然后发射这些子 Observable 而不是单个数据项。
+简单来说:创建多个"窗口",每个窗口是一个 Observable,将数据分配到不同窗口中。
+
+```java
+ public static void main(String[] args) {
+ // buffer: 直接发射List
+ Observable.range(1, 5)
+ .buffer(2)
+ .subscribe(list ->
+ System.out.println("buffer输出List: " + list)
+ );
+
+ // window: 发射Observable,需要进一步处理
+ Observable.range(1, 5)
+ .window(2)
+ .flatMapSingle(Observable::toList // 需要手动转换
+ )
+ .subscribe(list ->
+ System.out.println("window输出List: " + list)
+ );
+
+ // 两者输出相同,但window更灵活:
+ // 输出:
+ // [1, 2]
+ // [3, 4]
+ // [5]
+ }
+```
+
+window 和 buffer 的区别就是:
+
+* window 返回的是被观察者的集合。
+* buffer 返回的是数据的集合。
+
+## 过滤操作符
+
+过滤操作符用于从数据流中筛选出需要的数据,过滤掉不需要的数据。
+
+### filter
+
+filter() 是 RxJava 中最基本的过滤操作符,用于根据指定条件筛选 Observable 发射的数据,只让满足条件的数据通过,不满足条件的被过滤掉。
+
+对每个数据项进行判断,返回 true则通过,返回 false则丢弃。
+
+```java
+ public static void main(String[] args) {
+ Observable.range(1, 10) // 发射1-10
+ .filter(number -> number % 2 == 0) // 只保留偶数
+ .subscribe(even -> System.out.print(even + " "));
+
+ // 输出:
+ // 2 4 6 8 10
+ }
+```
+
+### ofType
+
+ofType() 是一个类型过滤操作符,用于过滤 Observable 发射的数据,只保留指定类型的数据,其他类型的数据会被过滤掉。
+核心思想:只让指定类型的数据通过,相当于 filter(item -> item instanceof TargetType) 的简化版。
+
+```java
+ public static void main(String[] args) {
+ Observable