diff --git a/README.md b/README.md
index af1d7d8..6cccd56 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,19 +161,26 @@
### :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)
+
### :jack_o_lantern: 其他 ###
- [良心推荐!几款收藏的神级IDEA插件分享](https://mp.weixin.qq.com/s?__biz=Mzg4Nzc3NjkzOA==&mid=2247488457&idx=1&sn=f771ccebb84f226e7302b89caa5c056b&chksm=cf84600cf8f3e91aab4564d91feacb8822b53a2b3a79547439d64d2c0b7b293435a1ae79f994#rd)
-
- [实战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/\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/Maven\345\256\236\346\210\230.md" "b/docs/md/\345\205\266\344\273\226/Maven\345\256\236\346\210\230.md"
new file mode 100644
index 0000000..272fdb2
--- /dev/null
+++ "b/docs/md/\345\205\266\344\273\226/Maven\345\256\236\346\210\230.md"
@@ -0,0 +1,672 @@
+
+
+# Maven实战
+
+## 一、Maven介绍
+
+### 1.1 现存问题
+
+jar包问题
+
+* jar包需要在本地保存,而且在使用的时候需要将jar复制到项目中,再build才可以生效。
+* jar包的体量不小,一个项目中可能需要上百的jar的支持,这样一个项目就太大了。
+* 如果jar包的版本需要升级,需要重新去搜集新版本的jar包,重新去build,时间成本太高了。
+* 做一些功能时,可能需要因为几个,甚至十几个jar包,才能完成一个功能,都需要自己维护,甚至记住。
+
+项目结构的问题
+
+* 之前开发工具很多,有Eclipse,MyEclipse,IDEA,VSCode等等……不同的开发工具的项目的结构会有一些不同,多人协同开发时,就会造成冲突,甚至还需要统一开发工具。
+
+整体项目的生命流程
+
+* 整个项目从立项开发,到最后的发布上线到生产环境,没有一套统一的流程来控制。
+
+### 1.2 Maven
+
+- Maven可以帮助我们更好地去管理jar包,只需要指定好jar的一些基本的标识,就可以让jar包支持我们的项目。而且Maven可以帮助咱们导入一个jar包后,自动将和它绑定好的其他jar包引入。
+- Maven可以提供一个统一的项目结构。
+- Maven也对整体项目的生命周期有响应的管理,从开始的编译、测试、打包、部署等操作,都提供了相应的支持。
+- Maven还提供了分模块开发的功能。
+
+Maven是apache组织的一个顶级开源项目。 http://maven.apache.org
+
+## 二、Maven安装&环境变量配置
+
+### 2.1 Maven的安装
+
+首先下载Maven,直接去官网即可
+
+在点击Download之后,需要注意看一下对JDK版本的支持。
+
+Maven需要JDK的环境变量支持,一定要看一下自己又没有设置上JAVA_HOME
+
+
+
+需要根据自己的环境变量,下载对应的压缩包。
+
+Linux、Mac选择.tar.gz的压缩包
+
+Windows选择zip的压缩包
+
+
+
+下载好之后,得到一个压缩包。
+
+解压的目录最好没有任何的中文和空格等特殊字符。推荐放到磁盘的根目录下即可。
+
+
+
+> bin:含有mvn运行的脚本。
+>
+> boot:含有类加载器框架,Maven使用这个框架来加载自己的类库。
+>
+> conf:含有非常核心的settings.xml文件。
+>
+> lib:含有Maven运行时需要的一些类库。
+
+### 2.2 Maven的环境变量的配置
+
+首先配置Maven的环境变量前,必须先查看一下JDK环境变量配置。
+
+
+
+其次,查看一下前面说过的JAVA_HOME。
+
+上述两点都ok的话,直接开始配置环境变量
+
+* 配置MAVEN_HOME
+* 配置到path
+
+**配置完毕后,记得重新打开一下cmd窗口。别直接在之前的cmd窗口测试**。
+
+在cmd窗口执行mvn -v
+
+
+
+> Ps:常见错误,没有配置正确的JAVA_HOME
+
+
+
+## 三、仓库&settings.xml配置(重要)
+
+### 3.1 仓库
+
+Maven可以帮助咱们管理jar文件,但是,jar包是需要从网上下载下来的。
+
+仓库很多,有官方的中央仓库,还有国内公司的仓库,还有公司内部会搭建的私服
+
+
+
+咱们后面需要配置好国内公司的一些仓库。
+
+### 3.2 settings.xml配置(重要)
+
+在MAVEN_HOME目录下,有一个conf目录。在conf目录下就有需要修改的settings.xml文件。
+
+需要修改三点内容
+
+#### 3.2.1 本地仓库地址
+
+默认情况下,本地仓库在C盘。
+
+> Default: ${user.home}/.m2/repository
+
+根据配置文件中的注释,默认是仍在用户目录下的.m2目录下的repository目录中。
+
+这个本地仓库会随着项目越来越多,这个仓库也会越来越大。可能会占用10多个G,甚至更多。
+
+所以推荐放在系统盘之外。(如果就C盘,那就用默认的吧…………)
+
+
+
+#### 3.2.2 配置阿里云/华为云……仓库
+
+配置阿里云仓库
+
+
+
+```xml
+
+
+
+ aliyun
+ *
+ 阿里云公共仓库
+ https://maven.aliyun.com/repository/public
+
+
+```
+
+华为云的仓库地址:`https://repo.huaweicloud.com/repository/maven/`
+
+#### 3.2.3 JDK编译版本配置
+
+Maven默认采用JDK1.5的编译方式去编译项目。
+为了让Maven支持现在JDK的编译版本,可以指定一下现在采用JDK1.8
+
+
+
+```xml
+
+
+
+ jdk1.8
+
+ true
+ 1.8
+
+
+ 1.8
+ 1.8
+ 1.8
+
+
+
+
+
+ jdk1.8
+
+```
+
+## 四、IDEA配置Maven
+
+**先看老版本的,再看新版本的!!!**
+
+### 4.1 2019.1.3 IDEA配置Maven
+
+打开IDEA的初始窗口
+
+
+
+右下角的Configure的位置打开settings,点开后,在左上角可以看到是Settings for New Projects
+
+
+
+因为IDEA版本的原因,对Maven的版本也是有要求的。
+
+比如现在的2019.1.3的IDEA版本,无法支撑3.6.1以上的Maven版本
+
+
+
+一定要记得,点击Apply,然后ok,确认生效。
+
+### 4.2 2024.1 IDEA配置Maven
+
+首先一定要记住,选择Settings for new projects
+
+
+
+
+
+
+
+## 五、IDEA构建Maven项目
+
+**先看老版本的,再看新版本的!!!**
+
+### 5.1 2019.1.3 IDEA构建Maven项目
+
+点击Create New Project
+
+
+
+next后,指定当前项目的三围,包名,项目名,版本号
+
+
+
+指定好项目名和存放地址。这里对存放地址修改一下就ok。
+
+
+
+指定好之后,点击Finish即可。
+
+
+
+进来后,可以看到右下角的进度条,在下载一些Maven必要的插件
+
+在下载插件时,可能需要一定的时间,等插件下载好,为了确认咱们阿里云私服的配置是否生效,随便复制下面内容到当前位置。 **一定一定一定记得点击右下角的import Changes**
+
+```xml
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ 2.1.6.RELEASE
+
+
+```
+
+
+
+快速地点击右下角的进度条,查看下载的链接地址,确认一下是否是阿里云的地址
+
+
+
+再次查看右侧的Maven栏,确认profiles中的JDK1.8编译版本已经生效
+
+
+
+最后查看完毕后,要对Maven项目的目录结构有个了解
+
+
+
+### 5.2 2014.1 IDEA构建Maven项目
+
+
+
+### 5.3 IDEA构建Maven的Web项目
+
+这个新老版本是一致的!!!
+
+这里是先构建Maven的基础项目,然后将基础项目修改为Web项目。
+
+
+
+正常,构建的基础maven项目,打包的方式是jar文件。需要将当前web项目的打包方式修改为war的形式。
+
+需要修改pom.xml文件指定打包方式。
+
+默认情况下,这个packaging是jar的打包形式。需要指定好war的形式,一定一定一定记得import Changes
+
+
+
+然后选中项目,点击左上角的file,选择Project Structure
+
+
+
+选择左侧导航栏中的facets选项,如果你的Facets界面没有这个Web,说明之前的war没配置好!!
+
+
+
+然后点击右上角的+,追加一个web.xml文件,记得一定要放到webapp资源目录下
+
+
+
+点击ok,就会自动生成webapp目录,以及目录下的web.xml文件
+
+
+
+## 六、导入依赖jar(重要)
+
+创建好Maven项目之后,需要导入具体的jar包时,要通过 **坐标** 导入
+
+* 每个jar都需要三个内容形成一个唯一的坐标,需要groupId + artifactId + version导入一个具体的jar。
+* 在maven项目中,只需要导入配置的坐标,Maven便会自动地去网上下载jar文件,并且添加到项目中。
+
+当需要使用某个jar时,知道大概的名字,但是不会背下来具体的坐标信息,可以去一个地方搜索
+
+[https://mvnrepository.com/](https://mvnrepository.com/)
+
+可以去这个地址搜索具体的jar包坐标
+
+
+
+进入具体的依赖内部后,选择对应的版本
+
+
+
+找到需要导入的dependency
+
+
+
+复制好之后,扔到项目的pom.xml文件中
+
+
+
+如果本地仓库出现了.lastUpdated后缀的文件,可能有两个情况
+
+* 这个坐标的jar文件不存在
+* 因为网络原因下载失败了
+
+**这种.lastUpdated后缀的文件,会导致后续依赖下载失败,记得如果出现了依赖失败,检查坐标都没问题,并且也是走阿里云或者华为云去下载的,依然失败。记得去本地仓库看一下,是不是有.lastUpdated后缀的文件导致无法下载成功**!
+
+## 七、依赖的作用域
+
+所谓的依赖作用域就是当前这个jar文件在什么情况下,项目会使用到。
+
+这个所谓的情况,可以分成三点来聊:
+
+* 编译阶段
+* 测试阶段
+* 运行阶段
+
+Maven中给予依赖五种作用域:
+
+* compile(默认作用域):编译,测试,运行都会提供当前依赖的功能
+ ```
+
+ commons-io
+ commons-io
+ 2.11.0
+
+ ```
+* provided:编译,测试会提供当前依赖的功能。 一般Servlet,JSP会涉及。
+ ```
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+ ```
+* runtime:测试,运行会提供当前依赖的功能。一般MySQL会涉及。
+ ```
+
+ mysql
+ mysql-connector-java
+ 8.0.28
+ runtime
+
+ ```
+* test:测试会提供当前依赖的功能。
+ ```
+
+ junit
+ junit
+ 4.13.2
+ test
+
+ ```
+* system:不是在什么情况下用,这个比较特殊,是将一些本地仓库没有的jar文件,引入到当前项目。
+ ```
+
+ com.oracle.database.jdbc
+ ojdbc10
+ 19.21.0.0
+ system
+ D:/ojdbc10-19.21.0.0.jar
+
+ ```
+
+**system,不推荐用,哪怕一些依赖,本地仓库无法下载,也别用system去引入。这种引入方式会导致后期打包还是更换了环境之后,无法使用。(后面咱们会根据maven的命令,可以将本地的jar包安装到本地仓库)**
+
+```shell
+mvn install:install-file -Dfile=D:/ojdbc10-19.21.0.0.jar -DgroupId=laozheng -DartifactId=laozheng-oracle -Dversion=yeyeye -Dpackaging=jar
+```
+
+搞定后,本地仓库可以看到install的jar文件和路径
+
+
+
+然后就可以在项目中引用了。
+
+```xml
+
+ laozheng
+ laozheng-oracle
+ yeyeye
+
+```
+
+## 八、依赖冲突
+
+首先,咱们要先了解一下Maven依赖的传递特性。
+
+当咱们导入一个jar包后,如果这个jar为了完成一些功能,还需要其他的jar的功能。
+
+比如有A,有B,其中A依赖了B。
+
+咱们只需要导入A包,B会自动被依赖过来。优点大大的:
+
+* 不需要刻意的去记导入A之后,还需要导入什么其他的依赖。
+* 关于某个版本的A需要哪个版本的B也不需要关注。
+
+上面是优点,但是也存在着一些问题。
+
+当前项目 -> A -> B(1.0.0)
+
+当前项目 -> C -> B(2.0.0)
+
+此时,当前项目会出现相同的依赖,有两个,但是版本不一样,此时就会产生依赖冲突问题。
+
+一般依赖冲突会在启动或者测试项目时,直接给你甩异常。而且这个依赖不太好处理。需要解决这种依赖冲突。
+
+### 8.1 就近原则
+
+
+
+明显,当前项目通过D依赖C的路径最近,基于就近原则,会使用2.0.0的版本
+
+### 8.2 优先声明原则
+
+当出现依赖传递导致相同jar包版本不一致时,此时会根据优先声明原则来决定使用谁。
+
+如果是你主动导入的依赖,此时会根据你最后引用的版本决定
+
+### 8.3 手动排除依赖
+
+可以手动的形式,在引入A依赖时,将B依赖中A依赖排除掉
+
+
+
+
+
+```xml
+
+
+ org.springframework
+ spring-context
+ 5.3.12
+
+
+ org.springframework
+ spring-beans
+
+
+ org.springframework
+ spring-core
+
+
+
+
+ org.springframework
+ spring-aop
+ 5.2.10.RELEASE
+
+
+
+```
+
+### 8.4 声明依赖版本
+
+可以通过dependencyManagement标签,提前声明依赖的版本。
+
+dependencyManagement标签只会声明版本,不会将依赖导入,导入依赖依然需要借助dependencies
+
+配置完下面的内容后,再导入spring-beans、spring-core无论什么方式,都使用dependencyManagement中声明的版本。
+
+```xml
+
+
+
+
+ org.springframework
+ spring-beans
+ 5.1.8.RELEASE
+
+
+ org.springframework
+ spring-core
+ 5.3.9
+
+
+
+```
+
+如果前面已经声明好了依赖的版本。
+
+但是你在pom.xml文件中,直接引入了一个具体的版本的依赖,和dependencyManagement不一致,那么会使用你指定好的版本。这种依赖传递的版本会严格遵循dependencyManagement。
+
+其次,如果基于dependencyManagement声明好了版本,在dependencies中导入依赖时,是可以不写版本号的,可以直接基于dependencyManagement中的版本导入。
+
+
+
+## 九、Maven指令
+
+Maven为整个项目生命周期的各个阶段,提供了各种各样的指令。
+
+先了解常用的几个:
+
+```java
+mvn clean:清空target目录。
+mvn compile:编译整个项目,生成到target
+mvn test:专门针对test目录下的内容做测试
+mvn package:会将当前项目打包,jar,war。
+mvn install:将当前项目进行编译,测试,打包,并且将jar包安装到本地仓库。
+// mvn deploy:私服的位置再讲
+```
+
+* compile:这里是将main目录下的内容编译,生成一个target目录,将编译后的内容全部放到target目录下,java和resources都可以称为classpath,因为编译后的内容都是放在classes目录下的。
+* clean:就是将编译后的内容全部清除掉。
+* test:测试会优先进行编译,并且会针对test目录下以Test结尾的类中追加了@Test注解的方法运行测试,如果报错,控制台会有显示。直接Build失败。
+* package:将项目进行打包,但是打包会经历compile以及test,并且成功后,才会将项目打包成具体的jar或者是war。打包后的具体文件,会存放在target目录下。项目打包无法跳过编译过程的,但是可以跳过测试的过程,需要自行敲命令
+
+ ```
+ mvn package -DskipTests
+ ```
+
+ 
+* install:将当前项目做好编译,测试,打包,并且将项目安装到本地仓库。如果安装到本地仓库的是一个jar包,其他项目就可以将这个jar依赖过来使用。!
+
+## 十、聚合工程
+
+在项目打包的方式中,前面聊过jar,还有war的形式。
+
+除此之外,还有一个打包的形式,叫做pom。pom就是所谓的聚合工程。
+
+
+
+构建最外层的电商聚合工程,聚合工程不需要写任何的业务代码,它的目的就是管理其他的子工程
+
+
+
+构建好聚合工程后,可以再构建子工程,流程如下。
+
+好处是可以在聚合工程内去管理依赖的版本。同时可以基于聚合工程做统一的多个项目的打包或者其他操作。而且拆分模块去写项目。
+
+## 十一、Maven私服
+
+### 11.1 Maven私服的概念
+
+> * 私服是搭建在局域网的一种特殊的远程仓库,目的是代理远程仓库,让下载依赖的效率更高。
+> * 有了私服之后,使用Maven需要下载依赖时,直接请求私服下载依赖,将私服中的依赖下载到本地仓库中。如果私服中没有具体依赖,私服会去外部的远程仓库下载。
+> * 私服可以解决在业务做开发时,有一些内部的依赖,是中央仓库没有提供的,是公司开发人员自行封装的一些依赖。可以将公司自研的一些框架和依赖上传到私服中,让公司内部人员可以通过私服将这种依赖下载到本地仓库。
+
+
+
+> 搭建私服的方式非常多,Apache Archiva,Sonatype Nexus。 一般都会采用后者。
+
+### 11.2 搭建Nexus私服
+
+去官网下载最新的安装包。
+
+http://www.sonatype.com
+
+但是在官网想找到Download挺麻烦的,下载的话,直接进入到下面这个地址
+
+https://help.sonatype.com/en/download.html
+
+
+
+下载完毕是一个zip的压缩包,最好解压到非系统盘的位置,路径不要带 **中文和空格** !!!!
+
+解压后,有两个目录
+
+
+
+进入到nexus-3.67.1-01目录下,再进入bin目录下。
+
+
+
+启动时,需要基于doc窗口去运行Nexus私服,但是一定要以 **超级管理员** 的身份打开cmd。
+
+
+
+在bin目录下执行指定,访问外网慢的话,可能需要至少9~10分钟左右甚至更多。
+
+```shell
+nexus.exe /run
+```
+
+
+
+启动成功后,直接访问http://localhost:8081/
+
+进入首页后,需要加载一小会,可以访问到首页,第一个要做的事情是登录
+
+
+
+登录即可,默认用户名是admin,密码在下面图中的文件里
+
+
+
+登录成功后,第二步需要重新设置密码
+
+
+
+设置私服下载依赖的权限信息
+
+
+
+关注前四个即可
+
+
+
+### 11.3 Nexus私服配置&下载依赖
+
+
+
+将私服仓库的代理,设置为国内的仓库镜像源
+
+
+
+配置完,拉到最下面,记得Save保存一下。
+
+接下来配置好私服的地址,让项目基于私服下载依赖
+
+
+
+
+
+
+
+因为初始化Nexus时,选择的是下载依赖不需要认证信息。
+
+如果选择的是需要,要如何配置。
+
+
+
+### 11.4 上传依赖到私服
+
+首先在Maven私服的位置,找到release和snapshot的仓库地址
+
+
+
+
+
+然后在pom.xml文件中配置相应的信息
+
+```xml
+
+
+ zjw
+ http://localhost:8081/repository/maven-releases/
+
+
+ zjw
+ http://localhost:8081/repository/maven-snapshots/
+
+
+```
+
+
+
+准备好之后,直接在项目右侧,点击deploy上传当前项目的jar到私服
+
+
+
+上传成功后,可以在私服中找到
+
+
+
+其他的项目在配置没问题的情况下,就可以使用私服中的各种依赖了。
diff --git "a/docs/md/\345\205\266\344\273\226/\344\270\215\347\224\250Mockito\345\206\231\345\215\225\345\205\203\346\265\213\350\257\225\357\274\237\344\275\240\345\217\257\350\203\275\345\234\250\346\265\252\350\264\271\344\270\200\345\215\212\346\227\266\351\227\264(1).md" "b/docs/md/\345\205\266\344\273\226/\344\270\215\347\224\250Mockito\345\206\231\345\215\225\345\205\203\346\265\213\350\257\225\357\274\237\344\275\240\345\217\257\350\203\275\345\234\250\346\265\252\350\264\271\344\270\200\345\215\212\346\227\266\351\227\264(1).md"
new file mode 100644
index 0000000..597c1ea
--- /dev/null
+++ "b/docs/md/\345\205\266\344\273\226/\344\270\215\347\224\250Mockito\345\206\231\345\215\225\345\205\203\346\265\213\350\257\225\357\274\237\344\275\240\345\217\257\350\203\275\345\234\250\346\265\252\350\264\271\344\270\200\345\215\212\346\227\266\351\227\264(1).md"
@@ -0,0 +1,532 @@
+你是不是也经常在写单元测试时,被数据库连接、第三方接口这些折腾得头疼?明明只是想验证自己的业务逻辑,却不得不花半天时间处理各种外部依赖——这种体验就像是想喝杯咖啡却发现要自己种咖啡豆。
+
+好在Mockito这个神器能让你的测试飞起来!它帮你模拟复杂依赖,让测试回归到代码逻辑本身。无论是验证某个方法是否被正确调用,还是模拟异常来测试程序的健壮性,Mockito 都能让测试变得专注而高效。
+
+# 简介
+
+Mockito是一个用于Java单元测试的mock框架,用于创建**模拟对象**(mock object)来替代真实对象,帮助开发者隔离外部依赖,从而专注于单元测试的逻辑,Mockito通常配合单元测试框架(如JUnit)使用。
+
+- 官方网站:https://site.mockito.org/
+- 官方文档:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
+
+# 依赖
+
+```xml
+
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+```
+
+如果使用Spring Boot Test 则不需要引入,Spring Boot Test 默认集成了 Mockito。
+
+# 常见用法
+
+Mockito的核心功能包括:
+
+- **创建mock对象**:使用`mock()`创建mock对象。
+- **打桩**:使用`when()`和`thenReturn()`等方法指定mock对象的特定方法被调用时的行为(如返回值或抛出异常)。
+- **验证行为**:使用`verify()`检查mock对象的特定方法是否被调用,参数和调用次数是否符合预期。
+
+下面通过示例展开介绍Mockito的用法。
+
+## 验证行为
+
+Mockito 的 `verify()` 用于验证**模拟对象的方法是否按预期被调用**,包括调用次数、参数匹配等。它支持精确验证(如 `times(2)`)、最少/最多次数(`atLeast()`/`atMost()`)、未调用(`never()`)及顺序验证(结合 `InOrder`)等,确保代码执行逻辑正确。
+
+```java
+public class MockTest {
+
+ @Test
+ public void testBasicVerification() {
+ List mockList = mock(List.class);
+
+ // 模拟调用
+ mockList.add("apple");
+ mockList.add("banana");
+ mockList.add("apple");
+ mockList.add("orange");
+
+ // 1. 验证方法被调用【恰好一次】(默认行为)
+ verify(mockList).add("banana");
+
+ // 2. 验证方法被调用【指定次数】
+ verify(mockList, times(2)).add("apple"); // 精确2次
+
+ // 3. 验证方法【从未调用】
+ verify(mockList, never()).clear();
+
+ // 4. 验证【调用顺序】
+ InOrder inOrder = inOrder(mockList);
+ inOrder.verify(mockList).add("apple");
+ inOrder.verify(mockList).add("banana");
+ inOrder.verify(mockList).add("apple");
+
+ verifyNoMoreInteractions(mockList);
+ }
+
+}
+```
+
+`org.mockito.Mockito`类的`mock()`方法用于创建指定类或接口的mock对象。一旦创建,mock对象就会记住所有的方法调用。之后可以选择性地验证感兴趣的方法调用。
+
+- **验证单次调用**:`verify(mockList).add("banana");`→ 检查 `add("banana")` 被调用 1 次。
+
+- **验证精确次数**:`verify(mockList, times(2)).add("apple");`→ 检查 `add("apple")` 被调用 2 次。
+
+- **验证禁止调用**:`verify(mockList, never()).clear();`→ 确保 `clear()` 从未调用。
+
+- **验证调用顺序**:
+
+ ```java
+ InOrder inOrder = inOrder(mockList);
+ inOrder.verify(mockList).add("apple");
+ inOrder.verify(mockList).add("banana");
+ inOrder.verify(mockList).add("apple");
+ ```
+
+ 严格按顺序验证调用链。
+
+- **未验证的调用**:`verifyNoMoreInteractions()` 用来检查mock对象没有未验证的调用。由于`mockList.add("orange")`被调用过,但没有验证,因此最后的测试将会失败。
+
+## 打桩
+
+**打桩**是为模拟对象(Mock)的方法调用预设返回值或行为,使得测试代码可以**隔离外部依赖**,并控制方法的输出或异常,一旦被打桩,方法将返回指定的值,无论调用多少次。通过打桩,可以模拟数据库、网络请求等复杂或不可控的操作。
+
+```java
+ @Test
+ public void testStubbing() {
+ // 1. 创建模拟对象
+ List mockList = mock(List.class);
+
+ // 2. 基础打桩:返回固定值
+ when(mockList.get(0)).thenReturn("apple");
+ assertEquals("apple", mockList.get(0));
+
+ // 3. 抛出异常
+ when(mockList.get(1)).thenThrow(new RuntimeException("索引错误"));
+ assertThrows(RuntimeException.class, () -> mockList.get(1));
+
+ // 4. 多次调用不同返回值
+ when(mockList.size())
+ .thenReturn(1)
+ .thenReturn(2);
+ assertEquals(1, mockList.size());
+ assertEquals(2, mockList.size());
+
+ // 5. 参数匹配器(如 anyInt())
+ when(mockList.get(anyInt())).thenReturn("default");
+ assertEquals("default", mockList.get(999));
+
+ // 6. Void 方法打桩(如抛出异常)
+ doThrow(new IllegalStateException("清空失败")).when(mockList).clear();
+ assertThrows(IllegalStateException.class, mockList::clear);
+ }
+```
+
+**语法优先级**:
+
+- `when(...).thenX()` 适用于有返回值的方法。
+- `doX().when(mock).method()` 适用于 void 方法。
+
+**参数匹配器**:使用 `any()`、`eq()` 等灵活匹配参数,但需注意参数一致性(不能混用具体值和匹配器)。
+
+**覆盖规则**:最后一次打桩会覆盖之前的定义(例如多次对 `mock.get(0)` 打桩,以最后一次为准)。
+
+**默认情况下,对于所有返回值的方法,mock对象将返回适当的默认值**。例如,对于`int`或`Integer`返回0,对于`boolean`或`Boolean`返回`false`,对于集合类型返回空集合,对于其他对象类型(例如字符串)返回`null`。
+
+## 连续打桩和回调打桩
+
+**连续打桩(Chained Stubbing)**:为同一个方法的连续多次调用定义不同的返回值或行为,常用于模拟多次调用时的动态响应。
+
+```java
+ @Test
+ public void testChainedStubbing() {
+ List mockList = mock(List.class);
+
+ // 定义连续打桩:第一次调用返回 "A",第二次返回 "B",第三次抛出异常
+ when(mockList.get(0))
+ .thenReturn("A")
+ .thenReturn("B")
+ .thenThrow(new RuntimeException("No more elements"));
+
+ // 验证
+ assertEquals("A", mockList.get(0)); // 第一次返回 "A"
+ assertEquals("B", mockList.get(0)); // 第二次返回 "B"
+ assertThrows(RuntimeException.class, () -> mockList.get(0)); // 第三次抛出异常
+ }
+```
+
+超出定义的调用次数后,最后一次行为会持续生效(例如第三次后继续调用会一直抛异常)。
+
+**回调打桩(Callback Stubbing)**:`thenAnswer()` 可以实现动态返回值逻辑,根据方法参数或外部条件生成响应。
+
+```java
+ @Test
+ public void testChainedStubbing() {
+ List mockList = mock(List.class);
+
+ // 根据参数动态返回:参数是偶数时返回 "even",奇数返回 "odd"
+ when(mockList.get(anyInt())).thenAnswer(invocation -> {
+ int index = invocation.getArgument(0); // 获取第一个参数
+ return (index % 2 == 0) ? "even" : "odd";
+ });
+
+ // 验证
+ assertEquals("even", mockList.get(0)); // 0是偶数
+ assertEquals("odd", mockList.get(1)); // 1是奇数
+ }
+```
+
+- **灵活控制**:可在 `thenAnswer()` 中编写任意 Java 代码,甚至访问外部变量。
+- **参数获取**:通过 `invocation.getArgument(n)` 获取第 `n` 个参数(从 0 开始)。
+
+## 参数匹配器
+
+Mockito默认使用`equals()`方法验证参数值。当需要额外的灵活性时,可以使用参数匹配器。
+
+参数匹配器是 Mockito 提供的一种灵活的参数验证机制,允许开发者通过匹配器来匹配方法参数,而无需指定具体值。
+
+参数匹配器广泛用于 `when()` 打桩和 `verify()` 验证中。
+
+```java
+ @Test
+ public void testMatchers() {
+ List mockList = mock(List.class);
+
+ // 1. 通用匹配器:anyInt(), anyString()
+ when(mockList.get(anyInt())).thenReturn("default");
+ assertEquals("default", mockList.get(999));
+
+ // 2. 条件匹配器:startsWith(), endsWith()
+ when(mockList.add(startsWith("app"))).thenReturn(true);
+ assertTrue(mockList.add("apple"));
+ assertFalse(mockList.add("banana"));
+
+ // 3. 混合使用具体值和匹配器(必须用 eq() 包裹具体值)
+ when(mockList.set(eq(0), anyString())).thenReturn("old_value");
+ assertEquals("old_value", mockList.set(0, "new_value"));
+ }
+```
+
+**通用匹配器**
+
+- **作用**:匹配任意参数或特定类型参数。
+
+- **常见方法**:
+- `any()`:匹配任意对象(包括 `null`)。
+
+- `anyInt()`, `anyString()`, `anyList()`:匹配特定类型参数。
+
+- `isNull()`, `isNotNull()`:匹配 `null` 或非 `null` 参数。
+
+**条件匹配器**
+
+- **作用**:根据逻辑条件匹配参数。
+
+- **常见方法**:
+
+ - `eq(value)`:严格匹配具体值(等同于直接写值)。
+
+ - `startsWith("prefix")`:匹配以指定前缀开头的字符串。
+
+ - `endsWith("suffix")`, `contains("substr")`:匹配字符串后缀或子串。
+
+ - `argThat(condition)`:自定义条件(如集合大小、对象属性)。
+
+**混合使用规则**
+
+- **强制要求**:若方法参数中至少有一个匹配器,则所有参数必须用匹配器。
+
+ 错误示例:
+
+ ```java
+ // 错误:混合具体值和匹配器
+ when(mock.method("value", anyInt())).thenReturn(true);
+ ```
+
+ 修复方法:将具体值用 `eq()`包裹:
+
+ ```java
+ when(mock.method(eq("value"), anyInt())).thenReturn(true);
+ ```
+
+ **自定义匹配器**
+
+通过 `argThat()` 实现复杂条件:
+
+```java
+// 自定义匹配器:验证集合大小大于2
+when(mockList.addAll(argThat(list -> list.size() > 2))).thenReturn(true);
+assertTrue(mockList.addAll(List.of("A", "B", "C")));
+```
+
+更多的内置参数匹配器参考:
+
+- https://javadoc.io/static/org.mockito/mockito-core/4.11.0/org/mockito/ArgumentMatchers.html
+
+- https://javadoc.io/static/org.mockito/mockito-core/4.11.0/org/mockito/hamcrest/MockitoHamcrest.html
+
+## 间谍(spy)
+
+`spy()` 可以创建部分真实对象的代理(保留原有行为,可选择性地对某些方法打桩),适合需要混合真实逻辑与模拟行为的场景。
+
+对比 `mock()`:
+
+| 特性 | `mock()` | `spy()` |
+| :----------: | :----------------------------------: | :------------------------: |
+| **默认行为** | 所有方法返回默认值(如 `null`、`0`) | 调用真实方法,除非显式打桩 |
+| **适用场景** | 完全隔离被测对象依赖 | 需保留部分真实逻辑的测试 |
+
+```java
+ @Test
+ public void testSpyBasic() {
+ // 1. 创建一个 ArrayList 的 spy 对象
+ List spyList = spy(new ArrayList<>());
+
+ // 2. 调用真实方法
+ spyList.add("apple");
+ spyList.add("banana");
+
+ // 3. 验证真实行为
+ assertEquals(2, spyList.size()); // 实际调用了 add 和 size 方法
+
+ // 4. 对某个方法打桩
+ when(spyList.size()).thenReturn(100);
+ assertEquals(100, spyList.size()); // 打桩生效
+
+ // 5. 验证方法调用次数
+ verify(spyList, times(2)).add(anyString()); // 验证 add 被调用两次
+ }
+```
+
+当对 `spy` 对象的方法打桩时,若直接使用 `when(...)` 会触发真实方法调用,可能导致异常。
+
+错误示例:
+
+```java
+List spyList = spy(new ArrayList<>());
+// 会被真实执行,但此时列表为空,导致 IndexOutOfBoundsException
+when(spyList.get(0)).thenReturn("mock-value");
+```
+
+正确方式:使用 `doReturn().when()` 语法避免真实调用
+
+```java
+ List spyList = spy(new ArrayList<>());
+ // 正确:不会触发 get(0) 的真实调用
+ doReturn("mock-value").when(spyList).get(0);
+ assertEquals("mock-value", spyList.get(0));
+```
+
+**最佳实践**:
+
+1. **优先使用 `mock()`**:除非需要保留部分真实行为,否则优先用 `mock()` 隔离依赖。
+2. **谨慎打桩**:使用 `doReturn().when()` 替代 `when().thenReturn()`,避免意外触发真实方法。
+3. **避免复杂间谍**:不要对复杂对象(如 Spring Bean)滥用 `spy()`,可能导致测试不可控。
+
+## 参数捕获(ArgumentCaptor)
+
+ArgumentCaptor 用于在测试中捕获方法调用时传递的参数,便于后续对参数值进行详细验证(如对象属性、集合内容等)。
+
+完整示例:
+
+```java
+ @Test
+ public void testCaptureArgument() {
+ // 1. 创建 Mock 对象
+ UserService mockService = mock(UserService.class);
+
+ // 2. 调用被测试方法
+ User user = new User("Alice", 30);
+ mockService.processUser(user);
+
+ // 3. 创建 ArgumentCaptor
+ ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class);
+
+ // 4. 验证方法调用并捕获参数
+ verify(mockService).processUser(userCaptor.capture());
+
+ // 5. 获取捕获的参数并验证
+ User capturedUser = userCaptor.getValue();
+ assertEquals("Alice", capturedUser.getName());
+ assertEquals(30, capturedUser.getAge());
+ }
+
+ @Data
+ static class User {
+ private String name;
+ private int age;
+
+ public User(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ static class UserService {
+ public void processUser(User user) {
+ // 实际业务逻辑(在测试中被 Mock)
+ }
+ }
+```
+
+## 静态方法Mock
+
+`Mockito.mockStatic(Class)` 可以创建静态类的 Mock 作用域,并在其中定义行为。
+
+```java
+ @Test
+ public void testMockStaticMethod() {
+ // 1. 创建静态类(如 LocalDate)的 Mock 作用域
+ try (MockedStatic mockedLocalDate = mockStatic(LocalDate.class)) {
+
+ // 2. 定义静态方法 now() 的行为
+ LocalDate fixedDate = LocalDate.of(2023, 10, 1);
+ mockedLocalDate.when(LocalDate::now).thenReturn(fixedDate);
+
+ // 3. 验证静态方法调用
+ assertEquals(fixedDate, LocalDate.now()); // 返回固定日期
+ mockedLocalDate.verify(LocalDate::now); // 验证 now() 被调用
+ }
+
+ // 4. 作用域结束后,静态方法恢复原始行为
+ assertNotEquals("2023-10-01", LocalDate.now().toString());
+ }
+```
+
+**作用域限制**:
+
+- 静态 Mock 仅在 `try-with-resources` 或 `MockedStatic.close()` 前有效。
+- 必须关闭:确保使用 `try-with-resources` 或手动 `close()`,避免影响其他测试。
+
+# 注解
+
+## @Mock
+
+@Mock用于快速创建 Mock 对象,替代 `Mockito.mock(Class)` 方法。
+
+**方式 1:通过 `MockitoJUnitRunner` 自动初始化**
+
+```java
+// 自动初始化 @Mock 注解
+@RunWith(MockitoJUnitRunner.class)
+public class MockTest {
+
+ @Mock // 自动创建 List 的 Mock 对象
+ private List mockList;
+
+ @Test
+ public void testMockAnnotation() {
+ mockList.add("test");
+ verify(mockList).add("test");
+ }
+
+
+}
+```
+
+**JUnit 5 适配**:需使用`@ExtendWith(MockitoExtension.class)`。
+
+**方式 2:手动调用 `MockitoAnnotations.openMocks()`**
+
+```java
+public class MockTest {
+ @Mock
+ private List mockList;
+
+ @Before
+ public void init() {
+ MockitoAnnotations.openMocks(this); // 手动初始化 @Mock 注解
+ }
+
+ @Test
+ public void testMockAnnotation() {
+ mockList.add("test");
+ verify(mockList).add("test");
+ }
+}
+```
+
+## @MockBean
+
+在Spring Boot 集成测试中,@MockBean用于向 ApplicationContext 注入一个Mock 对象,替换原有 Bean。适用于需要隔离外部依赖(如数据库、第三方服务)的集成测试。
+
+示例场景:测试 `UserService` 时,Mock 其依赖的 `UserRepository`,避免真实数据库操作。
+
+```java
+@SpringBootTest // 启动 Spring 上下文
+public class UserServiceTest {
+
+ @Autowired
+ private UserService userService; // 被测服务
+
+ @MockBean // 自动替换 Spring 容器中的 UserRepository Bean
+ private UserRepository userRepository;
+
+ @Test
+ public void testGetUserById() {
+ // 1. 定义 Mock 行为
+ when(userRepository.findById(1L)).thenReturn(new User("Alice"));
+
+ // 2. 调用被测方法
+ User user = userService.getUserById(1L);
+
+ // 3. 验证结果和交互
+ assertEquals("Alice", user.getName());
+ verify(userRepository).findById(1L); // 确保方法被调用
+ }
+}
+```
+
+- **替换规则**:若 Spring 上下文中已存在同名 Bean,`@MockBean` 会覆盖它;若不存在,则新增 Mock Bean。
+- **多 Bean 类型冲突**:若同一类型有多个 Bean,需结合 `@Qualifier` 指定名称。
+
+## @InjectMock
+
+- **核心功能**:自动将 `@Mock` 或 `@Spy` 创建的依赖对象注入到被测试类中,简化依赖管理。
+
+- **适用场景**:单元测试中,快速构建被测试类(如 Service 层),并自动注入其依赖的 Mock 对象(如 Repository)。
+
+示例场景:测试 `UserService`,其依赖 `UserRepository`(需要 Mock)。
+
+```java
+@ExtendWith(MockitoExtension.class)
+public class MockTest {
+
+
+ @Mock // 创建 UserRepository 的 Mock 对象
+ private UserRepository userRepository;
+
+ @InjectMocks // 自动将 userRepository 注入 UserService
+ private UserService userService;
+
+ @Test
+ public void testGetUserById() {
+ // 1. 定义 Mock 行为
+ when(userRepository.findById(1L)).thenReturn(new User("Alice"));
+
+ // 2. 调用被测试方法
+ User user = userService.getUserById(1L);
+
+ // 3. 验证结果和交互
+ assertEquals("Alice", user.getName());
+ verify(userRepository).findById(1L); // 确保方法被调用
+ }
+}
+```
+
+`@InjectMocks` 按以下顺序尝试注入依赖:
+
+1. **构造函数注入**(优先选择参数最多的构造函数)。
+2. **Setter 方法注入**(按方法名匹配,如 `setUserRepository()`)。
+3. **字段注入**(直接注入到 `private` 字段,需匹配名称和类型)。
+
+# 结尾
+
+Mockito 的魅力在于它用简单的语法解决了测试中的复杂问题。通过模拟对象、打桩预设行为、验证调用细节,开发者可以轻松隔离外部依赖,像搭积木一样构造测试场景。无论是新手还是经验丰富的工程师,Mockito 的直观设计都能让人快速上手。
+
+下次当你面对一个难以测试的方法时,不妨试试 Mockito——让它帮你把“不确定”变成“可控”,把“复杂依赖”变成“精准验证”。毕竟,好的测试不是为了证明代码完美,而是为了让它足够可靠,而 Mockito 正是这条路上值得信赖的工具。
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