diff --git a/.gitignore b/.gitignore index 815e8cc827..c2c269f735 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,6 @@ package-lock.json dump.rdb docs/.vuepress/.cache/ docs/.vuepress/.temp/ -docs/dist/ -dist.zip images *.log .yarn diff --git a/README.md b/README.md index c84b4eb65c..a00f3e8f85 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/tobebetterjavaer-map.png) -一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **10000 多名** 球友加入了(马上涨价到 169 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。 +一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **9000 多名** 球友加入了(戳[链接](https://javabetter.cn/zhishixingqiu/)了解详情),如果你也需要一个良好的学习环境,扫描下方的优惠券加入我们吧。新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。

@@ -54,13 +54,11 @@

-新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。 -这是一个 **简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 +这是一个**编程学习指南 + Java 项目实战 + LeetCode 刷题+简历精修**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 - [二哥精修简历服务,让你投了就有笔试&面试✌️](https://javabetter.cn/zhishixingqiu/jianli.html) - [二哥的RAG知识库项目派聪明上线了,AI时代你必须拥有的实战项目✌️](https://javabetter.cn/zhishixingqiu/paismart.html) -- [Go 版本的派聪明RAG知识库项目上线了,学习 Go 语言的小伙伴有福了✌️](https://javabetter.cn/zhishixingqiu/paismart-go.html) - [二哥的技术派实战项目更新了,秋招&暑期/日常实习大杀器✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) - [二哥的PmHub微服务实战项目上线了,校招和社招均可用✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) - [二哥的Java面试指南专栏更新了,求职面试必备✌️](https://javabetter.cn/zhishixingqiu/mianshi.html) @@ -573,9 +571,6 @@ ![二哥的 Java 进阶之路首页](https://cdn.tobebetterjavaer.com/stutymore/README-20230829162301.png) -如果想部署服务器,可以执行 `pnpm docs:build` 打包生成 dist 目录,里面就是静态资源文件了。 - -执行 `zip -r dist.zip dist` 压缩为 dist.zip 包,然后上传到服务器的 Nginx 对应的静态资源目录下。再执行 `unzip dist.zip` 解压即可。 # 联系作者 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..411fb0d34b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ + +node_modules/ +src/.vuepress/.cache/ +src/.vuepress/.temp/ +src/.vuepress/dist/ diff --git a/docs/src/.vuepress/sidebar.ts b/docs/src/.vuepress/sidebar.ts index 3868568ae2..2e7f743cbc 100644 --- a/docs/src/.vuepress/sidebar.ts +++ b/docs/src/.vuepress/sidebar.ts @@ -5,7 +5,6 @@ export default sidebar({ "readme.md", "jianli", "paismart", - "paismart-go", "paicoding", "pmhub", "mianshi", diff --git a/docs/src/.vuepress/theme.ts b/docs/src/.vuepress/theme.ts index c272586648..0370473217 100644 --- a/docs/src/.vuepress/theme.ts +++ b/docs/src/.vuepress/theme.ts @@ -184,9 +184,9 @@ export default hopeTheme({ notice: [ { - match: /^(?!\/zhishixingqiu\/).*$/, + path: "/", title: "二哥的编程星球", - content: "这是一个简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题的私密圈子,已经有 10000+ 名球友加入(即将涨价至 169 元)", + content: "这是一个Java面试指南 + 编程项目实战 + 简历精修 + LeetCode 刷题的私密圈子,已有 9000 名球友加入", actions: [ { text: "这就去加入", @@ -247,7 +247,6 @@ export default hopeTheme({ // 如果你需要 PWA。安装 @vuepress/plugin-pwa 并取消下方注释 pwa: { - update: "hint", favicon: "https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/favicon.ico", cacheHTML: true, cacheImage: true, diff --git a/docs/src/home.md b/docs/src/home.md index e81c2def18..567d064138 100644 --- a/docs/src/home.md +++ b/docs/src/home.md @@ -53,7 +53,7 @@ head: ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/tobebetterjavaer-map.png) -一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **10000 多名** 球友加入了(马上要涨价到 169 元,抓紧时间趁没涨价前加入吧),如果你也需要一个优质的学习环境,扫描下方的优惠券加入我们吧。 +一个人可以走得很快,但一群人才能走得更远。[二哥的编程星球](https://javabetter.cn/zhishixingqiu/)已经有 **9000 多名** 球友加入了(戳[链接](https://javabetter.cn/zhishixingqiu/)了解详情),如果你也需要一个良好的学习环境,扫描下方的优惠券加入我们吧。新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。

@@ -63,13 +63,11 @@ head:

-新人可免费体验 3 天,不满意可全额退款(只能帮你到这里了😄)。 -这是一个 **简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题**的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 +这是一个**编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子**,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的内容,**相信能帮助你走的更快、更稳、更远**。 - [二哥精修简历服务,让你投了就有笔试&面试✌️](https://javabetter.cn/zhishixingqiu/jianli.html) - [二哥的RAG知识库项目派聪明上线了,AI时代你必须拥有的实战项目✌️](https://javabetter.cn/zhishixingqiu/paismart.html) -- [Go 版本的派聪明RAG知识库项目上线了,学习 Go 语言的小伙伴有福了✌️](https://javabetter.cn/zhishixingqiu/paismart-go.html) - [二哥的技术派实战项目更新了,秋招&暑期/日常实习大杀器✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) - [二哥的PmHub微服务实战项目上线了,校招和社招均可用✌️](https://javabetter.cn/zhishixingqiu/paicoding.html) - [二哥的Java面试指南专栏更新了,求职面试必备✌️](https://javabetter.cn/zhishixingqiu/mianshi.html) diff --git a/docs/src/sidebar/sanfene/collection.md b/docs/src/sidebar/sanfene/collection.md index da3a654be8..be0f3faa2d 100644 --- a/docs/src/sidebar/sanfene/collection.md +++ b/docs/src/sidebar/sanfene/collection.md @@ -96,11 +96,11 @@ head: Java 中的队列主要通过 Queue 接口和并发包下的 BlockingQueue 两个接口来实现。 -优先级队列 PriorityQueue 是一个无界队列,它的元素按照自然顺序排序或者 Comparator 比较器进行排序。 +优先级队列 PriorityQueue 实现了 Queue 接口,是一个无界队列,它的元素按照自然顺序排序或者 Comparator 比较器进行排序。 ![李豪:优先级队列](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/PriorityQueue-8dca2f55-a7c7-49e1-95a5-df1a34f2aef5.png) -双端队列 ArrayDeque 是一个基于数组的,可以在两端插入和删除元素的队列。 +双端队列 ArrayDeque 也实现了 Queue 接口,是一个基于数组的,可以在两端插入和删除元素的队列。 ![李豪:双端队列](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/collection/arraydeque-1e7086a3-3d31-4553-aa16-5eaf2193649e.png) @@ -122,11 +122,11 @@ LinkedList 实现了 Queue 接口的子类 Deque,所以也可以当做双端 #### 队列和栈的区别了解吗? -队列是一种先进先出(FIFO, First-In-First-Out)的数据结构,第一个加入队列的元素会成为第一个被移除的元素,适用于需要按顺序处理任务的场景,比如消息队列、任务调度等。 +队列是一种先进先出(FIFO, First-In-First-Out)的数据结构,第一个加入队列的元素会成为第一个被移除的元素。 ![疯狂的技术宅:队列](https://cdn.tobebetterjavaer.com/stutymore/collection-20240412224341.png) -栈是一种后进先出(LIFO, Last-In-First-Out)的数据结构,最后一个加入栈的元素会成为第一个被移除的元素,适用于需要回溯的场景,比如函数调用栈、浏览器历史记录等。 +栈是一种后进先出(LIFO, Last-In-First-Out)的数据结构,最后一个加入栈的元素会成为第一个被移除的元素。 ![Wang Wei:栈](https://cdn.tobebetterjavaer.com/stutymore/collection-20240412224549.png) @@ -215,9 +215,9 @@ ArrayList 适用于: LinkedList 适用于: -- 在列表中间频繁插入和删除元素的场景。 -- 顺序访问多于随机访问的场景。 -- LinkedList 可以实现队列(FIFO)和栈(LIFO)。 +- 频繁插入和删除:在列表中间频繁插入和删除元素的场景。 +- 不需要快速随机访问:顺序访问多于随机访问的场景。 +- 队列和栈:由于其双向链表的特性,LinkedList 可以实现队列(FIFO)和栈(LIFO)。 #### 链表和数组有什么区别? @@ -233,10 +233,6 @@ LinkedList 适用于: > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 2 一面面试原题:ArrayList和LinkedList区别 > 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:集合里面的arraylist和linkedlist的区别是什么?有何优缺点? -memo:2025 年 9 月 07 日修改至此,[今天在帮球友修改简历时](https://javabetter.cn/zhishixingqiu/jianli.html),收到反馈说,蚂蚁集团转正了。希望看到这里的你也能顺利通过面试,拿到心仪的 offer。 - -![球友蚂蚁转正了](https://cdn.tobebetterjavaer.com/stutymore/collection-20250920183621.png) - ### 3.ArrayList 的扩容机制了解吗? 了解。当往 ArrayList 中添加元素时,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。 @@ -391,15 +387,6 @@ HashMap 的初始容量是 16,随着元素的不断添加,HashMap 就需要 扩容后的数组大小是原来的 2 倍,然后把原来的元素重新计算哈希值,放到新的数组中。 -#### 负载因子干什么用的? - -负载因子(load factor)是一个介于 0 和 1 之间的数值,用于衡量哈希表的填充程度。它表示哈希表中已存储的元素数量与哈希表容量之间的比例。 - -- 负载因子过高(接近 1)会导致哈希冲突增加,影响查找、插入和删除操作的效率。 -- 负载因子过低(接近 0)会浪费内存,因为哈希表中有大量未使用的空间。 - -默认的负载因子是 0.75,这个值在时间和空间效率之间提供了一个良好的平衡。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:讲一讲 HashMap 的原理 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为一面原题:说下 Java 容器和 HashMap > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为一面原题:说下 Redis 和 HashMap 的区别 @@ -410,10 +397,6 @@ HashMap 的初始容量是 16,随着元素的不断添加,HashMap 就需要 > 8. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:hashmap 的底层实现原理、put()方法实现流程、扩容机制? > 9. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 27 云后台技术一面面试原题:Hashmap的底层?为什么链表要变成红黑树?为什么不用平衡二叉树? -memo:2025 年 9 月 24 日修改至此。今天[有球友发私信](https://javabetter.cn/zhishixingqiu/)口碑了面渣逆袭,说老有用了,他最近拿到了招银网络科技的 offer。 - -![口碑面渣逆袭](https://cdn.tobebetterjavaer.com/stutymore/collection-20250924154732.png) - ### 9.你对红黑树了解多少? 红黑树是一种自平衡的二叉查找树: @@ -852,21 +835,15 @@ if (e.hash == hash && 树化发生在 table 数组的长度大于 64,且链表的长度大于 8 的时候。 -#### 为什么是 8 呢? - -![三分恶面渣逆袭:源码注释](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/collection-21.png) - -和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为 8 的情况,发生概率仅为 `0.00000006`。 +为什么是 8 呢?源码的注释也给出了答案。 -也就是说,在正常情况下,链表长度达到 8 是个小概率事件。 +![源码注释](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/collection-21.png) -8 是一个平衡点。当链表长度小于 8 时,即使是 O(n) 的查找,由于 n 比较小,实际性能还是可以接受的,而且链表的内存开销小。当链表长度达到 8 时,查找性能已经比较差了,这时候转换为红黑树的收益就比较明显了,因为红黑树的查找、插入、删除操作的时间复杂度都是 O(log n)。 +红黑树节点的大小大概是普通节点大小的两倍,所以转红黑树,牺牲了空间换时间,更多的是一种兜底的策略,保证极端情况下的查找效率。 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 19 JDS后端技术一面面试原题:HashMap 链表转红黑树阈值 +阈值为什么要选 8 呢?和统计学有关。理想情况下,使用随机哈希码,链表里的节点符合泊松分布,出现节点个数的概率是递减的,节点个数为 8 的情况,发生概率仅为`0.00000006`。 -memo:2025 年 8 月 9 日修改到此。 今天有读者反馈,最近在[看面渣逆袭](https://javabetter.cn/sidebar/sanfene/nixi.html),面试中碰见的概率非常高,好多别的八股没有提到的你总结的都有。能收到这样的正反馈,对我真的很重要,感谢。 - -![面渣逆袭的口碑](https://cdn.tobebetterjavaer.com/stutymore/collection-二哥您好,我不是您球友,我是后来才.png) +至于红黑树转回链表的阈值为什么是 6,而不是 8?是因为如果这个阈值也设置成 8,假如发生碰撞,节点增减刚好在 8 附近,会发生链表和红黑树的不断转换,导致资源浪费。 ### 20.HashMap扩容发生在什么时候呢? @@ -1034,10 +1011,6 @@ if (hiHead != null) > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的奇安信面经同学 1 Java 技术一面面试原题:map 集合在使用时候一般都需要写容量值?为什么要写?扩容机制? > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:hashmap 的底层实现原理、put()方法实现流程、扩容机制? -memo:2025 年 8 月 9 日修改到此。今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,收到这样一个反馈:很感谢二哥让我至少暑期上岸了,目前在荣耀。感谢球友们每一次的口碑。 - -![荣耀实习球友对简历修改的口碑](https://cdn.tobebetterjavaer.com/stutymore/collection-20250925180029.png) - ### 22.JDK 8 对 HashMap 做了哪些优化呢? ①、底层数据结构由数组 + 链表改成了数组 + 链表或红黑树的结构。 diff --git a/docs/src/sidebar/sanfene/fenbushi.md b/docs/src/sidebar/sanfene/fenbushi.md index b16cf3e258..f3cb3ce8aa 100644 --- a/docs/src/sidebar/sanfene/fenbushi.md +++ b/docs/src/sidebar/sanfene/fenbushi.md @@ -2,7 +2,6 @@ title: 分布式面试题,12道分布式八股文(8千字25张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-分布式 author: 三分恶 -date: 2025-09-23 category: - 面渣逆袭 tag: @@ -18,43 +17,19 @@ head: ## 分布式理论 -### 1. 说说 CAP 原则? +### 1\. 说说 CAP 原则? -CAP 是分布式系统设计中的一个非常重要的定理,它指出分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个特性,最多只能同时满足其中两个。 +CAP 原则又称 CAP 定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这 3 个基本需求,最多只能同时满足其中的 2 个。 -![面渣逆袭三分恶:CAP](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//fenbushi-6b0609de-e2ce-4778-b76f-018af80c617f.jpg) +![](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//fenbushi-6b0609de-e2ce-4778-b76f-018af80c617f.jpg) -一致性指的是所有节点在同一时间看到的数据都是一致的。比如我在北京的服务器上更新了用户的余额,那么上海的服务器上也应该立即看到这个更新后的余额。强一致性要求数据更新完成后,任何后续的访问都能返回更新后的值。 +| 选项 | 描述 | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Consistency(一致性) | 指数据在多个副本之间能够保持一致的特性(严格的一致性) | +| Availability(可用性) | 指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据) | +| Partition tolerance(分区容错性) | 分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障 | -可用性是指系统在正常响应时间内返回合理的响应,即使部分节点出现故障,系统仍然可以继续提供服务。比如电商网站不能因为某个数据库节点挂了就完全无法访问,用户至少应该能看到商品信息,可能只是下单功能暂时不可用。 - -分区容错性是指当网络分区故障发生时,系统仍然能够继续运行。网络分区是分布式系统中不可避免的问题,比如机房之间的网络中断,这时候不同机房的节点就无法正常通信了。 - -#### 谈一下最终一致性和强一致性个人的理解 - -强一致性要求任何时候所有节点看到的数据都是完全相同的。也就是数据一旦写入成功,系统中所有的读操作都必须能立即读到最新的值。 - -![强一致性](https://cdn.tobebetterjavaer.com/stutymore/fenbushi-20250923204736.png) - -就像银行转账一样,钱从 A 账户扣除的同时必须立即加到 B 账户上,不能存在中间状态。 - -也就是说,涉及到金额计算的,必须保证强一致性。比如用户充值,扣款成功就必须立即反映到用户余额上,而且所有的服务节点都要能看到最新的余额。 - -强一致性的代价是比较高的。为了保证所有节点的数据同步,系统需要在写操作时等待所有相关节点都确认更新完成,这会带来较高的延迟。而且在网络分区或节点故障时,系统可能会选择暂停服务来保证一致性,可用性会受到影响。 - -最终一致性相对比较宽松,它允许系统在短时间内存在数据不一致的情况,但保证经过一段时间后,所有节点的数据最终会达到一致的状态。 - -![最终一致性](https://cdn.tobebetterjavaer.com/stutymore/fenbushi-20250923204913.png) - -最终一致性的典型应用就是分布式缓存系统。比如 Redis 集群中,主节点写入成功后立即返回,然后异步复制到从节点。这种模式下,读操作可能会读到稍微过期的数据,但系统的整体性能和可用性都很好。 - ->1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的网易同学 4 云音乐后端技术面试原题:介绍一下CAP理论,谈一下最终一致性和强一致性个人的理解 - -memo:2025 年 9 月 23 日修改至此,今天[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,收到一位球友的反馈说,非常感谢二哥编程星球这个平台,让他有机会再这里学习教程、项目,以及和广大球友交流,这对于他的成长有很大帮助。这种正反馈对我真的太重要了,感谢。 - -![球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/fenbushi-20250923205115.png) - -### 2. 为什么 CAP 不可兼得呢? +### 2\. 为什么 CAP 不可兼得呢? 首先对于分布式系统,分区是必然存在的,所谓分区指的是分布式系统可能出现的字区域网络不通,成为孤立区域的的情况。 diff --git a/docs/src/sidebar/sanfene/javase.md b/docs/src/sidebar/sanfene/javase.md index 129236a607..584ab6d470 100644 --- a/docs/src/sidebar/sanfene/javase.md +++ b/docs/src/sidebar/sanfene/javase.md @@ -2,7 +2,7 @@ title: Java面试题之Java基础篇,56道Java基础八股文(2.3万字68张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-Java SE author: 三分恶&沉默王二 -date: 2025-09-19 +date: 2025-06-14 category: - 面渣逆袭 tag: @@ -246,12 +246,6 @@ System.out.println("Integer.MIN_VALUE in binary: " + Integer.toBinaryString(Inte > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的用友面试原题:说说 8 大数据类型? > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 2 一面面试原题:给Integer最大值+1,是什么结果 -memo:2025 年 8 月 23 日修改至此。 今天在修改简历的时候碰到这样一个反馈,一位球友蚂蚁已经转正了,但为了防患于未然,打算再冲一段秋招,黑马点评已经换成了[派聪明 RAG](https://javabetter.cn/zhishixingqiu/paismart.html),简历的写法直接照搬了例子,很好很新项目,直接用了。 - ->派聪明简历写法:[https://paicoding.com/column/10/2](https://paicoding.com/column/10/2) - -![蚂蚁转正的球友对派聪明的认可](https://cdn.tobebetterjavaer.com/stutymore/javase-20250926113204.png) - ### 8.自动类型转换、强制类型转换了解吗? 推荐阅读:[聊聊基本数据类型的转换](https://javabetter.cn/basic-grammar/type-cast.html) @@ -391,7 +385,7 @@ int autoAdd(int count) 推荐阅读:[计算机系统基础(四)浮点数](http://kaito-kidd.com/2018/08/08/computer-system-float-point/) -float 表示小数的方式是基于 IEEE 754 标准的,采用二进制浮点数格式。 +`float`类型的小数在计算机中是通过 IEEE 754 标准的单精度浮点数格式来表示的。 $$ V = (-1)^S \times M \times 2^E @@ -408,7 +402,9 @@ $$ ![kaito:浮点数](https://cdn.tobebetterjavaer.com/stutymore/javase-20240321112428.png) -1 位符号位、8 位指数位、23 位尾数位。符号位决定正负,0 表示正数,1 表示负数。指数位存储的是偏置后的指数,实际指数要减去 127。尾数位存储的是小数部分的二进制表示。 +1. **符号位(Sign bit)**:1 位 +2. **指数部分(Exponent)**:10 位 +3. **尾数部分(Mantissa,或 Fraction)**:21 位 按照这个规则,将十进制数 25.125 转换为浮点数,转换过程是这样的: @@ -426,10 +422,6 @@ $$ > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的帆软同学 3 Java 后端一面的原题:float 是怎么表示小数的 -memo:2025 年 08 月 23 日更新至此。[星球里其实来了](https://javabetter.cn/zhishixingqiu/)蛮多海外读书的同学,能帮助到大家我真的非常开心。 - -![海外留学生](https://cdn.tobebetterjavaer.com/stutymore/javase-20250926113917.png) - ### 16.讲一下数据准确性高是怎么保证的?(补充) > 2024 年 04 月 21 日增补 @@ -746,13 +738,13 @@ class Wanger { > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 16 暑期实习一面面试原题:请说说多态、重载和重写 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学19番茄小说一面面试原题:多态的用法,多态的实现原理 -### 20.🌟重载和重写的区别? +### 20.重载和重写的区别? 推荐阅读:[方法重写 Override 和方法重载 Overload 有什么区别?](https://javabetter.cn/basic-extra-meal/override-overload.html) -如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。 +如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载(overload)。如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。 -如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。 +如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写(override)。方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。 ![二哥的 Java 进阶之路](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/core-points/21-01.png) @@ -861,11 +853,6 @@ class ShapeDrawer { > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的帆软同学 3 Java 后端一面的原题:设计方法,李氏原则,还了解哪些设计原则 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 16 暑期实习一面面试原题:请说说多态、重载和重写 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的招银网络科技面经同学 9 Java 后端技术一面面试原题:Java设计模式中的开闭原则,里氏替换了解嘛 -> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的中小厂面经同学6 广州中厂面试原题:重写和重载 - -memo:2025 年 9 月 24 日修改至此,今天[有球友发私信](https://javabetter.cn/zhishixingqiu/)说拿到了招银网络科技的 offer,问还能不能谈薪,总包还可以,但讲真银行谈薪的空间真不大,😄 - -![拿到了招银的 offer](https://cdn.tobebetterjavaer.com/stutymore/javase-20250924154354.png) ### 21.访问修饰符 public、private、protected、以及默认时的区别? @@ -2243,28 +2230,10 @@ Java IO 流的划分可以根据多个维度进行,包括数据流的方向( #### IO 流用到了什么设计模式? -用到了——**装饰器模式**,装饰器模式的核心思想是在不改变原有对象结构的前提下,动态地给对象添加新的功能。 +其实,Java 的 IO 流体系还用到了一个设计模式——**装饰器模式**。 ![Java IO流用到装饰器模式](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javase-25.png) -具体到 Java IO 中,InputStream 和 OutputStream 这些抽象类定义了基本的读写操作,然后通过各种装饰器类来增强功能。比如 BufferedInputStream 给基础的输入流增加了缓冲功能,DataInputStream 增加了读取基本数据类型的能力,它们都是对基础流的装饰和增强。 - -```java -InputStream input = new BufferedInputStream( - new DataInputStream( - new FileInputStream("data.txt") - ) -); -``` - -这里 FileInputStream 提供基本的文件读取能力,DataInputStream 装饰它增加了数据类型转换功能,BufferedInputStream 再装饰它增加了缓冲功能。每一层装饰都在原有功能基础上增加新特性,而且可以灵活组合。 - -我对装饰器模式的理解是它很好地体现了“组合优于继承”的设计原则。优势在于运行时动态组合功能,而且遵循开闭原则,可以在不修改现有代码的情况下增加新功能。 - -memo:2025 年 9 月 19 日修改至此,今天[有球友在 VIP 群里](https://javabetter.cn/zhishixingqiu/)报喜说上岸美团了,恭喜他!9 月份正是一个收获的季节,大家都要加油哦。 - -![球友拿到美团 offer 了](https://cdn.tobebetterjavaer.com/stutymore/javase-20250919160609.png) - #### Java 缓冲区溢出,如何预防 Java 缓冲区溢出主要是由于向缓冲区写入的数据超过其能够存储的数据量。可以采用这些措施来避免: @@ -2738,7 +2707,7 @@ Person 类的信息在编译时就确定了,那假如在编译期无法确定 ![三分恶面渣逆袭:Java反射相关类](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javase-36.png) -比如说我们可以动态加载类并创建对象: +比如说我们可以装来动态加载类并创建对象: ```java String className = "java.util.Date"; @@ -2774,7 +2743,7 @@ Class clazz = Class.forName("com.example.MyClass"); Object instance = clazz.newInstance(); ``` -②、Java 的动态代理机制就使用了反射来创建代理类。代理类可以在运行时动态处理方法调用,这在实现 AOP 和拦截器时非常有用。 +②、Java 的动态代理(Dynamic Proxy)机制就使用了反射来创建代理类。代理类可以在运行时动态处理方法调用,这在实现 AOP 和拦截器时非常有用。 ```java InvocationHandler handler = new MyInvocationHandler(); @@ -2792,24 +2761,9 @@ Method testMethod = testClass.getMethod("testSomething"); testMethod.invoke(testInstance); ``` -④、最常见的是写通用的工具类,比如对象拷贝工具。比如说 BeanUtils、MapStruct 等等,能够自动拷贝两个对象之间的同名属性,就是通过反射来实现的。 - -![技术派:mapstruct](https://cdn.tobebetterjavaer.com/stutymore/javase-20250927172849.png) - - #### 反射的原理是什么? -每个类在加载到 JVM 后,都会在方法区生成一个对应的 Class 对象,这个对象包含了类的所有元信息,比如字段、方法、构造器、注解等。 - -通过这个 Class 对象,我们就能在运行时动态地创建对象、调用方法、访问字段。 - -### 反射的优缺点是什么? - -反射的优点还是很明显的。首先是能够在运行时动态操作类和对象。其次是能够编写通用的代码,一套代码可以处理不同类型的对象。还有就是能够突破访问限制,访问 private 字段和方法,这在反编译场景下很有用。 - -但反射的缺点也不少。最明显的是性能问题,反射操作比直接调用要慢很多,因为需要在运行时解析类信息、进行类型检查、权限验证等。 - -其次是反射能够绕过访问控制,访问和修改 private 成员,这会破坏类的封装。 +Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 2 Java 后端技术一面面试原题:Java 反射用过吗? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 18 成都到家面试原题:反射及其应用场景 @@ -2817,10 +2771,6 @@ testMethod.invoke(testInstance); > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 3 Java 后端技术一面面试原题:java 的反射机制,反射的应用场景 AOP 的实现原理是什么,与动态代理和反射有什么区别 > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的比亚迪面经同学 12 Java 技术面试原题:java的反射 -memo:2025 年 9 月 27 日修改至此,今天[在帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到这样一个反馈,很感动:两个月前在特别焦虑迷茫时找过一次二哥,你说了别担心先去做先去投之类的话那次沟通后让我清醒了很多,我开始停止设想坏的结果,照着简历学习八股,投简历,最后收到了面试通知,运气也很好背的八股也派上用场,虽然没有一下子变得特别厉害,但是实打实地感觉自己比以前成熟稳重点了。学习过程中发现自己蛮喜欢软开的,也能获得成就感,所以特别感谢二哥一直以来对大家的帮助。 - -![球友在星球里的成长](https://cdn.tobebetterjavaer.com/stutymore/javase-20250927172411.png) - ## JDK1.8 新特性 JDK 已经出到 17 了,但是你迭代你的版本,我用我的 8。JDK1.8 的一些新特性,当然现在也不新了,其实在工作中已经很常用了。 diff --git a/docs/src/sidebar/sanfene/javathread.md b/docs/src/sidebar/sanfene/javathread.md index ec1def96be..339b971690 100644 --- a/docs/src/sidebar/sanfene/javathread.md +++ b/docs/src/sidebar/sanfene/javathread.md @@ -1,13 +1,13 @@ --- -title: Java并发编程面试题,73道Java多线程八股文(3.5万字145张手绘图),面渣逆袭必看👍 +title: Java并发编程面试题,71道Java多线程八股文(3.5万字145张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-Java并发编程 author: 三分恶&沉默王二 category: - 面渣逆袭 tag: - 面渣逆袭 -description: 下载次数超 1 万次,3.5 万字 145 张手绘图,详解 73 道 Java 多线程面试高频题(让天下没有难背的八股),面渣背会这些并发编程八股文,这次吊打面试官,我觉得稳了(手动 dog)。 -date: 2025-09-19 +description: 下载次数超 1 万次,3.5 万字 145 张手绘图,详解 71 道 Java 多线程面试高频题(让天下没有难背的八股),面渣背会这些并发编程八股文,这次吊打面试官,我觉得稳了(手动 dog)。 +date: 2024-10-08 head: - - meta - name: keywords @@ -19,7 +19,7 @@ head: ## 前言 -3.5 万字 145 张手绘图,详解 73 道 Java 多线程面试高频题(让天下没有难背的八股),面渣背会这些并发编程八股文,这次吊打面试官,我觉得稳了(手动 dog)。 +3.5 万字 145 张手绘图,详解 71 道 Java 多线程面试高频题(让天下没有难背的八股),面渣背会这些并发编程八股文,这次吊打面试官,我觉得稳了(手动 dog)。 第一版作者是二哥编程星球的嘉宾三分恶,第二版由二哥结合球友们的面经+技术派+PmHub+mydb 的项目进行全新升级。更适合拿来背诵突击面试+底层原理理解。 @@ -165,9 +165,7 @@ fun main() = runBlocking { > 10. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的vivo 面经同学 10 技术一面面试原题:线程的概念,线程有哪些状态 > 11. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的海康威视同学 4面试原题:对协程的了解,为什么协程比线程还有更低的资源消耗 -memo:2025 年 8 月 17 日修改至此。[今天在帮球友修改简历的时候](https://javabetter.cn/zhishixingqiu/jianli.html),收到他的反馈说:上次也麻烦我帮他改了改简历,也顺利找到了实习,秋招逼近,希望我能再帮他看看实习经历。感谢球友的每一次口碑。 - -![球友对简历修改的认可。](https://cdn.tobebetterjavaer.com/stutymore/javathread-眼看秋招已经开始,打算最近离职了,更新了一版简历,麻烦二哥帮我看看。.png) +memo:2025 年 1 月 23 日修改至此。 ### 3.🌟说说线程有几种创建方式? @@ -292,14 +290,6 @@ Thread: Finalizer (ID=3) - `Thread: Signal Dispatcher (ID=4)` - 信号调度线程,处理来自操作系统的信号,将它们转发给 JVM 进行进一步处理,例如响应中断、停止等信号。 - `Thread: Monitor Ctrl-Break (ID=5)` - 监视器线程,通常由一些特定的 IDE 创建,用于在开发过程中监控和管理程序执行或者处理中断。 -#### 你平时有用过多线程吗?你在代码中是哪些场景用呢? - -用得比较多,批量数据处理、异步任务处理、定时任务调度都需要用到多线程。 - -比如说在[技术派的首页内容加载](https://javabetter.cn/zhishixingqiu/paicoding.html)中,就用到了多线程来并行加载不同的模块,提高页面的响应速度。 - -![技术派:并行首页内容加载](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250927175506.png) - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:有多少种实现线程的方法? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的农业银行同学 1 面试原题:实现线程的方式和区别 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的农业银行面经同学 3 Java 后端面试原题:说说线程的创建方法 @@ -309,9 +299,7 @@ Thread: Finalizer (ID=3) > 7. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:java 如何创建线程?每次都要创建新线程来实现异步操作,很繁琐,有了解线程池吗? > 8. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 4 一面面试原题:平时怎么使用多线程 -memo:2025 年 9 月 26 日修改至此。今天[有球友在星球里](https://javabetter.cn/zhishixingqiu/)报喜说拿到了字节的意向,感谢二哥的面渣逆袭。 - -![拿到字节的意向了](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250927175050.png) +memo:2025 年 1 月 24 日修改至此。 ### 4.🌟调用 start 方法时会执行 run 方法,那怎么不直接调用 run方法? @@ -1170,25 +1158,6 @@ localVariable.remove(); 在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。 -```java -public class DsContextHolder { - private static final ThreadLocal CONTEXT_HOLDER = new InheritableThreadLocal<>(); - - public static void reset() { - DsNode ds = CONTEXT_HOLDER.get(); - if (ds == null) { - return; - } - - if (ds.pre != null) { - CONTEXT_HOLDER.set(ds.pre); - } else { - CONTEXT_HOLDER.remove(); // 清除ThreadLocal中的数据 - } - } -} -``` - 在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。 #### ThreadLocal 有哪些优点? @@ -1205,10 +1174,6 @@ ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在 > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程) > 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 9 一面面试原题:threadlocal的优点? -memo:2025 年 8 月 23 日修改至此,今天[有球友在 VIP 群里](https://javabetter.cn/zhishixingqiu/)发贴说拿到了美团背景的转正 offer,真的要恭喜啊,8 月下旬就能转正,太舒服了。 - -![美团背景意向](https://cdn.tobebetterjavaer.com/stutymore/javathread-意向可以直接接受吧?真正下了.png) - ### 13.你在工作中用到过 ThreadLocal 吗? 有用到过,用来存储用户信息。 @@ -1229,6 +1194,9 @@ memo:2025 年 8 月 23 日修改至此,今天[有球友在 VIP 群里](https ![三分恶面渣逆袭:ThreadLoca存放用户上下文](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javathread-12.png) + +memo:2025 年 1 月 31 日修改至此。 + ### 14.🌟ThreadLocal 怎么实现的呢? 当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。 @@ -1256,20 +1224,17 @@ static class Entry extends WeakReference> { } ``` -简版回答: +总结一下: ThreadLocal 的实现原理是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。 -- 1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。 -- 2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。 -- 3、Map 的大小由 ThreadLocal 对象的多少决定。 - -![ThreadLocal 的结构](https://cdn.tobebetterjavaer.com/stutymore/javathread-20240407205747.png) +1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。 +2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。 -memo:2025 年 9 月 18 日修改至此,今天有 27 届的球友拿到了小红书的日常实习 offer,特意来报喜,还称赞了二哥的面渣逆袭和[派聪明项目](https://javabetter.cn/zhishixingqiu/paismart.html),祝贺! +3、Map 的大小由 ThreadLocal 对象的多少决定。 -![拿到小红书 offer 的球友称赞二哥的八股和派聪明](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250919154520.png) +![ThreadLocal 的结构](https://cdn.tobebetterjavaer.com/stutymore/javathread-20240407205747.png) #### 什么是弱引用,什么是强引用? @@ -1306,9 +1271,7 @@ userThreadLocal 是一个强引用,`new ThreadLocal<>()` 是一个强引用对 > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程) > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的虾皮面经同学 13 一面面试原题:threadlocal 原理 怎么避免垃圾回收? -memo:2025 年 8 月 23 日修改至此。[今天又有球友在 VIP 群里](https://javabetter.cn/zhishixingqiu/)报喜说拿到了美团的转正 offer,并且直言:暑期真的可以冲团子,转正率高达百分百。 - -![美团转正 offer+1](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250926111135.png) +memo:2025 年 02 月 01 日修改至此。 ### 15.🌟ThreadLocal 内存泄露是怎么回事? @@ -1362,36 +1325,6 @@ public void clear() { ![二哥的Java进阶之路:expungeStaleEntry](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250428095121.png) -#### 你每次操作都会remove吗? - -我不是每次操作都 remove,主要是根据使用场景来决定的。在一些短生命周期的场景中,比如处理单个 HTTP 请求的上下文信息,我通常会在请求结束时统一 remove。 - -```java -public class ReqInfoContext { - private static TransmittableThreadLocal contexts = new TransmittableThreadLocal<>(); - - public static void addReqInfo(ReqInfo reqInfo) { - contexts.set(reqInfo); - } - - public static void clear() { - contexts.remove(); // 清除ThreadLocal中的数据 - } - - public static ReqInfo getReqInfo() { - return contexts.get(); - } -} -``` - -但在一些需要跨多个方法调用保持状态的场景中,就不会每次都 remove。 - -我的使用原则是: - -- 在方法级别使用时,try-finally 保证 remove -- 在请求级别使用时,通过拦截器或 Filter 统一清理 -- 如果存储的对象比较大,使用完立即 remove -- 定期检查 ThreadLocal 的使用情况,避免遗漏 #### 那为什么 key 要设计成弱引用? @@ -1453,9 +1386,7 @@ String value = context.get(); > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:ThreadLocal,(作用,演进,软指针,删除过程) > 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 9 一面面试原题:threadlocal他会出现什么问题?出现内存泄漏怎么解决? -memo:2025 年 08 月 23 日修改至此。今天[在帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,收到这样一个反馈:1 月份加入星球,3 月份拿到一个小日常,后面又拿到了蚂蚁的暑期,周三答辩,转正没什么问题。真的非常感谢球友们的正反馈,这是我一路坚持下去的最强动力。 - -![蚂蚁暑期的球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250926111517.png) +memo:2025 年 02 月 02 日修改至此。 ### 16.ThreadLocalMap 的源码看过吗? @@ -1999,20 +1930,18 @@ volatile_write(); // 写入 volatile 变量 StoreLoad; // 保证写入后,其他线程立即可见 ``` ----这部分面试中可以不背 start--- 在 x86 架构下,通常会使用 `lock` 指令来实现写屏障,例如: ``` mov [a], 2 ; 将值 2 写入内存地址 a lock add [a], 0 ; lock 指令充当写屏障,确保内存可见性 ``` ----这部分面试中可以不背 end--- 当线程对 volatile 变量进行读操作时,JVM 会插入一个读屏障指令,这个指令会强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值。 ![三分恶面渣逆袭:volatile写插入内存屏障后生成的指令序列示意图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javathread-29.png) -当声明一个 volatile 变量 x: +我们来声明一个 volatile 变量 x: ```java volatile int x = 0 @@ -2031,88 +1960,6 @@ JVM 会在 volatile 变量的读写前后插入 “内存屏障”,以约束 C - LoadLoad 屏障会禁止 volatile 读与后续普通读操作重排 - LoadStore 屏障会禁止 volatile 读与后续普通写操作重排 -#### 单线程下不加volatile和加volatile性能开销有很大区别吗? - -单线程下,volatile 的性能开销相对较小,因为没有线程竞争和上下文切换的开销。 - -但 volatile 仍然会引入一些额外的内存屏障指令,以确保内存可见性。我之前做过一个测试,普通变量1亿次操作只用了2-3毫秒,volatile 变量 1亿次操作用了25-29毫秒,大概是普通变量的 10 倍左右。 - -![二哥的 Java 进阶之路:volatile的性能开销](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250920182509.png) - -虽然相对差异很大,但绝对时间差异很小,都是毫秒级。 - -#### volatile不是会强制写和查主内存吗,这样不会影响性能吗,像AQS等等java的工具都有用到volatile,他们是怎么解决这个性能问题的 - -volatile 确实会带来一些开销,主要包括: - -- 禁止 CPU 缓存优化,每次都要同步到主内存 -- 插入内存屏障,防止指令重排序 -- 在某些架构上,会导致 CPU 缓存行失效 - -但是!现代 CPU 和 JVM 都做了大量优化,volatile 的开销已经降低到可以接受的范围。 - -第一,现代 CPU 都有多级缓存(L1、L2、L3),volatile 变量虽然不能在寄存器中缓存,但还是可以利用 CPU 缓存的。 - -![Alance:多级缓存架构](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250922095159.png) - -只是需要通过缓存一致性协议(MESI)来保证可见性。 - -![FynnWang:MESI](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250922095121.png) - -``` -// 比如在x86架构下 -volatile int state = 0; -// 读操作:会从L1/L2/L3缓存读,只要缓存行是最新的 -// 写操作:写入缓存,同时通过MESI协议通知其他CPU缓存失效 -``` - -第二,JVM 会根据不同的 CPU 架构选择最优的内存屏障实现: - -``` -// x86架构下,volatile写入的汇编大致是: -// mov [内存地址], 寄存器 // 普通写入 -// lock addl $0, (%rsp) // 内存屏障,但x86的强内存模型下开销很小 - -// ARM架构下需要更多屏障: -// str 寄存器, [内存地址] // 写入 -// dmb sy // 数据内存屏障 -``` - -AQS 的设计非常精妙,只在绝对必要的地方使用 volatile。比如 state 必须是 volatile,因为所有线程都要看到最新值,但 Node 中的 nextWaiter 就不需要,因为它只在持有锁的情况下访问。 - -```java -public abstract class AbstractQueuedSynchronizer { - // 只有state是volatile的 - private volatile int state; - - // 队列节点大部分字段都不是volatile - static final class Node { - volatile Node prev; // 需要保证可见性的才用volatile - volatile Node next; - volatile Thread thread; - Node nextWaiter; // 不需要强一致性的就不用volatile - } -} -``` - -AQS 大量使用 Unsafe 类进行更细粒度的控制: - -```java -// 普通的volatile读写 -volatile int state; -int s = state; // volatile读 -state = s + 1; // volatile写 - -// AQS使用Unsafe -private static final Unsafe unsafe = Unsafe.getUnsafe(); -private static final long stateOffset; - -// 可以选择性地使用不同级别的内存语义 -unsafe.getInt(this, stateOffset); // 普通读 -unsafe.getIntVolatile(this, stateOffset); // volatile读 -unsafe.compareAndSwapInt(this, stateOffset, expect, update); // CAS -``` - #### volatile 和 synchronized 的区别? volatile 关键字用于修饰变量,确保该变量的更新操作对所有线程是可见的,即一旦某个线程修改了 volatile 变量,其他线程会立即看到最新的值。 @@ -2149,9 +1996,7 @@ private volatile SomeObject obj = new SomeObject(); -memo:2025 年 9 月 20 日修改至此,[今天有球友](https://javabetter.cn/zhishixingqiu/)在咨询问题的时候,提到自己腾讯暑期转正了,那么恭喜他,真的太强了。鹅厂可以说是国内最好的互联网公司了,没有之一。 - -![球友腾讯转正了](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250920183212.png) +memo:2025 年 02 月 08 日修改至此,昨天主要是做 [deepseek API 技术派的集成](https://mp.weixin.qq.com/s/F6BOxQvRELUJaU_O4dmwmQ)。 ## 锁 @@ -2254,9 +2099,7 @@ ObjectMonitor() { > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的去哪儿面经同学 1 技术二面面试原题:synchronized 底层,会不会牵扯到 os 层面 -memo:2025 年 9 月 24 日修改至此。今天[有球友在星球](https://javabetter.cn/zhishixingqiu/)发咨询说拿到了美团的日常实习 offer,问后面还要不要再找一段,那这里必须先恭喜一下他,明年三四月份可以直接暑期实习了。 - -![拿到美团日常了](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250924160421.png) +memo:2025 年 02 月 09 日修改至此。 ### 28.synchronized 怎么保证可见性? @@ -2381,23 +2224,12 @@ memo:2025 年 02 月 10 日修改至此。 推荐阅读:[偏向锁、轻量级锁、重量级锁到底是什么?](https://javabetter.cn/thread/synchronized.html) -JDK 1.6 的时候,为了提升 synchronized 的性能,引入了锁升级机制,从低开销的锁可以最大程度减少锁的竞争。 +JDK 1.6 的时候,为了提升 synchronized 的性能,引入了锁升级机制,从低开销的锁逐步升级到高开销的锁,以最大程度减少锁的竞争。 ![三分恶面渣逆袭:Mark Word变化](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javathread-34.png) 没有线程竞争时,就使用低开销的“偏向锁”,此时没有额外的 CAS 操作;轻度竞争时,使用“轻量级锁”,采用 CAS 自旋,避免线程阻塞;只有在重度竞争时,才使用“重量级锁”,由 Monitor 机制实现,需要线程阻塞。 -#### synchronized 为什么没有锁降级? - -主要原因我认为是降级的收益不大。降级不是简单地把标志位改一下就完事。重量级锁涉及到操作系统的互斥量(mutex),还有等待队列、阻塞线程。要降级的话,需要确保: - -- 没有线程在等待队列中 -- 当前没有竞争发生 -- 要安全地释放系统资源 -- 要重新初始化轻量级锁的状态 - -这一套检查和操作下来,开销不小。而且什么时候检查?每次释放锁都检查?性能损耗太大了。 - #### 了解 synchronized 四种锁状态吗? 了解。 @@ -2481,9 +2313,7 @@ JDK 1.6 的时候,为了提升 synchronized 的性能,引入了锁升级机 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的去哪儿面经同学 1 技术二面面试原题:锁升级,synchronized 底层,会不会牵扯到 os 层面 > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 2 一面面试原题:锁升级的过程? -memo:2025 年 09 月 11 日修改至此。synchronized 的锁升级是一块非常重要的内容,第二版的优化对这块内容进行了重新梳理,自认为更容易懂了,等大家的实际效果。今天有腾讯转正的球友在简历修改的邮件里提到:面渣逆袭写的真好,真的很感谢认可。 - -![球友对面渣逆袭的认可非常高](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250922094514.png) +memo:2025 年 02 月 11 日修改至此。synchronized 的锁升级是一块非常重要的内容,第二版的优化对这块内容进行了重新梳理,自认为更容易懂了,等大家的实际效果。 ### 30.🌟synchronized 和 ReentrantLock 的区别了解吗? @@ -2871,9 +2701,8 @@ class Account { ``` > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:cas 和 aba(原子操作+时间戳) -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的Oppo面经同学 15 线下面试原题:CAS操作,带来哪些问题 -memo:2025 年 2 月 13 日修改至此,[VIP 群里](https://javabetter.cn/zhishixingqiu/)已经有球友在催下一个主题了,说实话最近事情有点多,认真修改起来又会比较花时间,所以只能希望大家多理解了。 +memo:2025 年 2 月 13 日修改至此,VIP 群里已经有球友在催下一个主题了,说实话最近事情有点多,认真修改起来又会比较花时间,所以只能希望大家多理解了。 ![不过我会加油的](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250213151028.png) @@ -3623,10 +3452,6 @@ JDK 8 使用了一种更加细粒度的锁——桶锁,再配合 CAS + synchro 对于写操作,ConcurrentHashMap 优先使用 CAS 尝试插入,如果成功就直接返回;否则使用 synchronized 代码块进行加锁处理。 -memo:2025 年 9 月 19 日修改至此,今天[有球友在 VIP 群里](https://javabetter.cn/zhishixingqiu/)报喜说拿到了美团的 offer,那么必须恭喜他🎉,现在是 9 月中旬,大家一定要扛住哦,胜利就在前方。 - -![球友拿到了美团的 offer](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250919154007.png) - #### 说一下 JDK 7 中 ConcurrentHashMap 的实现原理? 好的。 @@ -3825,7 +3650,7 @@ JDK 1.7 中的 ConcurrentHashMap 使用了分段锁机制,每个 Segment 都 > 12. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:刚刚提到了Spring使用ConcurrentHashMap来实现单例模式,大致说下ConcurrentHashMap的put和get方法流程? > 13. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 29 Java 后端一面原题:ConcurrentHashMap底层是怎么实现的? -memo:2025 年 2 月 20 日修改至此,今天要[修改大量简历](https://javabetter.cn/zhishixingqiu/jianli.html),所以面渣逆袭的进度只能耽误一下了。 +memo:2025 年 2 月 20 日修改至此,今天要修改大量简历,所以面渣逆袭的进度只能耽误一下了。 ![二哥的编程星球:给星球用户修改简历](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250220113043.png) @@ -3890,13 +3715,11 @@ public V get(Object key) { > 2024 年 04 月 23 日增补,推荐阅读:[吊打 Java 并发面试官之 CopyOnWriteArrayList](https://javabetter.cn/thread/CopyOnWriteArrayList.html) -CopyOnWriteArrayList 是 ArrayList 的线程安全版本,适用于读多写少的场景。 - -CopyOnWrite 的核心思想是写操作时创建一个新数组,修改后再替换原数组,这样就能够确保读操作无锁,从而提高并发性能。 +CopyOnWriteArrayList 是 ArrayList 的线程安全版本,适用于读多写少的场景。它的核心思想是写操作时创建一个新数组,修改后再替换原数组,这样就能够确保读操作无锁,从而提高并发性能。 ![CL0610:最终一致性](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/CopyOnWriteArrayList-01.png) -内部使用 volatile 变量来修饰数组 array,以确保读操作的内存可见性。 +内部使用 volatile 变量来修饰数组 array,以读操作的内存可见性。 ```java private transient volatile Object[] array; @@ -3930,10 +3753,6 @@ public boolean add(E e) { > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯云智面经同学 16 一面面试原题:ConcurrentHashMap、CopyOnWriteArrayList 的实现原理? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 26 暑期实习微信支付面试原题:说一说常用的并发容器 -memo:2025 年 8 月 19 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,碰到这样一段正反馈:暑期 6 月底靠二哥改的简历救命拿了一家近千人的小厂实习,跪谢一波。实习润了[派聪明](https://javabetter.cn/zhishixingqiu/paismart.html),刚好和公司的智能助手匹配。 - -![](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250922165408.png) - ### 52. 能说一下 BlockingQueue 吗?(补充) > 2024 年 08 月 18 日增补,从集合框架移动到并发编程这里 @@ -3998,10 +3817,6 @@ memo:2025 年 02 月 21 日修改至此。今天的主要工作仍然是[修 举个例子:就像你开了一家餐厅,线程池就相当于固定数量的服务员,顾客(任务)来了就安排空闲的服务员(线程)处理,避免了频繁招人和解雇的成本。 -#### 线程池里面的任务队列是什么队列? - -阻塞队列。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米春招同学 K 一面面试原题:说一下为什么项目中使用线程池,重要参数,举个例子说一下这些参数的变化 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 7 Java 后端实习一面的原题:讲一下为什么引入线程池? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的微众银行同学 1 Java 后端一面的原题:说说你对线程池的理解 @@ -4171,7 +3986,7 @@ class ThreadPoolDemo { **③、workQueue**:任务队列,存储等待执行的任务。 -**④、handler**:拒绝策略,任务超载时的处理方式。也就是线程数达到 maximumPoolSize,任务队列也满了的时候,就会触发拒绝策略。 +**④、handler**:拒绝策略,任务超载时的处理方式。也就是线程数达到 maximumPoolSiz,任务队列也满了的时候,就会触发拒绝策略。 **⑤、threadFactory**:线程工厂,用于创建线程,可自定义线程名。 @@ -4338,9 +4153,7 @@ public static ExecutorService newCachedThreadPool() { > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的微众银行同学 1 Java 后端一面的原题:线程池的阻塞队列有哪些实现方式? -memo:2025年 9 月 04 日修改至此,今天在帮球友修改简历的时候,有球友反馈,靠[二哥修改的简历](https://javabetter.cn/zhishixingqiu/jianli.html)拿到了字节的实习,现在秋招,希望能继续优化一下简历。 - -![球友拿到了字节跳动的实习,简历修改的时候得到了正反馈](https://cdn.tobebetterjavaer.com/stutymore/javathread-靠二哥修改的简历找到的实习,现在要投秋招,麻烦二哥指导下简历怎么继续修改优化.png) +memo:2025 年 2 月 22 日修改至此。 ### 59.线程池提交 execute 和 submit 有什么区别? @@ -4460,9 +4273,7 @@ jstack | grep -A 20 "BLOCKED" // 查看阻塞线程 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的oppo 面经同学 8 后端开发秋招一面面试原题:线程池都有哪些以及核心参数介绍下 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的理想汽车面经同学 2 一面面试原题:JAVA中线程池有哪些? -memo:2025 年 8 月 17 日修改至此。[今天收到球友的反馈说](https://javabetter.cn/zhishixingqiu/),偶然的机会加入了星球,学习下来感觉还挺好的,感谢球友每一次的口碑,笔芯芯。 - -![球友对星球的口碑](https://cdn.tobebetterjavaer.com/stutymore/javathread-偶然一机会就加入了,确实还挺好的.png) +memo:2025 年 2 月 23 日修改至此。 ### 63.能说一下四种常见线程池的原理吗? @@ -4681,8 +4492,6 @@ memo:2025 年 2 月 24 日修改至此。今天是出考研成绩的一天, > 2024 年 03 月 16 日增补,推荐阅读:[Java线程池实现原理及其在美团业务中的实践](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) -线程池的配置优化是针对多线程应用性能调优的关键环节。 - ![三分恶面渣逆袭:线程池调优](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/javathread-82.png) 首先我会根据任务类型设置核心线程数参数,比如 IO 密集型任务会设置为 CPU 核心数\*2 的经验值。 @@ -4691,10 +4500,6 @@ memo:2025 年 2 月 24 日修改至此。今天是出考研成绩的一天, 最后,我会通过内置的监控指标建立容量预警机制。比如通过 JMX 监控线程池的运行状态,设置阈值,当线程池的任务队列长度超过阈值时,触发告警。 -memo:2025 年 9 月 26 日优化至此,今天有球友在 VIP 群里讲,投了 1 天就接面了,[二哥改的简历还是太好用](https://javabetter.cn/zhishixingqiu/jianli.html)。很感谢他的认可。 - -![球友对简历修改的认可](https://cdn.tobebetterjavaer.com/stutymore/javathread-投了1天就接面了,二哥改的简历还是太好用.png) - ### 68.线程池在使用的时候需要注意什么?(补充) > 2024 年 03 月 16 日增补 @@ -4999,26 +4804,7 @@ class SimpleConnectionPool { ## 并发容器和框架 -### 71.并发容器有哪些? - -Java 提供了多种并发容器,主要包括: - -1. **ConcurrentHashMap**:线程安全的哈希表,支持高并发读写操作。 -2. **CopyOnWriteArrayList**:写时复制的 ArrayList,适用于读多写少的场景。 -3. **BlockingQueue**:阻塞队列,常用实现有 ArrayBlockingQueue 和 LinkedBlockingQueue,适用于生产者-消费者场景。 -4. **ConcurrentLinkedQueue**:非阻塞的线程安全队列。 -5. **ConcurrentSkipListMap**:基于跳表实现的线程安全有序 Map。 - -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 34 Java 后端技术一面面试原题:并发容器有哪些? - -### 72.说说 Java 的并发关键字? - -最常用的有两个关键字,分别是: - -1. **synchronized**:用于方法或者代码块,确保同一时间只有一个线程可以执行被 synchronized 修饰的代码,适用于保护共享资源的访问。 -2. **volatile**:用于变量,确保变量的可见性,防止指令重排序,适用于状态标志等场景。 - -### 73.Fork/Join 框架了解吗? +### 71.Fork/Join 框架了解吗? 关于 Fork/Join 框架,我了解一些,它是 Java 7 引入的一个并行框架,主要用于分治算法的并行执行。这个框架通过将大的任务递归地分解成小任务,然后并行执行,最后再合并结果,以达到最高效率处理大量数据的目的。 @@ -5159,7 +4945,7 @@ memo:2025 年 2 月 26 日修改至此。终于搞定,面渣逆袭并发编 --- -图文详解 73 道 Java 并发面试高频题,这次面试,一定吊打面试官。 +图文详解 71 道 Java 并发面试高频题,这次面试,一定吊打面试官。 _没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟_。 diff --git a/docs/src/sidebar/sanfene/jvm.md b/docs/src/sidebar/sanfene/jvm.md index e764e89413..5a090456f1 100644 --- a/docs/src/sidebar/sanfene/jvm.md +++ b/docs/src/sidebar/sanfene/jvm.md @@ -68,28 +68,13 @@ JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。 这样就实现了 Java 一次编译,处处运行的特性。 -#### 如果我们要执行hello world,那虚拟机干了什么呢?谁把字节码翻译成机器码,操作时机是什么?Java虚拟机是一个执行单元吗? - -当我们写好一个 HelloWorld 程序,编译成 .class 文件后,执行 `java HelloWorld` 这条命令时,操作系统会创建一个 JVM 进程,JVM 进程启动起来后,会先初始化运行环境,包括创建类加载器、初始化内存空间、启动垃圾回收线程等等。 - -接下来,JVM 的类加载器会去找 HelloWorld.class 这个文件,读取它的字节码内容。 - -JVM 的执行引擎会将这些字节码翻译成机器码,有两种方式: - -- 第一种是解释执行。JVM 的解释器会逐条读取字节码指令,然后把它翻译成机器码执行。这个过程是动态的,每次执行都要翻译一遍。所以解释执行的速度相对较慢。 -- 第二种是即时编译。JVM 里有一个 JIT 编译器,他发现某段代码被频繁执行(比如循环里的代码或者热点方法)时,JIT 编译器就会把这段字节码直接编译成本地机器码并缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。 - -至于执行的时机,解释器的执行是立即的,当 JVM 启动后,就开始解释执行字节码了。JIT 编译器的执行时机是延后的,它需要先监控哪些代码是热点代码(通常需要执行数千次或者数万次),然后才会启动编译。 - -操作系统层面,JVM 进程是一个执行单元,由操作系统调度执行。 - #### 说说 JVM 的其他特性? ①、JVM 可以自动管理内存,通过垃圾回收器回收不再使用的对象并释放内存空间。 ②、JVM 包含一个即时编译器 JIT,它可以在运行时将热点代码缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。 -![截图来自美团技术:JIT](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-9a62fc02-1a6a-451e-bb2b-19fc086d5be0.png) +![截图来自美团技术](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/jvm/jit-9a62fc02-1a6a-451e-bb2b-19fc086d5be0.png) ③、任何可以通过 Java 编译的语言,比如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运行。 @@ -108,9 +93,6 @@ JVM 的执行引擎会将这些字节码翻译成机器码,有两种方式: > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 10 后端实习一面的原题:有了解 JVM 吗 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 20 测开一面的原题:了解过 JVM 么?讲一下 JVM 的特性 -memo:2025 年 11 月 6 日修改至此,[今天有球友](https://javabetter.cn/zhishixingqiu/)提问说拿到了京东测开的 offer,60 万总包;还有华为的 14a offer,真的恭喜他,太强了呀。 - -![京东和华为 offer](https://cdn.tobebetterjavaer.com/stutymore/jvm-20251106120847.png) ### 2.说说 JVM 的组织架构(补充) @@ -120,7 +102,7 @@ memo:2025 年 11 月 6 日修改至此,[今天有球友](https://javabetter. JVM 大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。 -![截图来源于网络:jvm 的结构](https://cdn.tobebetterjavaer.com/stutymore/what-is-jvm-20231030185742.png) +![截图来源于网络](https://cdn.tobebetterjavaer.com/stutymore/what-is-jvm-20231030185742.png) ① 类加载器,负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。 @@ -128,41 +110,9 @@ JVM 大致可以划分为三个部分:类加载器、运行时数据区和执 ③ 执行引擎,也是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器、即时编译器 JIT 和垃圾回收器。 -#### JVM 是个什么东西? - -JVM 本质上就是一个进程。当我们执行 `java -jar application.jar` 命令时,操作系统会创建一个名为 JVM 的进程。这个进程在内存里运行着一个虚拟机,这个虚拟机有自己的指令集、内存模型、执行引擎等等。 - -#### Java虚拟机和操作系统的关系到底什么,假如我是个完全不懂技术的人,举例说明让我明白 - -想象操作系统是一个大剧院。这个剧院有舞台、灯光、音响、观众座位等各种资源。剧院的管理员负责分配这些资源,决定谁在什么时候用哪个舞台。 - -现在,一个话剧团想在这个剧院里演一场就在河南。话剧团就向剧院管理员说:"我需要租用你的舞台、灯光、音响,从下午 2 点到 5 点。"剧院管理员就给他们分配了一个舞台和相关资源。 - -![jvm vs os](https://cdn.tobebetterjavaer.com/stutymore/jvm-20251106124728.png) - -操作系统是底层,它直接控制硬件资源,比如 CPU、内存、磁盘、网络等。操作系统的工作就是把这些硬件资源分配给各个应用程序使用。 - -JVM 是运行在操作系统上的一个应用程序。JVM 本身是一个进程,也就是说,从操作系统的角度看,JVM 就是一个普通的应用程序,并没有什么特别的。操作系统不知道也不关心 JVM 里面运行的是什么,它只是按照进程管理的规则来管理 JVM 这个进程。 - -### 一个操作系统有两个Java程序的话,有几个虚拟机?有没有单独的JVM进程存在?启动一个hello world编译的时候,有几个进程 - -1、每一个 Java 程序都需要一个独立的 JVM 进程来运行,两个 Java 程序就需要两个 JVM。这两个 JVM 进程是完全独立的,互不干扰。 - -2、JVM 不存在什么公共的、被所有 Java 程序共享的进程。每一个 Java 程序都必须启动自己的 JVM 进程。 - -3、如果只是编译,不运行,至少有两个进程。一个是 javac 的编译器进程。当执行 `javac HelloWorld.java` 时,操作系统会创建一个 javac 进程,这个进程会读取 .java 源文件,生成 .class 字节码文件。完成后 javac 进程就退出了。另一个是当前的 shell 进程,运行 javac 进程的终端,这是一直存在的。 - -#### JVM什么时候启动 比如执行一条Java命令的时候对应一个进程,然后这个JVM虚拟机到底是不是在这个进程里面,还是说要先启动一个JVM虚拟机的进程 - -当执行 `java HelloWorld` 这条命令的时候,操作系统就会创建 JVM 进程。没有"先启动 JVM"然后"再运行程序"这种操作。而是一步到位:操作系统启动了 JVM 进程,JVM 进程同时运行 Java 程序。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯 Java 后端实习一面原题:说说 JVM 的组织架构 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:JVM的架构,具体阐述一下各个部分的功能? -memo:2025 年 11 月 04 日修改至此,[今天有球友](https://javabetter.cn/zhishixingqiu/)私信说美团开奖了,白菜,但今年美团整体开的都是这个价,24k 左右,去年其实也差不多这个价,好在美团业务稳定,整体氛围还可以。 - -![美团开奖白菜](https://cdn.tobebetterjavaer.com/stutymore/jvm-团子今年好像给的都不高.jpg) - ## 二、内存管理 ### 3.🌟能说一下 JVM 的内存区域吗? @@ -384,10 +334,6 @@ G1|| -XX:+UseG1GC > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 2 Java 后端技术一面面试原题:说说创建对象的流程? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配) -memo:2025 年 8 月 13 日修改到此,[就像球友说的](https://javabetter.cn/zhishixingqiu/),帮大家拿下暑期实习,也会帮大家拿下秋招。 - -![暑期实习和秋招都拿下](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250925184546.png) - ### 7.堆内存是如何分配的? 在堆中为对象分配内存时,主要使用两种策略:指针碰撞和空闲列表。 @@ -472,13 +418,15 @@ class TLABDemo { 直接出现了两次 GC,因为没有 TLAB,Eden 区更快被填满,导致年轻代 GC。年轻代 GC 频繁触发,一部分长生命周期对象被晋升到老年代,间接导致老年代 GC 触发。 -### 9.🌟能说一下对象的内存布局吗? +### 9.能说一下对象的内存布局吗? 好的。 对象的内存布局是由 Java 虚拟机规范定义的,但具体的实现细节各有不同,如 HotSpot 和 OpenJ9 就不一样。 -就拿我们常用的 HotSpot 来说吧。对象在内存中包括三部分:对象头、实例数据和对齐填充。 +就拿我们常用的 HotSpot 来说吧。 + +对象在内存中包括三部分:对象头、实例数据和对齐填充。 ![三分恶面渣逆袭:对象的存储布局](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/jvm-12.png) @@ -639,11 +587,8 @@ ReferenceHolder.reference 的大小为 4 字节。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的帆软同学 3 Java 后端一面的原题:Object a = new object()的大小,对象引用占多少大小? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的去哪儿面经同学 1 技术二面面试原题:Object 底层的数据结构(蒙了) -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 19 JDS 后端一面的原题:一个对象的结构是什么,包含哪些内容 -memo:2025 年 8 月 14 日修改到此。[今天收到球友的反馈说](https://javabetter.cn/zhishixingqiu/),暑期拒了腾讯的客户端,暑期实习已经拿到了美团的意向,坐等 A 薪资。😄 - -![美团暑期转正了](https://cdn.tobebetterjavaer.com/stutymore/jvm-二哥,暑期拒了鹅客户端。团子现在拿到校招意向了~.png) +memo:2025 年 1 月 11 日修改到此 ### 10.JVM 怎么访问对象的? @@ -1187,7 +1132,7 @@ public class LargeLocalVariables { > 本题是增补的内容,by 2024 年 03 月 09 日;参照:[深入理解 JVM 的垃圾回收机制](https://javabetter.cn/jvm/gc.html) -垃圾回收就是对堆内存中已经死亡的或者长时间没有使用的对象进行清除或回收。 +垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。 JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。 @@ -1209,73 +1154,6 @@ java -XX:+UseConcMarkSweepGC \ Java 的垃圾回收过程主要分为标记存活对象、清除无用对象、以及内存压缩/整理三个阶段。不同的垃圾回收器在执行这些步骤时会采用不同的策略和算法。 -![IT 宅:垃圾回收的过程](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250922170756.png) - -#### 哪些阶段会触发 STW? - -STW 是 JVM 为了垃圾回收而暂停所有用户线程的机制。 - -![STW](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250922172410.png) - -总结一下:在标记阶段,STW 是必需的。无论是哪种垃圾回收算法,标记阶段都需要准确找出所有存活对象。如果标记的时候业务线程还在运行,就会标记不准确。因为业务线程可能会在 GC 标记某个对象为垃圾的同时,访问这个对象。为了避免这种冲突,标记阶段必须暂停所有业务线程。 - -在清除阶段,是否需要 STW 取决于算法的实现。 - -如果使用的是复制算法或标记-整理算法(需要移动对象),那就必须 STW。因为对象被移动后,所有指向这个对象的引用都要更新,这个过程中不能让业务线程访问这些对象。 - -如果使用的是标记-清除算法(不移动对象),理论上可以不 STW,业务线程和 GC 线程可以并发执行。 - -对于新生代的垃圾收集器 Serial 和 ParNew,整个垃圾回收过程都会触发 STW,但因为年轻代小,通常很快(几毫秒到几十毫秒)。 - -``` -1. 初始标记(STW) - - 暂停所有用户线程 - - 标记 GC Roots 直接引用的对象 - -2. 复制存活对象(STW) - - 将 Eden 和 Survivor0 中的存活对象复制到 Survivor1 - - 清空 Eden 和 Survivor0 - -3. 更新引用(STW) - - 更新所有指向移动对象的引用 -``` - -CMS 在此基础上进行了改进,通过并发执行来减少 STW 时间,会在初始标记和重新标记阶段触发 STW。 - -``` -1. 初始标记 Initial Mark(STW)✅ - - 时间很短 - - 仅标记 GC Roots 直接引用的对象 - - 标记所有年轻代对象引用的老年代对象 - -2. 并发标记 Concurrent Mark(并发执行) - - 用户线程继续运行 - - GC 线程遍历整个对象图 - - 标记所有存活对象 - -3. 重新标记 Remark(STW)✅ - - 修正并发标记期间的变化 - - 使用增量更新算法 - - 这个阶段时间比初始标记长 - -4. 并发清除 Concurrent Sweep(并发执行) - - 用户线程继续运行 - - 清理未标记的对象 - - 不进行压缩,会产生碎片 -``` - -G1 采用的是增量回收的思想,不是一次性把整个堆都清理,而是把堆分成很多小的区域,每次只清理一部分区域。这样就可以分散 STW 的时间,单次 STW 的时间就短了。 - -ZGC 采用了并发整理的技术,可以在应用线程运行的同时进行对象的整理和移动,几乎消除了 STW 时间。ZGC 的 STW 时间通常在 10 毫秒以内。 - -#### 垃圾回收机制的时机?能手动触发垃圾回收吗?垃圾回收会抢占业务代码的CPU吗? - -1、JVM 会监控堆内存的使用情况,当内存快要不够用时,就会自动触发垃圾回收。 - -2、理论上能手动触发垃圾回收,JVM 提供了 `System.gc()` 这个方法,可以手动调用它来触发垃圾回收,但一般不建议这么做。 - -3、垃圾回收会抢占 CPU,垃圾回收时会触发 STW,暂停所有业务线程。Minor GC 的 STW 时间较短,Major GC 的 STW 时间较长。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为 OD 技术一面遇到的一道原题。 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 2 Java 后端技术一面面试原题:了解 GC 吗?不可达判断知道吗? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 26 暑期实习微信支付面试原题:JVM 垃圾删除 @@ -1289,10 +1167,6 @@ ZGC 采用了并发整理的技术,可以在应用线程运行的同时进行 > 11. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的理想汽车面经同学 2 一面面试原题:说说你对GC的了解? > 12. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 29 Java 后端一面原题:JVM垃圾回收机制? -memo:2025 年 8 月 19 日修改到此,今天有球友发私信说[加入星球了](https://javabetter.cn/zhishixingqiu/) ,主要是因为[面渣逆袭](https://javabetter.cn/sidebar/sanfene/nixi.html)给他的体验非常不错,真的感谢口碑和支持。 - -![球友加入星球以感谢面渣逆袭对他的帮助](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250922170910.png) - ### 24.🌟如何判断对象仍然存活? Java 通过可达性分析算法来判断一个对象是否还存活。 @@ -1505,10 +1379,6 @@ class ConstantPoolReference { > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 9 面试原题:问了垃圾回收算法,针对问了每个算法的优缺点 > 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 D 小米一面原题:gc垃圾回收算法有哪些 -memo:2025 年 11 月 03 日,[今天有球友私信](https://javabetter.cn/zhishixingqiu/)问 offer 怎么选,他有一个成都的京东 sp,还有一个抖音电商,都是非常顶级的 offer,真的要恭喜他。 - -![京东 sp和字节抖音电商 offer](https://cdn.tobebetterjavaer.com/stutymore/jvm-20251106130702.png) - ### 28.Minor GC、Major GC、Mixed GC、Full GC 都是什么意思? Minor GC 也称为 Young GC,是指发生在年轻代的垃圾收集。年轻代包含 Eden 区以及两个 Survivor 区。 @@ -1699,7 +1569,7 @@ G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集 ![有梦想的肥宅:G1 收集器](https://cdn.tobebetterjavaer.com/stutymore/gc-collector-20231228213824.png) -G1 把 Java 堆划分为多个大小相等的独立区域 Region,每个区域都可以扮演新生代或老年代的角色。 +G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代或老年代的角色。 同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。 @@ -1719,55 +1589,11 @@ G1 收集器的运行过程大致可划分为这几个步骤: ![三分恶面渣逆袭:G1收集器运行示意图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/jvm-36.png) -#### 计算Region的时候怎么和业务代码并行执行 - -在 G1 中,堆被划分成很多个大小相等的区域,每个区域叫做 Region。G1 的工作方式是,每次不对整个堆进行垃圾回收,而是计算哪些 Region 中的垃圾最多、回收效率最高,然后只回收这些 Region。 - -``` -┌─────────────────────────────────────────────────┐ -│ Region1 │ Region2 │ Region3 │ Region4 │ Region5 │ -│ 年轻代 │ 年轻代 │ 老年代 │ 老年代 │ 空闲 │ -│ 垃圾: 10%│ 垃圾: 5% │ 垃圾: 70%│ 垃圾: 15%│ │ -└─────────────────────────────────────────────────┘ -``` - -第一步,G1 会启动一些 GC 标记线程,这些线程和业务线程并发运行。业务线程执行代码,标记线程同时遍历堆中的对象,找出哪些对象是活的,哪些是垃圾。 - -这里需要用到写屏障(Write Barrier)技术,每当业务线程改变对象的引用时,写屏障就会记录下来:"有个引用被改变了"。GC 线程会根据这些记录来调整标记结果。 - -```java -// 简化的写屏障概念示意 - -class Object { - Object ref; - - // 业务线程执行:obj.ref = newRef; - // 实际执行过程: - public void setRef(Object newRef) { - // 原始操作 - this.ref = newRef; - - // 写屏障操作(由 JVM 自动插入) - writeBarrier(newRef); // 通知 GC:"这个对象的引用被改变了" - } -} -``` - -第二步,并发计算每个 Region 的垃圾占比。 - -第三步,G1 根据计算结果选择垃圾最多的几个 Region 进行回收。这时候才会触发 STW,暂停业务线程,对这些 Region 进行垃圾清理。 - -因为只清理少数几个 Region,而不是整个堆。所以 STW 的时间很短。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 1 Java 技术一面面试原题:说说 G1 垃圾回收器的原理 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 1 Java 后端技术一面面试原题:对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配) > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度同学 4 面试原题:G1 垃圾回收器了解吗? > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的理想汽车面经同学 2 一面面试原题:了解过G1垃圾回收器吗? -memo:2025 年 9 月 17 日修改至此。今天[有球友特意来感谢星球](https://javabetter.cn/zhishixingqiu/),项目、八股都很有帮助,说那点钱都不好意思麻烦我。哈哈,我就想做这么一个人,让大家花最小的代码就能获得巨大的成长,这样才有口碑嘛。 - -![星球优惠完仅需 129 元,但提供项目、简历修改等各种溢价服务](https://cdn.tobebetterjavaer.com/stutymore/jvm-记得来反馈进度,我给你调整.jpg) - ### 34.有了 CMS,为什么还要引入 G1? | 特性 | CMS | G1 | @@ -1898,8 +1724,6 @@ JDK 自带的命令行工具层面,我用过 jps、jstat、jinfo、jmap、jhat ### 39.JVM 的常见参数配置知道哪些? -答:我自己用过的 JVM 调优参数主要有 `-Xms` 设置初始堆大小,`-Xmx` 设置最大堆大小,`-XX:+UseG1GC` 使用 G1 垃圾收集器,`-XX:MaxGCPauseMillis=n` 设置最大垃圾回收停顿时间,`-XX:+PrintGCDetails` 输出 GC 详细日志等。 - #### 配置堆内存大小的参数有哪些? - `-Xms`:初始堆大小 @@ -1929,12 +1753,6 @@ JDK 自带的命令行工具层面,我用过 jps、jstat、jinfo、jmap、jhat - `-XX:+PrintGCTimeStamps`:输出 GC 的时间戳(以基准时间的形式) - `-Xloggc:filename`:日志文件的输出路径 ->1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的网易面经同学 4 云音乐后端技术面试原题:jvm调优有哪些参数 - -memo:2025 年 9 月 23 日修改至此。今天在帮球友修改简历的时候,碰到这样一个反馈:没有听二哥的建议修改简历,还用了旧的,结果效果很差,现在已经按照二哥的建议增加了[派聪明 RAG 项目](https://javabetter.cn/zhishixingqiu/paismart.html)到实习经历中,希望二哥能帮忙再看一下。 - -![听劝很重要啊](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250923210204.png) - ### 40.做过 JVM 调优吗? 做过。 @@ -1955,10 +1773,6 @@ JVM 调优是一个复杂的过程,调优的对象包括堆内存、垃圾收 ### 41.CPU 占用过高怎么排查? -答:首先,使用 top 命令查看 CPU 占用情况,找到占用 CPU 较高的进程 ID。接着,使用 jstack 命令查看对应进程的线程堆栈信息。然后再使用 top 命令查看进程中线程的占用情况,找到占用 CPU 较高的线程 ID。 - -接着在 jstack 的输出中搜索这个十六进制的线程 ID,找到对应的堆栈信息。最后,根据堆栈信息定位到具体的业务方法,查看是否有死循环、频繁的垃圾回收、资源竞争导致的上下文频繁切换等问题。 - ![三分恶面渣逆袭:CPU飙高](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/jvm-43.png) 首先,使用 top 命令查看 CPU 占用情况,找到占用 CPU 较高的进程 ID。 @@ -2002,25 +1816,8 @@ printf "%x\n" PID 最后,根据堆栈信息定位到具体的业务方法,查看是否有死循环、频繁的垃圾回收、资源竞争导致的上下文频繁切换等问题。 -#### 接口超时的问题排查过吗? - -接口超时的排查要从多个层面分析。首先看应用层的监控,比如接口的响应时间分布、错误率、QPS 等指标。 - -如果有 Skywalking 这种工具,可以看调用链路的详细耗时分布,定位是哪个环节慢了。 - -![PmHub:Skywalking](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250925190010.png) - -数据库层面要检查慢查询日志,看是不是某些 SQL 执行时间过长。可能是缺少索引、查询条件不合适、或者锁等待。我会用 EXPLAIN 分析 SQL 的执行计划,看是否需要优化。 - -网络层面也要考虑,比如下游服务响应慢、网络抖动等。可以通过 ping、telnet 等工具测试网络连通性和响应时间。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里面经同学 1 闲鱼后端一面的原题:上线的业务出了问题怎么调试,比如某个线程 cpu 占用率高,怎么看堆栈信息 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:服务器的CPU占用持续升高,有哪些排查问题的手段?排查后发现是项目产生了内存泄露,如何确定问题出在哪里? -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 19 JDS 面试原题:如果一个线上服务内存、CPU飙高,或者接口超时,说说大概的排查思路?具体可以用哪些工具? - -memo:2025 年 8 月 14 日修改至此。 今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候(硬件的简历也可以修改的很好哦),碰到这样一个反馈:专业是硬件方向,也几乎每天看二哥的文章,😄 - -![硬件人也天天看二哥的文章](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250925185500.png) ### 42.内存飙高问题怎么排查? @@ -2028,7 +1825,9 @@ memo:2025 年 8 月 14 日修改至此。 今天在[帮球友修改简历](htt 排查的方法主要分为以下几步: -第一,先观察垃圾回收的情况,可以通过 `jstat -gc PID 1000` 查看 GC 次数和时间。或者使用 `jmap -histo PID | head -20` 查看堆内存占用空间最大的前 20 个对象类型。 +第一,先观察垃圾回收的情况,可以通过 `jstat -gc PID 1000` 查看 GC 次数和时间。 + +或者使用 `jmap -histo PID | head -20` 查看堆内存占用空间最大的前 20 个对象类型。 第二步,通过 jmap 命令 dump 出堆内存信息。 @@ -2102,11 +1901,6 @@ jmap -dump:format=b,file=heap pid 假如是因为 GC 参数配置不合理导致的频繁 Full GC,可以通过调整 GC 参数来优化 GC 行为。或者直接更换更适合的 GC 收集器,如 G1、ZGC 等。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 8 一面面试原题:Java 中 full gc 频繁,有哪些原因 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的网易面经同学 4 云音乐面试原题:如果频繁发生full gc,可以怎么调整,调整什么参数最好 - -memo:2025 年 9 月 17 日修改至此。今天[有球友在 VIP 群里](https://javabetter.cn/zhishixingqiu/)讲,面试中遇到 90% 的问题都在星球里,百度这边的面试官甚至直接打开二哥的 Java 进阶之路来面试([他在百度实习,已转正(实习面经贴这里了)](https://t.zsxq.com/jTYm3))。 - -![球友对星球和进阶之路的赞美](https://cdn.tobebetterjavaer.com/stutymore/jvm-感谢二哥哈哈,面试遇到的差不多90.png) @@ -2120,29 +1914,15 @@ memo:2025 年 9 月 17 日修改至此。今天[有球友在 VIP 群里](https JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终转化成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。 -![darrenqiao:类加载器](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250924160643.png) - 其中最重要的三个概念就是:类加载器、类加载过程和双亲委派模型。 - **类加载器**:负责加载类文件,将类文件加载到内存中,生成 Class 对象。 - **类加载过程**:包括加载、验证、准备、解析和初始化等步骤。 - **双亲委派模型**:当一个类加载器接收到类加载请求时,它会把请求委派给父——类加载器去完成,依次递归,直到最顶层的类加载器,如果父——类加载器无法完成加载请求,子类加载器才会尝试自己去加载。 -#### 说说类的加载过程? - -类从被加载到 JVM 开始,到卸载出内存,整个生命周期分为七个阶段,分别是载入、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析这三个阶段统称为连接。 - -![类加载过程](https://cdn.tobebetterjavaer.com/stutymore/class-load-20231031202641.png) - -除去使用和卸载,就是 Java 的类加载过程。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米暑期实习同学 E 一面面试原题:你了解类的加载机制吗? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 3 Java 后端技术一面面试原题:java 的类加载机制 双亲委派机制 这样设计的原因是什么 -memo:2025 年 9 月 24 日修改至此。今天[有球友在星球里发帖说](https://javabetter.cn/zhishixingqiu/)拿到了京东的 offer,特意感谢了[派聪明RAG](https://javabetter.cn/zhishixingqiu/paismart.html)这个项目,那真的要恭喜他! - -![京东 offer 了](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250924160802.png) - ### 46.类加载器有哪些? 主要有四种: @@ -2159,19 +1939,6 @@ memo:2025 年 9 月 24 日修改至此。今天[有球友在星球里发帖说 通过继承`java.lang.ClassLoader`类来实现。 -#### 类加载谁来执行? - -类加载由类加载器来执行,JVM 有三个重要的类加载器:启动类加载器、扩展类加载器、应用类加载器。它们遵循双亲委派机制。 - -![程序新视界:类加载器](https://cdn.tobebetterjavaer.com/stutymore/jvm-20251106114027.png) - -#### 类加载器是个什么东西? - -类加载器是 JVM 内部的一个组件。当 JVM 启动时,会在内存里创建几个类加载器实例,这些类加载器的作用就是根据类名去找到对应的 .class 文件,读取字节码内容,然后交给 JVM 进行解析和初始化。 - -![JVM 和类加载器的关系](https://cdn.tobebetterjavaer.com/stutymore/jvm-20251106114437.png) - - ### 47.能说一下类的生命周期吗? 一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 、验证、准备、解析、初始化、使用和卸载。 @@ -2280,9 +2047,7 @@ DriverManager 使用了线程上下文类加载器来加载 SPI 的实现类, 如 Spring Boot 的 DevTools 通常会自定义类加载器,优先加载新的类版本。 -memo:2025 年 8 月 16 日修改至此。今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,收到这样一个反馈:暑期实习的时候用了[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html),也找我修改了简历,最后也顺利拿到了哈啰实习,非常感谢。 - -![拿到了哈啰实习](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250924162337.png) +memo:2025 年 1 月 19 日修改至此。 ### 52.Tomcat 的类加载机制了解吗? diff --git a/docs/src/sidebar/sanfene/mysql.md b/docs/src/sidebar/sanfene/mysql.md index 10017ebb6f..2375dc8e5c 100644 --- a/docs/src/sidebar/sanfene/mysql.md +++ b/docs/src/sidebar/sanfene/mysql.md @@ -907,10 +907,10 @@ MySQL 8.0 默认的行格式是 DYNAMIC,由COMPACT 演变而来,意味着这 MySQL 支持多种存储引擎,常见的有 MyISAM、InnoDB、MEMORY 等。 -![二哥的 Java 进阶之路:存储引擎](https://cdn.tobebetterjavaer.com/stutymore/mysql-20240408073338.png) - ---这部分是帮助大家理解 start,面试中可不背--- +![二哥的 Java 进阶之路:存储引擎](https://cdn.tobebetterjavaer.com/stutymore/mysql-20240408073338.png) + 我来做一个表格对比: | 功能 | InnoDB | MyISAM | MEMORY | @@ -1337,8 +1337,6 @@ binlog 是追加写入的,文件写满后会新建文件继续写入,不会 另外,为保证两种日志的一致性,innodb 采用了两阶段提交策略,redo log 在事务执行过程中持续写入,并在事务提交前进入 prepare 状态;binlog 在事务提交的最后阶段写入,之后 redo log 会被标记为 commit 状态。 -![阿里:MySQL 两阶段提交](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250316104456.png) - 可以通过回放 binlog 实现数据同步或者恢复到指定时间点;redo log 用来确保事务提交后即使系统宕机,数据仍然可以通过重放 redo log 恢复。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 2 优选物流调度技术 2 面面试原题:redo log、bin log @@ -2504,7 +2502,7 @@ memo:2025 年 3 月 29 日修改至此,今天[有球友说](https://javabett 除了查得快,索引还能加速排序、分组、连接等操作。 -项目中最常见的做法就是通过 `create index` 为经常用作查询条件的字段建索引,比如: +可以通过 `create index` 创建索引,比如: ```sql create index idx_name on students(name); @@ -3752,7 +3750,6 @@ memo:2025 年 04 月 07 日增补至此,今天[有球友反馈](https://java >empname 和 job 两个字段是一个联合索引,而查询也恰好是这两个字段,这时候单次查询就可以达到目的,不需要回表。 可以将高频查询的字段(如 WHERE 条件和 SELECT 列)组合为联合索引,实现覆盖索引。 - 例如: ```sql @@ -4458,8 +4455,6 @@ T2 在插入 `(7, 7, '王五')` 时,会被阻塞,可以在另外一个会话 推荐阅读:[六个案例搞懂间隙锁](https://www.51cto.com/article/779551.html)、[MySQL中间隙锁的加锁机制](https://blog.csdn.net/javaanddonet/article/details/111187345) ----- 这部分是帮助大家理解 end,面试中可不背 ---- - #### 执行什么命令会加上间隙锁? 在可重复读隔离级别下,执行 FOR UPDATE / LOCK IN SHARE MODE 等加锁语句,且查询条件是范围查询时,就会自动加上间隙锁。 diff --git a/docs/src/sidebar/sanfene/network.md b/docs/src/sidebar/sanfene/network.md index f2478af6e2..a324d75761 100644 --- a/docs/src/sidebar/sanfene/network.md +++ b/docs/src/sidebar/sanfene/network.md @@ -2,8 +2,8 @@ title: 计算机网络面试题,63道计算机网络八股文(2.2万字80张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-计算机网络 description: 下载次数超 1 万次,2.2 万字 80 张手绘图,详解 63 道计算机网络面试高频题(让天下没有难背的八股),面渣背会这些计算机网络八股文,这次吊打面试官,我觉得稳了(手动 dog)。 -author: 三分恶&沉默王二 -date: 2025-09-19 +author: 三分恶 +date: 2024-12-01 category: - 面渣逆袭 tag: @@ -554,7 +554,7 @@ HTTP/2.0 基于 TCP 协议,而 HTTP/3.0 则基于 QUIC 协议,Quick UDP Conn > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 27 云后台技术一面面试原题:HTTP怎么保持长连接呢? -### 18.🌟说说 HTTP 与 HTTPS 有哪些区别? +### 18.说说 HTTP 与 HTTPS 有哪些区别? HTTPS 是 HTTP 的增强版,在 HTTP 的基础上加入了 SSL/TLS 协议,确保数据在传输过程中是加密的。 @@ -568,10 +568,6 @@ HTTP 的默认端⼝号是 80,URL 以`http://`开头;HTTPS 的默认端⼝ > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:介绍一下http和https的区别?为什么https安全? > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 30 腾讯音乐面试原题:http 和Https 有啥区别呢?https 安全在哪里 -memo:2025 年 9 月 03 日修改至此,今天[有球友发私信说](https://javabetter.cn/zhishixingqiu/)拿到了京东的正式批 offer,并且业务看上去很核心,那真的要恭喜🎉啊,你付出多少就会有多少的收获,在没有开花结果之前,就是要坚持,鼓励自己。 - -![球友拿到京东 offer 了](https://cdn.tobebetterjavaer.com/stutymore/network-二哥,忘了给你报喜了.png) - ### 19.为什么要用 HTTPS? HTTP 是明文传输的,存在数据窃听、数据篡改和身份伪造等问题。而 HTTPS 通过引入 SSL/TLS,解决了这些问题。 @@ -594,10 +590,10 @@ SSL/TLS 在加密过程中涉及到了两种类型的加密方法: ### 20.HTTPS是怎么建立连接的? -HTTPS 的连接建立在 SSL/TLS 握手之上,其过程可以分为两个阶段:握手阶段和数据传输阶段。 - ![二哥的Java进阶之路:HTTPS 连接建立过程](https://cdn.tobebetterjavaer.com/stutymore/network-20240418124713.png) +HTTPS 的连接建立在 SSL/TLS 握手之上,其过程可以分为两个阶段:握手阶段和数据传输阶段。 + ①、客户端向服务器发起请求 ②、服务器接收到请求后,返回自己的数字证书,包含了公钥、颁发机构等信息。 @@ -658,9 +654,6 @@ HTTPS 通过 SSL/TLS 协议确保了客户端与服务器之间交换的数据 > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 27 云后台技术一面面试原题:HTTPS怎么建立连接的?HTTPS怎么保证建立的信道是安全的? > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的虾皮面经同学 13 一面面试原题:https能不能抓包 -memo:2025 年 9 月 20 日修改至此,[今天有球友发喜报](https://javabetter.cn/zhishixingqiu/)说拿到了美的的 offer,虽然不是大厂,但他自己感觉很开心,并且特意感谢了我对他的鼓励。有了保底也可以无所畏惧的继续向前冲了。 - -![球友拿到美的的 offer 了](https://cdn.tobebetterjavaer.com/stutymore/network-二哥,今天我收到美的暑期实习的转正了,内容是MCU开发,虽然不是.png) ### 21.客户端怎么去校验证书的合法性? @@ -689,24 +682,7 @@ CA 签发证书的过程是非常严格的: 假如在 HTTPS 的通信过程中,中间人篡改了证书,但由于他没有 CA 机构的私钥,所以无法生成正确的 Signature,因此就无法通过校验。 -#### 如果服务端发送给客户端的加密算法是客户端没有的,这种情况会怎样? - -如果客户端不支持服务器建议的任何加密算法,那么安全连接将建立失败。 - -当客户端向服务器发起一个 HTTPS 请求时,它会发送一个 ClientHello 消息。 - -![ClientHello+ServerHello](https://cdn.tobebetterjavaer.com/stutymore/network-20250920211930.png) - -这个消息包含了客户端支持的加密算法列表(称为密码套件),以及其他一些参数。 - -服务器收到 ClientHello 后,会从客户端提供的密码套件中选择一个它也支持的密码套件,并在 ServerHello 消息中返回给客户端。 - -![ERR_SSL_VERSION_OR_CIPHER_MISMATCH](https://cdn.tobebetterjavaer.com/stutymore/network-20250920211743.png) - -如果服务器无法找到一个双方都支持的密码套件,那么它会发送一个警告消息,表示无法协商出一个共同的加密算法,随后连接将被终止。 - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 1 面试原题:HTTPS,中间人伪造证书怎么办,伪造证书机构 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 34 Java 后端一面面试原题:客户端怎么检验ca证书的合法性 ### 22.如何理解 HTTP 协议是无状态的? @@ -856,14 +832,12 @@ SYN 不仅确保了序列号的同步,使得后续的数据能够有序传输 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 9 飞书后端技术一面面试原题:TCP 为什么要三次握手 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的TP联洲同学 5 Java 后端一面的原题:Tcp三次握手,Syn的概念 -### 25.🌟TCP 握手为什么是三次,为什么不能是两次?不能是四次? +### 25.TCP 握手为什么是三次,为什么不能是两次?不能是四次? 使用三次握手可以建立一个可靠的连接。这一过程的目的是确保双方都知道对方已准备好进行通信,并同步双方的序列号,从而保持数据包的顺序和完整性。 #### 为什么 TCP 握手不能是两次? ----面试中可以不背,但需要理解 start--- - - 为了防止服务器一直等,等到黄花菜都凉了。 - 为了防止客户端已经失效的连接请求突然又传送到了服务器。 @@ -887,7 +861,7 @@ SYN 不仅确保了序列号的同步,使得后续的数据能够有序传输 但是,过了很久,那封延误的旧邮件突然也到了你朋友那里。如果没有一种机制来识别和处理这种延误的邮件,你的朋友可能会以为这是一个新的连接请求,并尝试响应它,但其实你已经重新发了请求,原来的不需要了。这就导致了不必要的混乱和资源浪费。 ----面试中可以不背,但需要理解 end--- +所以我们需要“三次握手”来确认这个过程: - 第一次握手:客户端发送 SYN 包(连接请求)给服务器,如果这个包延迟了,客户端不会一直等待,它可能会重试并发送一个新的连接请求。 - 第二次握手:服务器收到 SYN 包后,发送一个 SYN-ACK 包(确认接收到连接请求)回客户端。 @@ -977,7 +951,7 @@ SYN Flood 是一种典型的 DDos 攻击,它在短时间内,伪造**不存 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 30 腾讯音乐面试原题:tcp半连接是什么样一个状态? -### 30.🌟说说 TCP 四次挥手的过程? +### 30.说说 TCP 四次挥手的过程? TCP 连接的断开过程被形象地概括为四次挥手。 @@ -1010,7 +984,7 @@ TCP 连接的断开过程被形象地概括为四次挥手。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯同学 25 后端开发实习一面面试原题:TCP和UDP,TCP连接和断开过程 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学19番茄小说一面面试原题:TCP断开连接过程 -### 31.🌟TCP 挥手为什么需要四次呢? +### 31.TCP 挥手为什么需要四次呢? 因为 TCP 是全双工通信协议,数据的发送和接收需要两次一来一回,也就是四次,来确保双方都能正确关闭连接。 @@ -1023,11 +997,6 @@ TCP 连接的断开过程被形象地概括为四次挥手。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 30 腾讯音乐面试原题:tcp 的挥手为什么是四次,而不是三次呢? -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学34一面面试原题:tcp握手为什么是3次,挥手为什么是4次 - -memo:2025 年 9 月 04 日修改至此,今天[有球友在星球里提问说](https://javabetter.cn/zhishixingqiu/)拿到了科大讯飞和长鑫存储的 offer,长鑫直接给开奖了,真的速度。 - -![球友拿到科大讯飞和长鑫存储的 offer](https://cdn.tobebetterjavaer.com/stutymore/network-20250920194015.png) ### 32.TCP 四次挥手过程中,为什么需要等待 2MSL, 才进入 CLOSED 关闭状态? diff --git a/docs/src/sidebar/sanfene/nixi.md b/docs/src/sidebar/sanfene/nixi.md index 8b79a4b0a8..63bdc31eef 100644 --- a/docs/src/sidebar/sanfene/nixi.md +++ b/docs/src/sidebar/sanfene/nixi.md @@ -12,95 +12,41 @@ head: content: Java面试题,JavaSE面试题,Java基础面试题,Java集合框架面试题,Java容器面试题,Java虚拟机面试题,JVM面试题,Spring面试题,Redis面试题,MyBatis面试题,MySQL面试题,操作系统面试题,OS面试题,计算机网络面试题,RocketMQ面试题,面试题,八股文,java,springboot,spring,jvm,redis,mybatis,mysql,操作系统,计算机网络,RocketMQ,分布式,微服务,设计模式,Linux --- -大家好,我是二哥呀,今天给大家隆重介绍一下面渣逆袭 PDF 2.0 版,共有 30 多万字,400+张手绘图,涵盖了 `Java基础`、`Java集合`、`Java并发`、`JVM`、`Spring`、`MyBatis`、`计算机网络`、`操作系统`、`MySQL`、`Redis`、`RocketMQ`、`分布式`、微服务、设计模式、Linux 等 16 个大的主题,可以说是诚意满满。 +大家好,我是二哥呀,今天给大家隆重推荐一下星球嘉宾三分恶的面渣逆袭,一份高质量的面试题八股文,涵盖了`Java基础`、`Java集合`、`Java并发`、`JVM`、`Spring`、`MyBatis`、`计算机网络`、`操作系统`、`MySQL`、`Redis`、`RocketMQ`、`分布式`、微服务、设计模式、Linux 等 16 个大的主题,共有 30 多万字,400+张手绘图,可以说是诚意满满。 -![](https://cdn.tobebetterjavaer.com/paicoding/689744a38afaac9da561338752cd88d9.png) - -二哥带背,顺嘴、好背! - -先来看一下小红书上吹捧面渣逆袭的,证据 1: - - -![](https://cdn.tobebetterjavaer.com/paicoding/d3d4ca028c6f6e036b49177d3a9af743.png) - -再来看一下牛客上吹捧面渣逆袭的,证据 2: - - -![](https://cdn.tobebetterjavaer.com/paicoding/dcb1536acc0e40ab1cb05a024cafc55b.png) - -再来看一下我平常收到的感谢面渣逆袭的私信,证据 3: - - -![](https://cdn.tobebetterjavaer.com/paicoding/687db7e27337320281294075ce2ab6ae.png) - -真的非常感谢大家的口碑,能活到现在,全靠大家的正反馈啊。 - -![](https://cdn.tobebetterjavaer.com/paicoding/a1cac5901110b8cda307d8c90beafef7.jpg) - -由于 PDF 没办法自我更新,所以需要最新版的同学,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 - -
- 微信扫码或者长按识别,或者微信搜索“沉默王二” -
- - -百度网盘、阿里云盘、夸克网盘都可以下载到最新版本,我会第一时间更新上去。 - -![回复 222](https://cdn.tobebetterjavaer.com/stutymore/javase-20241230171125.png) - -## 持续迭代 - -第二版的 PDF,我升级了很多内容: - -- 对于高频题,会标注在《[Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。 -- 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。 -- 结合项目([技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)、[pmhub](https://javabetter.cn/zhishixingqiu/pmhub.html)、[派聪明 RAG](https://javabetter.cn/zhishixingqiu/paismart.html))来组织语言,让面试官最大程度感受到你的诚意,而不是机械化的背诵。 -- 修复第一版中出现的问题,包括球友们的私信反馈,网站留言区的评论,以及 [GitHub 仓库](https://github.com/itwanger/toBeBetterJavaer/issues)中的 issue,让这份面试指南更加完善。 -- 增加[二哥编程星球](https://javabetter.cn/zhishixingqiu/)的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。 -- 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。 - -## 好评如潮 - -有球友在做面试官的时候偷偷打开[面渣逆袭网站](https://javabetter.cn/sidebar/sanfene/nixi.html),然后随便挑点问题问。 +有球友在做面试官的时候偷偷打开面渣逆袭,然后随便挑点问题问。 ![做面试官的球友通过面渣逆袭来考察求职者](https://cdn.tobebetterjavaer.com/stutymore/nixi-20240802112009.png) -还有球友[付费加入星球](https://javabetter.cn/zhishixingqiu/)就是为了回馈面渣逆袭给他的帮助,虽然面渣逆袭是开源的、公开的、免费的。但他还是愿意支持二哥,真的很感动,很感动。 +还有球友付费就是为了交面渣逆袭的学费,虽然我不敢说这是市面上最好的八股,但确实对大家应该是有帮助的。 ![求职的同学靠面渣逆袭吊打面试官,闭环了](https://cdn.tobebetterjavaer.com/stutymore/nixi-20240802112329.png) -还有不少同学把[面渣逆袭](https://javabetter.cn/sidebar/sanfene/nixi.html)打印成纸质版,看起来真的非常治愈,封面好看不说,排版也是精美。 +还有读者问怎么付费购买纸质版[面渣逆袭](https://javabetter.cn/sidebar/sanfene/nixi.html),说看到网友有这个,好羡慕啊。说实话,第一眼看到这个封面,真的觉得挺惊艳(虽然是我设计的)。😄 ![读者已经把面渣逆袭彩印了,好漂亮啊](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250319163123.png) -说真心话,能帮助到大家,能给打印店增加一点收入,我还是挺骄傲的,😄 - -![](https://cdn.tobebetterjavaer.com/paicoding/a80923e02504842bcd29e7f334c399e8.jpg) - -## 循序渐进 - -下面我再针对面渣逆袭的内容进行一个说明。先给大家看看面渣的内容结构: - - -![](https://cdn.tobebetterjavaer.com/paicoding/d86e89175a26c0a61018cda1efa86706.png) -每一部分都是循序渐进,由浅入深,看看目录就知道了: +>下面我将用三弟来代替作者三分恶滴滴,方便行文逻辑。 +## 内容体系全面 -![Java 基础+集合框架](https://cdn.tobebetterjavaer.com/paicoding/def927beff6b1f698c526539ee28705d.png) +话不多说,先给大家看看面渣的内容结构: +![面渣逆袭一共 6 大部分](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-a1c872c8-ab8e-454f-b8ab-7bb3d1cab5f0.jpg) -![并发编程+JVM](https://cdn.tobebetterjavaer.com/paicoding/7200a16b6bda5bcfcea43674f9f6ec36.png) +而且内容不是杂乱无章的,每一部分都是循序渐进,由浅入深,看看目录就知道了: -![Spring+MySQL+Redis](https://cdn.tobebetterjavaer.com/paicoding/254996528801685c52c16cf7cc2aa460.jpg) +![Java 篇(Java 基础、并发编程、JVM)](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//nixi-75f87853-ca11-4e6e-ab3f-ba8335a24783.png) +![Spring+MySQL+Redis](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//nixi-fda2401a-1162-438d-8fe8-19876224d2b1.png) ![操作系统+计算机网络+RocketMQ](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//nixi-3b3d55c6-7b18-4b0a-83b4-5a8535461ddb.png) 成体系的知识才是最有价值的,你不仅可以把面渣当做一本面试题解,还可以当作学习指南,带着面渣中的问题,相信你学习起来也会事半功倍。 -## 图文并茂 +## 图文并貌,深入浅出 大家都知道,图比文字更好理解、更好记忆,看过面渣逆袭系列的同学应该都会有这个感受,每个主题经常是三十问、五十图,基本上做到了图比问题多,能用图说话就使劲肝图。 @@ -108,7 +54,7 @@ head: ![三分恶面渣逆袭:HTTPS](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-a037df66-a8ad-4494-aa52-2df6bc9c1c1f.jpg) -也会用一些有意思的比喻去讲解技术,努力让枯燥的知识点变得生动起来。比如,经典的四次挥手,很多同学看完,直呼“满脑子都是分手”: +三弟还喜欢用一些有意思的比喻去讲解技术,努力让枯燥的知识点变得生动起来。比如,经典的四次挥手,很多同学看完,直呼“满脑子都是分手”: ![三分恶面渣逆袭: 四次挥手](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-8ec371eb-9f68-44ad-8d94-0ddbeae72b48.jpg) @@ -124,28 +70,19 @@ head: 这样的例子还有很多,我就不再一一举例了。 -## 在线阅读 +## 持续迭代,不断完善 -考虑到有些同学喜欢在线版,我们也同时上架了 Java 进阶之路网站,记住这个网址:`javabetter.cn`。 +这份八股不是一揽子买卖,而是像一个产品一样,既持续修复 bug、优化体验,又不断迭代新功能。 +二哥也会在此基础上进行优化和更新,包括: -![](https://cdn.tobebetterjavaer.com/paicoding/a8a4641879fcb24995405bd21e282b04.png) +- “迭代新的功能”——分布式、微服务、Dubbo、Elasticsearch、数据结构与算法、系统设计…… +- “修复现有的小 Bug”——随时接受大家的反馈,精益求精,不断打磨内容…… +- “优化用户体验”——优化答案、添加更多图解,后面还会推出突击版。 -Google 搜【面渣逆袭】关键字也行。 +## 在线版阅读 - -![](https://cdn.tobebetterjavaer.com/paicoding/3cd42f3189594836b597b63c243898e1.png) - -bing 搜【面渣逆袭】也是排名第一。 - -![](https://cdn.tobebetterjavaer.com/paicoding/4eba15bf352c722a9208d818d34ac211.png) - -百度 AI 搜【面渣逆袭】也可以。 - -![](https://cdn.tobebetterjavaer.com/paicoding/187ec06a8c71079f09beb73b3be86dc3.jpg) - - -包括 Java 基础(JavaSE)、Java 集合框架、Java 并发编程(Java 多线程)、Java 虚拟机(JVM)、Spring、MySQL、Redis、MyBatis、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式、Linux 等等,助你拿到心仪 offer! +硬核理解版八股文,包括 Java 基础(JavaSE)、Java 集合框架、Java 并发编程(Java 多线程)、Java 虚拟机(JVM)、Spring、MySQL、Redis、MyBatis、操作系统、计算机网络、RocketMQ、分布式、微服务、设计模式等等,助你拿到心仪 offer! - [面渣逆袭(MySQL 面试题八股文)必看 👍](/sidebar/sanfene/mysql.md) - [面渣逆袭(Redis 面试题八股文)必看 👍](/sidebar/sanfene/redis.md) @@ -163,70 +100,73 @@ bing 搜【面渣逆袭】也是排名第一。 - [面渣逆袭(设计模式面试题八股文)必看 👍](/sidebar/sanfene/shejimoshi.md) - [面渣逆袭(Linux 面试题八股文)必看 👍](/sidebar/sanfene/linux.md) -## PDF介绍 +## PDF 介绍 有些同学喜欢打印或者阅读 PDF 版本,这里也安排上了。先带大家预览一下: -1、面渣逆袭Java 基础 PDF 亮白版 +1、Java 基础 -![面渣逆袭Java 基础 PDF 亮白版](https://cdn.tobebetterjavaer.com/stutymore/javase-20241230173846.png) +![1、Java 基础](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-472ca179-c5f6-4af2-9a06-75dd8902599a.jpg) -2、面渣逆袭Java 集合框架/容器epub 版 +2、Java 集合 -![面渣逆袭Java 集合框架/容器epub 版](https://cdn.tobebetterjavaer.com/stutymore/collection-20250108182335.png) +![2、Java 集合](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-723dc8ec-c3a8-47ea-96c6-40520b064ffb.jpg) -3、面渣逆袭Java 并发编程暗黑版PDF +3、Java 并发 -![面渣逆袭Java 并发编程暗黑版](https://cdn.tobebetterjavaer.com/stutymore/javathread-20250226113113.png) +![3、Java 并发](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-f1beb175-0099-4615-847f-9ea375e391ee.jpg) -4、面渣逆袭JVM亮白版 PDF +4、JVM -![面渣逆袭JVM亮白版 PDF](https://cdn.tobebetterjavaer.com/stutymore/jvm-20250121142158.png) +![4、JVM](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-ab2a4a0f-9006-4b83-a8c9-56a853829bb5.jpg) -5、面渣逆袭 Spring 亮白版 PDF +5、Spring -![面渣逆袭 Spring 亮白版 PDF](https://cdn.tobebetterjavaer.com/stutymore/spring-20250818102050.png) +![5、Spring](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-12c239b3-0b95-414b-b83f-f5a2f46dbde0.jpg) -6、面渣逆袭MyBatis PDF +6、MyBatis ![6、MyBatis](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-862defac-32d6-4089-a5dd-e57d114b83b2.jpg) -7、面渣逆袭计算机网络PDF +7、计算机网络 ![7、计算机网络](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-71d6389c-d984-4c02-af74-45038616520f.jpg) -8、面渣逆袭操作系统PDF +8、操作系统 ![8、操作系统](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-705651d8-1417-4c26-be5d-3f155f4b3551.jpg) -9、面渣逆袭MySQL PDF 2.0 版 +9、MySQL + +![9、MySQL](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-705acc6c-6554-4c36-a47f-d2c850287126.jpg) -![面渣逆袭MySQL PDF 2.0 版](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250427104843.png) +10、Redis -10、面渣逆袭Redis PDF 2.0 版 +![10、Redis](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-b98e0491-72f5-468a-a3a9-b55d8c205a14.jpg) -![面渣逆袭Redis PDF 2.0 版](https://cdn.tobebetterjavaer.com/stutymore/redis-20250614154601.png) +11、RocketMQ -11、面渣逆袭RocketMQ PDF +![11、RocketMQ](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-18d350fd-5609-430e-8f7f-8f0848015a30.jpg) -![面渣逆袭RocketMQ PDF](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-tuxbgzdtdl-18d350fd-5609-430e-8f7f-8f0848015a30.jpg) +12、分布式 -12、面渣逆袭分布式 PDF +![分布式](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//nixi-cdfba194-459d-4ec6-8b9f-07f6cca4ac80.png) -![面渣逆袭分布式 PDF](http://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene//nixi-cdfba194-459d-4ec6-8b9f-07f6cca4ac80.png?&cause=saveError!) +13、微服务 -13、面渣逆袭微服务PDF +![微服务](https://cdn.tobebetterjavaer.com/stutymore/nixi-20230918213033.png) -![面渣逆袭微服务PDF](https://cdn.tobebetterjavaer.com/stutymore/nixi-20230918213033.png) +由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 -由于 PDF 没办法自我更新,所以需要最新版的同学,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 +当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。 + +更别说我付出的时间和精力了。
微信扫码或者长按识别,或者微信搜索“沉默王二”
- 百度网盘、阿里云盘、夸克网盘都可以下载到最新版本,我会第一时间更新上去。 ![回复 222](https://cdn.tobebetterjavaer.com/stutymore/javase-20241230171125.png) \ No newline at end of file diff --git a/docs/src/sidebar/sanfene/redis.md b/docs/src/sidebar/sanfene/redis.md index b833621224..2da7188b8f 100644 --- a/docs/src/sidebar/sanfene/redis.md +++ b/docs/src/sidebar/sanfene/redis.md @@ -3,7 +3,7 @@ title: Redis面试题,57道Redis八股文(4.6万字286张手绘图),面 shortTitle: 面渣逆袭-Redis description: 下载次数超 1 万次,4.6 万字 286 张手绘图,详解 57 道 Redis 面试高频题(让天下没有难背的八股),面渣背会这些 Redis 八股文,这次吊打面试官,我觉得稳了(手动 dog)。 author: 三分恶 -date: 2025-09-23 +date: 2024-10-31 category: - 面渣逆袭 tag: @@ -171,13 +171,6 @@ redis-cli -h slaveof no one redis-cli -h slaveof ``` -#### 用过哪些缓存数据库,除redis以外? - -[技术派实战项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中还用到了 Guava Cache 和 Caffeine 作为本地缓存,Guava Cache 适合小规模缓存,Caffeine 性能更好,支持更多高级特性。 - -![Guava Cache 和 Caffeine](https://cdn.tobebetterjavaer.com/stutymore/redis-20250923201021.png) - -Caffeine 通常用来作为二级缓存来使用,主要用于存储一些不经常变动的数据,以减轻 Redis 的压力。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为一面原题:说下 Redis 和 HashMap 的区别 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动商业化一面的原题:Redis 和 MySQL 的区别 @@ -193,10 +186,6 @@ Caffeine 通常用来作为二级缓存来使用,主要用于存储一些不 > 12. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为 OD 面经同学 1 一面面试原题:Redis 的了解, 部署方案? > 13. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 30 腾讯音乐面试原题:redis的部署方式都有哪些呢,各自有什么优缺点? -memo:2025 年 9 月 23 日修改至此,今天[帮球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,收到一位球友的反馈说,从 8.16 加入星球以来,每天都在星球里充电学习,学到了很多东西。对于这种正反馈我是非常开心的。 - -![球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/redis-M8.16加入星球以来,每天都在星球充电学习,学到了很多东西,也祝二哥的星球越办越好!.png) - ### 2.Redis 可以用来干什么? Redis 可以用来做缓存,比如说把高频访问的文章详情、商品信息、用户信息放入 Redis 当中,并通过设置过期时间来保证数据一致性,这样就可以减轻数据库的访问压力。 @@ -258,14 +247,6 @@ String key = "token_bucket:user:123"; Long allowed = (Long) redis.eval(luaScript, 1, key, String.valueOf(capacity), String.valueOf(rate), String.valueOf(now)); ``` -#### redis做缓存要考虑哪些问题,在业务方面呢 - -一类是经典的缓存系统设计问题(穿透、击穿、雪崩),另一类是与业务逻辑紧密相关的业务缓存问题(数据一致性、缓存粒度等)。 - -当修改了数据库的数据后,如何保证缓存里的数据也同步更新?如果处理不好,用户就会看到“脏数据”。 - -另外就是我们应该缓存一个完整的、包含各种关联信息的复杂对象,还是只缓存那些最常用的基础字段? - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的农业银行面经同学 7 Java 后端面试原题:Redis 相关的基础知识 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 7 Java 后端实习一面的原题:讲一下为什么要用 Redis 去存权限列表? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 20 测开一面的原题:redis 有什么好处,为什么用 redis @@ -2483,9 +2464,8 @@ memo:2025 年 5 月 20 日,今天[有球友发贴](https://javabetter.cn/zhi ![技术派教程:MySQL 和 Redis 一致性](https://cdn.tobebetterjavaer.com/stutymore/redis-20240325221330.png) -具体做法是读取时先查 Redis,未命中再查 MySQL,同时为缓存设置一个合理的过期时间;更新时先更新 MySQL,再删除 Redis。 -这种方式简单有效,适用于读多写少的场景。TTL 过期时间也能够保证即使更新操作失败,未能及时删除缓存,过期时间也能确保数据最终一致。 +具体做法是读取时先查 Redis,未命中再查 MySQL,同时为缓存设置一个合理的过期时间;更新时先更新 MySQL,再删除 Redis。 ```java // 读取逻辑 @@ -2516,6 +2496,8 @@ public void updateUser(UserInfo user) { } ``` +这种方式简单有效,适用于读多写少的场景。TTL 过期时间也能够保证即使更新操作失败,未能及时删除缓存,过期时间也能确保数据最终一致。 + #### 那再来说说为什么要删除缓存而不是更新缓存? 最初设计缓存策略时,我也考虑过直接更新缓存,但通过实践发现,删除缓存是更优的选择。 diff --git a/docs/src/sidebar/sanfene/rocketmq.md b/docs/src/sidebar/sanfene/rocketmq.md index f9030a64c4..5c134eff61 100644 --- a/docs/src/sidebar/sanfene/rocketmq.md +++ b/docs/src/sidebar/sanfene/rocketmq.md @@ -3,7 +3,6 @@ title: 消息队列面试题之RocketMQ篇,23道RocketMQ八股文(1.1万字4 shortTitle: 面渣逆袭-RocketMQ description: 下载次数超 1 万次,1.1 万字 45 张手绘图,详解 23 道 RocketMQ 面试高频题(让天下没有难背的八股),面渣背会这些 RocketMQ 八股文,这次吊打面试官,我觉得稳了(手动 dog)。 author: 三分恶 -date: 2025-09-24 category: - 面渣逆袭 tag: @@ -14,209 +13,73 @@ head: content: RocketMQ面试题,RocketMQ,面试题,八股文 --- -![面渣逆袭RocketMQ篇封面图](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-mianzhanixi-rocketmq1.jpg) - -## 前言 - 1.1 万字 45 张手绘图,详解 23 道 RocketMQ 面试高频题(让天下没有难背的八股),面渣背会这些 RocketMQ 八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/N6wq52pBGh8xkS-5uRcO2g),作者:三分恶,戳[原文链接](https://mp.weixin.qq.com/s/IvBt3tB_IWZgPjKv5WGS4A)。 -亮白版本更适合拿出来打印,这也是很多学生党喜欢的方式,打印出来背诵的效率会更高。 - -![面渣逆袭RocketMQ篇.pdf第二版](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20250427104843.png) - -2025 年 11 月 02 日开始着手第二版更新。 - -- 对于高频题,会标注在《[Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。 -- 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。 -- 结合项目([Spring Boot+React 前后端分离 web 项目技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)、[微服务pmhub](https://javabetter.cn/zhishixingqiu/pmhub.html)、[RAG 项目派聪明AI 知识库](https://javabetter.cn/zhishixingqiu/paismart.html))来组织语言,让面试官最大程度感受到你的诚意,而不是机械化的背诵。 -- 修复第一版中出现的问题,包括球友们的私信反馈,网站留言区的评论,以及 [GitHub 仓库](https://github.com/itwanger/toBeBetterJavaer/issues)中的 issue,让这份面试指南更加完善。 -- 增加[二哥编程星球](https://javabetter.cn/zhishixingqiu/)的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。 -- 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。 - -![面渣逆袭已经提交 1457 次 GitHub 记录](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250427100320.png) - -由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 - -
- 微信扫码或者长按识别,或者微信搜索“沉默王二” -
- -当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。 - -更别说我付出的时间和精力了,大家觉得有帮助还请给个口碑,让你身边的同事、同学都能受益到。 - -![回复 222](https://cdn.tobebetterjavaer.com/stutymore/collection-20250512160410.png) - -我把二哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有面渣逆袭的版本都放进来了,涵盖 Java基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机网络、操作系统、MySQL、Redis、RocketMQ、分布式、微服务、设计模式、Linux 等 16 个大的主题,共有 40 多万字,2000+张手绘图,可以说是诚意满满。 - -展示一下暗黑版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服一点。 - -![面渣逆袭MySQL篇.pdf暗黑版](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250427105032.png) - - - ## 基础 ### 1.为什么要使用消息队列呢? -我认为消息队列的核心价值主要体现在四个方面。首先是解耦,这是最重要的。 +消息队列(Message Queue, MQ)是一种非常重要的中间件技术,广泛应用于分布式系统中,以提高系统的可用性、解耦能力和异步通信效率。 -![三分恶面渣逆袭:消息队列解耦](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-dd332b3f-d5e5-41bc-813a-9f612e582255.jpg) +①、**解耦** -比如在[派聪明 RAG 项目](https://javabetter.cn/zhishixingqiu/paismart.html)中,文件上传完成之后,会有很多后续的任务,比如提取元数据、生成全文索引、做 AI 向量化处理。 +生产者将消息放入队列,消费者从队列中取出消息,这样一来,生产者和消费者之间就不需要直接通信,生产者只管生产消息,消费者只管消费消息,这样就实现了解耦。 -这些处理不仅数据量大,而且任务本身也比较消耗资源,没有消息队列的话,文件上传服务就得等这些任务都处理完才能返回结果给用户,体验会很差。 - -![派聪明:消息队列配置](https://cdn.tobebetterjavaer.com/paicoding/3e546d9fd7c4e11f7c88aaf1f4a4dce7.png) - -于是我们引入了 Kafka 来做消息队列,文件上传服务只需要把文件处理任务发送到消息队列里就可以结束了。其他服务各自去消费这条消息,独立处理自己的业务逻辑。这样即使某个服务宕机了,也不会影响文件上传的核心流程,系统的容错能力就大大提升了。 +![三分恶面渣逆袭:消息队列解耦](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-dd332b3f-d5e5-41bc-813a-9f612e582255.jpg) -再比如书在 [PmHub](https://javabetter.cn/zhishixingqiu/pmhub.html) 中,任务审批就用了 RocketMQ 来做解耦。 +像 [PmHub](https://javabetter.cn/zhishixingqiu/pmhub.html) 中的任务审批,就用了 RocketMQ 来做解耦。 ![PmHub 的面试系列教程](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20240808102425.png) -其次是异步处理,系统可以将那些耗时的任务放在消息队列中异步处理,从而快速响应用户的请求。比如说,用户下单后,系统可以先返回一个下单成功的消息,然后将订单信息放入消息队列中,后台系统再去处理订单信息。 +②、**异步**: + +系统可以将那些耗时的任务放在消息队列中异步处理,从而快速响应用户的请求。比如说,用户下单后,系统可以先返回一个下单成功的消息,然后将订单信息放入消息队列中,后台系统再去处理订单信息。 ![三分恶面渣逆袭:消息队列异步](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-40d8f782-08cf-48c4-98b2-cb125d287e93.jpg) -再有就是削峰填谷,这一点在高并发场景下特别重要。比如秒杀活动,瞬间可能来了几十万个请求。如果直接打到数据库,系统肯定会崩溃。但通过消息队列,所有请求先进队列,后端消费者按照自己的处理能力逐个消费,即使暂时处理不过来,消息也能安全地存储在队列里。这样系统就不会被突发流量打倒。 +③、**削峰**: -![三分恶面渣逆袭:消息队列削峰](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-f028cb0c-b1a3-47ef-b290-f7d6f46512fb.jpg) +削峰填谷是一种常见的技术手段,用于应对系统高并发请求的瞬时流量高峰,通过消息队列,可以将瞬时的高峰流量转化为持续的低流量,从而保护系统不会因为瞬时的高流量而崩溃。 -除此之外,消息队列还支持持久化存储,支持消息重试和事务机制。这样即使消费者在处理消息时出现异常,消息也不会丢失,可以重新投递处理,最终保证业务逻辑一定会被正确执行。 +![三分恶面渣逆袭:消息队列削峰](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-f028cb0c-b1a3-47ef-b290-f7d6f46512fb.jpg) #### 如何用RocketMQ做削峰填谷的? -我的理解是:用户的所有请求不直接打到后端服务,而是先发送到 RocketMQ 的消息队列里。RocketMQ 作为一个高吞吐量的中间件,能够快速接收这些请求。然后消费者端根据自己的处理能力,按照一定的速度从队列里拉取消息进行处理。这样就形成了一个缓冲区,能够吸收掉突发的流量。 +用户请求到达系统后,由生产者接收请求并将其转化为消息,发送到 RocketMQ 队列中。队列用来充当缓冲区,将大量请求按照顺序排队,这样就可以削减请求高峰时对后端服务的直接压力。 -就拿秒杀场景来举例吧。首先,用户的秒杀请求不是直接去扣减库存,而是先发一条消息到 RocketMQ。这个操作很快,因为只是把消息丢到队列里,不涉及任何业务逻辑处理。然后在消费端,我们启动一个消费者线程,这些消费者以一个相对稳定的速度去消费消息,一条一条地处理秒杀逻辑,比如检查库存、扣库存、生成订单等。 +不仅如此,生产者通过异步方式发送消息,还可以快速响应用户请求。 -```java -// 生产者端 - 接收秒杀请求 -@PostMapping("/seckill") -public Result seckill(Long productId, Long userId) { - // 直接发送消息到 RocketMQ,快速返回 - Message message = new Message("seckill_topic", - JSON.toJSONString(new SeckillRequest(productId, userId)).getBytes()); - - try { - SendResult sendResult = rocketMQTemplate.syncSend("seckill_topic", message); - return Result.success("秒杀请求已提交,请稍候"); - } catch (Exception e) { - return Result.fail("系统繁忙,请稍后重试"); - } -} +消费者从 RocketMQ 队列中按照一定速率读取消息并进行处理。可以根据后端处理能力和当前负载情况动态调整消费者的消费速率,达到填谷的效果。 -// 消费者端 - 按照自己的能力消费消息 -@RocketMQMessageListener(topic = "seckill_topic", - consumerGroup = "seckill_consumer_group") -public class SeckillConsumer implements RocketMQListener { - - @Override - public void onMessage(SeckillRequest request) { - // 这里按照相对稳定的速度处理秒杀逻辑 - // 消费者能处理多快就处理多快,不会被突发流量冲击 - seckillService.processSeckill(request.getProductId(), request.getUserId()); - } -} -``` - -这里有一个需要注意的地方,就是消息堆积的问题,如果消费者一直跟不上生产速度,消息会无限堆积,可能最终会导致磁盘满或者消息过期被删除。所以在实际项目中,我们需要监控队列的堆积情况,必要时通过增加消费者或优化消费逻辑来加快处理速度。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 7 Java 后端实习一面的原题:有了解过 MQ 吗? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 24 面试原题:如何用消息队列做削峰填谷的? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 4 一面面试原题:项目里用 RocketMQ 做削峰,还有什么场景适合消息队列 > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 20 测开一面的原题:RocketMQ有什么用,你一般拿来做什么 -memo:2025 年 11 月 03 日修改至此,今天有[球友反馈说深信服开奖了](https://javabetter.cn/zhishixingqiu/),AI 软开能到 30k+,真的已经非常高了,赶超互联网大厂的 SSP。AI 软开岗位,未来几年会非常吃香,大家可以重点关注一下,球友用的就是 [PmHub](https://javabetter.cn/zhishixingqiu/pmhub.html)+RAG 项目。 - -![球友深信服开奖,AI 软开](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251104110022.png) - ### 2.为什么要选择 RocketMQ? -首先,在事务支持方面,RocketMQ 做得特别好。比如说在转账场景下,我们要保证"扣款"的本地事务和"发送转账消息"这两个操作要么都成功,要么都失败,不能出现只扣款但没发消息的情况。RocketMQ 的事务消息机制就能很好地解决这个问题。 - -```java -// RocketMQ 事务消息示例 -TransactionSendResult sendResult = rocketMQTemplate.executeAndReplyTransaction( - "transfer_topic", - new Message("transfer_topic", - JSON.toJSONString(new TransferRequest(fromId, toId, amount)).getBytes()), - new RocketMQLocalTransactionListener() { - @Override - public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { - try { - // 执行本地事务 - 扣款 - accountService.deductAccount(fromId, amount); - return RocketMQLocalTransactionState.COMMIT; - } catch (Exception e) { - return RocketMQLocalTransactionState.ROLLBACK; - } - } - - @Override - public RocketMQLocalTransactionState checkLocalTransaction(Message msg) { - // 事务回查逻辑 - return accountService.isDeducted(fromId, amount) ? - RocketMQLocalTransactionState.COMMIT : - RocketMQLocalTransactionState.ROLLBACK; - } - } -); -``` - -其次是对顺序消息的支持。在很多场景下,消息的顺序很重要。比如订单的生命周期,应该是:下单 → 支付 → 发货 → 确认收货。如果消息乱序了,就会出现还没付款就发货的逻辑混乱。 - -RocketMQ 的顺序消息能够保证同一个 OrderId 的消息,它们能够被发送到同一个队列,然后被同一个消费者按顺序消费。 - -[PmHub](https://javabetter.cn/zhishixingqiu/pmhub.html) 中的任务审批流程,就是用的 RocketMQ 来保证审批步骤的正确顺序。 +![四大消息队列对比](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-c3493e70-67c7-4f0d-bb99-f0fe8074c807.jpg) -![pmhub 用的是 RocketMQ](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251104112142.png) - -再有就是 RocketMQ 支持 Master-Slave 模式的高可用部署。当 Master 节点宕机时,Slave 可以自动转换为 Master,从而提供更好的容错能力。 - -![三分恶面渣逆袭:四大消息队列对比](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-c3493e70-67c7-4f0d-bb99-f0fe8074c807.jpg) - -如果是日志收集和流式处理场景,Kafka 更合适,因为它天生为大数据场景设计。[派聪明 RAG 项目](https://javabetter.cn/zhishixingqiu/paismart.html)中的文件上传后的向量化、索引构建任务,就是用的 Kafka 来做消息队列。 - -![派聪明用的 Kafka](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251104111840.png) - - -如果是需要轻量级的消息传递,RabbitMQ 更好,因为它实现了 AMQP 协议,支持丰富的路由和交换机类型。 - -[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中的点赞、收藏、评论等功能的异步处理,就是用的 RabbitMQ 来做消息队列。 - -![技术派用的是 RabbitMQ](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251104112016.png) +我们系统主要面向 C 端用户,有一定的并发量,对性能也有比较高的要求,所以选择了低延迟、吞吐量比较高,可用性比较好的 RocketMQ。 ### 3.RocketMQ 有什么优缺点? -首先是支持事务消息,这是 RocketMQ 最大的亮点。我的理解是,RocketMQ 通过两阶段提交的方式,保证了消息发送和本地事务的原子性。这对于需要保证数据一致性的场景特别重要。比如库存扣减和订单创建,要么两个都成功,要么都回滚,不能出现不一致的状态。 - -其次是支持顺序消息。对于同一个业务主体(比如同一个订单),RocketMQ 能保证消息的有序性。这个设计特别巧妙,通过 ShardingKey 将消息路由到同一个队列,然后再由同一个消费者线程顺序消费。这解决了很多实际业务的需求。 +RocketMQ 优点: -再有就是高吞吐量和低延迟。RocketMQ 的单机吞吐能达到几十万 TPS。 +- 单机吞吐量:十万级 +- 可用性:非常高,分布式架构 +- 消息可靠性:经过参数优化配置,消息可以做到 0 丢失 +- 功能支持:MQ 功能较为完善,还是分布式的,扩展性好 +- 支持 10 亿级别的消息堆积,不会因为堆积导致性能下降 +- 源码是 Java,方便结合公司自己的业务二次开发 +- 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况 +- **RoketMQ**在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择**RocketMQ** -缺点方面,由于 RocketMQ 的消息去重不是自动的,所以需要消费者端自己实现幂等,否则容易出现重复消费的问题。 +RocketMQ 缺点: -```java -// 需要在消费端自己实现幂等 -@RocketMQMessageListener(topic = "order_topic", - consumerGroup = "order_consumer_group") -public class OrderConsumer implements RocketMQListener { - - @Override - public void onMessage(OrderMessage message) { - // 需要根据消息的唯一标识检查是否已经处理过 - if (orderService.isProcessed(message.getOrderId())) { - // 已经处理过,直接返回 - return; - } - - // 处理订单逻辑 - orderService.processOrder(message); - } -} -``` +- 支持的客户端语言不多,目前是 Java 及 c++,其中 c++不成熟 +- 没有在 MQ 核心中去实现**JMS**等接口,有些系统要迁移需要修改大量代码 #### 说说你对 RocketMQ 的理解? @@ -226,214 +89,152 @@ RocketMQ 是阿里巴巴开源的一款分布式消息中间件,具有高吞 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 4 云实习面试原题:说说你对RocketMQ的理解 -memo:2025 年 11 月 4 日修改至此,今天有球友刚好面到了星球嘉宾的公司,三个项目,mydb+[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)+[派聪明](https://javabetter.cn/zhishixingqiu/paismart.html),这下也是稳稳拿下了。 - -![](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-这个要们是二哥的星球成员吧.png) - ### 4.消息队列有哪些消息模型? -我认为消息队列的消息模型可以分为两大类:点对点模型和发布-订阅模型。 +消息队列有两种模型:**队列模型**和**发布/订阅模型**。 -点对点模型的特点是一条消息只能被一个消费者消费。生产者把消息发送到一个队列里,消费者从这个队列里拉取消息进行处理。一旦消息被某个消费者消费了,这条消息就被删除了,其他消费者是看不到这条消息的。 +- **队列模型** -![三分恶面渣逆袭:点对点模型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-d94a9bf9-3fed-40a6-8aef-0d0395b6e409.jpg) +这是最初的一种消息队列模型,对应着消息队列“发-存-收”的模型。生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,也就是说每条消息只能被一个消费者消费。 -发布-订阅模型的特点是一条消息可以被多个订阅者消费。生产者发布消息到一个主题(Topic),所有订阅了这个主题的消费者都会收到这条消息。 +![队列模型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-d94a9bf9-3fed-40a6-8aef-0d0395b6e409.jpg) -![三分恶面渣逆袭:发布-订阅模型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-692ec6a0-8499-4de2-be17-a577996bdaef.jpg) +- **发布/订阅模型** -这个模型特别适合用来做事件通知。比如说在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,作者发布了一篇内容,可以同时通知所有关注了这个作者的用户,让他们收到更新提醒。系统级的消息通知也是类似的道理。 +如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。解决的方式就是发布/订阅模型。 -![技术派:消息通知](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251118090719.png) +在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。 -### 5.那 RocketMQ 的消息模型呢? +![发布-订阅模型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-692ec6a0-8499-4de2-be17-a577996bdaef.jpg) -RocketMQ 采用的是一个统一的、基于 Topic 和 Group 的消息模型。同一个消费者组内可以算是点对点,不同消费者组之间算是发布-订阅。 +它和 “队列模式” 的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费。 -![三分恶面渣逆袭:RocketMQ消息的组成](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-4ab7f942-23d7-4462-8e36-e305cc0a045f.jpg) - -在 RocketMQ 中,主题(Topic)是消息的逻辑分类。生产者把消息发送到某个 Topic,消费者从某个 Topic 拉取消息。一个 Topic 可以有多个生产者向它发送消息,也可以有多个消费者从它消费消息。 - -一个 Topic 在物理上被分成了多个队列(Queue)。生产者发送消息时,消息会根据某个 key 被路由到不同的 Queue 中。这个设计的巧妙之处在于,它既保证了单个 Queue 内的消息顺序,又能通过多个 Queue 实现并行处理。 - -![三分恶面渣逆袭:RocketMQ 的 Topic 和 Queue 关系](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-e470b972-f4ac-4b76-bcde-0df5d4765ca7.jpg) +### 5.那 RocketMQ 的消息模型呢? -消费者端,消费者归属于某一个消费者组(Consumer Group)。一个 消费者组内的多个消费者会协同消费同一个主题的消息。RocketMQ 会把主题下的多个队列分配给这个消费者组内的消费者进行消费。 +RocketMQ 使用的消息模型是标准的发布-订阅模型,在 RocketMQ 的术语表中,生产者、消费者和主题,与发布-订阅模型中的概念是完全一样的。 -``` -场景 1:一个消费者,一个 Topic 有 4 个 Queue +RocketMQ 本身的消息是由下面几部分组成: -ConsumerGroup: order_consumer_group - └─ Consumer 1 - ├─ Queue 0 - ├─ Queue 1 - ├─ Queue 2 - └─ Queue 3 +![RocketMQ消息的组成](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-4ab7f942-23d7-4462-8e36-e305cc0a045f.jpg) -一个消费者消费所有 4 个队列。 -``` +- **Message** -``` -场景 2:两个消费者,一个 Topic 有 4 个 Queue - -ConsumerGroup: order_consumer_group - ├─ Consumer 1 - │ ├─ Queue 0 - │ └─ Queue 2 - │ - └─ Consumer 2 - ├─ Queue 1 - └─ Queue 3 - -两个消费者各消费 2 个队列,实现了负载均衡。 -``` +**Message**(消息)就是要传输的信息。 -``` -场景 3:四个消费者,一个 Topic 有 4 个 Queue +一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。 -ConsumerGroup: order_consumer_group - ├─ Consumer 1 → Queue 0 - ├─ Consumer 2 → Queue 1 - ├─ Consumer 3 → Queue 2 - └─ Consumer 4 → Queue 3 +一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 Key 并在 Broker 上查找此消息以便在开发期间查找问题。 -四个消费者各消费 1 个队列,充分并行。 -``` +- **Topic** -memo:2025 年 11 月 15 日修改至此,今天有球友发喜报说携程开了 SP,非常满意,感谢星球里的项目,他用的是[派聪明RAG](https://javabetter.cn/zhishixingqiu/paismart.html)+mydb 轮子。 +**Topic**(主题)可以看做消息的归类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。 -![携程狠狠拿下,本科和研究生还有两年 gap](https://cdn.tobebetterjavaer.com/stutymore/2025nian11yue15ri2008-image5708.png) +**Topic** 与生产者和消费者的关系非常松散,一个 Topic 可以有 0 个、1 个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。 -### 6.消息的消费模式了解吗? +一个 Topic 也可以被 0 个、1 个、多个消费者订阅。 -我认为消费模式可以从两个维度来分类:一个是消费的方向,一个是消费的范围。 +- **Tag** -从消费方向来分的话,有两种模式,一种是 pull 模式,一种是 push 模式。 +**Tag**(标签)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 **Tag** 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 **Tag** 。 -![二哥的 Java 进阶之路:pull 和 push 的消费模式](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251125101659.png) +标签有助于保持你的代码干净和连贯,并且还可以为 **RocketMQ** 提供的查询系统提供帮助。 -pull 模式需要消费者主动去消息队列中拉取消息,消费者可以控制拉取的速度、数量,但需要不断地轮询,比较浪费资源。 +- **Group** -push 模式则是消息队列主动把消息推送给消费者,消费者只需要注册一个监听器,消息一到达就触发回调进行处理,响应速度快,但可能会出现消息堆积的情况。 +RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。 -从消费范围来分的话,也有两种模式,一种是集群消费,一种是广播消费。 +消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。默认情况,如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不会再收到这条消息。 -![三分恶面渣逆袭:集群消费和广播消费](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-2c574635-1eaa-4bdd-8aa8-1bdc3f10b274.jpg) +- **Message Queue** -集群消费是指,同一个消费者组中的多个消费者共同消费一个主题中的消息。消息被分散分配给这个消费者组中的各个消费者,每条消息只被这个消费者组中的一个消费者消费。 +**Message Queue**(消息队列),一个 Topic 下可以设置多个消息队列,Topic 包括多个 Message Queue ,如果一个 Consumer 需要获取 Topic 下所有的消息,就要遍历所有的 Message Queue。 -换句话说,RocketMQ 会把主题下的所有队列均匀地分配给消费者组内的消费者,实现负载均衡。这样也能保证同一条消息只会被消费者组内的一个消费者消费,避免重复消费。 +RocketMQ 还有一些其它的 Queue——例如 ConsumerQueue。 -``` -Topic: order_topic - ├─ Queue 0 → [消息1] [消息3] [消息5] - ├─ Queue 1 → [消息2] [消息4] [消息6] - ├─ Queue 2 → [消息7] [消息9] [消息11] - └─ Queue 3 → [消息8] [消息10] [消息12] - -ConsumerGroup: order_consumer_group - ├─ Consumer 1 消费 Queue 0, 1 - ├─ Consumer 2 消费 Queue 2, 3 - -同一条消息只被 Consumer 1 或 Consumer 2 之一消费,不会重复消费。 -``` +- **Offset** -广播消费是指,同一个主题的每条消息都会被消费者组内的每个消费者消费一次。也就是说,消费者组内的每个消费者都会收到主题下的所有消息,从而实现消息的广播效果。 +在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。 -``` -Topic: config_update_topic - └─ [配置更新消息1] [配置更新消息2] [配置更新消息3] - -ConsumerGroup: config_consumer_group - ├─ Consumer 1(服务器1上的应用) - │ └─ 收到:消息1, 消息2, 消息3(完整的) - │ - ├─ Consumer 2(服务器2上的应用) - │ └─ 收到:消息1, 消息2, 消息3(完整的) - │ - └─ Consumer 3(服务器3上的应用) - └─ 收到:消息1, 消息2, 消息3(完整的) - -三个消费者都收到了所有的消息,各自独立处理。 -``` +也可以这么说,`Queue` 是一个长度无限的数组,**Offset** 就是下标。 -### 7.RocketMQ 的基本架构了解吗? +RocketMQ 的消息模型中,这些就是比较关键的概念了。画张图总结一下: -RocketMQ 的架构由四个核心部分组成:NameServer、Broker、生产者和消费者。 +![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-e470b972-f4ac-4b76-bcde-0df5d4765ca7.jpg) -![三分恶面渣逆袭:RocketMQ架构](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-d4c0e036-0f0e-466f-bd4b-7e6ee10daca4.jpg) +### 6.消息的消费模式了解吗? -我的理解是,NameServer 就是 RocketMQ 的路由中心,负责维护 Topic 和 Broker 之间的路由信息。生产者在发送消息前,消费者在消费消息前,都会先从 NameServer 获取最新的路由信息。 +消息消费模式有两种:**Clustering**(集群消费)和**Broadcasting**(广播消费)。 -- 每个 Broker 会向 NameServer 注册自己的信息,包括 Broker 的地址、端口、存储的 Topic 和 Queue 等。 -- NameServer 会根据 Topic 名称告诉生产者和消费者对应的 Broker 地址。 -- Broker 会定期向 NameServer 发送心跳,报告自己的状态。 +![两种消费模式](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-2c574635-1eaa-4bdd-8aa8-1bdc3f10b274.jpg) -![帅旋:RocketMQ运行原理](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251125103932.png) +默认情况下就是集群消费,这种模式下`一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费`,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。 -Broker 是消息存储中心,它的职责包括: +而广播消费消息会发给消费者组中的每一个消费者进行消费。 -- 所有生产者发送的消息都会被存储在 Broker 上,以文件的形式持久化到磁盘。 -- 消费者从 Broker 拉取消息时,Broker 需要根据消费者的 Offset 找到对应的消息,返回给消费者。 -- 如果配置了高可用,Broker Master 会把消息同步到 Broker Slave,实现主从备份。 +### 7.RoctetMQ 基本架构了解吗? -生产者在发送消息时,会先从 NameServer 获取 Topic 的路由信息,然后根据路由信息把消息发送到对应的 Broker 上。 +先看图,RocketMQ 的基本架构: -消费者在消费消息时,也会先从 NameServer 获取 Topic 的路由信息,然后根据路由信息从对应的 Broker 上拉取消息进行处理。 +![RocketMQ架构](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-d4c0e036-0f0e-466f-bd4b-7e6ee10daca4.jpg) -memo:2025 年 11 月 25 日修改至此,今天有[球友反馈说](https://javabetter.cn/zhishixingqiu/)拿到了科大讯飞和华为的 offer,都开奖了,秋招算是告一段落。她特意感谢了[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)让她找到暑期实习,秋招靠[派聪明 RAG](https://javabetter.cn/zhishixingqiu/paismart.html)和星球的实习搭子稳稳拿下。还有[我改的简历给她的招聘](https://javabetter.cn/zhishixingqiu/jianli.html)加了不少分,线下被夸了无数次。 +RocketMQ 一共有四个部分组成:NameServer,Broker,Producer 生产者,Consumer 消费者,它们对应了:发现、发、存、收,为了保证高可用,一般每一部分都是集群部署的。 -![球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251127153851.png) +### 8.那能介绍一下这四部分吗? -### 8.能详细介绍一下RocketMQ的NameServer吗? +类比一下我们生活的邮政系统—— -NameServer 是一个路由中心和服务发现中心。他的第一个职责是存储和维护路由信息。当 Broker 启动时,会向 NameServer 注册自己的信息。 +邮政系统要正常运行,离不开下面这四个角色, 一是发信者,二 是收信者, 三是负责暂存传输的邮局, 四是负责协调各个地方邮局的管理机构。对应到 RocketMQ 中,这四个角色就是 Producer、 Consumer、 Broker 、NameServer。 -![极客时间:RocketMQ 的整体架构](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251127110113.png) +![RocketMQ类比邮政体系](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-00175355-5532-4ee6-a48c-e3e3a9b87d64.jpg) -NameServer 把这些信息存储在内存里,形成一个路由表。 +##### NameServer -![极客时间:broker 注册](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251127110749.png) +NameServer 是一个无状态的服务器,角色类似于 Kafka 使用的 Zookeeper,但比 Zookeeper 更轻量。 -它的第二个职责是提供路由查询服务。当生产者或消费者需要知道某个主题在哪个 Broker 上时,就向 NameServer 查询。NameServer 会根据主题名称返回对应的 Broker 地址和队列信息。 +特点: -第三个职责是监控 Broker 的状态。Broker 会定期向 NameServer 发送心跳,报告自己的状态。如果某个 Broker 长时间没有发送心跳,NameServer 会将其标记为不可用,并从路由表中移除。 +- 每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。 +- Nameserver 被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群,Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息,Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 Topic 到 NameServer。 -#### 请说说Broker的作用? +功能主要有两个: -Broker 是一个消息存储服务器,它负责接收生产者的消息,并将其存储起来,然后在消费者拉取时返回给它们。 +- 1、和 Broker 结点保持长连接。 +- 2、维护 Topic 的路由信息。 -![RocketMQ 的 broker 存储](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-789379a9-4a0c-4992-9de1-e49283d089a4.jpg) +##### Broker -#### 请说说生产者? +消息存储和中转角色,负责存储和转发消息。 -生产者的核心职责是把应用程序的数据转化为消息,发送到 Broker。 +- Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)。 -![极客时间:生产者](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251127112024.png) +![RocketMQ存储-图片来源官网](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-789379a9-4a0c-4992-9de1-e49283d089a4.jpg) -RocketMQ 提供了三种方式发送消息:同步、异步和单向。 +- 单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。 -- **同步发送**:生产者发送消息后会阻塞等待 Broker 的响应。只有收到 Broker 确认消息已存储的响应后,才会返回给应用程序。 -- **异步发送**:生产者发送消息后立即返回,不阻塞。Broker 的响应会通过回调函数返回给应用程序。 -- **单向发送**:生产者发送消息后直接返回,不等待响应,也不需要回调。这个模式用于一些不关心发送结果的场景。 +##### Producer -#### 请说说消费者? +消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。 -消费者是消息的接收方,它的核心职责就是从 Broker 拉取消息,进行业务处理,然后提交消费位移。 +- **Producer**由用户进行分布式部署,消息由**Producer**通过多种负载均衡模式发送到**Broker**集群,发送低延时,支持快速失败。 +- **RocketMQ** 提供了三种方式发送消息:同步、异步和单向 -![消费者](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20251127113326.png) +- **同步发送**:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。 +- **异步发送**:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。 +- **单向发送**:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。 -RocketMQ 同时支持 Pull、Push、Pop 三种消费模型。 +##### Consumer -Pull 模型是最基础的消费方式。消费者主动向 Broker 发起请求,拉取消息。Push 模型在使用上看起来像是服务端在推送消息,但实际上底层仍然是 Pull 模型。 +消息消费者,负责消费消息,一般是后台系统负责异步消费。 -当消费者很多的时候,消费重平衡会消耗很长的时间,于是 RocketMQ 提供了 Pop 模型。Pop 模型把消费重平衡完全移到了服务端,以减轻消费者的负担。 +- **Consumer**也由用户部署,支持 PUSH 和 PULL 两种消费模式,支持**集群消费**和**广播消费**,提供**实时的消息订阅机制**。 +- **Pull**:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。 +- **Push**:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。 - +GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) -memo:2025 年 11 月 27 日修改至此,今天有球友反馈说拿到了字节的 offer,SSP offer,还有 8 万签字费,特意感谢了[星球的项目](https://javabetter.cn/zhishixingqiu/)和[面渣逆袭八股](https://javabetter.cn/sidebar/sanfene/nixi.html)。 +微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 -![球友拿到的字节 offer 非常夸张,恭喜](https://cdn.tobebetterjavaer.com/stutymore/2025nian11yue25ri9826-image3689.png) +![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) ## 进阶 @@ -613,43 +414,28 @@ long id = IdUtil.getSnowflakeNextId(); ### 12.顺序消息如何实现? -RocketMQ 提供了两种级别的顺序消息:全局顺序和局部顺序。 +RocketMQ 实现顺序消息的关键在于保证消息生产和消费过程中严格的顺序控制,即确保同一业务的消息按顺序发送到同一个队列中,并由同一个消费者线程按顺序消费。 ![三分恶面渣逆袭:顺序消息](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-e3de6bb5-b5db-47af-8ae3-73aedd269f32.jpg) -全局顺序是指整个 Topic 的所有消息都严格按照发送顺序消费,这种方式性能比较低,实际项目中用得不多。 -![三分恶面渣逆袭:全局顺序消息](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-8e98ac61-ad47-4ed4-aac6-223201f9aae2.jpg) +#### 局部顺序消息如何实现? -局部顺序是指特定分区内的消息保证顺序,这是我们常用的方式。 +局部顺序消息保证在某个逻辑分区或业务逻辑下的消息顺序,例如同一个订单或用户的消息按顺序消费,而不同订单或用户之间的顺序不做保证。 ![三分恶面渣逆袭:部分顺序消息](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-14ab3700-8538-473e-bb66-8acfdd6a77a2.jpg) -要保证顺序,关键是要把需要保证顺序的消息发送到同一个 MessageQueue 中。 +#### 全局顺序消息如何实现? -```java -// 根据订单ID选择队列,保证同一订单的消息在同一队列 -producer.send(message, new MessageQueueSelector() { - @Override - public MessageQueue select(List mqs, Message msg, Object arg) { - String orderId = (String) arg; - int index = orderId.hashCode() % mqs.size(); - return mqs.get(index); - } -}, orderId); -``` +全局顺序消息保证消息在整个系统范围内的严格顺序,即消息按照生产的顺序被消费。 + +可以将所有消息发送到一个单独的队列中,确保所有消息按生产顺序发送和消费。 -每个 MessageQueue 在 Broker 中对应一个 ConsumeQueue,消息按照到达 Broker 的顺序依次写入。 +![三分恶面渣逆袭:全局顺序消息](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/nice-article/weixin-mianznxrocketmqessw-8e98ac61-ad47-4ed4-aac6-223201f9aae2.jpg) -当消费者开始消费某个 MessageQueue 时,会在 Broker 端对该队列加锁,其他消费者就无法同时消费这个队列。这样确保了同一时间只有一个消费者在处理某个队列的消息,从而保证了消费顺序。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 2 后端面试原题:说说mq原理,怎么保证消息接受顺序? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的收钱吧面经同学 1 Java 后端一面面试原题:RocketMQ的顺序消息? -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的中小厂面经同学6 广州中厂面试原题:RocketMQ怎么保证消息顺序? - -memo:2025 年 8 月 15 日修改至此,今天在[帮球友修改简历时](https://javabetter.cn/zhishixingqiu/jianli.html),收到这样一个反馈:目前正在高德暑期实习,3 月底找二哥修改过简历,觉得改的非常好。 - -![高德实习的球友](https://cdn.tobebetterjavaer.com/stutymore/rocketmq-20250924165532.png) ### 13.如何实现消息过滤? diff --git a/docs/src/sidebar/sanfene/shejimoshi.md b/docs/src/sidebar/sanfene/shejimoshi.md index 265f815deb..3a08fb3f88 100644 --- a/docs/src/sidebar/sanfene/shejimoshi.md +++ b/docs/src/sidebar/sanfene/shejimoshi.md @@ -2,7 +2,7 @@ title: 设计模式面试题,5道设计模式八股文(3000字10张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-设计模式 description: 下载次数超 1 万次,3000 字 10 张手绘图,详解 5 道 设计模式 面试高频题(让天下没有难背的八股),面渣背会这些 设计模式 八股文,这次吊打面试官,我觉得稳了(手动 dog)。 -date: 2025-09-20 +date: 2024-11-08 author: 沉默王二 category: - 面渣逆袭 @@ -267,49 +267,8 @@ public class FactoryMethodPatternDemo { 1. **数据库访问层(DAL)组件**:工厂方法模式适用于数据库访问层,其中需要根据不同的数据库(如MySQL、PostgreSQL、Oracle)创建不同的数据库连接。工厂方法可以隐藏这些实例化逻辑,只提供一个统一的接口来获取数据库连接。 2. **日志记录**:当应用程序需要实现多种日志记录方式(如向文件记录、数据库记录或远程服务记录)时,可以使用工厂模式来设计一个灵活的日志系统,根据配置或环境动态决定具体使用哪种日志记录方式。 -### 线程不是有线程名吗,怎么做到让线程的前缀名统一,通过factory来实现? - -可以通过工厂模式创建线程,并在工厂方法中设置线程的前缀名。这样可以确保所有通过工厂创建的线程都具有统一的命名规范。 - -```java -class NamedThreadFactory implements ThreadFactory { - private final String prefix; - private int count = 0; - - public NamedThreadFactory(String prefix) { - this.prefix = prefix; - } - - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r); - thread.setName(prefix + "-" + count++); - return thread; - } -} - -public class ThreadFactoryDemo { - public static void main(String[] args) { - ThreadFactory factory = new NamedThreadFactory("MyThread"); - - Runnable task = () -> { - System.out.println("线程名称: " + Thread.currentThread().getName()); - }; - - for (int i = 0; i < 5; i++) { - Thread thread = factory.newThread(task); - thread.start(); - } - } -} -``` > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为一面原题:说下工厂模式,场景 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 34 Java 后端技术一面面试原题:工厂模式了解吗? - -memo:2025 年 9 月 20 日修改至此,今天球友在面快手的时候,被问了很多[派聪明 RAG 项目](https://javabetter.cn/zhishixingqiu/paismart.html)的题目,说面试官对这个项目非常感兴趣。再次感谢球友的口碑。 - -![](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-差不多都命中预測的了啊.png) ## 03、什么是单例模式? @@ -391,6 +350,8 @@ class Singleton { 当 instance 创建后,再次调用 getInstance 方法时,不会进入同步代码块,从而提高了性能。 + + #### ④、静态内部类如何实现单例? 利用 Java 的[静态内部类](https://javabetter.cn/oo/static.html)(Static Nested Class)和[类加载机制](https://javabetter.cn/jvm/class-load.html)来实现线程安全的延迟初始化。 @@ -432,11 +393,6 @@ public enum Singleton { 单例模式有 5 种实现方式,常见的有饿汉式、懒汉式、双重检查锁定、静态内部类和枚举。 -### synchronized加到代码块上和方法上有什么区别? - -将 synchronized 加到方法上时,锁定的是整个方法,任何线程在调用该方法时都会获得该对象的锁,直到方法执行完毕才释放锁。 - -将 synchronized 加到代码块上 `synchronized (Singleton.class)`时,锁定的是类的 Class 对象,所有对这个类的 synchronized 代码块都会串行执行。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为一面原题:说下单例模式,有几种 > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的腾讯面经同学 22 暑期实习一面面试原题:单例模式的好处 @@ -446,13 +402,9 @@ public enum Singleton { > 6. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的携程面经同学 10 Java 暑期实习一面面试原题:单例模式,如何线程安全 > 7. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 D 小米一面原题:单例模式有几种 -memo:2025 年 8 月 12 日修改至此,今天有球友在VIP 群里讲,他师兄的简历一眼[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html),🤣,看来这个项目的口碑是真不错。 - -![顺丰 offer 的师兄用的技术派项目](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-20250925184038.png) - ## 04、了解哪些设计模式? -单例模式、策略模式。 +单例模式、策略模式和工厂模式。 在需要控制资源访问,如配置管理、连接池管理时经常使用单例模式。它确保了全局只有一个实例,并提供了一个全局访问点。 @@ -464,185 +416,6 @@ memo:2025 年 8 月 12 日修改至此,今天有球友在VIP 群里讲,他 后面想添加新的 AI 服务,只需要增加一个新的策略类,不需要修改原有代码,这样就提高了代码的可扩展性。 -### 你想做一个导出的数据导出的功能,然后他可能导出的格式有Excel、有JSON,可能有XML,然后如果你用面向对象这些方法去做,会怎么去设计这个实现? - -我会使用策略模式+工厂模式来实现。 - -首先,我会定义一个导出接口 `DataExporter`,该接口包含一个导出方法 `export(data)`,用于导出数据。 - -```java -public interface DataExporter { - void export(List data, OutputStream outputStream) throws Exception; - String getContentType(); - String getFileExtension(); -} -``` - -然后,我会为每种导出格式(Excel、JSON、XML)创建具体的策略类,这些类实现了 `ExportStrategy` 接口,并提供各自的导出逻辑。 - -```java -public class ExcelExporter implements DataExporter { - @Override - public void export(List data, OutputStream outputStream) throws Exception { - Workbook workbook = new XSSFWorkbook(); - Sheet sheet = workbook.createSheet("Data"); - - // 写入表头 - if (!data.isEmpty()) { - Object firstRow = data.get(0); - Row headerRow = sheet.createRow(0); - Field[] fields = firstRow.getClass().getDeclaredFields(); - for (int i = 0; i < fields.length; i++) { - headerRow.createCell(i).setCellValue(fields[i].getName()); - } - - // 写入数据 - for (int i = 0; i < data.size(); i++) { - Row dataRow = sheet.createRow(i + 1); - Object obj = data.get(i); - for (int j = 0; j < fields.length; j++) { - fields[j].setAccessible(true); - Object value = fields[j].get(obj); - dataRow.createCell(j).setCellValue(value != null ? value.toString() : ""); - } - } - } - - workbook.write(outputStream); - workbook.close(); - } - - @Override - public String getContentType() { - return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - } - - @Override - public String getFileExtension() { - return "xlsx"; - } -} - -public class JsonExporter implements DataExporter { - private ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public void export(List data, OutputStream outputStream) throws Exception { - objectMapper.writeValue(outputStream, data); - } - - @Override - public String getContentType() { - return "application/json"; - } - - @Override - public String getFileExtension() { - return "json"; - } -} - -public class XmlExporter implements DataExporter { - @Override - public void export(List data, OutputStream outputStream) throws Exception { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.newDocument(); - - Element root = document.createElement("data"); - document.appendChild(root); - - for (Object obj : data) { - Element item = document.createElement("item"); - Field[] fields = obj.getClass().getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - Element element = document.createElement(field.getName()); - Object value = field.get(obj); - element.setTextContent(value != null ? value.toString() : ""); - item.appendChild(element); - } - root.appendChild(item); - } - - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - DOMSource source = new DOMSource(document); - StreamResult result = new StreamResult(outputStream); - transformer.transform(source, result); - } - - @Override - public String getContentType() { - return "application/xml"; - } - - @Override - public String getFileExtension() { - return "xml"; - } -} -``` - -接下来,我会创建一个工厂类 `DataExporterFactory`,用于根据导出格式创建相应的导出策略实例。 - -```java -public class DataExporterFactory { - private static final Map exporters = new HashMap<>(); - - static { - exporters.put("excel", new ExcelExporter()); - exporters.put("json", new JsonExporter()); - exporters.put("xml", new XmlExporter()); - } - - public static DataExporter getExporter(String format) { - DataExporter exporter = exporters.get(format.toLowerCase()); - if (exporter == null) { - throw new IllegalArgumentException("不支持的导出格式: " + format); - } - return exporter; - } - - public static Set getSupportedFormats() { - return exporters.keySet(); - } -} -``` - -最后,我会定义导出服务的上下文类,我根据请求参数选择合适的导出策略,并调用其导出方法。 - -```java -@Service -public class DataExportService { - - public void exportData(List data, String format, OutputStream outputStream) - throws Exception { - DataExporter exporter = DataExporterFactory.getExporter(format); - exporter.export(data, outputStream); - } - - public ResponseEntity exportDataAsResponse(List data, String format) - throws Exception { - DataExporter exporter = DataExporterFactory.getExporter(format); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - exporter.export(data, baos); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.parseMediaType(exporter.getContentType())); - headers.setContentDispositionFormData("attachment", - "export_" + System.currentTimeMillis() + "." + exporter.getFileExtension()); - - return ResponseEntity.ok() - .headers(headers) - .body(baos.toByteArray()); - } -} -``` - - > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动面经同学 1 Java 后端技术一面面试原题:了解哪些设计模式? > 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的奇安信面经同学 1 Java 技术一面面试原题:你真正使用过哪些设计模式? > 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的农业银行面经同学 7 Java 后端面试原题:介绍你熟悉的设计模式 @@ -652,9 +425,6 @@ public class DataExportService { > 7. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的vivo 面经同学 10 技术一面面试原题:了解哪些设计模式,开闭原则 > 8. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 5 Java 后端技术一面面试原题:设计模式 -memo:2025 年 9 月 20 日修改至此,今天在帮球友修改简历的时候,收到这样一个反馈:在合肥马上要转正了,薪资给的还可以,已经给周围很多人[安利二哥的项目](https://javabetter.cn/zhishixingqiu/)了,反向很不错。 - -![球友对星球的口碑](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-20250925180409.png) ## 05、什么是策略模式? @@ -711,68 +481,6 @@ public class XunFeiAiServiceImpl extends AbsChatService { > 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团面经同学 4 一面面试原题:策略模式,自己的代码用过什么设计模式 > 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的字节跳动同学 17 后端技术面试原题:用过哪些策略模式 -## 06、什么是模板模式? - -模板模式(Template Method Pattern)是一种行为型设计模式,它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重新定义算法的某些特定步骤。 - -![refactoringguru.cn:模板模式](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-20250925181116.png) - -在 Spring 框架中,有很多模板方法的应用。比如 JdbcTemplate、RestTemplate 这些,它们定义了数据访问的通用流程,我们只需要提供具体的 SQL 或者回调方法。 - -![JdbcTemplate](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-20250925181306.png) - -#### 模版模式在java中具体怎么实现的? - -模板方法通常通过抽象类和继承来实现。抽象类定义了一个模板方法,该方法包含了一系列步骤的调用顺序,而具体的步骤则由子类实现。 - -```java -abstract class AbstractClass { - // 模板方法,定义算法的骨架 - public final void templateMethod() { - stepOne(); - stepTwo(); - } - protected abstract void stepOne(); - protected abstract void stepTwo(); -} -class ConcreteClassA extends AbstractClass { - @Override - protected void stepOne() { - System.out.println("ConcreteClassA: Step One"); - } - @Override - protected void stepTwo() { - System.out.println("ConcreteClassA: Step Two"); - } -} -class ConcreteClassB extends AbstractClass { - @Override - protected void stepOne() { - System.out.println("ConcreteClassB: Step One"); - } - @Override - protected void stepTwo() { - System.out.println("ConcreteClassB: Step Two"); - } -} -public class TemplateMethodPatternDemo { - public static void main(String[] args) { - AbstractClass classA = new ConcreteClassA(); - classA.templateMethod(); - - AbstractClass classB = new ConcreteClassB(); - classB.templateMethod(); - } -} -``` - - -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东同学 19 JDS 面试原题:模版模式在java中具体怎么实现的? - -memo:2025 年 8 月 12 日修改至此,今天在[帮球友修改简历的时候](https://javabetter.cn/zhishixingqiu/jianli.html)收到这样一个反馈:谢谢暑期实习时对他的简历修改,没有二哥绝对进不了字节。 - -![字节球友对简历修改的口碑](https://cdn.tobebetterjavaer.com/stutymore/shejimoshi-二哥晚上好,谢谢您春招时对我简历的修改,没有二哥我绝对进不了字节。.png) - --- *没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟*。 diff --git a/docs/src/sidebar/sanfene/spring.md b/docs/src/sidebar/sanfene/spring.md index 620efca1f3..6697bced10 100644 --- a/docs/src/sidebar/sanfene/spring.md +++ b/docs/src/sidebar/sanfene/spring.md @@ -1,9 +1,9 @@ --- -title: Spring面试题,41道Spring八股文(3.3万字180张手绘图),面渣逆袭必看👍 +title: Spring面试题,41道Spring八股文(1.3万字63张手绘图),面渣逆袭必看👍 shortTitle: 面渣逆袭-Spring -description: 下载次数超 1 万次,3.3 万字 180 张手绘图,详解 41 道 Spring 面试高频题(让天下没有难背的八股),面渣背会这些 Spring 八股文,这次吊打面试官,我觉得稳了(手动 dog)。 +description: 下载次数超 1 万次,1.3 万字 63 张手绘图,详解 41 道 Spring 面试高频题(让天下没有难背的八股),面渣背会这些 Spring 八股文,这次吊打面试官,我觉得稳了(手动 dog)。 author: 三分恶&沉默王二 -date: 2025-08-17 +date: 2025-07-17 category: - 面渣逆袭 tag: @@ -20,13 +20,13 @@ head: ## 前言 -3.3 万字 180 张手绘图,详解 41 道 Spring 面试高频题(让天下没有难背的八股),面渣背会这些 Spring 八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/EQge6DmgIqYITM3mAxkatg),作者:三分恶,戳[原文链接](https://mp.weixin.qq.com/s/Y17S85ntHm_MLTZMJdtjQQ)。 +1.3 万字 63 张手绘图,详解 41 道 Spring 面试高频题(让天下没有难背的八股),面渣背会这些 Spring 八股文,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/EQge6DmgIqYITM3mAxkatg),作者:三分恶,戳[原文链接](https://mp.weixin.qq.com/s/Y17S85ntHm_MLTZMJdtjQQ)。 亮白版本更适合拿出来打印,这也是很多学生党喜欢的方式,打印出来背诵的效率会更高。 -![面渣逆袭Spring篇.pdf第二版](https://cdn.tobebetterjavaer.com/stutymore/spring-20250818102050.png) +![面渣逆袭Redis篇.pdf第二版](https://cdn.tobebetterjavaer.com/stutymore/redis-20250614154255.png) -2025 年 06 月 15 日开始着手第二版更新,历经两个月,主要是中间有段时间把精力放到了[派聪明 RAG 这个项目](https://javabetter.cn/zhishixingqiu/paismart.html)的教程撰写上,第二版,我们升级了很多内容。 +2025 年 06 月 15 日开始着手第二版更新。 - 对于高频题,会标注在《[Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)》中出现的位置,哪家公司,原题是什么,并且会加🌟,目录一目了然;如果你想节省时间的话,可以优先背诵这些题目,尽快做到知彼知己,百战不殆。 - 区分八股精华回答版本和原理底层解释,让大家知其然知其所以然,同时又能做到面试时的高效回答。 @@ -35,7 +35,7 @@ head: - 增加[二哥编程星球](https://javabetter.cn/zhishixingqiu/)的球友们拿到的一些 offer,对面渣逆袭的感谢,以及对简历修改的一些认可,以此来激励大家,给大家更多信心。 - 优化排版,增加手绘图,重新组织答案,使其更加口语化,从而更贴近面试官的预期。 -![面渣逆袭已经提交 1506 次 GitHub 记录](https://cdn.tobebetterjavaer.com/stutymore/spring-20250817110453.png) +![面渣逆袭已经提交 1478 次 GitHub 记录](https://cdn.tobebetterjavaer.com/stutymore/redis-20250614154416.png) 由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 @@ -53,8 +53,7 @@ head: 展示一下暗黑版本的 PDF 吧,排版清晰,字体优雅,更加适合夜服,晚上看会更舒服一点。 -![面渣逆袭Spring篇.pdf暗黑版](https://cdn.tobebetterjavaer.com/stutymore/spring-20250818102228.png) - +![面渣逆袭RSpring篇.pdf暗黑版](https://cdn.tobebetterjavaer.com/stutymore/redis-20250614154601.png) ## 基础 @@ -82,7 +81,7 @@ Spring 的特性还是挺多的,我按照在实际工作/学习中用得最多 ![三分恶面渣逆袭:Spring特性](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-a0f0ef9d-3289-41ea-94c2-34b7e37ef854.png) -首先最核心的就是 IoC 控制反转和 DI 依赖注入,让 Spring 有能力帮我们管理对象的创建和依赖关系。 +首先最核心的就是 IoC 控制反转和 DI 依赖注入。这个我前面也提到了,就是 Spring 能帮我们管理对象的创建和依赖关系。 比如我写一个 UserService,需要用到 UserDao,以前得自己 new 一个 UserDao 出来,现在只要在 UserService 上加个 `@Service` 注解,在 UserDao 字段上加个 `@Autowired`,Spring 就会自动帮我们处理好这些依赖关系。 @@ -356,1614 +355,1611 @@ public class QuickForumApplication { > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的去哪儿同学 1 技术二面原题:spring的容器、web容器、springmvc的容器之间的区别 -memo:2025 年 6 月 27 日修改至此,今天看到[有球友发的 offer 选择提问贴](https://javabetter.cn/zhishixingqiu/),其中一个是杭州六小龙群核科技,我个人认为还是非常值得去的,毕竟是杭州的独角兽公司,薪资待遇都不错。 - -![球友拿到了杭州群核科技的 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627103801.png) - -### 6.你是怎么理解Bean的? + -在我看来,Bean 本质上就是由 Spring 容器管理的 Java 对象,但它和普通的 Java 对象有很大区别。普通的 Java 对象我们是通过 new 关键字创建的。而 Bean 是交给 Spring 容器来管理的,从创建到销毁都由容器负责。 +memo:2024 年 7 月 11 日修改至此,今天在帮[球友们修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,碰到北京交通大学本,北京航空航天大学硕的球友,她的简历上有很多校园荣誉奖项,像优秀学生、奖学金、英语四六级等,这些都是非常好的加分项。 -![stack overflow:bean 的初始化过程](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628110931.png) +![北京交通大学本,北京航空航天大学硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250621064205.png) -从实际使用的角度来说,我们项目里的 Service、Dao、Controller 这些都是 Bean。比如 UserService 被标注了 `@Service` 注解,它就成了一个 Bean,Spring 会自动创建它的实例,管理它的依赖关系,当其他地方需要用到 UserService 的时候,Spring 就会把这个实例注入进去。 +## IoC -![技术派源码:UserService](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628111222.png) +### 6.🌟说一说什么是IoC? -这种依赖注入的方式让对象之间的关系变得松耦合。 +推荐阅读:[IoC 扫盲](https://javabetter.cn/springboot/ioc.html) -Spring 提供了多种 Bean 的配置方式,基于注解的方式是最常用的。 +IoC 的全称是 Inversion of Control,也就是控制反转。这里的“控制”指的是对象创建和依赖关系管理的控制权。 -![二哥的 Java 进阶之路:Bean 的声明方式](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224163146.png) +![图片来源于网络:IoC](https://cdn.tobebetterjavaer.com/stutymore/spring-20240310191630.png) -基于 XML 配置的方式在 Spring Boot 项目中已经不怎么用了。Java 配置类的方式则可以用来解决一些比较复杂的场景,比如说主从数据源,我们可以用 `@Primary` 注解标注主数据源,用 `@Qualifier` 来指定备用数据源。 +以前我们写代码的时候,如果 A 类需要用到 B 类,我们就在 A 类里面直接 new 一个 B 对象出来,这样 A 类就控制了 B 类对象的创建。 ```java -@Configuration -public class AppConfig { - - @Bean - @Primary // 主要候选者 - public DataSource primaryDataSource() { - return new HikariDataSource(); - } +// 传统方式:对象主动创建依赖 +public class UserService { + private UserDao userDao; - @Bean - @Qualifier("secondary") - public DataSource secondaryDataSource() { - return new BasicDataSource(); + public UserService() { + // 主动创建依赖对象 + this.userDao = new UserDaoImpl(); } } ``` -那在使用的时候,当我们直接用 `@Autowired` 注解注入 DataSource 时,Spring 默认会使用 HikariDataSource;当加上 `@Qualifier("secondary")` 注解时,Spring 则会注入 BasicDataSource。 - -```java -@Autowired -private DataSource dataSource; // 会注入 primaryDataSource(因为有 @Primary) - -@Autowired -@Qualifier("secondary") -private DataSource secondaryDataSource; -``` - -#### @Component 和 @Bean 有什么区别? - -首先从使用上来说,`@Component` 是标注在类上的,而 `@Bean` 是标注在方法上的。`@Component` 告诉 Spring 这个类是一个组件,请把它注册为 Bean,而 `@Bean` 则告诉 Spring 请将这个方法返回的对象注册为 Bean。 +有了 IoC 之后,这个控制权就“反转”了,不再由 A 类来控制 B 对象的创建,而是交给外部的容器来管理。 ```java -@Component // Spring自动创建UserService实例 -public class UserService { +/** + * 使用 Spring IoC 容器来管理 UserDao 的创建和注入 + * 技术派源码:https://github.com/itwanger/paicoding + */ +@Service +public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; -} - -@Configuration -public class AppConfig { - @Bean // 我们手动创建DataSource实例 - public DataSource dataSource() { - HikariDataSource ds = new HikariDataSource(); - ds.setJdbcUrl("jdbc:mysql://localhost:3306/test"); - ds.setUsername("root"); - ds.setPassword("123456"); - return ds; // 返回给Spring管理 + + // 不需要主动创建 UserDao,由 Spring 容器注入 + public BaseUserInfoDTO getAndUpdateUserIpInfoBySessionId(String session, String clientIp) { + // 直接使用注入的 userDao + return userDao.getBySessionId(session); } } ``` -从控制权的角度来说,`@Component` 是由 Spring 自动创建和管理的。 +----这部分面试中可以不背 start---- -![技术派源码:@Component](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628114006.png) +没有 IoC 之前: -而 `@Bean` 则是由我们手动创建的,然后再交给 Spring 管理,我们对对象的创建过程有完全的控制权。 +>我需要一个女朋友,刚好大街上突然看到了一个小姐姐,人很好看,于是我就自己主动上去搭讪,要她的微信号,找机会聊天关心她,然后约她出来吃饭,打听她的爱好,三观。。。 -![技术派源码:@Bean](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628114149.png) -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 9 面试原题:怎么理解spring的bean,@Component 和 @Bean 的区别 +有了 IoC 之后: -memo:2025 年 6 月 28 日修改至此,今天在帮球友[修改简历](https://javabetter.cn/zhishixingqiu/)的时候,又碰到一个杭电本硕的球友。我这里想说的一点是,杭电的计算机专业非常强,虽然他只是一所双非,如果能把项目经历、专业技能好好写的话,拿个大厂的顶级 offer 是完全没问题的。 +>我需要一个女朋友,于是我就去找婚介所,告诉婚介所,我需要一个长的像赵露思的,会打 Dota2 的,于是婚介所在它的人才库里开始找,找不到它就直接说没有,找到它就直接介绍给我。 -![杭州电子科技大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-计算机科学与技术.png) +婚介所就相当于一个 IoC 容器,我就是一个对象,我需要的女朋友就是另一个对象,我不用关心女朋友是怎么来的,我只需要告诉婚介所我需要什么样的女朋友,婚介所就帮我去找。 -### 7.🌟能说一下Bean的生命周期吗? +![三分恶面渣逆袭:引入IoC之前和引入IoC之后](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-619da277-c15e-4dd7-9f2b-dbd809a9aaa0.png) -推荐阅读:[三分恶:Spring Bean 生命周期,好像人的一生](https://mp.weixin.qq.com/s/zb6eA3Se0gQoqL8PylCPLw) +----这部分面试中可以不背 end---- -好的。 +#### DI和IoC的区别了解吗? -Bean 的生命周期可以分为 5 个主要阶段,我按照实际的执行顺序来说一下。 +IoC 的思想是把对象创建和依赖关系的控制权由业务代码转移给 Spring 容器。这是一个比较抽象的概念,告诉我们应该怎么去设计系统架构。 -![三分恶面渣逆袭:Bean生命周期五个阶段](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-595fce5b-36cb-4dcb-b08c-8205a1e98d8a.png) +![Martin Fowler’s Definition](https://cdn.tobebetterjavaer.com/stutymore/spring-20241117132929.png) -第一个阶段是实例化。Spring 容器会根据 BeanDefinition,通过反射调用 Bean 的构造方法创建对象实例。如果有多个构造方法,Spring 会根据依赖注入的规则选择合适的构造方法。 +而 DI,也就是依赖注入,它是实现 IoC 这种思想的具体技术手段。在 Spring 里,我们用 `@Autowired` 注解就是在使用 DI 的字段注入方式。 -![三分恶面渣逆袭:Spring Bean生命周期](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-942a927a-86e4-4a01-8f52-9addd89642ff.png) +```java +@Service +public class ArticleReadServiceImpl implements ArticleReadService { + @Autowired + private ArticleDao articleDao; // 字段注入 + + @Autowired + private UserDao userDao; +} +``` -第二阶段是属性赋值。这个阶段 Spring 会给 Bean 的属性赋值,包括通过 `@Autowired`、`@Resource` 这些注解注入的依赖对象,以及通过 `@Value` 注入的配置值。 +从实现角度来看,DI 除了字段注入,还有构造方法注入和 Setter 方法注入等方式。在做[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)项目的时候,我就尝试过构造方法注入的方式。 -![二哥的 Java 进阶之路:doCreateBean 方法源码](https://cdn.tobebetterjavaer.com/stutymore/spring-20240311101430.png) +![技术派源码:构造方法的注入方式](https://cdn.tobebetterjavaer.com/stutymore/spring-20250622091928.png) -第三阶段是初始化。这个阶段会依次执行: +当然了,DI 并不是实现 IoC 的唯一方式,还有 Service Locator 模式,可以通过实现 ApplicationContextAware 接口来获取 Spring 容器中的 Bean。 -- `@PostConstruct` 标注的方法 -- InitializingBean 接口的 afterPropertiesSet 方法 -- 通过 `@Bean` 的 initMethod 指定的初始化方法 +![技术派源码:IoC 的Service Locator 模式](https://cdn.tobebetterjavaer.com/stutymore/spring-20250622093007.png) -![三分恶面渣逆袭:Bean生命周期源码追踪](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-d2da20a3-08d0-4648-b9a3-2fff8512b159.png) +之所以 ID 后成为 IoC 的首选实现方式,是因为代码更清晰、可读性更高。 -我在项目中经常用 `@PostConstruct` 来做一些初始化工作,比如缓存预加载、DB 配置等等。 +``` +IoC(控制反转) +├── DI(依赖注入) ← 主要实现方式 +│ ├── 构造器注入 +│ ├── 字段注入 +│ └── Setter注入 +├── 服务定位器模式 +├── 工厂模式 +└── 其他实现方式 +``` -```java -// CategoryServiceImpl中的缓存初始化 -@PostConstruct -public void init() { - categoryCaches = CacheBuilder.newBuilder().maximumSize(300).build(new CacheLoader() { - @Override - public CategoryDTO load(@NotNull Long categoryId) throws Exception { - CategoryDO category = categoryDao.getById(categoryId); - // ... - } - }); -} +#### 为什么要使用 IoC 呢? -// DynamicConfigContainer中的配置初始化 -@PostConstruct -public void init() { - cache = Maps.newHashMap(); - bindBeansFromLocalCache("dbConfig", cache); +在日常开发中,如果我们需要实现某一个功能,可能至少需要两个以上的对象来协助完成,在没有 Spring 之前,每个对象在需要它的合作对象时,需要自己 new 一个,比如说 A 要使用 B,A 就对 B 产生了依赖,也就是 A 和 B 之间存在了一种耦合关系。 + +```java +// 传统方式:对象自己创建依赖 +public class UserService { + private UserDao userDao = new UserDaoImpl(); // 硬编码依赖 + + public User getUser(Long id) { + return userDao.findById(id); + } } ``` -初始化后,Spring 还会调用所有注册的 BeanPostProcessor 后置处理方法。这个阶段经常用来创建代理对象,比如 AOP 代理。 - -第五阶段是使用 Bean。比如我们的 Controller 调用 Service,Service 调用 DAO。 +有了 Spring 之后,创建 B 的工作交给了 Spring 来完成,Spring 创建好了 B 对象后就放到容器中,A 告诉 Spring 我需要 B,Spring 就从容器中取出 B 交给 A 来使用。 ```java -// UserController中的使用示例 -@Autowired -private UserService userService; -@GetMapping("/users/{id}") -public UserDTO getUser(@PathVariable Long id) { - return userService.getUserById(id); -} -// UserService中的使用示例 -@Autowired -private UserDao userDao; -public UserDTO getUserById(Long id) { - return userDao.getById(id); -} -// UserDao中的使用示例 -@Autowired -private JdbcTemplate jdbcTemplate; -public UserDTO getById(Long id) { - String sql = "SELECT * FROM users WHERE id = ?"; - return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper()); +// IoC 方式:依赖由外部注入 +@Service +public class UserServiceImpl implements UserService { + @Autowired + private UserDao userDao; // 依赖注入,不关心具体实现 + + public User getUser(Long id) { + return userDao.findById(id); + } } ``` -最后是销毁阶段。当容器关闭或者 Bean 被移除的时候,会依次执行: +至于 B 是怎么来的,A 就不再关心了,Spring 容器想通过 newnew 创建 B 还是 new 创建 B,无所谓。 -- `@PreDestroy` 标注的方法 -- DisposableBean 接口的 destroy 方法 -- 通过 `@Bean` 的 destroyMethod 指定的销毁方法 +这就是 IoC 的好处,它降低了对象之间的耦合度,让每个对象只关注自己的业务实现,不关心其他对象是怎么创建的。 -![二哥的 Java 进阶之路:close 源码](https://cdn.tobebetterjavaer.com/stutymore/spring-20240311101658.png) +推荐阅读:[孤傲苍狼:谈谈对 Spring IOC 的理解](https://www.cnblogs.com/xdp-gacl/p/4249939.html) -#### Aware 类型的接口有什么作用? +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:说说你对 AOP 和 IoC 的理解。 +> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小公司面经合集同学 1 Java 后端面试原题:介绍 Spring IoC 和 AOP? +> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的招商银行面经同学 6 招银网络科技面试原题:SpringBoot框架的AOP、IOC/DI? +> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 8 面试原题:IOC,AOP +> 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:解释下什么是IOC和AOP?分别解决了什么问题?IOC和DI的区别? -Aware 接口在 Spring 中是一个很有意思的设计,它们的作用是让 Bean 能够感知到 Spring 容器的一些内部组件。 +memo:2025 年 6 月 22 日修改至此,今天[有球友发喜报说拿到了两个 offer](https://javabetter.cn/zhishixingqiu/),一个是做 B 端电商的,另一个是外企,主要做 Power BI 的低代码开发,我的建议是去外企,因为实习最重要的是混个 title,有更多的时间,可以去学习星球里的项目,其实会更实在。 -从设计理念来说,Aware 接口实现了一种“回调”机制。正常情况下,Bean 不应该直接依赖 Spring 容器,这样可以保持代码的独立性。但有些时候,Bean 确实需要获取容器的一些信息或者组件,Aware 接口就提供了这样一个能力。 +![球友拿到了外企和电商的 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-二哥,目前拿到了两个offer。第一个是做b端电.png) -我最常用的 Aware 接口是 ApplicationContextAware,它可以让 Bean 获取到 ApplicationContext 容器本身。 +### 7.能说一下IoC的实现机制吗? -![技术派源码:ApplicationContextAware](https://cdn.tobebetterjavaer.com/stutymore/spring-20250630100429.png) +好的,Spring IoC 的实现机制还是比较复杂的,我尽量用比较通俗的方式来解释一下整个流程。 -在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,我就通过实现 ApplicationContextAware 和 EnvironmentAware 接口封装了一个 SpringUtil 工具类,通过 getBean 和 getProperty 方法来获取 Bean 和配置属性。 +![面渣逆袭:mini版本Spring IoC](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-1d55c63d-2d12-43b1-9f43-428f5f4a1413.png) -```java -// 静态方法获取Bean,方便在非Spring管理的类中使用 -public static T getBean(Class clazz) { - return context.getBean(clazz); -} -// 获取配置属性 -public static String getProperty(String key) { - return environment.getProperty(key); +第一步是加载 Bean 的定义信息。Spring 会扫描我们配置的包路径,找到所有标注了 `@Component`、`@Service`、`@Repository` 这些注解的类,然后把这些类的元信息封装成 BeanDefinition 对象。 + +```java +// Bean定义信息 +public class BeanDefinition { + private String beanClassName; // 类名 + private String scope; // 作用域 + private boolean lazyInit; // 是否懒加载 + private String[] dependsOn; // 依赖的Bean + private ConstructorArgumentValues constructorArgumentValues; // 构造参数 + private MutablePropertyValues propertyValues; // 属性值 } ``` -#### 如果配置了 init-method 和 destroy-method,Spring 会在什么时候调用其配置的方法? +第二步是 Bean 工厂的准备。Spring 会创建一个 DefaultListableBeanFactory 作为 Bean 工厂来负责 Bean 的创建和管理。 -init-method 指定的初始化方法会在 Bean 的初始化阶段被调用,具体的执行顺序是: +![技术派源码:DefaultListableBeanFactory](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623094742.png) -- 先执行 `@PostConstruct` 标注的方法 -- 然后执行 InitializingBean 接口的 `afterPropertiesSet()` 方法 -- 最后再执行 init-method 指定的方法 +第三步是 Bean 的实例化和初始化。这个过程比较复杂,Spring 会根据 BeanDefinition 来创建 Bean 实例。 -也就是说,init-method 是在所有其他初始化方法之后执行的。 +![IoC的实现机制](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623101221.png) + +对于单例 Bean,Spring 会先检查缓存中是否已经存在,如果不存在就创建新实例。创建实例的时候会通过反射调用构造方法,然后进行属性注入,最后执行初始化回调方法。 ```java -@Component -public class MyService { - @Autowired - private UserDao userDao; +// 简化的Bean创建流程 +public class AbstractBeanFactory { - @PostConstruct - public void postConstruct() { - System.out.println("1. @PostConstruct执行"); + protected Object createBean(String beanName, BeanDefinition bd) { + // 1. 实例化前处理 + Object bean = resolveBeforeInstantiation(beanName, bd); + if (bean != null) { + return bean; + } + + // 2. 实际创建Bean + return doCreateBean(beanName, bd); } - public void customInit() { // 通过@Bean的initMethod指定 - System.out.println("3. init-method执行"); - } -} - -@Configuration -public class AppConfig { - @Bean(initMethod = "customInit") - public MyService myService() { - return new MyService(); + protected Object doCreateBean(String beanName, BeanDefinition bd) { + // 2.1 实例化 + Object bean = createBeanInstance(beanName, bd); + + // 2.2 属性填充(依赖注入) + populateBean(beanName, bd, bean); + + // 2.3 初始化 + Object exposedObject = initializeBean(beanName, bean, bd); + + return exposedObject; } } ``` -destroy-method 会在 Bean 销毁阶段被调用。 - -```java -@Component -public class MyService { - @PreDestroy - public void preDestroy() { - System.out.println("1. @PreDestroy执行"); - } - - public void customDestroy() { // 通过@Bean的destroyMethod指定 - System.out.println("3. destroy-method执行"); - } -} -``` +依赖注入的实现主要是通过反射来完成的。比如我们用 `@Autowired` 标注了一个字段,Spring 在创建 Bean 的时候会扫描这个字段,然后从容器中找到对应类型的 Bean,通过反射的方式设置到这个字段上。 -不过在实际开发中,通常用 `@PostConstruct` 和 `@PreDestroy` 就够了,它们更简洁。 +![贰师兄的屠宰场:各个注解的注入流程](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628110426.png) -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:说说 Bean 的生命周期 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:Spring中bean生命周期 -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的8 后端开发秋招一面面试原题:讲一下Spring Bean的生命周期 -> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 1 贝壳找房后端技术一面面试原题:bean生命周期 -> 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:介绍下Bean的生命周期?Aware类型接口的作用?如果配置了init-method和destroy-method,Spring会在什么时候调用其配置的方法? +#### 你是怎么理解 Spring IoC 的? -memo:2025 年 6 月 30 日修改至此。昨天有[读者发消息说有三个 offer 要选择](https://javabetter.cn/zhishixingqiu/),中科大读博、中海油、商飞北研,问我该怎么选择?说实话,这三个都是非常优质的选择,我个人的建议是优先考虑中科大读博,毕竟是国内顶尖学府,博士毕业后可以选择在高校任教,会更符合他的家庭条件,当然了,我深知,读博的产出压力非常大。 +IoC 本质上一个超级工厂,这个工厂的产品就是各种 Bean 对象。 -![读者拿到中科大读博、中海油、商飞北研](https://cdn.tobebetterjavaer.com/stutymore/spring-20250630101647.png) +![三分恶面渣逆袭:工厂运行](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-7678c40f-a48d-4bd5-80f8-e902ad688e11.png) +我们通过 `@Component`、`@Service` 这些注解告诉工厂:“我要生产什么样的产品,这个产品有什么特性,需要什么原材料”。 -### 8.Bean的作用域有哪些? +然后工厂里各种生产线,在 Spring 中就是各种 BeanPostProcessor。比如 `AutowiredAnnotationBeanPostProcessor` 专门负责处理 `@Autowired` 注解。 -Bean 的作用域决定了 Bean 实例的生命周期和创建策略,singleton 是默认的作用域。整个 Spring 容器中只会有一个 Bean 实例。不管在多少个地方注入这个 Bean,拿到的都是同一个对象。 +工厂里还有各种缓存机制用来存放产品,比如说 singletonObjects 是成品仓库,存放完工的单例 Bean;earlySingletonObjects 是半成品仓库,用来解决循环依赖问题。 ```java -@Component // 默认就是singleton -public class UserService { - // 整个应用中只有一个UserService实例 +// Spring单例Bean注册表 +public class DefaultSingletonBeanRegistry { + // 一级缓存:完成初始化的单例Bean + private final Map singletonObjects = new ConcurrentHashMap<>(256); + + // 二级缓存:早期暴露的单例Bean(解决循环依赖) + private final Map earlySingletonObjects = new HashMap<>(16); + + // 三级缓存:单例Bean工厂 + private final Map> singletonFactories = new HashMap<>(16); + + public Object getSingleton(String beanName) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + return singletonObject; + } } ``` -生命周期和 Spring 容器相同,容器启动时创建,容器销毁时销毁。 +最有意思的是,这个工厂还很智能,它知道产品之间的依赖关系。它会根据依赖关系来决定 Bean 的创建顺序。如果发现循环依赖,它还会用三级缓存机制来巧妙地解决。 -实际开发中,像 Service、Dao 这些业务组件基本都是单例的,因为单例既能节省内存,又能提高性能。 +#### 能手写一个简单的 IoC 容器吗? -当把 scope 设置为 prototype 时,每次从容器中获取 Bean 的时候都会创建一个新的实例。 +1、首先定义基础的注解,比如说 `@Component`、`@Autowired` 等。 ```java -@Component -@Scope("prototype") -public class OrderProcessor { - // 每次注入或获取都是新的实例 +// 组件注解 +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Component { } -``` -当需要处理一些有状态的 Bean 时会用到 prototype,比如每个订单处理器需要维护不同的状态信息。 +// 自动注入注解 +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Autowired { +} +``` -需要注意的是,在 singleton Bean 中注入 prototype Bean 时要小心,因为 singleton Bean 只创建一次,所以 prototype Bean 也只会注入一次。这时候可以用 `@Lookup` 注解或者 ApplicationContext 来动态获取。 +2、核心的 IoC 容器类,负责扫描包路径,创建 Bean 实例,并处理依赖注入。 ```java -@Component -public class SingletonService { - // 错误的做法,prototypeBean只会注入一次 - @Autowired - private PrototypeBean prototypeBean; +public class SimpleIoC { + // Bean容器 + private Map, Object> beans = new HashMap<>(); - // 正确的做法,每次调用都获取新实例 - @Lookup - public PrototypeBean getPrototypeBean() { - return null; // Spring会重写这个方法 + /** + * 注册Bean + */ + public void registerBean(Class clazz) { + try { + // 创建实例 + Object instance = clazz.getDeclaredConstructor().newInstance(); + beans.put(clazz, instance); + } catch (Exception e) { + throw new RuntimeException("创建Bean失败: " + clazz.getName(), e); + } + } + + /** + * 获取Bean + */ + @SuppressWarnings("unchecked") + public T getBean(Class clazz) { + return (T) beans.get(clazz); + } + + /** + * 依赖注入 + */ + public void inject() { + for (Object bean : beans.values()) { + injectFields(bean); + } + } + + /** + * 字段注入 + */ + private void injectFields(Object bean) { + Field[] fields = bean.getClass().getDeclaredFields(); + for (Field field : fields) { + if (field.isAnnotationPresent(Autowired.class)) { + try { + field.setAccessible(true); + Object dependency = getBean(field.getType()); + field.set(bean, dependency); + } catch (Exception e) { + throw new RuntimeException("注入失败: " + field.getName(), e); + } + } + } } } ``` -除了 singleton 和 prototype,Spring 还支持其他作用域,比如 request、session、application 和 websocket。 - -![三分恶面渣逆袭:Spring Bean支持作用域](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-08a9cb31-5a4f-4224-94cd-0c0f643a57ea.png) - -如果作用于是 request,表示在 Web 应用中,每个 HTTP 请求都会创建一个新的 Bean 实例,请求结束后 Bean 就被销毁。 +3、使用示例,定义一些 Bean 类,并注册到 IoC 容器中。 ```java +// DAO层 @Component -@Scope("request") -public class RequestContext { - // 每个HTTP请求都有自己的实例 +class UserDao { + public void save(String user) { + System.out.println("保存用户: " + user); + } } -``` - -如果作用于是 session,表示在 Web 应用中,每个 HTTP 会话都会创建一个新的 Bean 实例,会话结束后 Bean 被销毁。 -```java +// Service层 @Component -@Scope("session") -public class UserSession { - // 每个用户会话都有自己的实例 +class UserService { + @Autowired + private UserDao userDao; + + public void createUser(String name) { + userDao.save(name); + System.out.println("用户创建完成"); + } } -``` - -典型的使用场景是购物车、用户登录状态这些需要在整个会话期间保持的信息。 - -application 作用域表示在整个应用中只有一个 Bean 实例,类似于 singleton,但它的生命周期与 ServletContext 绑定。 -```java -@Component -@Scope("application") -public class AppConfig { - // 整个应用中只有一个实例 +// 测试 +public class Test { + public static void main(String[] args) { + SimpleIoC ioc = new SimpleIoC(); + + // 注册Bean + ioc.registerBean(UserDao.class); + ioc.registerBean(UserService.class); + + // 依赖注入 + ioc.inject(); + + // 使用 + UserService userService = ioc.getBean(UserService.class); + userService.createUser("王二"); + } } ``` -websocket 作用域表示在 WebSocket 会话中每个连接都有自己的 Bean 实例。WebSocket 连接建立时创建,连接关闭时销毁。 +4、可以加上组件扫描。 ```java -@Component -@Scope("websocket") -public class WebSocketHandler { - // 每个WebSocket连接都有自己的实例 -} -``` - -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 1 贝壳找房后端技术一面面试原题:bean是单例还是多例的,具体怎么修改 - -memo:2025 年 7 月 3 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个郑州大学硕,河北师范大学本的球友,整体在校的经历非常出色,奖学金、论文期刊基本上都拉满了。那这么多优秀的球友选择来到这里,也是对星球的又一次认可和肯定,我也一定会继续努力,提供更多优质的内容和服务。 - -![郑州大学硕,河北师范大学本的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250704144758.png) - -### 9.Spring中的单例Bean会存在线程安全问题吗? - -首先要明确一点。Spring 容器本身保证了 Bean 创建过程的线程安全,也就是说不会出现多个线程同时创建同一个单例 Bean 的情况。但是 Bean 创建完成后的使用过程,Spring 就不管了。 - -换句话说,单例 Bean 在被创建后,如果它的内部状态是可变的,那么在多线程环境下就可能会出现线程安全问题。 - -![三分恶面渣逆袭:Spring单例Bean线程安全问题](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-35dacef4-1a9e-45e1-b3f2-5a91227eb244.png) +import java.lang.reflect.Field; +import java.util.*; -比如说在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,有一个敏感词过滤的 Bean,我们就需要使用 volatile 关键字来保证多线程环境下的可见性。 - -```java -@Service -public class SensitiveService { - private volatile SensitiveWordBs sensitiveWordBs; // 使用volatile保证可见性 +public class SimpleIoC { + private Map, Object> beans = new HashMap<>(); - @PostConstruct - public void refresh() { - // 重新初始化sensitiveWordBs + /** + * 扫描并注册组件 + */ + public void scan(String packageName) { + // 简化版:手动添加需要扫描的类 + List> classes = getClassesInPackage(packageName); + + for (Class clazz : classes) { + if (clazz.isAnnotationPresent(Component.class)) { + registerBean(clazz); + } + } + + // 依赖注入 + inject(); + } + + /** + * 获取包下的类(简化实现) + */ + private List> getClassesInPackage(String packageName) { + // 面试时可以说:"实际实现需要扫描classpath,这里简化处理" + return Arrays.asList(UserDao.class, UserService.class); + } + + private void registerBean(Class clazz) { + try { + Object instance = clazz.getDeclaredConstructor().newInstance(); + beans.put(clazz, instance); + } catch (Exception e) { + throw new RuntimeException("创建Bean失败", e); + } + } + + @SuppressWarnings("unchecked") + public T getBean(Class clazz) { + return (T) beans.get(clazz); + } + + private void inject() { + for (Object bean : beans.values()) { + Field[] fields = bean.getClass().getDeclaredFields(); + for (Field field : fields) { + if (field.isAnnotationPresent(Autowired.class)) { + try { + field.setAccessible(true); + Object dependency = getBean(field.getType()); + field.set(bean, dependency); + } catch (Exception e) { + throw new RuntimeException("注入失败", e); + } + } + } + } } } ``` -如果 Bean 中没有成员变量,或者成员变量都是不可变的,final 修饰的,那么就不存在线程安全问题。 +IoC 容器的核心是管理对象和依赖注入,首先定义注解,然后实现容器的三个核心方法:注册Bean、获取Bean、依赖注入;关键是用反射创建对象和注入依赖。 -```java -@Service -public class UserServiceImpl implements UserService { - @Resource - private UserDao userDao; - @Autowired - private CountService countService; - // 只有依赖注入的无状态字段 -} +memo:2025 年 6 月 23 日修改至此,今天[有球友发喜报说拿到了京东的社招 offer](https://javabetter.cn/zhishixingqiu/),这真的要恭喜他,也希望所有看到这里的小伙伴都能有一个好的结果。 -@Service -public class ConfigService { - private final String appName; // final修饰,不可变 - - public ConfigService(@Value("${app.name}") String appName) { - this.appName = appName; - } -} -``` +![球友拿到京东社招 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623105438.png) -#### 单例Bean的线程安全问题怎么解决呢? +### 8.说说BeanFactory和ApplicantContext的区别? -第一种,使用局部变量,也就是使用无状态的单例 Bean,把所有状态都通过方法参数传递: +BeanFactory 算是 Spring 的“心脏”,而 ApplicantContext 可以说是 Spring 的完整“身躯”。 + +![三分恶面渣逆袭:BeanFactory和ApplicantContext](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-66328446-f89f-4b7a-8d9f-0e1145dd9b2f.png) + +BeanFactory 提供了最基本的 IoC 能力。它就像是一个 Bean 工厂,负责 Bean 的创建和管理。他采用的是懒加载的方式,也就是说只有当我们真正去获取某个 Bean 的时候,它才会去创建这个 Bean。 + +![三分恶面渣逆袭:Spring5 BeanFactory继承体系](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-6e6d4b69-f36c-41e6-b8ba-9277be147c9b.png) + +它最主要的方法就是 `getBean()`,负责从容器中返回特定名称或者类型的 Bean 实例。 ```java -@Service -public class UserService { - @Autowired - private UserDao userDao; - - // 无状态方法,所有数据通过参数传递 - public User processUser(Long userId, String operation) { - User user = userDao.findById(userId); - // 处理逻辑... - return user; +public class BeanFactoryExample { + public static void main(String[] args) { + // 创建 BeanFactory + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + // 手动注册 Bean 定义 + BeanDefinition beanDefinition = new RootBeanDefinition(UserService.class); + beanFactory.registerBeanDefinition("userService", beanDefinition); + + // 懒加载:此时才创建 Bean 实例 + UserService userService = beanFactory.getBean("userService", UserService.class); } } ``` -第二种,当确实需要维护线程相关的状态时,可以使用 [ThreadLocal](https://javabetter.cn/thread/ThreadLocal.html) 来保存状态。ThreadLocal 可以保证每个线程都有自己的变量副本,互不干扰。 +ApplicationContext 是 BeanFactory 的子接口,在 BeanFactory 的基础上扩展了很多企业级的功能。它不仅包含了 BeanFactory 的所有功能,还提供了国际化支持、事件发布机制、AOP、JDBC、ORM 框架集成等等。 + +![三分恶面渣逆袭:Spring5 ApplicationContext部分体系类图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-e201c9a3-f23c-4768-b844-ac7e0ba4bcec.png) + +ApplicationContext 采用的是饿加载的方式,容器启动的时候就会把所有的单例 Bean 都创建好,虽然这样会导致启动时间长一点,但运行时性能更好。 ```java -@Service -public class UserContextService { - private static final ThreadLocal userThreadLocal = new ThreadLocal<>(); - - public void setCurrentUser(User user) { - userThreadLocal.set(user); - } - - public User getCurrentUser() { - return userThreadLocal.get(); +@Configuration +public class AppConfig { + @Bean + public UserService userService() { + return new UserService(); } - - public void clear() { - userThreadLocal.remove(); // 防止内存泄漏 +} + +public class ApplicationContextExample { + public static void main(String[] args) { + // 创建 ApplicationContext,启动时就创建所有 Bean + ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); + + // 获取 Bean + UserService userService = context.getBean(UserService.class); + + // 发布事件 + context.publishEvent(new CustomEvent("Hello World")); } } ``` -第三种,如果需要缓存数据或者计数,使用 JUC 包下的线程安全类,比如说 [AtomicInteger](https://javabetter.cn/thread/atomic.html)、[ConcurrentHashMap](https://javabetter.cn/thread/ConcurrentHashMap.html)、[CopyOnWriteArrayList](https://javabetter.cn/thread/CopyOnWriteArrayList.html) 等。 +从使用场景来说,实际开发中用得最多的是 ApplicationContext。像 AnnotationConfigApplicationContext、WebApplicationContext 这些都是 ApplicationContext 的实现类。 + +另外一个重要的区别是生命周期管理。ApplicationContext 会自动调用 Bean 的初始化和销毁方法,而 BeanFactory 需要我们手动管理。 + +在 Spring Boot 项目中,我们可以通过 `@Autowired` 注入 ApplicationContext,或者通过实现 ApplicationContextAware 接口来获取 ApplicationContext。 + +![技术派源码:获取ApplicationContext](https://cdn.tobebetterjavaer.com/stutymore/spring-20250625111259.png) + +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 2 优选物流调度技术 2 面面试原题:BeanFactory和ApplicationContext + +memo:2025 年 6 月 25 日修改至此,今天给一个华科本硕研 0 的[球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)后,发来这样的感慨,要是早点知道你的[网站](https://javabetter.cn/home.html)和[星球](https://javabetter.cn/zhishixingqiu/)就好了,[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)不比外卖强多了?再次感谢二哥。 + +![球友对星球相见恨晚](https://cdn.tobebetterjavaer.com/stutymore/spring-20250625111617.png) + +### 9.🌟项目启动时Spring的IoC会做什么? + +第一件事是扫描和注册 Bean。IoC 容器会根据我们的配置,比如 `@ComponentScan` 指定的包路径,去扫描所有标注了 `@Component`、`@Service`、`@Controller` 这些注解的类。然后把这些类的元信息包装成 BeanDefinition 对象,注册到容器的 BeanDefinitionRegistry 中。这个阶段只是收集信息,还没有真正创建对象。 + +![pdai.tech:IoC](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627101759.png) + +第二件事是 Bean 的实例化和注入。这是最核心的过程,IoC 容器会按照依赖关系的顺序开始创建 Bean 实例。对于单例 Bean,容器会通过反射调用构造方法创建实例,然后进行属性注入,最后执行初始化回调方法。 + +![Tom弹架构:Bean 的实例化和注入](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627102651.png) + +在依赖注入时,容器会根据 `@Autowired`、`@Resource` 这些注解,把相应的依赖对象注入到目标 Bean 中。比如 UserService 需要 UserDao,容器就会把 UserDao 的实例注入到 UserService 中。 + +#### 说说Spring的Bean实例化方式? + +Spring 提供了 4 种方式来实例化 Bean,以满足不同场景下的需求。 + +第一种是通过构造方法实例化,这是最常用的方式。当我们用 `@Component`、`@Service` 这些注解标注类的时候,Spring 默认通过无参构造器来创建实例的。如果类只有一个有参构造方法,Spring 会自动进行构造方法注入。 ```java @Service -public class CacheService { - // 使用线程安全的集合 - private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - private final AtomicLong counter = new AtomicLong(0); +public class UserService { + private UserDao userDao; - public void put(String key, Object value) { - cache.put(key, value); - counter.incrementAndGet(); + public UserService(UserDao userDao) { // 构造方法注入 + this.userDao = userDao; } } ``` -第四种,对于复杂的状态操作,可以使用 synchronized 或 Lock: +第二种是通过静态工厂方法实例化。有时候对象的创建比较复杂,我们会写一个静态工厂方法来创建,然后用 `@Bean` 注解来标注这个方法。Spring 会调用这个静态方法来获取 Bean 实例。 ```java -@Service -public class CacheService { - private final Map cache = new HashMap<>(); - private final ReentrantLock lock = new ReentrantLock(); - - public void put(String key, Object value) { - lock.lock(); - try { - cache.put(key, value); - } finally { - lock.unlock(); - } +@Configuration +public class AppConfig { + @Bean + public static DataSource createDataSource() { + // 复杂的DataSource创建逻辑 + return new HikariDataSource(); } } ``` -第五种,如果 Bean 确实需要维护状态,可以考虑将其改为 prototype 作用域,这样每次注入都会创建一个新的实例,避免了多线程共享同一个实例的问题。 +第三种是通过实例工厂方法实例化。这种方式是先创建工厂对象,然后通过工厂对象的方法来创建Bean: ```java -@Service -@Scope("prototype") // 每次注入都创建新实例 -public class StatefulService { - private String state; // 现在每个实例都有独立状态 +@Configuration +public class AppConfig { + @Bean + public ConnectionFactory connectionFactory() { + return new ConnectionFactory(); + } - public void setState(String state) { - this.state = state; + @Bean + public Connection createConnection(ConnectionFactory factory) { + return factory.createConnection(); } } ``` -或者使用 request 作用域,这样每个 HTTP 请求都会创建一个新的实例。 +第四种是通过 FactoryBean 接口实例化。这是 Spring 提供的一个特殊接口,当我们需要创建复杂对象的时候特别有用: ```java -@Service -@Scope("request") -public class RequestScopedService { - private String requestData; - // 每个请求都有独立的实例 +@Component +public class MyFactoryBean implements FactoryBean { + @Override + public MyObject getObject() throws Exception { + // 复杂的对象创建逻辑 + return new MyObject(); + } + + @Override + public Class getObjectType() { + return MyObject.class; + } } ``` -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里面经同学 1 闲鱼后端一面的原题:spring的bean的并发安全问题 +在实际工作中,用得最多的还是构造方法实例化,因为简单直接。工厂方法一般用在需要复杂初始化逻辑的场景,比如数据库连接池、消息队列连接这些。FactoryBean 主要是在框架开发或者需要动态创建对象的时候使用。 -memo:2025 年 7 月 4 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个武汉理工大学本硕的球友。说真的,和武汉理工大学挺有缘的,2023 年去武汉,就线下见了一名武理的球友,[他当时签约的是小米](https://t.zsxq.com/LfG3B),非常优秀。 +Spring 在实例化的时候会根据 Bean 的定义自动选择合适的方式,我们作为开发者主要是通过注解和配置来告诉 Spring 应该怎么创建对象。 -![武汉理工大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-2023.09-2026.06.png) - -### 10.为什么IDEA不推荐使用@Autowired注解注入Bean? - -前情提要:当使用 `@Autowired` 注解注入 Bean 时,IDEA 会提示“Field injection is not recommended”。 +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为面经同学 8 技术二面面试原题:说说 Spring 的 Bean 实例化方式 +> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 2 优选物流调度技术 2 面面试原题:bean加工有哪些方法? -![二哥的 Java 进阶之路:@Autowired](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224164722.png) +memo:2025 年 6 月 27 日修改至此,今天看到[有球友发的 offer 选择提问贴](https://javabetter.cn/zhishixingqiu/),其中一个是杭州六小龙群核科技,我个人认为还是非常值得去的,毕竟是杭州的独角兽公司,薪资待遇都不错。 -面试回答: +![球友拿到了杭州群核科技的 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627103801.png) -主要有几个原因。 +### 10.你是怎么理解Bean的? -第一个是字段注入不利于单元测试。字段注入需要使用反射或 Spring 容器才能注入依赖,测试更复杂;而构造方法注入可以直接通过构造方法传入 Mock 对象,测试起来更简单。 +在我看来,Bean 本质上就是由 Spring 容器管理的 Java 对象,但它和普通的 Java 对象有很大区别。普通的 Java 对象我们是通过 new 关键字创建的。而 Bean 是交给 Spring 容器来管理的,从创建到销毁都由容器负责。 -```java -// 字段注入的测试困难 -@Test -public void testUserService() { - UserService userService = new UserService(); - // 无法直接设置userRepository,需要反射或Spring容器 - // userService.userRepository = Mockito.mock(UserRepository.class); - // 需要手动设置依赖,测试不方便 - ReflectionTestUtils.setField(userService, "userRepository", Mockito.mock(UserRepository.class)); - userService.doSomething(); - // ... -} +![stack overflow:bean 的初始化过程](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628110931.png) -// 构造方法注入的测试简单 -@Test -public void testUserService() { - UserRepository mockRepository = Mockito.mock(UserRepository.class); - UserService userService = new UserService(mockRepository); // 直接注入 -} -``` +从实际使用的角度来说,我们项目里的 Service、Dao、Controller 这些都是 Bean。比如 UserService 被标注了 `@Service` 注解,它就成了一个 Bean,Spring 会自动创建它的实例,管理它的依赖关系,当其他地方需要用到 UserService 的时候,Spring 就会把这个实例注入进去。 -第二个是字段注入会隐藏循环依赖问题,而构造方法注入会在项目启动时就去检查依赖关系,能更早发现问题。 +![技术派源码:UserService](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628111222.png) -第三个是构造方法注入可以使用 final 字段确保依赖在对象创建时就被初始化,避免了后续修改的风险。 +这种依赖注入的方式让对象之间的关系变得松耦合。 -在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,我们已经在使用构造方法注入的方式来管理依赖关系。 +Spring 提供了多种 Bean 的配置方式,基于注解的方式是最常用的。 -![技术派:构造方法注入](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224165628.png) +![二哥的 Java 进阶之路:Bean 的声明方式](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224163146.png) -不过话说回来,`@Autowired` 的字段注入方式在一些简单的场景下还是可以用的,主要看团队的编码规范吧。 +基于 XML 配置的方式在 Spring Boot 项目中已经不怎么用了。Java 配置类的方式则可以用来解决一些比较复杂的场景,比如说主从数据源,我们可以用 `@Primary` 注解标注主数据源,用 `@Qualifier` 来指定备用数据源。 -#### @Autowired 和 @Resource 注解的区别? +```java +@Configuration +public class AppConfig { + + @Bean + @Primary // 主要候选者 + public DataSource primaryDataSource() { + return new HikariDataSource(); + } + + @Bean + @Qualifier("secondary") + public DataSource secondaryDataSource() { + return new BasicDataSource(); + } +} +``` -首先从来源上说,`@Autowired` 是 Spring 框架提供的注解,而 `@Resource` 是 Java EE 标准提供的注解。换句话说,`@Resource` 是 JDK 自带的,而 `@Autowired` 是 Spring 特有的。 +那在使用的时候,当我们直接用 `@Autowired` 注解注入 DataSource 时,Spring 默认会使用 HikariDataSource;当加上 `@Qualifier("secondary")` 注解时,Spring 则会注入 BasicDataSource。 -虽然 IDEA 不推荐使用 `@Autowired`,但对 `@Resource` 注解却没有任何提示。 +```java +@Autowired +private DataSource dataSource; // 会注入 primaryDataSource(因为有 @Primary) -![技术派:@Resource](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224170055.png) +@Autowired +@Qualifier("secondary") +private DataSource secondaryDataSource; +``` -从注入方式上说,`@Autowired` 默认按照类型,也就是 byType 进行注入,而 `@Resource` 默认按照名称,也就是 byName 进行注入。 +#### @Component 和 @Bean 有什么区别? -当容器中存在多个相同类型的 Bean, 比如说有两个 UserRepository 的实现类,直接用 `@Autowired` 注入 UserRepository 时就会报错,因为 Spring 容器不知道该注入哪个实现类。 +首先从使用上来说,`@Component` 是标注在类上的,而 `@Bean` 是标注在方法上的。`@Component` 告诉 Spring 这个类是一个组件,请把它注册为 Bean,而 `@Bean` 则告诉 Spring 请将这个方法返回的对象注册为 Bean。 ```java -@Component -public class UserRepository21 implements UserRepository2 {} - -@Component -public class UserRepository22 implements UserRepository2 {} - -@Component -public class UserService2 { +@Component // Spring自动创建UserService实例 +public class UserService { @Autowired - private UserRepository2 userRepository; // 冲突 + private UserDao userDao; } -``` - -这时候,有两种解决方案,第一种是使用 `@Autowired` + `@Qualifier` 指定具体的 Bean 名称来解决冲突。 -```java -@Component("userRepository21") -public class UserRepository21 implements UserRepository2 { -} -@Component("userRepository22") -public class UserRepository22 implements UserRepository2 { +@Configuration +public class AppConfig { + @Bean // 我们手动创建DataSource实例 + public DataSource dataSource() { + HikariDataSource ds = new HikariDataSource(); + ds.setJdbcUrl("jdbc:mysql://localhost:3306/test"); + ds.setUsername("root"); + ds.setPassword("123456"); + return ds; // 返回给Spring管理 + } } -@Autowired -@Qualifier("userRepository22") -private UserRepository2 userRepository22; ``` -第二种是使用 `@Resource` 注解按名称进行注入。 +从控制权的角度来说,`@Component` 是由 Spring 自动创建和管理的。 -```java -@Resource(name = "userRepository21") -private UserRepository2 userRepository21; -``` +![技术派源码:@Component](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628114006.png) -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 9 面试原题:依赖注入的时候,直接Autowired比较直接,为什么推荐构造方法注入呢 +而 `@Bean` 则是由我们手动创建的,然后再交给 Spring 管理,我们对对象的创建过程有完全的控制权。 -memo:2025 年 7 月 1 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个郑州大学本硕的球友,这也是我们河南省最好的大学了,但也仅仅是一所 211,所以希望所有河南的同学都能加把劲,证明自己的实力,去拿到更好的 offer,为校争光。 +![技术派源码:@Bean](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628114149.png) -![郑州大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250701154344.png) +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 9 面试原题:怎么理解spring的bean,@Component 和 @Bean 的区别 -### 11.@Autowired的实现原理了解吗? +memo:2025 年 6 月 28 日修改至此,今天在帮球友[修改简历](https://javabetter.cn/zhishixingqiu/)的时候,又碰到一个杭电本硕的球友。我这里想说的一点是,杭电的计算机专业非常强,虽然他只是一所双非,如果能把项目经历、专业技能好好写的话,拿个大厂的顶级 offer 是完全没问题的。 -`@Autowired` 是 Spring 实现依赖注入的核心注解,其实现原理基于反射机制和 BeanPostProcessor 接口。 +![杭州电子科技大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-计算机科学与技术.png) -整个过程分为两个主要阶段。第一个阶段是依赖收集阶段,发生在 Bean 实例化之后、属性赋值之前。`Autowired` 的 Processor 会扫描 Bean 的所有字段、方法和构造方法,找出标注了 `@Autowired` 注解的地方,然后把这些信息封装成 `Injection` 元数据对象缓存起来。这个过程用到了大量的反射操作,需要分析类的结构、注解信息等等。 +### 11.🌟能说一下Bean的生命周期吗? -![MarkusZhang:@Autowired](https://cdn.tobebetterjavaer.com/stutymore/spring-20250711165339.png) +推荐阅读:[三分恶:Spring Bean 生命周期,好像人的一生](https://mp.weixin.qq.com/s/zb6eA3Se0gQoqL8PylCPLw) -第二个阶段是依赖注入阶段,Spring 会取出之前缓存的 `Injection` 元数据对象,然后逐个处理每个注入点。对于每个 `@Autowired` 标注的字段或方法,Spring 会根据类型去容器中查找匹配的 Bean。 +好的。 -```java -// 1. 按类型查找(byType) -Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( - this.beanFactory, type); +Bean 的生命周期可以分为 5 个主要阶段,我按照实际的执行顺序来说一下。 -// 2. 如果找到多个候选者,按名称筛选(byName) -String autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); +![三分恶面渣逆袭:Bean生命周期五个阶段](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-595fce5b-36cb-4dcb-b08c-8205a1e98d8a.png) -// 3. 考虑@Primary和@Priority注解 -// 4. 最后按照字段名或参数名匹配 -``` +第一个阶段是实例化。Spring 容器会根据 BeanDefinition,通过反射调用 Bean 的构造方法创建对象实例。如果有多个构造方法,Spring 会根据依赖注入的规则选择合适的构造方法。 -在具体的注入过程中,Spring 会使用反射来设置字段的值或者调用 setter 方法。比如对于字段注入,会调用 `Field.set()` 方法;对于 setter 注入,会调用 `Method.invoke()` 方法。 +![三分恶面渣逆袭:Spring Bean生命周期](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-942a927a-86e4-4a01-8f52-9addd89642ff.png) -### 12.什么是自动装配? +第二阶段是属性赋值。这个阶段 Spring 会给 Bean 的属性赋值,包括通过 `@Autowired`、`@Resource` 这些注解注入的依赖对象,以及通过 `@Value` 注入的配置值。 -自动装配的本质就是让 Spring 容器自动帮我们完成 Bean 之间的依赖关系注入,而不需要我们手动去指定每个依赖。简单来说,就是“我们不用告诉 Spring 具体怎么注入,Spring 自己会想办法找到合适的 Bean 注入进来”。 +![二哥的 Java 进阶之路:doCreateBean 方法源码](https://cdn.tobebetterjavaer.com/stutymore/spring-20240311101430.png) -自动装配的工作原理简单来说就是,Spring 容器在启动时自动扫描 `@ComponentScan` 指定包路径下的所有类,然后根据类上的注解,比如 `@Autowired`、`@Resource` 等,来判断哪些 Bean 需要被自动装配。 +第三阶段是初始化。这个阶段会依次执行: -```java -@Configuration -@ComponentScan("com.github.paicoding.forum.service") -@MapperScan(basePackages = { - "com.github.paicoding.forum.service.article.repository.mapper", - "com.github.paicoding.forum.service.user.repository.mapper" - // ... 更多包路径 -}) -public class ServiceAutoConfig { - // Spring自动扫描指定包下的所有组件并注册为Bean -} -``` +- `@PostConstruct` 标注的方法 +- InitializingBean 接口的 afterPropertiesSet 方法 +- 通过 `@Bean` 的 initMethod 指定的初始化方法 -之后分析每个 Bean 的依赖关系,在创建 Bean 的时候,根据装配规则自动找到合适的依赖 Bean,最后根据反射将这些依赖注入到目标 Bean 中。 +![三分恶面渣逆袭:Bean生命周期源码追踪](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-d2da20a3-08d0-4648-b9a3-2fff8512b159.png) -#### Spring提供了哪几种自动装配类型? +我在项目中经常用 `@PostConstruct` 来做一些初始化工作,比如缓存预加载、DB 配置等等。 -Spring 的自动装配方式有好几种,在 XML 配置时代,主要有 byName、byType、constructor 和 autodetect 四种方式。 +```java +// CategoryServiceImpl中的缓存初始化 +@PostConstruct +public void init() { + categoryCaches = CacheBuilder.newBuilder().maximumSize(300).build(new CacheLoader() { + @Override + public CategoryDTO load(@NotNull Long categoryId) throws Exception { + CategoryDO category = categoryDao.getById(categoryId); + // ... + } + }); +} -![三分恶面渣逆袭:Spring四种自动装配类型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-034120d9-88c7-490b-af07-7d48f3b6b7bc.png) +// DynamicConfigContainer中的配置初始化 +@PostConstruct +public void init() { + cache = Maps.newHashMap(); + bindBeansFromLocalCache("dbConfig", cache); +} +``` -到了注解驱动时代,用得最多的是 `@Autowired` 注解,默认按照类型装配。 +初始化后,Spring 还会调用所有注册的 BeanPostProcessor 后置处理方法。这个阶段经常用来创建代理对象,比如 AOP 代理。 + +第五阶段是使用 Bean。比如我们的 Controller 调用 Service,Service 调用 DAO。 ```java -@Service -public class UserService { - @Autowired // 按类型自动装配 - private UserRepository userRepository; +// UserController中的使用示例 +@Autowired +private UserService userService; +@GetMapping("/users/{id}") +public UserDTO getUser(@PathVariable Long id) { + return userService.getUserById(id); +} +// UserService中的使用示例 +@Autowired +private UserDao userDao; +public UserDTO getUserById(Long id) { + return userDao.getById(id); +} +// UserDao中的使用示例 +@Autowired +private JdbcTemplate jdbcTemplate; +public UserDTO getById(Long id) { + String sql = "SELECT * FROM users WHERE id = ?"; + return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper()); } ``` -其次还有 `@Resource` 注解,它默认按照名称装配,如果找不到对应名称的 Bean,就会按类型装配。 - -Spring Boot 的自动装配还有一套更高级的机制,通过 `@EnableAutoConfiguration` 和各种 `@Conditional` 注解来实现,这个是框架级别的自动装配,会根据 classpath 中的类和配置来自动配置 Bean。 +最后是销毁阶段。当容器关闭或者 Bean 被移除的时候,会依次执行: -![ShawnBlog:Spring Boot 的自动装配](https://cdn.tobebetterjavaer.com/stutymore/spring-20250702101032.png) +- `@PreDestroy` 标注的方法 +- DisposableBean 接口的 destroy 方法 +- 通过 `@Bean` 的 destroyMethod 指定的销毁方法 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的 oppo 面经同学 15 技术面试原题:spring自动装配原理、启动原理,要在启动阶段自定义逻辑该怎么做? +![二哥的 Java 进阶之路:close 源码](https://cdn.tobebetterjavaer.com/stutymore/spring-20240311101658.png) -memo:2025 年 7 月 2 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个北京航空航天大学的球友,他在邮件中说到:在星球里学到了好多东西,目前正在准备[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)和 MYDB,打算好好冲秋招,能帮助到大家我真的很欣慰。 +#### Aware 类型的接口有什么作用? -![北航球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/spring-20250702101812.png) +Aware 接口在 Spring 中是一个很有意思的设计,它们的作用是让 Bean 能够感知到 Spring 容器的一些内部组件。 -### 13.什么是循环依赖? +从设计理念来说,Aware 接口实现了一种“回调”机制。正常情况下,Bean 不应该直接依赖 Spring 容器,这样可以保持代码的独立性。但有些时候,Bean 确实需要获取容器的一些信息或者组件,Aware 接口就提供了这样一个能力。 -简单来说就是两个或多个 Bean 相互依赖,比如说 A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。 +我最常用的 Aware 接口是 ApplicationContextAware,它可以让 Bean 获取到 ApplicationContext 容器本身。 -![三分恶面渣逆袭:Spring循环依赖](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-f8fea53f-56fa-4cca-9199-ec7f648da625.png) +![技术派源码:ApplicationContextAware](https://cdn.tobebetterjavaer.com/stutymore/spring-20250630100429.png) -### 14.🌟Spring怎么解决循环依赖呢? +在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,我就通过实现 ApplicationContextAware 和 EnvironmentAware 接口封装了一个 SpringUtil 工具类,通过 getBean 和 getProperty 方法来获取 Bean 和配置属性。 -Spring 通过三级缓存机制来解决循环依赖: +```java +// 静态方法获取Bean,方便在非Spring管理的类中使用 +public static T getBean(Class clazz) { + return context.getBean(clazz); +} -1. 一级缓存:存放完全初始化好的单例 Bean。 -2. 二级缓存:存放提前暴露的 Bean,实例化完成,但未初始化完成。 -3. 三级缓存:存放 Bean 工厂,用于生成提前暴露的 Bean。 +// 获取配置属性 +public static String getProperty(String key) { + return environment.getProperty(key); +} +``` -![三分恶面渣逆袭:三级缓存](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-01d92863-a2cb-4f61-8d8d-30ecf0279b28.png) +#### 如果配置了 init-method 和 destroy-method,Spring 会在什么时候调用其配置的方法? -以 A、B 两个类发生循环依赖为例: +init-method 指定的初始化方法会在 Bean 的初始化阶段被调用,具体的执行顺序是: -![三分恶面渣逆袭:循环依赖](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-cfc09f84-f8e1-4702-80b6-d115843e81fe.png) - -第 1 步:开始创建 Bean A。 - -- Spring 调用 A 的构造方法,创建 A 的实例。此时 A 对象已存在,但 b属性还是 null。 -- 将 A 的对象工厂放入三级缓存。 -- 开始进行 A 的属性注入。 - -![三分恶面渣逆袭:A 对象工厂](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-1a8bdc29-ff43-4ff4-9b61-3eedd9da59b3.png) - -第 2 步:A 需要注入 B,开始创建 Bean B。 - -- 发现需要 B,但 B 还不存在,所以开始创建 B。 -- 调用 B 的构造方法,创建 B 的实例。此时 B 对象已存在,但 a 属性还是 null。 -- 将 B 的对象工厂放入三级缓存。 -- 开始进行 B 的属性注入。 - -第 3 步:B 需要注入 A,从缓存中获取 A。 - -- B 需要注入 A,先从一级缓存找 A,没找到。 -- 再从二级缓存找 A,也没找到。 -- 最后从三级缓存找 A,找到了 A 的对象工厂。 -- 调用 A 的对象工厂得到 A 的实例。这时 A 已经实例化了,虽然还没完全初始化。 -- 将 A 从三级缓存移到二级缓存。 -- B 拿到 A 的引用,完成属性注入。 - -![三分恶面渣逆袭:A 放入二级缓存,B 放入一级缓存](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-bf2507bf-96aa-4b88-a58b-7ec41d11bc70.png) - -第 4 步:B 完成初始化。 - -- B 的属性注入完成,执行 `@PostConstruct` 等初始化逻辑。 -- B 完全创建完成,从三级缓存移除,放入一级缓存。 - -第 5 步:A 完成初始化。 - -- 回到 A 的创建过程,A 拿到完整的 B 实例,完成属性注入。 -- A 执行初始化逻辑,创建完成。 -- A 从二级缓存移除,放入一级缓存。 - -![三分恶面渣逆袭:AB 都好了](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-022f7cb9-2c83-4fe9-b252-b02bd0fb2435.png) +- 先执行 `@PostConstruct` 标注的方法 +- 然后执行 InitializingBean 接口的 `afterPropertiesSet()` 方法 +- 最后再执行 init-method 指定的方法 -用代码来模拟这个过程,是这样的: +也就是说,init-method 是在所有其他初始化方法之后执行的。 ```java -// 模拟Spring的解决过程 -public class CircularDependencyDemo { - // 三级缓存 - Map singletonObjects = new HashMap<>(); - Map earlySingletonObjects = new HashMap<>(); - Map singletonFactories = new HashMap<>(); +@Component +public class MyService { + @Autowired + private UserDao userDao; - public Object getBean(String beanName) { - // 先从一级缓存获取 - Object bean = singletonObjects.get(beanName); - if (bean != null) return bean; - - // 再从二级缓存获取 - bean = earlySingletonObjects.get(beanName); - if (bean != null) return bean; - - // 最后从三级缓存获取 - ObjectFactory factory = singletonFactories.get(beanName); - if (factory != null) { - bean = factory.getObject(); - earlySingletonObjects.put(beanName, bean); // 移到二级缓存 - singletonFactories.remove(beanName); // 从三级缓存移除 - } - - return bean; + @PostConstruct + public void postConstruct() { + System.out.println("1. @PostConstruct执行"); + } + + public void customInit() { // 通过@Bean的initMethod指定 + System.out.println("3. init-method执行"); } } -``` -#### 哪些情况下Spring无法解决循环依赖? - -Spring 虽然能解决大部分循环依赖问题,但确实有几种情况是无法处理的,我来详细说说。 - -![三分恶面渣逆袭:循环依赖的几种情形](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-37bb576d-b4af-42ed-91f4-d846ceb012b6.png) +@Configuration +public class AppConfig { + @Bean(initMethod = "customInit") + public MyService myService() { + return new MyService(); + } +} +``` -第一种,构造方法的循环依赖,这种情况 Spring 会直接抛出 BeanCurrentlyInCreationException 异常。 +destroy-method 会在 Bean 销毁阶段被调用。 ```java @Component -public class A { - private B b; - - public A(B b) { // 构造方法注入 - this.b = b; +public class MyService { + @PreDestroy + public void preDestroy() { + System.out.println("1. @PreDestroy执行"); } -} - -@Component -public class B { - private A a; - public B(A a) { // 构造方法注入 - this.a = a; + public void customDestroy() { // 通过@Bean的destroyMethod指定 + System.out.println("3. destroy-method执行"); } } ``` -因为构造方法注入发生在实例化阶段,创建 A 的时候必须先有 B,但创建 B又必须先有 A,这时候两个对象都还没创建出来,无法提前暴露到缓存中。 +不过在实际开发中,通常用 `@PostConstruct` 和 `@PreDestroy` 就够了,它们更简洁。 -第二种,prototype 作用域的循环依赖。prototype 作用域的 Bean 每次获取都会创建新实例,Spring 无法缓存这些实例,所以也无法解决循环依赖。 +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:说说 Bean 的生命周期 +> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:Spring中bean生命周期 +> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的8 后端开发秋招一面面试原题:讲一下Spring Bean的生命周期 +> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 1 贝壳找房后端技术一面面试原题:bean生命周期 +> 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:介绍下Bean的生命周期?Aware类型接口的作用?如果配置了init-method和destroy-method,Spring会在什么时候调用其配置的方法? -----面试中可以不背,方便大家理解 start---- +memo:2025 年 6 月 30 日修改至此。昨天有[读者发消息说有三个 offer 要选择](https://javabetter.cn/zhishixingqiu/),中科大读博、中海油、商飞北研,问我该怎么选择?说实话,这三个都是非常优质的选择,我个人的建议是优先考虑中科大读博,毕竟是国内顶尖学府,博士毕业后可以选择在高校任教,会更符合他的家庭条件,当然了,我深知,读博的产出压力非常大。 -我们来看一个实例,先是 PrototypeBeanA: +![读者拿到中科大读博、中海油、商飞北研](https://cdn.tobebetterjavaer.com/stutymore/spring-20250630101647.png) -```java -@Component -@Scope("prototype") -public class PrototypeBeanA { - private final PrototypeBeanB prototypeBeanB; +### 12.为什么IDEA不推荐使用@Autowired注解注入Bean? - @Autowired - public PrototypeBeanA(PrototypeBeanB prototypeBeanB) { - this.prototypeBeanB = prototypeBeanB; - } -} -``` +前情提要:当使用 `@Autowired` 注解注入 Bean 时,IDEA 会提示“Field injection is not recommended”。 -然后是 PrototypeBeanB: +![二哥的 Java 进阶之路:@Autowired](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224164722.png) -```java -@Component -@Scope("prototype") -public class PrototypeBeanB { - private final PrototypeBeanA prototypeBeanA; +面试回答: - @Autowired - public PrototypeBeanB(PrototypeBeanA prototypeBeanA) { - this.prototypeBeanA = prototypeBeanA; - } -} -``` +主要有几个原因。 -再然后是测试: +第一个是字段注入不利于单元测试。字段注入需要使用反射或 Spring 容器才能注入依赖,测试更复杂;而构造方法注入可以直接通过构造方法传入 Mock 对象,测试起来更简单。 ```java -@SpringBootApplication -public class DemoApplication { - - public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); - } +// 字段注入的测试困难 +@Test +public void testUserService() { + UserService userService = new UserService(); + // 无法直接设置userRepository,需要反射或Spring容器 + // userService.userRepository = Mockito.mock(UserRepository.class); + // 需要手动设置依赖,测试不方便 + ReflectionTestUtils.setField(userService, "userRepository", Mockito.mock(UserRepository.class)); + userService.doSomething(); + // ... +} - @Bean - CommandLineRunner commandLineRunner(ApplicationContext ctx) { - return args -> { - // 尝试获取PrototypeBeanA的实例 - PrototypeBeanA beanA = ctx.getBean(PrototypeBeanA.class); - }; - } +// 构造方法注入的测试简单 +@Test +public void testUserService() { + UserRepository mockRepository = Mockito.mock(UserRepository.class); + UserService userService = new UserService(mockRepository); // 直接注入 } ``` -运行结果: +第二个是字段注入会隐藏循环依赖问题,而构造方法注入会在项目启动时就去检查依赖关系,能更早发现问题。 -![二哥的 Java 进阶之路:循环依赖](https://cdn.tobebetterjavaer.com/stutymore/spring-20240310202703.png) +第三个是构造方法注入可以使用 final 字段确保依赖在对象创建时就被初始化,避免了后续修改的风险。 -----面试中可以不背,方便大家理解 end---- +在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,我们已经在使用构造方法注入的方式来管理依赖关系。 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:如何解决循环依赖? -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:Spring如何解决循环依赖? -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:Spring源码看过吗?Spring的三级缓存知道吗? -> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里云面经同学 22 面经:spring三级缓存解决循环依赖问题 +![技术派:构造方法注入](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224165628.png) -memo:2025 年 7 月 5 日修改至此。今天 [VIP 群](https://javabetter.cn/zhishixingqiu/)来了非常多的球友,不知不觉我们已经 12 群了,也是一个大家庭了,希望大家都能在这里找到自己的归属感,我们一起学习,一起进步。 +不过话说回来,`@Autowired` 的字段注入方式在一些简单的场景下还是可以用的,主要看团队的编码规范吧。 -![二哥的编程星球已经 12 群了](https://cdn.tobebetterjavaer.com/stutymore/spring-20250705072809.png) +#### @Autowired 和 @Resource 注解的区别? -### 15.为什么需要三级缓存而不是两级? +首先从来源上说,`@Autowired` 是 Spring 框架提供的注解,而 `@Resource` 是 Java EE 标准提供的注解。换句话说,`@Resource` 是 JDK 自带的,而 `@Autowired` 是 Spring 特有的。 -Spring 设计三级缓存主要是为了解决 AOP 代理的问题。 +虽然 IDEA 不推荐使用 `@Autowired`,但对 `@Resource` 注解却没有任何提示。 -我举个具体的例子来说明一下。假设我们有 A 和 B 两个类相互依赖,A 的某个方法上面还标注了 `@Transactional` 注解,这意味着 A 最终需要被 Spring 创建成一个代理对象。 +![技术派:@Resource](https://cdn.tobebetterjavaer.com/stutymore/spring-20241224170055.png) + +从注入方式上说,`@Autowired` 默认按照类型,也就是 byType 进行注入,而 `@Resource` 默认按照名称,也就是 byName 进行注入。 + +当容器中存在多个相同类型的 Bean, 比如说有两个 UserRepository 的实现类,直接用 `@Autowired` 注入 UserRepository 时就会报错,因为 Spring 容器不知道该注入哪个实现类。 ```java @Component -public class A { - @Autowired - private B b; - - @Transactional // A需要被AOP代理 - public void doSomething() { - // 业务逻辑 - } -} +public class UserRepository21 implements UserRepository2 {} @Component -public class B { +public class UserRepository22 implements UserRepository2 {} + +@Component +public class UserService2 { @Autowired - private A a; + private UserRepository2 userRepository; // 冲突 } ``` -如果只有二级缓存的话,当创建 A 的时候,我们需要把 A 的原始对象提前放到缓存里面,然后 B 在创建的时候从缓存中拿到 A 的原始对象。 +这时候,有两种解决方案,第一种是使用 `@Autowired` + `@Qualifier` 指定具体的 Bean 名称来解决冲突。 ```java -// 假设只有两级缓存 -Map singletonObjects = new HashMap<>(); // 完整Bean -Map earlySingletonObjects = new HashMap<>(); // 半成品Bean +@Component("userRepository21") +public class UserRepository21 implements UserRepository2 { +} +@Component("userRepository22") +public class UserRepository22 implements UserRepository2 { +} +@Autowired +@Qualifier("userRepository22") +private UserRepository2 userRepository22; ``` -但是问题来了,A 完成初始化后,由于有 `@Transactional` 注解,Spring 会把 A 包装成一个代理对象放到容器中。这样就出现了一个很严重的问题:B 里面持有的是 A 的原始对象,而容器中存的是 A 的代理对象,同一个 Bean 居然有两个不同的实例,这肯定是不对的。 +第二种是使用 `@Resource` 注解按名称进行注入。 -![三分恶面渣逆袭:二级缓存不行的原因](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-6ece8a46-25b1-459b-8cfa-19fc696dd7d6.png) +```java +@Resource(name = "userRepository21") +private UserRepository2 userRepository21; +``` -三级缓存就是为了解决这个问题而设计的。三级缓存里面存放的不是 Bean 的实例,而是一个对象工厂,这是一个函数式接口。 +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 9 面试原题:依赖注入的时候,直接Autowired比较直接,为什么推荐构造方法注入呢 -当 B 需要 A 的时候,会调用这个对象工厂的 getObject 方法,这个方法里面会判断 A 是否需要被代理。如果需要代理,就创建 A 的代理对象返回给 B;如果不需要代理,就返回 A 的原始对象。这样就保证了 B 拿到的 A 和最终放入容器的 A 是同一个对象。 +memo:2025 年 7 月 1 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个郑州大学本硕的球友,这也是我们河南省最好的大学了,但也仅仅是一所 211,所以希望所有河南的同学都能加把劲,证明自己的实力,去拿到更好的 offer,为校争光。 -```java -Map> singletonFactories = new HashMap<>(); -// Spring源码中的逻辑 -addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); -protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { - Object exposedObject = bean; - if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { - for (BeanPostProcessor bp : getBeanPostProcessors()) { - if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { - SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; - // 关键:如果需要代理,这里会创建代理对象 - exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); - } - } - } - return exposedObject; -} -``` - -简单来说,三级缓存的核心作用就是延迟决策。它让 Spring 在真正需要 Bean 的时候才决定返回原始对象还是代理对象,这样就避免了对象不一致的问题。如果没有三级缓存,Spring 要么无法在循环依赖的情况下支持 AOP,要么就会出现同一个 Bean 有多个实例的问题,这些都是不可接受的。 +![郑州大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250701154344.png) -![幸云教育:三级缓存和循环依赖](https://cdn.tobebetterjavaer.com/stutymore/spring-20250706065436.png) +### 13.@Autowired的实现原理了解吗? -#### 如果缺少二级缓存会有什么问题? +`@Autowired` 是 Spring 实现依赖注入的核心注解,其实现原理基于反射机制和 BeanPostProcessor 接口。 -二级缓存 earlySingletonObjects 主要是用来存放那些已经通过三级缓存的对象工厂创建出来的早期 Bean 引用。 +整个过程分为两个主要阶段。第一个阶段是依赖收集阶段,发生在 Bean 实例化之后、属性赋值之前。`Autowired` 的 Processor 会扫描 Bean 的所有字段、方法和构造方法,找出标注了 `@Autowired` 注解的地方,然后把这些信息封装成 `Injection` 元数据对象缓存起来。这个过程用到了大量的反射操作,需要分析类的结构、注解信息等等。 -![Minor王智:三级缓存](https://cdn.tobebetterjavaer.com/stutymore/spring-20250706065722.png) +![MarkusZhang:@Autowired](https://cdn.tobebetterjavaer.com/stutymore/spring-20250711165339.png) -假设我们有 A、B、C 三个 Bean,A 依赖 B 和 C,B 和 C 都依赖 A,形成了一个复杂的循环依赖。在没有二级缓存的情况下,每次 B 或者 C 需要获取 A 的时候,都需要调用三级缓存中 A 的 `ObjectFactory.getObject()` 方法。这意味着如果 A 需要被代理的话,代理对象可能会被重复创建多次,这显然是不合理的。 +第二个阶段是依赖注入阶段,Spring 会取出之前缓存的 `Injection` 元数据对象,然后逐个处理每个注入点。对于每个 `@Autowired` 标注的字段或方法,Spring 会根据类型去容器中查找匹配的 Bean。 ```java -// 没有二级缓存的伪代码 -public Object getSingleton(String beanName) { - Object singletonObject = singletonObjects.get(beanName); - - if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { - // 直接从三级缓存获取 - ObjectFactory singletonFactory = singletonFactories.get(beanName); - if (singletonFactory != null) { - return singletonFactory.getObject(); // 每次都会创建新的代理对象! - } - } - return singletonObject; -} +// 1. 按类型查找(byType) +Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + this.beanFactory, type); + +// 2. 如果找到多个候选者,按名称筛选(byName) +String autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); + +// 3. 考虑@Primary和@Priority注解 +// 4. 最后按照字段名或参数名匹配 ``` -我举个具体的例子。比如 A 有 `@Transactional` 注解需要被 AOP 代理,B 在初始化的时候需要 A,会调用一次对象工厂创建 A 的代理对象。接着 C 在初始化的时候也需要 A,又会调用一次对象工厂,可能又创建了一个 A 的代理对象。这样 B 和 C 拿到的可能就是两个不同的 A 代理对象,这就违反了单例 Bean 的语义。 +在具体的注入过程中,Spring 会使用反射来设置字段的值或者调用 setter 方法。比如对于字段注入,会调用 `Field.set()` 方法;对于 setter 注入,会调用 `Method.invoke()` 方法。 -```java -@Service -public class ServiceA { - @Autowired - private ServiceB serviceB; - - @Transactional // 需要 AOP 代理 - public void methodA() { - // 业务逻辑 - } -} +### 14.什么是自动装配? -@Service -public class ServiceB { - @Autowired - private ServiceA serviceA; // 获得代理对象 A1 - - @Autowired - private ServiceC serviceC; -} +自动装配的本质就是让 Spring 容器自动帮我们完成 Bean 之间的依赖关系注入,而不需要我们手动去指定每个依赖。简单来说,就是“我们不用告诉 Spring 具体怎么注入,Spring 自己会想办法找到合适的 Bean 注入进来”。 -@Service -public class ServiceC { - @Autowired - private ServiceA serviceA; // 可能获得代理对象 A2 +自动装配的工作原理简单来说就是,Spring 容器在启动时自动扫描 `@ComponentScan` 指定包路径下的所有类,然后根据类上的注解,比如 `@Autowired`、`@Resource` 等,来判断哪些 Bean 需要被自动装配。 + +```java +@Configuration +@ComponentScan("com.github.paicoding.forum.service") +@MapperScan(basePackages = { + "com.github.paicoding.forum.service.article.repository.mapper", + "com.github.paicoding.forum.service.user.repository.mapper" + // ... 更多包路径 +}) +public class ServiceAutoConfig { + // Spring自动扫描指定包下的所有组件并注册为Bean } ``` -二级缓存就是为了解决这个问题。当第一次通过对象工厂创建了 A 的早期引用之后,就把这个引用放到二级缓存中,同时从三级缓存中移除对象工厂。 +之后分析每个 Bean 的依赖关系,在创建 Bean 的时候,根据装配规则自动找到合适的依赖 Bean,最后根据反射将这些依赖注入到目标 Bean 中。 -```java -// 第一次获取 A -ObjectFactory factory = singletonFactories.get("serviceA"); -Object proxyA = factory.getObject(); // 创建代理 -earlySingletonObjects.put("serviceA", proxyA); // 缓存代理 -singletonFactories.remove("serviceA"); +#### Spring提供了哪几种自动装配类型? -// 第二次获取 A -Object cachedA = earlySingletonObjects.get("serviceA"); // 直接返回缓存的代理 -// proxyA == cachedA ✓ +Spring 的自动装配方式有好几种,在 XML 配置时代,主要有 byName、byType、constructor 和 autodetect 四种方式。 + +![三分恶面渣逆袭:Spring四种自动装配类型](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-034120d9-88c7-490b-af07-7d48f3b6b7bc.png) + +到了注解驱动时代,用得最多的是 `@Autowired` 注解,默认按照类型装配。 + +```java +@Service +public class UserService { + @Autowired // 按类型自动装配 + private UserRepository userRepository; +} ``` -后续如果再有其他 Bean 需要 A,就直接从二级缓存中获取,不需要再调用对象工厂了。 +其次还有 `@Resource` 注解,它默认按照名称装配,如果找不到对应名称的 Bean,就会按类型装配。 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:循环依赖有了解过吗?出现循环依赖的原因?三大缓存存储内容的区别?如何解决循环依赖?如果缺少第二级缓存会有什么问题? +Spring Boot 的自动装配还有一套更高级的机制,通过 `@EnableAutoConfiguration` 和各种 `@Conditional` 注解来实现,这个是框架级别的自动装配,会根据 classpath 中的类和配置来自动配置 Bean。 - +![ShawnBlog:Spring Boot 的自动装配](https://cdn.tobebetterjavaer.com/stutymore/spring-20250702101032.png) -memo:2024 年 7 月 11 日修改至此,今天在帮[球友们修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,碰到北京交通大学本,北京航空航天大学硕的球友,她的简历上有很多校园荣誉奖项,像优秀学生、奖学金、英语四六级等,这些都是非常好的加分项。 +memo:2025 年 7 月 2 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个北京航空航天大学的球友,他在邮件中说到:在星球里学到了好多东西,目前正在准备[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)和 MYDB,打算好好冲秋招,能帮助到大家我真的很欣慰。 -![北京交通大学本,北京航空航天大学硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250621064205.png) +![北航球友对星球的认可](https://cdn.tobebetterjavaer.com/stutymore/spring-20250702101812.png) -## IoC +### 15.Bean的作用域有哪些? -### 16.🌟说一说什么是IoC? +Bean 的作用域决定了 Bean 实例的生命周期和创建策略,singleton 是默认的作用域。整个 Spring 容器中只会有一个 Bean 实例。不管在多少个地方注入这个 Bean,拿到的都是同一个对象。 -推荐阅读:[IoC 扫盲](https://javabetter.cn/springboot/ioc.html) +```java +@Component // 默认就是singleton +public class UserService { + // 整个应用中只有一个UserService实例 +} +``` -IoC 的全称是 Inversion of Control,也就是控制反转。这里的“控制”指的是对象创建和依赖关系管理的控制权。 +生命周期和 Spring 容器相同,容器启动时创建,容器销毁时销毁。 -![图片来源于网络:IoC](https://cdn.tobebetterjavaer.com/stutymore/spring-20240310191630.png) +实际开发中,像 Service、Dao 这些业务组件基本都是单例的,因为单例既能节省内存,又能提高性能。 -以前我们写代码的时候,如果 A 类需要用到 B 类,我们就在 A 类里面直接 new 一个 B 对象出来,这样 A 类就控制了 B 类对象的创建。 +当把 scope 设置为 prototype 时,每次从容器中获取 Bean 的时候都会创建一个新的实例。 ```java -// 传统方式:对象主动创建依赖 -public class UserService { - private UserDao userDao; - - public UserService() { - // 主动创建依赖对象 - this.userDao = new UserDaoImpl(); - } +@Component +@Scope("prototype") +public class OrderProcessor { + // 每次注入或获取都是新的实例 } ``` -有了 IoC 之后,这个控制权就“反转”了,不再由 A 类来控制 B 对象的创建,而是交给外部的容器来管理。 +当需要处理一些有状态的 Bean 时会用到 prototype,比如每个订单处理器需要维护不同的状态信息。 + +需要注意的是,在 singleton Bean 中注入 prototype Bean 时要小心,因为 singleton Bean 只创建一次,所以 prototype Bean 也只会注入一次。这时候可以用 `@Lookup` 注解或者 ApplicationContext 来动态获取。 ```java -/** - * 使用 Spring IoC 容器来管理 UserDao 的创建和注入 - * 技术派源码:https://github.com/itwanger/paicoding - */ -@Service -public class UserServiceImpl implements UserService { +@Component +public class SingletonService { + // 错误的做法,prototypeBean只会注入一次 @Autowired - private UserDao userDao; + private PrototypeBean prototypeBean; - // 不需要主动创建 UserDao,由 Spring 容器注入 - public BaseUserInfoDTO getAndUpdateUserIpInfoBySessionId(String session, String clientIp) { - // 直接使用注入的 userDao - return userDao.getBySessionId(session); + // 正确的做法,每次调用都获取新实例 + @Lookup + public PrototypeBean getPrototypeBean() { + return null; // Spring会重写这个方法 } } ``` -----这部分面试中可以不背 start---- - -没有 IoC 之前: - ->我需要一个女朋友,刚好大街上突然看到了一个小姐姐,人很好看,于是我就自己主动上去搭讪,要她的微信号,找机会聊天关心她,然后约她出来吃饭,打听她的爱好,三观。。。 - +除了 singleton 和 prototype,Spring 还支持其他作用域,比如 request、session、application 和 websocket。 -有了 IoC 之后: +![三分恶面渣逆袭:Spring Bean支持作用域](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-08a9cb31-5a4f-4224-94cd-0c0f643a57ea.png) ->我需要一个女朋友,于是我就去找婚介所,告诉婚介所,我需要一个长的像赵露思的,会打 Dota2 的,于是婚介所在它的人才库里开始找,找不到它就直接说没有,找到它就直接介绍给我。 +如果作用于是 request,表示在 Web 应用中,每个 HTTP 请求都会创建一个新的 Bean 实例,请求结束后 Bean 就被销毁。 -婚介所就相当于一个 IoC 容器,我就是一个对象,我需要的女朋友就是另一个对象,我不用关心女朋友是怎么来的,我只需要告诉婚介所我需要什么样的女朋友,婚介所就帮我去找。 +```java +@Component +@Scope("request") +public class RequestContext { + // 每个HTTP请求都有自己的实例 +} +``` -![三分恶面渣逆袭:引入IoC之前和引入IoC之后](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-619da277-c15e-4dd7-9f2b-dbd809a9aaa0.png) +如果作用于是 session,表示在 Web 应用中,每个 HTTP 会话都会创建一个新的 Bean 实例,会话结束后 Bean 被销毁。 -----这部分面试中可以不背 end---- +```java +@Component +@Scope("session") +public class UserSession { + // 每个用户会话都有自己的实例 +} +``` -#### DI和IoC的区别了解吗? +典型的使用场景是购物车、用户登录状态这些需要在整个会话期间保持的信息。 -IoC 的思想是把对象创建和依赖关系的控制权由业务代码转移给 Spring 容器。这是一个比较抽象的概念,告诉我们应该怎么去设计系统架构。 +application 作用域表示在整个应用中只有一个 Bean 实例,类似于 singleton,但它的生命周期与 ServletContext 绑定。 -![Martin Fowler’s Definition](https://cdn.tobebetterjavaer.com/stutymore/spring-20241117132929.png) +```java +@Component +@Scope("application") +public class AppConfig { + // 整个应用中只有一个实例 +} +``` -而 DI,也就是依赖注入,它是实现 IoC 这种思想的具体技术手段。在 Spring 里,我们用 `@Autowired` 注解就是在使用 DI 的字段注入方式。 +websocket 作用域表示在 WebSocket 会话中每个连接都有自己的 Bean 实例。WebSocket 连接建立时创建,连接关闭时销毁。 ```java -@Service -public class ArticleReadServiceImpl implements ArticleReadService { - @Autowired - private ArticleDao articleDao; // 字段注入 - - @Autowired - private UserDao userDao; +@Component +@Scope("websocket") +public class WebSocketHandler { + // 每个WebSocket连接都有自己的实例 } ``` -从实现角度来看,DI 除了字段注入,还有构造方法注入和 Setter 方法注入等方式。在做[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)项目的时候,我就尝试过构造方法注入的方式。 +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的同学 1 贝壳找房后端技术一面面试原题:bean是单例还是多例的,具体怎么修改 -![技术派源码:构造方法的注入方式](https://cdn.tobebetterjavaer.com/stutymore/spring-20250622091928.png) +memo:2025 年 7 月 3 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个郑州大学硕,河北师范大学本的球友,整体在校的经历非常出色,奖学金、论文期刊基本上都拉满了。那这么多优秀的球友选择来到这里,也是对星球的又一次认可和肯定,我也一定会继续努力,提供更多优质的内容和服务。 -当然了,DI 并不是实现 IoC 的唯一方式,还有 Service Locator 模式,可以通过实现 ApplicationContextAware 接口来获取 Spring 容器中的 Bean。 +![郑州大学硕,河北师范大学本的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-20250704144758.png) -![技术派源码:IoC 的Service Locator 模式](https://cdn.tobebetterjavaer.com/stutymore/spring-20250622093007.png) +### 16.Spring中的单例Bean会存在线程安全问题吗? -之所以 ID 后成为 IoC 的首选实现方式,是因为代码更清晰、可读性更高。 +首先要明确一点。Spring 容器本身保证了 Bean 创建过程的线程安全,也就是说不会出现多个线程同时创建同一个单例 Bean 的情况。但是 Bean 创建完成后的使用过程,Spring 就不管了。 -``` -IoC(控制反转) -├── DI(依赖注入) ← 主要实现方式 -│ ├── 构造器注入 -│ ├── 字段注入 -│ └── Setter注入 -├── 服务定位器模式 -├── 工厂模式 -└── 其他实现方式 -``` +换句话说,单例 Bean 在被创建后,如果它的内部状态是可变的,那么在多线程环境下就可能会出现线程安全问题。 -#### 为什么要使用 IoC 呢? +![三分恶面渣逆袭:Spring单例Bean线程安全问题](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-35dacef4-1a9e-45e1-b3f2-5a91227eb244.png) -在日常开发中,如果我们需要实现某一个功能,可能至少需要两个以上的对象来协助完成,在没有 Spring 之前,每个对象在需要它的合作对象时,需要自己 new 一个,比如说 A 要使用 B,A 就对 B 产生了依赖,也就是 A 和 B 之间存在了一种耦合关系。 +比如说在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,有一个敏感词过滤的 Bean,我们就需要使用 volatile 关键字来保证多线程环境下的可见性。 ```java -// 传统方式:对象自己创建依赖 -public class UserService { - private UserDao userDao = new UserDaoImpl(); // 硬编码依赖 +@Service +public class SensitiveService { + private volatile SensitiveWordBs sensitiveWordBs; // 使用volatile保证可见性 - public User getUser(Long id) { - return userDao.findById(id); + @PostConstruct + public void refresh() { + // 重新初始化sensitiveWordBs } } ``` -有了 Spring 之后,创建 B 的工作交给了 Spring 来完成,Spring 创建好了 B 对象后就放到容器中,A 告诉 Spring 我需要 B,Spring 就从容器中取出 B 交给 A 来使用。 +如果 Bean 中没有成员变量,或者成员变量都是不可变的,final 修饰的,那么就不存在线程安全问题。 ```java -// IoC 方式:依赖由外部注入 @Service public class UserServiceImpl implements UserService { + @Resource + private UserDao userDao; @Autowired - private UserDao userDao; // 依赖注入,不关心具体实现 + private CountService countService; + // 只有依赖注入的无状态字段 +} + +@Service +public class ConfigService { + private final String appName; // final修饰,不可变 - public User getUser(Long id) { - return userDao.findById(id); + public ConfigService(@Value("${app.name}") String appName) { + this.appName = appName; } } ``` -至于 B 是怎么来的,A 就不再关心了,Spring 容器想通过 newnew 创建 B 还是 new 创建 B,无所谓。 - -这就是 IoC 的好处,它降低了对象之间的耦合度,让每个对象只关注自己的业务实现,不关心其他对象是怎么创建的。 - -推荐阅读:[孤傲苍狼:谈谈对 Spring IOC 的理解](https://www.cnblogs.com/xdp-gacl/p/4249939.html) - -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:说说你对 AOP 和 IoC 的理解。 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小公司面经合集同学 1 Java 后端面试原题:介绍 Spring IoC 和 AOP? -> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的招商银行面经同学 6 招银网络科技面试原题:SpringBoot框架的AOP、IOC/DI? -> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的京东面经同学 8 面试原题:IOC,AOP -> 5. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:解释下什么是IOC和AOP?分别解决了什么问题?IOC和DI的区别? - -memo:2025 年 6 月 22 日修改至此,今天[有球友发喜报说拿到了两个 offer](https://javabetter.cn/zhishixingqiu/),一个是做 B 端电商的,另一个是外企,主要做 Power BI 的低代码开发,我的建议是去外企,因为实习最重要的是混个 title,有更多的时间,可以去学习星球里的项目,其实会更实在。 - -![球友拿到了外企和电商的 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-二哥,目前拿到了两个offer。第一个是做b端电.png) - -### 17.能说一下IoC的实现机制吗? - -好的,Spring IoC 的实现机制还是比较复杂的,我尽量用比较通俗的方式来解释一下整个流程。 - -![面渣逆袭:mini版本Spring IoC](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-1d55c63d-2d12-43b1-9f43-428f5f4a1413.png) - +#### 单例Bean的线程安全问题怎么解决呢? -第一步是加载 Bean 的定义信息。Spring 会扫描我们配置的包路径,找到所有标注了 `@Component`、`@Service`、`@Repository` 这些注解的类,然后把这些类的元信息封装成 BeanDefinition 对象。 +第一种,使用局部变量,也就是使用无状态的单例 Bean,把所有状态都通过方法参数传递: ```java -// Bean定义信息 -public class BeanDefinition { - private String beanClassName; // 类名 - private String scope; // 作用域 - private boolean lazyInit; // 是否懒加载 - private String[] dependsOn; // 依赖的Bean - private ConstructorArgumentValues constructorArgumentValues; // 构造参数 - private MutablePropertyValues propertyValues; // 属性值 +@Service +public class UserService { + @Autowired + private UserDao userDao; + + // 无状态方法,所有数据通过参数传递 + public User processUser(Long userId, String operation) { + User user = userDao.findById(userId); + // 处理逻辑... + return user; + } } ``` -第二步是 Bean 工厂的准备。Spring 会创建一个 DefaultListableBeanFactory 作为 Bean 工厂来负责 Bean 的创建和管理。 - -![技术派源码:DefaultListableBeanFactory](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623094742.png) - -第三步是 Bean 的实例化和初始化。这个过程比较复杂,Spring 会根据 BeanDefinition 来创建 Bean 实例。 - -![IoC的实现机制](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623101221.png) - -对于单例 Bean,Spring 会先检查缓存中是否已经存在,如果不存在就创建新实例。创建实例的时候会通过反射调用构造方法,然后进行属性注入,最后执行初始化回调方法。 +第二种,当确实需要维护线程相关的状态时,可以使用 [ThreadLocal](https://javabetter.cn/thread/ThreadLocal.html) 来保存状态。ThreadLocal 可以保证每个线程都有自己的变量副本,互不干扰。 ```java -// 简化的Bean创建流程 -public class AbstractBeanFactory { +@Service +public class UserContextService { + private static final ThreadLocal userThreadLocal = new ThreadLocal<>(); - protected Object createBean(String beanName, BeanDefinition bd) { - // 1. 实例化前处理 - Object bean = resolveBeforeInstantiation(beanName, bd); - if (bean != null) { - return bean; - } - - // 2. 实际创建Bean - return doCreateBean(beanName, bd); + public void setCurrentUser(User user) { + userThreadLocal.set(user); } - protected Object doCreateBean(String beanName, BeanDefinition bd) { - // 2.1 实例化 - Object bean = createBeanInstance(beanName, bd); - - // 2.2 属性填充(依赖注入) - populateBean(beanName, bd, bean); - - // 2.3 初始化 - Object exposedObject = initializeBean(beanName, bean, bd); - - return exposedObject; + public User getCurrentUser() { + return userThreadLocal.get(); + } + + public void clear() { + userThreadLocal.remove(); // 防止内存泄漏 } } ``` -依赖注入的实现主要是通过反射来完成的。比如我们用 `@Autowired` 标注了一个字段,Spring 在创建 Bean 的时候会扫描这个字段,然后从容器中找到对应类型的 Bean,通过反射的方式设置到这个字段上。 - -![贰师兄的屠宰场:各个注解的注入流程](https://cdn.tobebetterjavaer.com/stutymore/spring-20250628110426.png) - -#### 你是怎么理解 Spring IoC 的? - -IoC 本质上一个超级工厂,这个工厂的产品就是各种 Bean 对象。 - -![三分恶面渣逆袭:工厂运行](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-7678c40f-a48d-4bd5-80f8-e902ad688e11.png) - -我们通过 `@Component`、`@Service` 这些注解告诉工厂:“我要生产什么样的产品,这个产品有什么特性,需要什么原材料”。 - -然后工厂里各种生产线,在 Spring 中就是各种 BeanPostProcessor。比如 `AutowiredAnnotationBeanPostProcessor` 专门负责处理 `@Autowired` 注解。 - -工厂里还有各种缓存机制用来存放产品,比如说 singletonObjects 是成品仓库,存放完工的单例 Bean;earlySingletonObjects 是半成品仓库,用来解决循环依赖问题。 +第三种,如果需要缓存数据或者计数,使用 JUC 包下的线程安全类,比如说 [AtomicInteger](https://javabetter.cn/thread/atomic.html)、[ConcurrentHashMap](https://javabetter.cn/thread/ConcurrentHashMap.html)、[CopyOnWriteArrayList](https://javabetter.cn/thread/CopyOnWriteArrayList.html) 等。 ```java -// Spring单例Bean注册表 -public class DefaultSingletonBeanRegistry { - // 一级缓存:完成初始化的单例Bean - private final Map singletonObjects = new ConcurrentHashMap<>(256); - - // 二级缓存:早期暴露的单例Bean(解决循环依赖) - private final Map earlySingletonObjects = new HashMap<>(16); - - // 三级缓存:单例Bean工厂 - private final Map> singletonFactories = new HashMap<>(16); +@Service +public class CacheService { + // 使用线程安全的集合 + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + private final AtomicLong counter = new AtomicLong(0); - public Object getSingleton(String beanName) { - Object singletonObject = this.singletonObjects.get(beanName); - if (singletonObject == null) { - singletonObject = this.earlySingletonObjects.get(beanName); - if (singletonObject == null) { - ObjectFactory singletonFactory = this.singletonFactories.get(beanName); - if (singletonFactory != null) { - singletonObject = singletonFactory.getObject(); - this.earlySingletonObjects.put(beanName, singletonObject); - this.singletonFactories.remove(beanName); - } - } - } - return singletonObject; + public void put(String key, Object value) { + cache.put(key, value); + counter.incrementAndGet(); } } ``` -最有意思的是,这个工厂还很智能,它知道产品之间的依赖关系。它会根据依赖关系来决定 Bean 的创建顺序。如果发现循环依赖,它还会用三级缓存机制来巧妙地解决。 - -#### 能手写一个简单的 IoC 容器吗? - -1、首先定义基础的注解,比如说 `@Component`、`@Autowired` 等。 +第四种,对于复杂的状态操作,可以使用 synchronized 或 Lock: ```java -// 组件注解 -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Component { -} - -// 自动注入注解 -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Autowired { +@Service +public class CacheService { + private final Map cache = new HashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + + public void put(String key, Object value) { + lock.lock(); + try { + cache.put(key, value); + } finally { + lock.unlock(); + } + } } ``` -2、核心的 IoC 容器类,负责扫描包路径,创建 Bean 实例,并处理依赖注入。 +第五种,如果 Bean 确实需要维护状态,可以考虑将其改为 prototype 作用域,这样每次注入都会创建一个新的实例,避免了多线程共享同一个实例的问题。 ```java -public class SimpleIoC { - // Bean容器 - private Map, Object> beans = new HashMap<>(); - - /** - * 注册Bean - */ - public void registerBean(Class clazz) { - try { - // 创建实例 - Object instance = clazz.getDeclaredConstructor().newInstance(); - beans.put(clazz, instance); - } catch (Exception e) { - throw new RuntimeException("创建Bean失败: " + clazz.getName(), e); - } - } - - /** - * 获取Bean - */ - @SuppressWarnings("unchecked") - public T getBean(Class clazz) { - return (T) beans.get(clazz); - } - - /** - * 依赖注入 - */ - public void inject() { - for (Object bean : beans.values()) { - injectFields(bean); - } - } +@Service +@Scope("prototype") // 每次注入都创建新实例 +public class StatefulService { + private String state; // 现在每个实例都有独立状态 - /** - * 字段注入 - */ - private void injectFields(Object bean) { - Field[] fields = bean.getClass().getDeclaredFields(); - for (Field field : fields) { - if (field.isAnnotationPresent(Autowired.class)) { - try { - field.setAccessible(true); - Object dependency = getBean(field.getType()); - field.set(bean, dependency); - } catch (Exception e) { - throw new RuntimeException("注入失败: " + field.getName(), e); - } - } - } + public void setState(String state) { + this.state = state; } } ``` -3、使用示例,定义一些 Bean 类,并注册到 IoC 容器中。 +或者使用 request 作用域,这样每个 HTTP 请求都会创建一个新的实例。 ```java -// DAO层 -@Component -class UserDao { - public void save(String user) { - System.out.println("保存用户: " + user); - } +@Service +@Scope("request") +public class RequestScopedService { + private String requestData; + // 每个请求都有独立的实例 } +``` -// Service层 -@Component -class UserService { - @Autowired - private UserDao userDao; - - public void createUser(String name) { - userDao.save(name); - System.out.println("用户创建完成"); - } -} +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里面经同学 1 闲鱼后端一面的原题:spring的bean的并发安全问题 -// 测试 -public class Test { - public static void main(String[] args) { - SimpleIoC ioc = new SimpleIoC(); +memo:2025 年 7 月 4 日修改至此,今天在[帮球友修改简历](https://javabetter.cn/zhishixingqiu/)的时候,碰到一个武汉理工大学本硕的球友。说真的,和武汉理工大学挺有缘的,2023 年去武汉,就线下见了一名武理的球友,[他当时签约的是小米](https://t.zsxq.com/LfG3B),非常优秀。 + +![武汉理工大学本硕的球友](https://cdn.tobebetterjavaer.com/stutymore/spring-2023.09-2026.06.png) + +### 17.什么是循环依赖? + +简单来说就是两个或多个 Bean 相互依赖,比如说 A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。 + +![三分恶面渣逆袭:Spring循环依赖](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-f8fea53f-56fa-4cca-9199-ec7f648da625.png) + +### 18.🌟Spring怎么解决循环依赖呢? + +Spring 通过三级缓存机制来解决循环依赖: + +1. 一级缓存:存放完全初始化好的单例 Bean。 +2. 二级缓存:存放提前暴露的 Bean,实例化完成,但未初始化完成。 +3. 三级缓存:存放 Bean 工厂,用于生成提前暴露的 Bean。 + +![三分恶面渣逆袭:三级缓存](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-01d92863-a2cb-4f61-8d8d-30ecf0279b28.png) + +以 A、B 两个类发生循环依赖为例: + +![三分恶面渣逆袭:循环依赖](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-cfc09f84-f8e1-4702-80b6-d115843e81fe.png) + +第 1 步:开始创建 Bean A。 + +- Spring 调用 A 的构造方法,创建 A 的实例。此时 A 对象已存在,但 b属性还是 null。 +- 将 A 的对象工厂放入三级缓存。 +- 开始进行 A 的属性注入。 + +![三分恶面渣逆袭:A 对象工厂](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-1a8bdc29-ff43-4ff4-9b61-3eedd9da59b3.png) + +第 2 步:A 需要注入 B,开始创建 Bean B。 + +- 发现需要 B,但 B 还不存在,所以开始创建 B。 +- 调用 B 的构造方法,创建 B 的实例。此时 B 对象已存在,但 a 属性还是 null。 +- 将 B 的对象工厂放入三级缓存。 +- 开始进行 B 的属性注入。 + +第 3 步:B 需要注入 A,从缓存中获取 A。 + +- B 需要注入 A,先从一级缓存找 A,没找到。 +- 再从二级缓存找 A,也没找到。 +- 最后从三级缓存找 A,找到了 A 的对象工厂。 +- 调用 A 的对象工厂得到 A 的实例。这时 A 已经实例化了,虽然还没完全初始化。 +- 将 A 从三级缓存移到二级缓存。 +- B 拿到 A 的引用,完成属性注入。 + +![三分恶面渣逆袭:A 放入二级缓存,B 放入一级缓存](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-bf2507bf-96aa-4b88-a58b-7ec41d11bc70.png) + +第 4 步:B 完成初始化。 + +- B 的属性注入完成,执行 `@PostConstruct` 等初始化逻辑。 +- B 完全创建完成,从三级缓存移除,放入一级缓存。 + +第 5 步:A 完成初始化。 + +- 回到 A 的创建过程,A 拿到完整的 B 实例,完成属性注入。 +- A 执行初始化逻辑,创建完成。 +- A 从二级缓存移除,放入一级缓存。 + +![三分恶面渣逆袭:AB 都好了](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-022f7cb9-2c83-4fe9-b252-b02bd0fb2435.png) + +用代码来模拟这个过程,是这样的: + +```java +// 模拟Spring的解决过程 +public class CircularDependencyDemo { + // 三级缓存 + Map singletonObjects = new HashMap<>(); + Map earlySingletonObjects = new HashMap<>(); + Map singletonFactories = new HashMap<>(); + + public Object getBean(String beanName) { + // 先从一级缓存获取 + Object bean = singletonObjects.get(beanName); + if (bean != null) return bean; - // 注册Bean - ioc.registerBean(UserDao.class); - ioc.registerBean(UserService.class); + // 再从二级缓存获取 + bean = earlySingletonObjects.get(beanName); + if (bean != null) return bean; - // 依赖注入 - ioc.inject(); + // 最后从三级缓存获取 + ObjectFactory factory = singletonFactories.get(beanName); + if (factory != null) { + bean = factory.getObject(); + earlySingletonObjects.put(beanName, bean); // 移到二级缓存 + singletonFactories.remove(beanName); // 从三级缓存移除 + } - // 使用 - UserService userService = ioc.getBean(UserService.class); - userService.createUser("王二"); + return bean; } } ``` -4、可以加上组件扫描。 +#### 哪些情况下Spring无法解决循环依赖? -```java -import java.lang.reflect.Field; -import java.util.*; +Spring 虽然能解决大部分循环依赖问题,但确实有几种情况是无法处理的,我来详细说说。 -public class SimpleIoC { - private Map, Object> beans = new HashMap<>(); - - /** - * 扫描并注册组件 - */ - public void scan(String packageName) { - // 简化版:手动添加需要扫描的类 - List> classes = getClassesInPackage(packageName); - - for (Class clazz : classes) { - if (clazz.isAnnotationPresent(Component.class)) { - registerBean(clazz); - } - } - - // 依赖注入 - inject(); - } - - /** - * 获取包下的类(简化实现) - */ - private List> getClassesInPackage(String packageName) { - // 面试时可以说:"实际实现需要扫描classpath,这里简化处理" - return Arrays.asList(UserDao.class, UserService.class); - } - - private void registerBean(Class clazz) { - try { - Object instance = clazz.getDeclaredConstructor().newInstance(); - beans.put(clazz, instance); - } catch (Exception e) { - throw new RuntimeException("创建Bean失败", e); - } - } +![三分恶面渣逆袭:循环依赖的几种情形](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-37bb576d-b4af-42ed-91f4-d846ceb012b6.png) + +第一种,构造方法的循环依赖,这种情况 Spring 会直接抛出 BeanCurrentlyInCreationException 异常。 + +```java +@Component +public class A { + private B b; - @SuppressWarnings("unchecked") - public T getBean(Class clazz) { - return (T) beans.get(clazz); + public A(B b) { // 构造方法注入 + this.b = b; } +} + +@Component +public class B { + private A a; - private void inject() { - for (Object bean : beans.values()) { - Field[] fields = bean.getClass().getDeclaredFields(); - for (Field field : fields) { - if (field.isAnnotationPresent(Autowired.class)) { - try { - field.setAccessible(true); - Object dependency = getBean(field.getType()); - field.set(bean, dependency); - } catch (Exception e) { - throw new RuntimeException("注入失败", e); - } - } - } - } + public B(A a) { // 构造方法注入 + this.a = a; } } ``` -IoC 容器的核心是管理对象和依赖注入,首先定义注解,然后实现容器的三个核心方法:注册Bean、获取Bean、依赖注入;关键是用反射创建对象和注入依赖。 - -memo:2025 年 6 月 23 日修改至此,今天[有球友发喜报说拿到了京东的社招 offer](https://javabetter.cn/zhishixingqiu/),这真的要恭喜他,也希望所有看到这里的小伙伴都能有一个好的结果。 - -![球友拿到京东社招 offer](https://cdn.tobebetterjavaer.com/stutymore/spring-20250623105438.png) +因为构造方法注入发生在实例化阶段,创建 A 的时候必须先有 B,但创建 B又必须先有 A,这时候两个对象都还没创建出来,无法提前暴露到缓存中。 -### 18.说说BeanFactory和ApplicantContext的区别? +第二种,prototype 作用域的循环依赖。prototype 作用域的 Bean 每次获取都会创建新实例,Spring 无法缓存这些实例,所以也无法解决循环依赖。 -BeanFactory 算是 Spring 的“心脏”,而 ApplicantContext 可以说是 Spring 的完整“身躯”。 +----面试中可以不背,方便大家理解 start---- -![三分恶面渣逆袭:BeanFactory和ApplicantContext](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-66328446-f89f-4b7a-8d9f-0e1145dd9b2f.png) +我们来看一个实例,先是 PrototypeBeanA: -BeanFactory 提供了最基本的 IoC 能力。它就像是一个 Bean 工厂,负责 Bean 的创建和管理。他采用的是懒加载的方式,也就是说只有当我们真正去获取某个 Bean 的时候,它才会去创建这个 Bean。 +```java +@Component +@Scope("prototype") +public class PrototypeBeanA { + private final PrototypeBeanB prototypeBeanB; -![三分恶面渣逆袭:Spring5 BeanFactory继承体系](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-6e6d4b69-f36c-41e6-b8ba-9277be147c9b.png) + @Autowired + public PrototypeBeanA(PrototypeBeanB prototypeBeanB) { + this.prototypeBeanB = prototypeBeanB; + } +} +``` -它最主要的方法就是 `getBean()`,负责从容器中返回特定名称或者类型的 Bean 实例。 +然后是 PrototypeBeanB: ```java -public class BeanFactoryExample { - public static void main(String[] args) { - // 创建 BeanFactory - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - - // 手动注册 Bean 定义 - BeanDefinition beanDefinition = new RootBeanDefinition(UserService.class); - beanFactory.registerBeanDefinition("userService", beanDefinition); - - // 懒加载:此时才创建 Bean 实例 - UserService userService = beanFactory.getBean("userService", UserService.class); +@Component +@Scope("prototype") +public class PrototypeBeanB { + private final PrototypeBeanA prototypeBeanA; + + @Autowired + public PrototypeBeanB(PrototypeBeanA prototypeBeanA) { + this.prototypeBeanA = prototypeBeanA; } } ``` -ApplicationContext 是 BeanFactory 的子接口,在 BeanFactory 的基础上扩展了很多企业级的功能。它不仅包含了 BeanFactory 的所有功能,还提供了国际化支持、事件发布机制、AOP、JDBC、ORM 框架集成等等。 +再然后是测试: -![三分恶面渣逆袭:Spring5 ApplicationContext部分体系类图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-e201c9a3-f23c-4768-b844-ac7e0ba4bcec.png) +```java +@SpringBootApplication +public class DemoApplication { -ApplicationContext 采用的是饿加载的方式,容器启动的时候就会把所有的单例 Bean 都创建好,虽然这样会导致启动时间长一点,但运行时性能更好。 + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } -```java -@Configuration -public class AppConfig { @Bean - public UserService userService() { - return new UserService(); + CommandLineRunner commandLineRunner(ApplicationContext ctx) { + return args -> { + // 尝试获取PrototypeBeanA的实例 + PrototypeBeanA beanA = ctx.getBean(PrototypeBeanA.class); + }; } } +``` -public class ApplicationContextExample { - public static void main(String[] args) { - // 创建 ApplicationContext,启动时就创建所有 Bean - ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); - - // 获取 Bean - UserService userService = context.getBean(UserService.class); - - // 发布事件 - context.publishEvent(new CustomEvent("Hello World")); +运行结果: + +![二哥的 Java 进阶之路:循环依赖](https://cdn.tobebetterjavaer.com/stutymore/spring-20240310202703.png) + +----面试中可以不背,方便大家理解 end---- + +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的小米 25 届日常实习一面原题:如何解决循环依赖? +> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的百度面经同学 1 文心一言 25 实习 Java 后端面试原题:Spring如何解决循环依赖? +> 3. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:Spring源码看过吗?Spring的三级缓存知道吗? +> 4. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的阿里云面经同学 22 面经:spring三级缓存解决循环依赖问题 + +memo:2025 年 7 月 5 日修改至此。今天 [VIP 群](https://javabetter.cn/zhishixingqiu/)来了非常多的球友,不知不觉我们已经 12 群了,也是一个大家庭了,希望大家都能在这里找到自己的归属感,我们一起学习,一起进步。 + +![二哥的编程星球已经 12 群了](https://cdn.tobebetterjavaer.com/stutymore/spring-20250705072809.png) + +### 19.为什么需要三级缓存而不是两级? + +Spring 设计三级缓存主要是为了解决 AOP 代理的问题。 + +我举个具体的例子来说明一下。假设我们有 A 和 B 两个类相互依赖,A 的某个方法上面还标注了 `@Transactional` 注解,这意味着 A 最终需要被 Spring 创建成一个代理对象。 + +```java +@Component +public class A { + @Autowired + private B b; + + @Transactional // A需要被AOP代理 + public void doSomething() { + // 业务逻辑 } } -``` -从使用场景来说,实际开发中用得最多的是 ApplicationContext。像 AnnotationConfigApplicationContext、WebApplicationContext 这些都是 ApplicationContext 的实现类。 - -另外一个重要的区别是生命周期管理。ApplicationContext 会自动调用 Bean 的初始化和销毁方法,而 BeanFactory 需要我们手动管理。 - -在 Spring Boot 项目中,我们可以通过 `@Autowired` 注入 ApplicationContext,或者通过实现 ApplicationContextAware 接口来获取 ApplicationContext。 +@Component +public class B { + @Autowired + private A a; +} +``` -![技术派源码:获取ApplicationContext](https://cdn.tobebetterjavaer.com/stutymore/spring-20250625111259.png) +如果只有二级缓存的话,当创建 A 的时候,我们需要把 A 的原始对象提前放到缓存里面,然后 B 在创建的时候从缓存中拿到 A 的原始对象。 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 2 优选物流调度技术 2 面面试原题:BeanFactory和ApplicationContext +```java +// 假设只有两级缓存 +Map singletonObjects = new HashMap<>(); // 完整Bean +Map earlySingletonObjects = new HashMap<>(); // 半成品Bean +``` -memo:2025 年 6 月 25 日修改至此,今天给一个华科本硕研 0 的[球友修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)后,发来这样的感慨,要是早点知道你的[网站](https://javabetter.cn/home.html)和[星球](https://javabetter.cn/zhishixingqiu/)就好了,[技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)不比外卖强多了?再次感谢二哥。 +但是问题来了,A 完成初始化后,由于有 `@Transactional` 注解,Spring 会把 A 包装成一个代理对象放到容器中。这样就出现了一个很严重的问题:B 里面持有的是 A 的原始对象,而容器中存的是 A 的代理对象,同一个 Bean 居然有两个不同的实例,这肯定是不对的。 -![球友对星球相见恨晚](https://cdn.tobebetterjavaer.com/stutymore/spring-20250625111617.png) +![三分恶面渣逆袭:二级缓存不行的原因](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/sidebar/sanfene/spring-6ece8a46-25b1-459b-8cfa-19fc696dd7d6.png) -### 19.🌟项目启动时Spring的IoC会做什么? +三级缓存就是为了解决这个问题而设计的。三级缓存里面存放的不是 Bean 的实例,而是一个对象工厂,这是一个函数式接口。 -第一件事是扫描和注册 Bean。IoC 容器会根据我们的配置,比如 `@ComponentScan` 指定的包路径,去扫描所有标注了 `@Component`、`@Service`、`@Controller` 这些注解的类。然后把这些类的元信息包装成 BeanDefinition 对象,注册到容器的 BeanDefinitionRegistry 中。这个阶段只是收集信息,还没有真正创建对象。 +当 B 需要 A 的时候,会调用这个对象工厂的 getObject 方法,这个方法里面会判断 A 是否需要被代理。如果需要代理,就创建 A 的代理对象返回给 B;如果不需要代理,就返回 A 的原始对象。这样就保证了 B 拿到的 A 和最终放入容器的 A 是同一个对象。 -![pdai.tech:IoC](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627101759.png) +```java +Map> singletonFactories = new HashMap<>(); +// Spring源码中的逻辑 +addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { + Object exposedObject = bean; + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { + SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; + // 关键:如果需要代理,这里会创建代理对象 + exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); + } + } + } + return exposedObject; +} +``` -第二件事是 Bean 的实例化和注入。这是最核心的过程,IoC 容器会按照依赖关系的顺序开始创建 Bean 实例。对于单例 Bean,容器会通过反射调用构造方法创建实例,然后进行属性注入,最后执行初始化回调方法。 +简单来说,三级缓存的核心作用就是延迟决策。它让 Spring 在真正需要 Bean 的时候才决定返回原始对象还是代理对象,这样就避免了对象不一致的问题。如果没有三级缓存,Spring 要么无法在循环依赖的情况下支持 AOP,要么就会出现同一个 Bean 有多个实例的问题,这些都是不可接受的。 -![Tom弹架构:Bean 的实例化和注入](https://cdn.tobebetterjavaer.com/stutymore/spring-20250627102651.png) +![幸云教育:三级缓存和循环依赖](https://cdn.tobebetterjavaer.com/stutymore/spring-20250706065436.png) -在依赖注入时,容器会根据 `@Autowired`、`@Resource` 这些注解,把相应的依赖对象注入到目标 Bean 中。比如 UserService 需要 UserDao,容器就会把 UserDao 的实例注入到 UserService 中。 +#### 如果缺少二级缓存会有什么问题? -#### 说说Spring的Bean实例化方式? +二级缓存 earlySingletonObjects 主要是用来存放那些已经通过三级缓存的对象工厂创建出来的早期 Bean 引用。 -Spring 提供了 4 种方式来实例化 Bean,以满足不同场景下的需求。 +![Minor王智:三级缓存](https://cdn.tobebetterjavaer.com/stutymore/spring-20250706065722.png) -第一种是通过构造方法实例化,这是最常用的方式。当我们用 `@Component`、`@Service` 这些注解标注类的时候,Spring 默认通过无参构造器来创建实例的。如果类只有一个有参构造方法,Spring 会自动进行构造方法注入。 +假设我们有 A、B、C 三个 Bean,A 依赖 B 和 C,B 和 C 都依赖 A,形成了一个复杂的循环依赖。在没有二级缓存的情况下,每次 B 或者 C 需要获取 A 的时候,都需要调用三级缓存中 A 的 `ObjectFactory.getObject()` 方法。这意味着如果 A 需要被代理的话,代理对象可能会被重复创建多次,这显然是不合理的。 ```java -@Service -public class UserService { - private UserDao userDao; +// 没有二级缓存的伪代码 +public Object getSingleton(String beanName) { + Object singletonObject = singletonObjects.get(beanName); - public UserService(UserDao userDao) { // 构造方法注入 - this.userDao = userDao; + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + // 直接从三级缓存获取 + ObjectFactory singletonFactory = singletonFactories.get(beanName); + if (singletonFactory != null) { + return singletonFactory.getObject(); // 每次都会创建新的代理对象! + } } + return singletonObject; } ``` -第二种是通过静态工厂方法实例化。有时候对象的创建比较复杂,我们会写一个静态工厂方法来创建,然后用 `@Bean` 注解来标注这个方法。Spring 会调用这个静态方法来获取 Bean 实例。 +我举个具体的例子。比如 A 有 `@Transactional` 注解需要被 AOP 代理,B 在初始化的时候需要 A,会调用一次对象工厂创建 A 的代理对象。接着 C 在初始化的时候也需要 A,又会调用一次对象工厂,可能又创建了一个 A 的代理对象。这样 B 和 C 拿到的可能就是两个不同的 A 代理对象,这就违反了单例 Bean 的语义。 ```java -@Configuration -public class AppConfig { - @Bean - public static DataSource createDataSource() { - // 复杂的DataSource创建逻辑 - return new HikariDataSource(); +@Service +public class ServiceA { + @Autowired + private ServiceB serviceB; + + @Transactional // 需要 AOP 代理 + public void methodA() { + // 业务逻辑 } } -``` - -第三种是通过实例工厂方法实例化。这种方式是先创建工厂对象,然后通过工厂对象的方法来创建Bean: -```java -@Configuration -public class AppConfig { - @Bean - public ConnectionFactory connectionFactory() { - return new ConnectionFactory(); - } +@Service +public class ServiceB { + @Autowired + private ServiceA serviceA; // 获得代理对象 A1 - @Bean - public Connection createConnection(ConnectionFactory factory) { - return factory.createConnection(); - } + @Autowired + private ServiceC serviceC; +} + +@Service +public class ServiceC { + @Autowired + private ServiceA serviceA; // 可能获得代理对象 A2 } ``` -第四种是通过 FactoryBean 接口实例化。这是 Spring 提供的一个特殊接口,当我们需要创建复杂对象的时候特别有用: +二级缓存就是为了解决这个问题。当第一次通过对象工厂创建了 A 的早期引用之后,就把这个引用放到二级缓存中,同时从三级缓存中移除对象工厂。 ```java -@Component -public class MyFactoryBean implements FactoryBean { - @Override - public MyObject getObject() throws Exception { - // 复杂的对象创建逻辑 - return new MyObject(); - } - - @Override - public Class getObjectType() { - return MyObject.class; - } -} -``` +// 第一次获取 A +ObjectFactory factory = singletonFactories.get("serviceA"); +Object proxyA = factory.getObject(); // 创建代理 +earlySingletonObjects.put("serviceA", proxyA); // 缓存代理 +singletonFactories.remove("serviceA"); -在实际工作中,用得最多的还是构造方法实例化,因为简单直接。工厂方法一般用在需要复杂初始化逻辑的场景,比如数据库连接池、消息队列连接这些。FactoryBean 主要是在框架开发或者需要动态创建对象的时候使用。 +// 第二次获取 A +Object cachedA = earlySingletonObjects.get("serviceA"); // 直接返回缓存的代理 +// proxyA == cachedA ✓ +``` -Spring 在实例化的时候会根据 Bean 的定义自动选择合适的方式,我们作为开发者主要是通过注解和配置来告诉 Spring 应该怎么创建对象。 +后续如果再有其他 Bean 需要 A,就直接从二级缓存中获取,不需要再调用对象工厂了。 -> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的华为面经同学 8 技术二面面试原题:说说 Spring 的 Bean 实例化方式 -> 2. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 2 优选物流调度技术 2 面面试原题:bean加工有哪些方法? +> 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的快手同学 4 一面原题:循环依赖有了解过吗?出现循环依赖的原因?三大缓存存储内容的区别?如何解决循环依赖?如果缺少第二级缓存会有什么问题? @@ -2515,56 +2511,6 @@ Spring AOP 借鉴了很多 AspectJ 的概念和注解,我们在 Spring 中使 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的得物面经同学 9 面试题目原题:抛开Spring,讲讲反射和动态代理?那三种代理模式怎么实现的? -#### AOP 和装饰器模式有什么区别? - -AOP 和装饰器模式都是为了在不修改原有代码的情况下,动态地为对象添加额外的行为。 - -装饰器模式是通过创建一个包装类来实现的,这个包装类持有被装饰对象的引用,并在调用方法时添加额外的逻辑。装饰器模式通常需要手动编写包装类,适用于单个对象的增强。 - -```java -// 基础组件接口 -interface Component { - void operation(); -} - -// 具体组件 -class ConcreteComponent implements Component { - public void operation() { - System.out.println("执行基本操作"); - } -} - -// 装饰器基类 -abstract class Decorator implements Component { - protected Component component; - - public Decorator(Component component) { - this.component = component; - } - - public void operation() { - component.operation(); - } -} - -// 具体装饰器 -class ConcreteDecorator extends Decorator { - public ConcreteDecorator(Component component) { - super(component); - } - - public void operation() { - addedBehavior(); - super.operation(); - addedBehavior(); - } - - private void addedBehavior() { - System.out.println("添加的新功能"); - } -} -``` - ### 24.🌟说说JDK动态代理和CGLIB代理的区别? JDK 动态代理和 CGLIB 代理是 Spring AOP 用来创建代理对象的两种方式。 @@ -3576,14 +3522,6 @@ public ConfigurableApplicationContext run(String... args) { } ``` -#### 要在启动阶段自定义逻辑该怎么做? - -可以通过实现 `ApplicationRunner` 接口来完成启动后的自定义逻辑。 - -比如说在[技术派项目](https://javabetter.cn/zhishixingqiu/paicoding.html)中,我们就在 run 方法中追加了:JSON 类型转换配置和动态设置应用访问地址等。 - -![技术派源码:启动后添加自定义逻辑](https://cdn.tobebetterjavaer.com/stutymore/spring-20250922175428.png) - #### 为什么 Spring Boot 在启动的时候能够找到 main 方法上的@SpringBootApplication 注解? 其实 Spring Boot 并不是自己找到 `@SpringBootApplication` 注解的,而是我们通过程序告诉它的。 @@ -3765,167 +3703,51 @@ memo:2025 年 8 月 16 日修改至此,今天[帮球友修改简历](https:/ ### 41.Spring Cache 了解吗? -Spring Cache 是 Spring 框架提供的一套缓存抽象,它通过提供统一的接口来支持多种缓存实现,如 Redis、Caffeine 等。 - -![二哥的Java 进阶之路:Spring Cache](https://cdn.tobebetterjavaer.com/stutymore/spring-20241031111306.png) - -我们只需要在方法上加几个注解,Spring 就会自动处理缓存的存取,这种声明式的缓存使用方式让业务代码和缓存逻辑能够完全分离。 - -最常用的注解是 `@Cacheable`,用来标识方法的返回值需要被缓存。 +Spring Cache 是 Spring 框架提供的一个缓存抽象,它通过统一的接口来支持多种缓存实现(如 Redis、Caffeine 等)。 -```java -@Cacheable(value = "users", key = "#id") -public User getUserById(Long id) { - return userDao.findById(id); -} -``` - -方法在第一次执行后会把结果缓存起来,后续的调用就直接从缓存中返回,不再执行方法体。 +它通过注解(如 `@Cacheable`、`@CachePut`、`@CacheEvict`)来实现缓存管理,极大简化了代码实现。 -还有 `@CacheEvict` 注解,用于在方法执行前或执行后清除缓存。 - -```java -@CacheEvict(value = "users", key = "#id") -public void deleteUserById(Long id) { - userDao.deleteById(id); -} -``` +- @Cacheable:缓存方法的返回值。 +- @CachePut:用于更新缓存,每次调用方法都会将结果重新写入缓存。 +- @CacheEvict:用于删除缓存。 -Spring Cache 是基于 AOP 实现的,通过拦截方法调用,在调用前后插入缓存逻辑。需要我们在配置中先启用缓存功能。 +使用示例: -```java -@Configuration -@EnableCaching -public class CacheConfig { - @Bean - public CacheManager cacheManager() { - RedisCacheManager.Builder builder = RedisCacheManager - .RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory()) - .cacheDefaults(cacheConfiguration()); - return builder.build(); - } -} -``` +![二哥的Java 进阶之路:Spring Cache](https://cdn.tobebetterjavaer.com/stutymore/spring-20241031111306.png) #### Spring Cache 和 Redis 有什么区别? -Spring Cache 和 Redis 的区别其实是抽象层和具体实现的区别。Spring Cache 只是提供了一套统一的接口和注解来管理缓存,本身并不提供缓存能力,而 Redis 是具体的缓存实现。 - -在使用层面上,Spring Cache 更简单,只需要在方法上添加注解就行,框架会帮我们自动处理。 +1. **Spring Cache** 是 Spring 框架提供的一个缓存抽象,它通过注解来实现缓存管理,支持多种缓存实现(如 Redis、Caffeine 等)。 +2. **Redis** 是一个分布式的缓存中间件,支持多种数据类型(如 String、Hash、List、Set、ZSet),还支持持久化、集群、主从复制等。 -```java -@Cacheable("users") -public User getUser(Long id) { - return userDao.findById(id); -} -``` - -如果用 Redis 则需要我们手动处理缓存逻辑: +Spring Cache 适合用于单机、轻量级和短时缓存场景,能够通过注解轻松控制缓存管理。 -```java -public User getUser(Long id) { - String key = "user:" + id; - User user = (User) redisTemplate.opsForValue().get(key); - if (user == null) { - user = userDao.findById(id); - redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES); - } - return user; -} -``` +Redis 是一种分布式缓存解决方案,支持多种数据结构和高并发访问,适合分布式系统和高并发场景,可以提供数据持久化和多种淘汰策略。 -在实际的项目当中,我通常会选择使用 Spring Cache 来处理一些简单的缓存业务,但对于一些复杂的业务场景,对于复杂的业务逻辑,比如分布式锁、计数器、排行榜等,我会直接用 Redis。 +在实际开发中,Spring Cache 和 Redis 可以结合使用,Spring Cache 提供管理缓存的注解,而 Redis 则作为分布式缓存的实现,提供共享缓存支持。 #### 有了 Redis 为什么还需要 Spring Cache? -虽然 Redis 非常强大,但 Spring Cache 可以简化缓存的管理。我们直接在方法上加注解就能实现缓存逻辑,减少了手动操作 Redis 的代码量。 +虽然 Redis 非常强大,但 Spring Cache 提供了一层缓存抽象,简化了缓存的管理。我们可以直接在方法上通过注解来实现缓存逻辑,减少了手动操作 Redis 的代码量。 -```java -@Cacheable("users") -public User getUser(Long id) { - return userDao.findById(id); -} -``` - -此外,Spring Cache 还能灵活切换底层的缓存实现,比如说从 Redis 切换到 Caffeine。 +Spring Cache 还能灵活切换底层缓存实现。此外,Spring Cache 支持事务性缓存和条件缓存,便于在复杂场景中确保数据一致性。 +#### 说说Spring Cache 的底层原理? -#### 说说 Spring Cache 的底层原理? - -Spring Cache 的底层是通过 AOP 实现的。当我们在方法上标注了 `@Cacheable` 注解时,Spring 会在项目启动的时候扫描这些注解,并创建代理对象。代理对象会拦截所有的方法调用,在方法执行前后插入缓存相关的逻辑。 +Spring Cache 是基于 AOP 和缓存抽象层实现的。它通过 AOP 拦截被 @Cacheable、@CachePut 和 @CacheEvict 注解的方法,在方法调用前后自动执行缓存逻辑。 ![铿然架构:Spring Cache 架构](https://cdn.tobebetterjavaer.com/stutymore/spring-20241031113743.png) -具体的执行流程是这样的: - -当用户调用一个被缓存注解标注的方法时,实际上调用的是代理对象而不是原始对象。 - -代理对象中的 CacheInterceptor 拦截器会先解析方法上的缓存注解,获取缓存名称、key 生成规则、过期时间这些配置信息。然后根据注解的类型执行不同的缓存策略,比如 `@Cacheable` 会先去缓存中查找数据,如果找到就直接返回,不执行原方法;如果没找到,就执行原方法获取结果,然后将结果存入缓存再返回。 - -缓存 key 的生成是通过 KeyGenerator 组件完成的,默认情况下会根据方法的参数来生成 key。如果我们在注解中指定了 key 属性,Spring 会使用 SpEL 表达式引擎来解析这个表达式,结合方法参数、返回值等上下文信息计算出具体的 key 值。 +其提供的 CacheManager 和 Cache 等接口,不依赖具体的缓存实现,因此可以灵活地集成 Redis、Caffeine 等多种缓存。 -底层的缓存存储是通过 CacheManager 和 Cache 这两个抽象接口来管理的。CacheManager 负责管理多个缓存区域,每个 Cache 实例对应一个具体的缓存区域。 - -不管我们使用 Redis、Caffeine 还是其他缓存技术,都需要实现这两个接口。这样 Spring Cache 就能以统一的方式操作不同的缓存实现,实现了很好的解耦。 +- ConcurrentMapCacheManager:基于 Java ConcurrentMap 的本地缓存实现。 +- RedisCacheManager:基于 Redis 的分布式缓存实现。 +- CaffeineCacheManager:基于 Caffeine 的缓存实现。 > 1. [Java 面试指南(付费)](https://javabetter.cn/zhishixingqiu/mianshi.html)收录的美团同学 9 一面面试原题:介绍一下springcache 和redis?Spring cache和redis之间的各应用在什么场景?有了redis为什么还要用springcahe?springcache 底层原理,基于什么实现的? -memo:今天在给[球友们修改简历](https://javabetter.cn/zhishixingqiu/jianli.html)的时候,有球友说,找实习的时候也找二哥修改了简历,最后也顺利找到了,我很喜欢这种反馈,说明我付出的心血是有回报的,也感谢同学们每一次的口碑。 - -![球友对简历修改的认可](https://cdn.tobebetterjavaer.com/stutymore/spring-眼看秋招已经开始,打算最近离职了,更新了一版简历,麻烦二哥帮我看看。.png) - --- -整整两个月,面渣逆袭 Spring 篇第二版终于整理完了,这一版几乎可以说是重写了,每天耗费了大量的精力在上面,可以说是改头换面,有一种士别俩月,当刮目相看的感觉(从 1.3 万字暴涨到 3.4 万字,加餐的同时区分高频低频版)。 - -![Spring、Redis、MySQL、Java 基础、集合框架、JVM、并发编程](https://cdn.tobebetterjavaer.com/stutymore/spring-20250818102407.png) - -网上的八股其实不少,有些还是付费的,我觉得是一件好事,可以给大家提供更多的选择,但面渣逆袭的含金量懂的都懂。 - -![打印成册,八股全中](https://cdn.tobebetterjavaer.com/stutymore/spring-20250817110926.png) - -面渣逆袭第二版是在星球嘉宾三分恶的初版基础上,加入了二哥自己的思考,加入了 1000 多份真实面经之后的结果,并且从 24 届到 25 届,再到 26 届,帮助了很多小伙伴。未来的 27、28 届,也将因此受益,从而拿到心仪的 offer。 - -能帮助到大家,我很欣慰,并且在重制面渣逆袭的过程中,我也成长了很多,很多薄弱的基础环节都得到了加强,因此第二版的面渣逆袭不只是给大家的礼物,也是我在技术上蜕变的记录。 - -![球友把面渣逆袭推荐给实验室的所有人](https://cdn.tobebetterjavaer.com/stutymore/mysql-我把你推荐给我们实验室的基本所有人了.png) - - -![学院本拿到滴滴 SP 给面渣口碑+1](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250427104304.png) - - -![市面上的八股看了不少,还是面渣逆袭最舒服](https://cdn.tobebetterjavaer.com/stutymore/mysql-20250427104416.png) - -很多时候,我觉得自己是一个佛系的人,不愿意和别人争个高低,也不愿意去刻意宣传自己的作品。 - -我喜欢静待花开。 - -如果你觉得面渣逆袭还不错,可以告诉学弟学妹们有这样一份免费的学习资料,帮我做个口碑。 - -我还会继续优化,也不确定第三版什么时候会来,但我会尽力。 - -愿大家都有一个光明的未来。 - - -由于 PDF 没办法自我更新,所以需要最新版的小伙伴,可以微信搜【**沉默王二**】,或者扫描/长按识别下面的二维码,关注二哥的公众号,回复【**222**】即可拉取最新版本。 - -
- 微信扫码或者长按识别,或者微信搜索“沉默王二” -
- -当然了,请允许我的一点点私心,那就是星球的 PDF 版本会比公众号早一个月时间,毕竟星球用户都付费过了,我有必要让他们先享受到一点点福利。相信大家也都能理解,毕竟在线版是免费的,CDN、服务器、域名、OSS 等等都是需要成本的。 - -更别说我付出的时间和精力了,大家觉得有帮助还请给个口碑,让你身边的同事、同学都能受益到。 - -![回复 222](https://cdn.tobebetterjavaer.com/stutymore/collection-20250512160410.png) - -我把二哥的 Java 进阶之路、JVM 进阶之路、并发编程进阶之路,以及所有面渣逆袭的版本都放进来了,涵盖 Java基础、Java集合、Java并发、JVM、Spring、MyBatis、计算机网络、操作系统、MySQL、Redis、RocketMQ、分布式、微服务、设计模式、Linux 等 16 个大的主题,共有 40 多万字,2000+张手绘图,可以说是诚意满满。 - -这次仍然是三个版本,亮白、暗黑和 epub 版本。给大家展示其中一个 epub 版本吧,有些小伙伴很急需这个版本,所以也满足大家了。 - -![面渣逆袭 Spring 篇 epub 版本](https://cdn.tobebetterjavaer.com/stutymore/spring-20250818102519.png) - 图文详解 41 道 Spring 面试高频题,这次吊打面试官,我觉得稳了(手动 dog)。整理:沉默王二,戳[转载链接](https://mp.weixin.qq.com/s/EQge6DmgIqYITM3mAxkatg),作者:三分恶,戳[原文链接](https://mp.weixin.qq.com/s/Y17S85ntHm_MLTZMJdtjQQ)。 _没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟_。 @@ -3948,3 +3770,10 @@ _没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫 - [面渣逆袭设计模式篇 👍](https://javabetter.cn/sidebar/sanfene/shejimoshi.html) - [面渣逆袭 Linux 篇 👍](https://javabetter.cn/sidebar/sanfene/linux.html) +--- + +GitHub 上标星 10000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 10000+ 的 Java 教程](https://javabetter.cn/overview/) + +微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 + +![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) diff --git a/docs/src/sidebar/sjtu/fly/electronic-information-and-electrical-engineering/[US]-16-wangyutong.md b/docs/src/sidebar/sjtu/fly/electronic-information-and-electrical-engineering/[US]-16-wangyutong.md index 7fbad218a6..ee09dd4290 100644 --- a/docs/src/sidebar/sjtu/fly/electronic-information-and-electrical-engineering/[US]-16-wangyutong.md +++ b/docs/src/sidebar/sjtu/fly/electronic-information-and-electrical-engineering/[US]-16-wangyutong.md @@ -75,7 +75,7 @@ 其实大三下被操作系统等课程压的也没啥太深刻的自我思考,主要就是焦虑,觉得自己菜,干啥啥不行,比菜第一名。当时觉得申请季马上要到了,申请ms吧,三围都不突出,申请phd吧,自己又没什么科研经历,况且当时的我既不知道自己对科研感不感兴趣,也没有勇气和信心去申请phd。但是都到这个时候了,绩点已经成为定局,唯一能弥补的就是自己的软背景了。 -就在我一筹莫展的时候,机缘巧合了解到了kaust的一个科研实习项目,Visiting Student Research Program (VSRP),项目内容是去沙特阿拉伯的土豪大学做三至六个月的带薪科研实习,心中突然产生一种我一定要去的冲动,然后我就开始着手去申请了。关于我为什么要去和一些申请的细节我都写在这篇文章《我为什么推荐你去KAUST做3-6个月的科研实习》里了,主要动机是想丰富自己的科研经历,给自己一个尝试和学习的机会。现在回头看一看,整个申请季最感谢的就是当初的这股冲动了,没有这段经历我真的发现不了自己对科研的向往,也基本不会有申请季中能够打动老师让他们对我产生兴趣的地方。 +就在我一筹莫展的时候,机缘巧合了解到了kaust的一个科研实习项目,Visiting Student Research Program (VSRP),项目内容是去沙特阿拉伯的土豪大学做三至六个月的带薪科研实习,心中突然产生一种我一定要去的冲动,然后我就开始着手去申请了。关于我为什么要去和一些申请的细节我都写在[这篇文章《我为什么推荐你去KAUST做3-6个月的科研实习》](oversea-program/semester-program/why_I_suggest_u_KAUST.md)里了,主要动机是想丰富自己的科研经历,给自己一个尝试和学习的机会。现在回头看一看,整个申请季最感谢的就是当初的这股冲动了,没有这段经历我真的发现不了自己对科研的向往,也基本不会有申请季中能够打动老师让他们对我产生兴趣的地方。 所以我现在站在申请季之后,来看申请季前的自我思考,我认为自己做的正确的一点就是:**及时发现自己的薄弱点,并且及时争取机会去弥补我的薄弱点,敢于试错,而不是接受现状**。 diff --git a/docs/src/socket/http.md b/docs/src/socket/http.md index e03583904c..ff35d5aaee 100644 --- a/docs/src/socket/http.md +++ b/docs/src/socket/http.md @@ -127,7 +127,7 @@ username=沉默王二&password=123456 不管是请求消息还是响应消息,都可以划分为三部分,这就为我们后面的处理简化了很多工作。 - 第一行:状态行 -- 第二行到第一个空行:header(请求头/相应头) +- 第二行到第一个空行:header(请求头/响应头) - 剩下所有:正文 #### 3\. HTTP 服务器设计 diff --git a/docs/src/zhishixingqiu/paismart-go.md b/docs/src/zhishixingqiu/paismart-go.md deleted file mode 100644 index 47fb81796b..0000000000 --- a/docs/src/zhishixingqiu/paismart-go.md +++ /dev/null @@ -1,440 +0,0 @@ ---- -title: RAG 项目派聪明 Go 语言重构版,AI 时代你值得拥有! -shortTitle: Go版RAG知识库派聪明 -category: - - 知识星球 -tag: - - 实战项目 ---- - - -大家好,我是二哥呀。 - -[派聪明 Java 版](https://javabetter.cn/zhishixingqiu/paismart.html)上线两个月以来,帮助很多球友拿到了称心如意的 offer。看到大家在星球里付出过那么多,有这样的好成绩,我是打心眼里高兴,甚至有一些骄傲和得意 😎。 - -RAG 这个 AI 项目是从 2025 年 3 月份开始着手做的,默默肝了 5 个月,8 月份和大家见面后口碑爆棚,感觉这一切的付出,都值得! - -给大家晒一些成绩吧。 - -![](https://cdn.tobebetterjavaer.com/paicoding/03b3016a1c6dc9659fbc7791bca55ccd.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/2ad94e8464c1be3cd3b8fee947c2775c.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/b1b3a12367cfc625311bd175774d49fe.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/22b4c5e7b760f3be89315885438b9c16.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/c460dcb29244ec470106763f48c1d087.png) - -但这还不够! - -因为有不少同学来找二哥的时候,希望也能推出 Go 相关的项目,最好是和 AI 相关的,那大家有需求,就必须满足! - -![](https://cdn.tobebetterjavaer.com/paicoding/b05a60f8d2999d7cf5364baa643d5ad6.png) - -于是 Java 版的派聪明上线后,我们就开始猛肝 Go 版本了,整整两个多月的时间,终于搞定! - - -![Go 版本源码](https://cdn.tobebetterjavaer.com/paicoding/2cbbe36701a374fd0f024235308f4839.png) - - -![Go版本的聊天助手 RAG](https://cdn.tobebetterjavaer.com/paicoding/754665f76be3ff5b0a65b684377a4d1e.png) - ->派聪明写到简历上的案例:[https://paicoding.com/column/10/2](https://paicoding.com/column/10/2) - - -![](https://cdn.tobebetterjavaer.com/stutymore/README-20251027094034.png) - - -来看一下 Java 版和 Go 版的技术栈对比,Web 框架由 Spring 切换到了 Gin,ORM 框架由 JPA 切换到了 GORM,文档解析由 Apache Tika 切换到了 Tika Server,中文分词从 HanLP 切换到了 gojieba。 - -Redis、MinIO、Kafka、ElasticSearch、WebSocket 仍然保持不变,因为 Go 和 Java 都是可以通用的。 - -| 功能模块 | 原 Java 技术栈 | 现 Go 技术栈 | 选型理由 | -| :--------- | :------------------------ | :--------------------------- | :----------------------------------------------------- | -| Web 框架 | Spring Web | **Gin** | Go Web 开发的事实标准 | -| 数据库 ORM | Spring Data JPA | **GORM** | 接近 JPA 的开发体验 | -| 安全认证 | Spring Security | **自定义中间件 + jwt-go** | 通过 Gin 中间件实现 JWT 校验 | -| 缓存 | Spring Data Redis | **go-redis/redis** | 官方推荐的 Go Redis 客户端 | -| 消息队列 | Spring Kafka | **segmentio/kafka-go** | 这是一个纯 Go 实现的客户端 | -| 实时通信 | Spring WebSocket | **gorilla/websocket** | Go 社区最流行、最稳定的 WebSocket 库 | -| 对象存储 | MinIO Java SDK | **minio-go** | MinIO 官方 Go SDK | -| 搜索引擎 | Elasticsearch Java Client | **elastic/go-elasticsearch** | Elasticsearch 官方 Go 客户端 | -| 文档解析 | Apache Tika (内嵌库) | **Tika Server (独立服务)** | 通过将 Tika 作为独立服务并通过 HTTP 调用,实现服务解耦 | -| 中文分词 | HanLP | **固定窗口重叠分块** | 为引入`gojieba`预留了接口 | -| 配置管理 | application.yml | **Viper** | 支持 YAML 等多种格式 | -| 日志 | Logback / SLF4j | **Zap** | Uber 出品的高性能结构化日志库 | -| 依赖注入 | Spring DI | **手动依赖注入** | 在 `main.go` 中手动组装所有依赖。 | - -这也再次印证一个观点,Java 岗转 Go 岗不会特别痛苦,只需要把语法层面搞定,后续的工程能力其实是丝滑切换的。 - -目前所有教程都已经完结,包括开篇词、工程篇、大厂篇、面试篇、进阶篇,以及 Go 语言版本的相关教程。 - -![](https://cdn.tobebetterjavaer.com/paicoding/3e5c348de0bd168f824f4537bed07594.png) - -在此基础上,我们还为大家加餐了 Go 面试题的预测、Go 的学习路线,以及面渣逆袭 Go 篇,消除大家在求职 Go 岗时的所有顾虑。 - -![](https://cdn.tobebetterjavaer.com/paicoding/1deb6bd3e53478489b7befdb6ea8f215.png) - -后面我们还会推出 gopai + mydb + redigo + gingo 四个 Go 版本的实战项目,一站式帮大家补齐 Go 项目的缺憾。 - -![](https://cdn.tobebetterjavaer.com/paicoding/59b57b452328f0503b165b5dc86a60f1.png) - -看到这,就想冲这个项目同学可以直接扫下面的优惠券(或者长按自动识别),[星球](https://javabetter.cn/zhishixingqiu/)目前定价 159 元/年,优惠完只需要 129 元,每天不到 0.35 元,绝对的超值。 - -![](https://cdn.tobebetterjavaer.com/paicoding/97601d7a337d7d944b02bb4a79cd6430.png) - -**星球目前已经 10100+ 人了,等这个项目发布后就要涨价到 169 元/年,想上车的同学,请趁早**。 - -星球只有 100 多的门票,但帮助球友们拿到了包括【腾讯、阿里、蚂蚁、淘天、字节跳动、小红书、快手、京东、美团、华为、荣耀、拼多多、vivo、oppo、小米、携程、得物、深信服、传音控股、美的】等等各大公司的 offer。 - -![](https://cdn.tobebetterjavaer.com/paicoding/b4dd5bc95fc81b52c453193b987c3b42.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/caa26c647f26e805c87ea783cf2d5fe7.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/cdf9d8faee78890fe2f19f60fcf917fe.png) - -还有非常多非常多。。。我就不一一晒了,能帮助到这么多同学,打心眼里觉得付出的这一切都值了。 - -阿基米德说过一句话,给我一个支点和杠杆,我能翘起整个地球。套用过来就是,100 多块钱买张星球的门票,我能帮你拿下该死的 offer,不管是互联网大厂,还是银行国企。 - -绝对物超所值,比你去培训班花几万块钱都值,真心话。 - -因为你远比你认知中的自己要更优秀,**你只是缺一个学习路线,缺一个学习氛围,缺一个实战项目,缺一个简历修改,缺一个面经八股,缺一个点燃你学习激情的人**。 - -而我和我的合伙人,完全能做到这一切。 - -![](https://cdn.tobebetterjavaer.com/paicoding/412c661107e0f4c7c1c434ca320c2e2c.jpeg) - -## 一、派聪明的技术架构 - -整个项目遵循 **Go 官方推荐的目录规范**,核心代码与业务模块分层清晰,公共组件、接口定义、工具函数都可以独立管理。 - -```plain -paismart-go/ -├── cmd/server/main.go # 应用主入口:负责启动、依赖注入和组装 -├── configs/ # 配置文件 (config.yaml) -├── deployments/ # Dockerfile, docker-compose.yml 等 -├── internal/ # 核心业务代码 -│ ├── config/ # 配置加载模块 (Viper) -│ ├── handler/ # HTTP 处理器 (Gin Handlers) -│ ├── middleware/ # HTTP 中间件 (认证、日志) -│ ├── model/ # 数据模型 (GORM Models, DTOs) -│ ├── pipeline/ # RAG 异步处理管道 -│ ├── repository/ # 数据访问层 (DB, Redis, ES) -│ └── service/ # 核心业务逻辑层 -├── pkg/ # 公共、可复用的基础库 -│ ├── database/ # 数据库连接 (GORM, go-redis) -│ ├── embedding/ # Embedding API 客户端 -│ ├── es/ # Elasticsearch 客户端封装 -│ ├── hash/ # 密码加密 (bcrypt) -│ ├── kafka/ # Kafka 客户端封装 -│ ├── llm/ # LLM API 客户端 -│ ├── log/ # 日志库封装 (Zap) -│ ├── storage/ # 对象存储客户端封装 (MinIO) -│ ├── tasks/ # Kafka 任务结构定义 -│ ├── tika/ # Tika Server 客户端封装 -│ └── token/ # JWT 工具 -└── go.mod # Go 模块依赖文件 -``` - -Go 版本的源码这次仍然采用了 gitcode 的邀请制,申请方式我放到了知识星球的这个帖子中,复制链接到浏览器即可打开。 - ->[https://t.zsxq.com/1ilWe](https://t.zsxq.com/1ilWe) - - -![](https://cdn.tobebetterjavaer.com/paicoding/713c073667887bf5ffcd310482d7d8d7.png) - - -看到一张图,和派聪明 RAG 知识库是比较吻合的,我直接贴出来,方便大家一睹为快。 - -![](https://cdn.tobebetterjavaer.com/paicoding/b693e6e8cdc26e06a3f6665adb77142e.png) - -底层大模型我们用的是 DeepSeek。上层的业务是,当你把企业私有的 Word、PDF、txt 丢给派聪明,他就会自动进行向量化处理,支持大文件的断点续传和分片上传,分片状态使用 Redis 的 BitMap 进行保存,文件本身通过 MinIO 进行存储。 - -![](https://cdn.tobebetterjavaer.com/paicoding/05f0977e49e8074c72b6ffa55cf70d4e.png) - -具体来说,系统会使用 Tika 从文档中提取出纯文本信息,然后将长文档智能切分成多个语义完整的文本块。 - -![](https://cdn.tobebetterjavaer.com/paicoding/8542ea753e1d6857599c1c46c1d24f0f.png) - -接下来调用阿里的 Embedding 模型,将文本块转换成 2048 维的向量表示,这些向量能够捕捉文本的深层语义信息。然后,会将所有的向量数据和原始文本存入到 Elasticsearch 中,形成一个强大的知识库索引。 - -```java -// 4c. 索引到 Elasticsearch -if err := es.IndexDocument(ctx, p.esCfg.IndexName, esDoc); err != nil { - log.Errorf("[Processor] 索引分块 %d 到Elasticsearch失败, Error: %v", docVector.ChunkID, err) - return fmt.Errorf("索引块 %d 到 Elasticsearch 失败: %w", docVector.ChunkID, err) -} -``` - -当用户提出问题后,派聪明会通过 ElasticSearch 进行混合检索:先进行语义的向量搜索,再进行关键词的精排。同时,会根据用户的组织权限,自动过滤出用户有权访问的文档内容。 - -![](https://cdn.tobebetterjavaer.com/paicoding/35b8ad4235edfe235153a785b1934997.png) - -之后,系统会将这些信息作为上下文,连同用户的问题一起发送给 DeepSeek。DeepSeek 会基于我们封装好的 Prompt,生成准确、相关的回答。整个对话过程采用流式输出,用户可以实时看到 AI 的回答过程,体验非常流畅。 - -## 二、为什么要学习派聪明? - -在大模型席卷全球的今天,掌握 AI 工程化能力已成为学生党和工作党在求职中脱颖而出的关键。 - -**在阿里实习的球友直言,现在没有 RAG 简历都过不去**,有关 AI 大模型的项目现在真的非常吃香。并且部门的 HR 也说了,要招聘懂点大模型的人。 - -包括社招也是,一位球友靠[派聪明 RAG 这个项目](https://javabetter.cn/zhishixingqiu/paismart.html)拿下京东、美团和蚂蚁,base 给的都很高。 - -![](https://cdn.tobebetterjavaer.com/paicoding/69c67d36a06531b00372582d9789192f.png) - -这位拿到三家大厂 offer 的球友也直言,派聪明是转 AI 开发必备的项目。 - -### 你将收获的核心能力 - -- 端到端 RAG 实战:Tika 抽取 → 固定窗口重叠分块 → Embedding(OpenAI 兼容接口)→ ES KNN + BM25 混合检索 → 上下文拼装 → 流式回答 -- Go 工程化:Gin 分层(handler/service/repository)、Viper 配置、Zap 结构化日志、优雅停机 -- 文件存储:分片上传(Redis 断点续传)、MinIO 单分片 Copy/多分片 Compose 合并、合并后后台清理 -- 异步解耦:Kafka 任务派发/消费、失败次数阈值控制(Redis 计数)、处理器接口化(TaskProcessor) -- 多租户与权限:JWT+基于 `org_tag` 的向上聚合,检索阶段就做“最小可见性”过滤(本人/公开/组织标签) -- 实时交互:Gorilla WebSocket 流式输出与“停止指令”中断、错误统一回包与完成事件 - -我相信,Go 版派聪明这个项目一定能解决大家的燃眉之急;我也相信,大家会在接下来的求职当中大展拳脚(Java 版已经证实了这一点)。 - -![](https://cdn.tobebetterjavaer.com/paicoding/48d290873522ac06a5fdc017c5751695.png) - -星球已经有了前后端分离项目技术派(里面也有 AI 的聊天对话服务),还有微服务项目 PmHub,以及轮子项目 mydb、涉及到 Spring AI 和 Agent 的校招派(同时进行的另外一个项目)等,教程和源码的获取方式可以查看 👉 星球的第一个置顶帖球友必看。 - ->复制到浏览器打开:[https://t.zsxq.com/91hPx](https://t.zsxq.com/91hPx) - -![](https://cdn.tobebetterjavaer.com/paicoding/047197ef09311d1be141530cbb5ab502.png) - -派聪明主打的技术栈和以上这些项目的技术栈也是完全不重叠的,尤其是 RAG 涉及到的一系列 AI 相关的内容,会让大家在 AI 时代吃进红利。 - -来看看每篇教程的字数吧,RAG 面试题 31 道,一共 10271 字,还有我亲自负责的手绘图;架构设计篇 25 道面试题,一共 11277 字,市面上能做到这种程度的教程,我敢拍着胸脯说,绝对对得起 100 多块钱的门票。 - ->[https://paicoding.com/column/10/19](https://paicoding.com/column/10/19) - -![](https://cdn.tobebetterjavaer.com/paicoding/8d169e23a5d349708c70ed5c17633e6c.jpg) - -![](https://cdn.tobebetterjavaer.com/paicoding/262b318558d90e9a3e5a0110db1b6a94.png) - -看到这就想迫不及待地解锁派聪明源码和教程的同学,请扫下面的优惠券(或者长按自动识别)加入我们吧,[星球](https://javabetter.cn/zhishixingqiu/)目前定价 159 元/年(会马上涨价至 169 元),优惠完只需要 129 元,每天不到 0.35 元,绝对的超值。 - -![](https://cdn.tobebetterjavaer.com/paicoding/97601d7a337d7d944b02bb4a79cd6430.png) - -超超超低价给到大家,你去其他机构/社群对比一下,这种硬核的教程和源码最起码要价 1999 元,我们现在只要一百多,为的就是尽量减轻大家的钱包负担,我希望用自己最大的诚意,去俘获大家发自内心的口碑。 - -![](https://cdn.tobebetterjavaer.com/paicoding/378ea370a2f7a378835c50988ba53014.png) - -## 三、如何给面试官介绍派聪明? - -答: - -派聪明作为一个基于 RAG 架构的企业级 AI 知识库系统,其核心意义在于解决现代企业知识库管理的痛点,推动组织智能化转型。 - -![](https://cdn.tobebetterjavaer.com/paicoding/6cc46aa009c03e9a4c04adbf79ad8772.png) - -Go 版本派聪明通过集成 Tika 文档解析、阿里 Embedding 向量模型、Elasticsearch 混合检索技术和 DeepSeek API,构建了一套完整的智能知识处理流水线。 - -系统能够自动解析 Word、PDF、TXT 等文档,将非结构化信息转化为可检索的知识资产。更重要的是,基于语义理解的向量检索技术突破了传统关键词匹配的局限,用户通过自然语言描述就能够获得要检索的相关内容。 - -![](https://cdn.tobebetterjavaer.com/paicoding/27295a1412addf313a9be871381a385b.png) - -与依赖预训练的模型不同,RAG 能够实时检索最新的企业内部知识,避免模型幻觉,保持回答的准确性。除此之外,派聪明还实现了细粒度的多租户权限控制,确保不同部门和层级的用户只能访问授权范围内的知识。 - -## 四、派聪明提供了哪些大模型开发经验? - -### 01、RAG - -派聪明最核心的能力就是 RAG,下一个项目校招派(快完结)已经增加了 MCP 和 Agent 能力,可以说覆盖了整个大模型应用开发的落地经验,并且我们会围绕 RAG 把 AI 相关的一些高频面试题全部讲透。 - ->[https://paicoding.com/column/10/25](https://paicoding.com/column/10/25) - - -![](https://cdn.tobebetterjavaer.com/paicoding/6d5a857ced065c1fd705f8cbf80f8a2c.png) - - -### 02、文档上传和解析 - -- 如何上传文档?包括断点续传和分片上传。 -- 如何将用户上传的文档转化为可检索的语义向量? -- 如何通过 ElasticSearch 实现混合检索,包括关键词搜索与语义搜索? - -都是 RAG 非常核心的内容模块。 - -传统的数据库内容查询主要依赖“关键字匹配”,比如说 MySQL 经常用到的 `like xxx%`,这种对查询的精确度要求很高,假如我们查询的是“如何提供编程技术”,那么数据库只有“Java、Python 等编程语言的教程”,那么就无法搜索到任何内容。 - -但向量数据库就可以有效解决这个问题,它会把各种知识都转成一组组 vector,这些 vector 可以代表知识的内容和特点,当我们在 RAG 知识库中输入要查找的信息时,系统能将输入信息也转成一组组数据,然后找出最相关的知识,从而实现“语义检索”。 - -![](https://cdn.tobebetterjavaer.com/paicoding/8d53a234c2e8cf4be0d8189d49950ce2.png) - -那基于向量知识库的语义检索,虽然解决了传统关键词匹配的局限性,但显然关键词搜索这种场景还是需要的,所以派聪明兼具了关键词检索和语义检索两种能力。 - -![](https://cdn.tobebetterjavaer.com/paicoding/ab8396ec99cdd47ffb948325ee43e666.png) - -### 03、集成 DeepSeek 和阿里 Embedding - -集成大模型 API 的工程实践,比如说如何集成 DeepSeek,实现流式响应与多轮对话? - -- 如何集成阿里 Embedding 进行文档分块的语义转化? -- 再比如说如何通过 WebSocket 实现实时通信,逐步推送生成内容? -- 如何通过 Redis 实现的多轮的对话记忆? - -都是 AI 时代非常关键的技术能力。那除了调用 DeepSeek API,我们还支持本地私有的 DeepSeek R1 模型部署。 - -## 五、提供了哪些企业技术栈? - -### 01、Go 1.23+Gin 框架+GORM - -Go 语言作为后端开发的核心。main.go 文件作为应用的入口,负责初始化所有依赖项并启动 HTTP 服务器。 - -Gin 是一个用 Go 编写的高性能 HTTP Web 框架。在派聪明项目中,Gin 被用于构建 RESTful API,处理所有 HTTP 请求。internal/handler 目录下的各个处理器都基于 Gin 编写,负责接收请求、调用业务逻辑并返回响应。 - -GORM 是 Go 语言中最流行的 ORM 库。在派聪明中,GORM 被用于与 MySQL 数据库交互,执行 CRUD 操作。 - -![](https://cdn.tobebetterjavaer.com/paicoding/43f0351b8f97db8fea766a9a9c820685.png) - -### 02、Kafka+Redis+MinIO - -Redis 的八股就不用多说了,有面渣逆袭 Redis 篇。 - ->复制到浏览器打开:[https://javabetter.cn/sidebar/sanfene/redis.html](https://javabetter.cn/sidebar/sanfene/redis.html) - - -![](https://cdn.tobebetterjavaer.com/paicoding/2d11cceafbf82b67a12049963b23eb21.png) - - -技术派和 PmHub 中都没有用 Kafka,用的的 RabbitMQ 和 RocketMQ,这次把消息队列中的 Kafka 直接补齐,从此以后再也不用担心 MQ 没有落地经验了(齐活)。 - -![](https://cdn.tobebetterjavaer.com/paicoding/6ae6bcc09105a73a8711fa61b2bfcc05.png) - -MinIO 的话,在处理文件的时候也经常用到,[编程喵当时有讲到](https://www.yuque.com/itwanger/vn4p17/ta5vr1),但技术派和 PmHub 中都没有应用,这次派聪明我们也一并补齐了。 - -### 03、ElasticSearch - -集成 Elasticsearch,实现「关键词+语义」的双引擎搜索。目前已经通过 ES 的 bool should 查询实现了关键词+语义的搜索方式。 - -![](https://cdn.tobebetterjavaer.com/paicoding/1f1015eb8801ad5edbb71ea6b13767b0.png) - -### 04、Vue 3+TypeScript+Vite - -派聪明全面采用了 Vue 3 的 Composition API,技术派当时用的是 React,等于说前端的三驾马车就只剩下了 Angular。 - -![](https://cdn.tobebetterjavaer.com/paicoding/019d7cdded144f2169256e473ce0431d.png) - -整个前端采用了 Monorepo 架构,在 frontend/packages 目录下组织了多个独立的功能包,包括 axios 封装、颜色工具、hooks 库、UI 组件、工具函数等。特别值得关注的是 index.ts 中的 HTTP 客户端封装,项目基于 axios 实现了企业级的请求库,支持请求/响应拦截、错误处理、请求取消、重试机制等高级功能。 - -![](https://cdn.tobebetterjavaer.com/paicoding/5457a9a50ebcb68a7f81c40a396ac305.png) - -### 05、JWT+RBAC - -用户系统的架构采用分层设计,确保了各层之间的职责分离。后端服务通过 Gin 框架处理 HTTP 请求,使用 JWT 进行认证,bcrypt 进行密码加密存储。前端应用通过 API 与后端交互,实现了用户界面和功能。 - -![](https://cdn.tobebetterjavaer.com/paicoding/6de5d6262cc733ad59359395fa282c57.png) - -派聪明的多租户权限控制设计了三个层级,非常贴合企业的实际需求。第一层是私人空间权限,以“PRIVATE\_”前缀标识的组织标签,只有资源的创建者才能访问,保证用户个人数据的绝对安全。第二层是组织权限,同一个组织标签下的用户可以共享资源,满足团队协作的需求。第三层是公开权限,标记为公开的资源所有用户都能访问,适用于公司公告、共享文档等场景。 - -![](https://cdn.tobebetterjavaer.com/paicoding/cda9a817ecda87a5a609986fd9008401.png) - -### 06、Mockito+TDD - -项目采用了 Mockito 注解驱动的测试模式,践行测试驱动开发(TDD)的理念,每个业务功能都有对应的测试用例,包括正常流程和异常流程。通过 `@Mock` 注解创建模拟对象,比如 UserRepository,这样就不需要真实的数据库连接,`@InjectMocks` 注解则自动将模拟对象注入到被测试的服务类当中,这种依赖注入的方式让测试变得非常干净和独立。 - -![](https://cdn.tobebetterjavaer.com/paicoding/e2adadd787040c8dcbab4750ec75efe7.png) - -### 07、汇总一下 - -- Go 1.23+、模块化目录:`cmd/` `internal/` `pkg/`;分层:`handler/service/repository` -- 配置/日志/关停:Viper、Zap(结构化日志)、Gin + Context 优雅停机 -- Gin(路由分组/中间件)、Gorilla WebSocket(双向通信、增量写出、停止指令) -- JWT(access/refresh)、基于 `org_tag` 的层级聚合,检索期过滤(should + minimum_should_match) -- MySQL 8 + GORM(文件/分片/向量等元数据持久化)、Redis 7(分片进度与重试计数) -- MinIO:分片对象存储;单分片 Copy、多分片 Compose;合并后后台清理分片对象 -- Kafka(segmentio/kafka-go):生产/消费、失败阈值重试、手动提交 offset -- 任务解耦:`TaskProcessor` 接口承载解析/向量化/索引流水线 -- Apache Tika(HTTP 服务):PDF/DOCX/PPT/XLS 等文本抽取 -- 分块策略:固定窗口 + 重叠切分(提升语义覆盖) -- Elasticsearch 8:KNN 语义召回 + BM25 rescore + 短语兜底 should;索引含 `userId/orgTag/isPublic` -- Embedding:OpenAI 兼容协议,已适配 DashScope(维度 2048 可配) -- LLM:DeepSeek Chat 流式;可按同协议切换本地 Ollama -- Docker 容器化:一键拉起 MySQL/Redis/ES/Kafka/MinIO/Tika -- 集中管理 LLM/Embedding/ES 等参数 - -## 六、解锁派聪明源码+教程 - -那这次为了避免盗版,这次的代码仓库采用的是邀请制,加入星球后,在星球第一个置顶帖【球友必看】中获取邀请链接,审核通过后即可查看。 - ->派聪明源码申请方式:[https://t.zsxq.com/1ilWe](https://t.zsxq.com/1ilWe) - -![](https://cdn.tobebetterjavaer.com/paicoding/0abd7b441b744b33d48277be776e58cc.png) - -派聪明的教程,这次托管在技术派教程上,之前只要在技术派上绑定过星球的成员编号,均可以解锁查看。 - -> 派聪明教程地址:[https://paicoding.com/column/10/1](https://paicoding.com/column/10/1) - -![](https://cdn.tobebetterjavaer.com/paicoding/9d395b7d05c72cff23e5a45ce22009f5.png) - -并且了照顾大家的阅读习惯,我们也会在星球里第一时间同步。 - -![](https://cdn.tobebetterjavaer.com/paicoding/d2c867d82d57ef1560fed6267eb02590.png) - -加入[「二哥的编程星球」](https://javabetter.cn/zhishixingqiu/)后,你还可以享受以下专属内容服务: - -- 1、**付费文档:** [派聪明 RAG Java 版](https://javabetter.cn/zhishixingqiu/paismart.html)、[微服务 PmHub](https://javabetter.cn/zhishixingqiu/pmhub.html)、[前后端分离技术派](https://javabetter.cn/zhishixingqiu/paicoding.html)、轮子 MYDB、入门编程喵、AI+MCP 的校招派等项目配套的 60 万+ 字教程查看权限 -- 2、**简历修改**: 提供价值超 600 元的[简历修改服务](https://javabetter.cn/zhishixingqiu/jianli.html),附赠星球 5000+优质简历模板可供参考 -- 3、**专属问答**: 向二哥和星球嘉宾发起 1v1 提问,内容不限于 offer 选择、学习路线、职业规划等 -- 4、**面试指南**: 获取针对校招、社招的 40 万+字面试求职攻略《[Java 面试指南](https://javabetter.cn/zhishixingqiu/mianshi.html)》,以及二哥的 LeetCode 刷题笔记、一灰的职场进阶之路、华为 OD 题库 -- 5、**学习环境:** 打造一个沉浸式的学习环境,有一种高考冲刺、大学考研的氛围 - -截止到 2025 年 10 月 27 日,已经有 10100+ 球友加入星球了,很多小伙伴在认真学习项目之后,都成功拿到了心仪的校招或者社招 offer,我就随便举两个例子。 - -![](https://cdn.tobebetterjavaer.com/stutymore/readme-20250703180225.png) - -![](https://cdn.tobebetterjavaer.com/stutymore/readme-20250703180738.png) - -目前,派聪明这个项目也收尾了,大家可以放心冲 😊。并且一次购买不需要额外付费,即可获取星球的所有付费资料,帮助你少走弯路,提高学习的效率。直接微信扫下面这个优惠券即可加入。 - -![](https://cdn.tobebetterjavaer.com/paicoding/97601d7a337d7d944b02bb4a79cd6430.png) - -> 步骤 ①:微信扫描上方二维码,点击「加入知识星球」按钮 - -> 步骤 ②:访问星球置顶帖球友必看:[https://t.zsxq.com/11rEo9Pdu](https://t.zsxq.com/11rEo9Pdu),获取项目的源码和配套教程 - -加入星球需要多少钱呢?星球目前定价 159 元,限时优惠 30 元,目前只需要 129 元就可以加入。 - -0 人的时候优惠完 69 元,1000 人的时候 79 元,2000 人的时候 89 元,3000 人的时候 99 元,5000 人的时候是 119 元,后面肯定还会继续涨。 - -付费社群我加入了很多,但从未见过比这更低价格,提供更多服务的社群,光派聪明这个项目的就能让你值回票价。 - -多说一句,任何时候,技术都是我们程序员的安身立命之本,如果你能认认真真跟完派聪明的源码和教程,相信你的编程功底会提升一大截。 - -再给大家展示一下派聪明教程的部分目录吧,真的是满满的诚意和干货。 - -![](https://cdn.tobebetterjavaer.com/paicoding/35ac73adcadd56af0a4979e2a179f9e8.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/da618bae0c5b934a3f49c8461a2e8db2.png) - -![](https://cdn.tobebetterjavaer.com/paicoding/1e5e0055300a70a4cb83791f889bec20.png) - -之前就有球友反馈说,“**二哥,你这套教程如果让培训机构来卖,1999 元都算少!** - -讲真心话,这个价格也不会持续很久,星球已经 10000 人了,马上会迎来一波新的涨价(169 元),所以早买早享受,不要等,想好了就去冲,错过不能说后悔一辈子,但至少会有遗憾。 - -![](https://cdn.tobebetterjavaer.com/stutymore/readme-20250106103555.png) - -我们的代码,严格按照大厂的标准来,无论是整体的架构,还是具体的细节,都是无可挑剔的学习对象。 - -![](https://cdn.tobebetterjavaer.com/paicoding/29714395fbc1a5420df271b77fd74fc5.png) - -之前曾有球友问我:“二哥,你的星球怎么不定价 199、299、399 啊,我感觉星球提供的价值远超这个价格啊。” - -答案很明确,我有自己的原则,**拒绝割韭菜,用心做内容,能帮一个是一个**。 - -![](https://cdn.tobebetterjavaer.com/paicoding/e946bb63f1fe5279888bb7f1fcb649b0.png) - -不为别的,为的就是给所有人提供一个可持续的学习环境。当然了,随着人数的增多,二哥付出的精力越来越多,星球也会涨价,今天这批 30 元的优惠券不仅 2025 年最大的优惠力度,也是 2026 年最大的优惠力度,现在入手就是最划算的,再犹豫就只能等着涨价了。 - -想想,QQ 音乐听歌连续包年需要 **88 元**,腾讯视频连续包年需要 **178 元**,腾讯体育包年 **233 元**。我相信,二哥编程星球回馈给你的,将是 10 倍甚至百倍的价值。 - -最后,希望同学们,能紧跟我们的步伐!不要掉队。今年,和二哥一起翻身、一起逆袭、一起晋升、一起拿高薪 offer! - -那无论你是社招还是校招,我们都希望你通过派聪明这个项目,能提升自己的简历含金量,拿到更好的 offer,也能更加从容的应对面试中各种 AI 相关的考察。 - -冲。 diff --git a/docs/src/zhishixingqiu/paismart.md b/docs/src/zhishixingqiu/paismart.md index 2b58839a57..860c38ac9a 100644 --- a/docs/src/zhishixingqiu/paismart.md +++ b/docs/src/zhishixingqiu/paismart.md @@ -19,28 +19,9 @@ tag: 不容易啊。 -派聪明是 9 月份上线的,截止到目前,已经取得了非常瞩目的成绩,我这里晒一下哈。 +![派聪明如何写到简历上](https://cdn.tobebetterjavaer.com/paicoding/3a9a163a5e8f68ef3a87047999aa92b5.png) - -![面渣逆袭+派聪明 拿下招银网络+科大讯飞](https://cdn.tobebetterjavaer.com/paicoding/fb5db62ab92092e2d74a4916b6a45710.png) - - -![派聪明拿到的日常实习](https://cdn.tobebetterjavaer.com/paicoding/da01a535b091c5ebeed70bf9a08a90c6.png) - - -![派聪明拿下合合信息](https://cdn.tobebetterjavaer.com/paicoding/3518a76f439c325de8df763482cbebc4.png) - - -![派聪明拿下小红书](https://cdn.tobebetterjavaer.com/paicoding/7bed4d34460749d68db9c0fcbc4621a8.png) - - -![派聪明拿下网易](https://cdn.tobebetterjavaer.com/paicoding/5f227edcb38ffe41aea8fc0880f64cad.png) - -说句真心话,看到这,就可以无脑冲这个项目了,因为这些,还只是冰山一角。扫下面的优惠券(或者长按自动识别)解锁派聪明源码和教程吧,[星球](https://javabetter.cn/zhishixingqiu/)目前定价 159 元/年,优惠完只需要 129 元,每天不到 0.35 元,绝对的超值。 - -![派聪明优惠券](https://cdn.tobebetterjavaer.com/paicoding/97601d7a337d7d944b02bb4a79cd6430.png) - ->派聪明如何写到简历上:[https://paicoding.com/column/10/2](https://paicoding.com/column/10/2) +>派聪明如何写到简历上:https://paicoding.com/column/10/2 这篇内容我先带大家了解一下什么是派聪明,我为什么要做派聪明这个企业级的 RAG 知识库?派聪明这个 AI 项目能让大家学到什么?以及如何解锁派聪明的源码仓库和教程? @@ -74,7 +55,7 @@ tag: 4 月份的时候,机缘巧合,我和朋友【我糖呢】闲聊,就讲起这个项目,他已经悄咪咪的成为一名前端大佬,于是索性我就喊他来做前端,就这样,我们的派聪明团队就算是正式成立了。 -下个项目校招派,我打算把 MCP 和 Agent 也加进来。所以,这个项目在 AI 时代,绝对是一个明星级、现象级的实战项目。 +下个版本,我打算把 MCP 和 Agent 也加进来。所以,这个项目在 AI 时代,绝对是一个明星级、现象级的实战项目。 ## 二、派聪明的技术栈 @@ -389,7 +370,7 @@ MinIO 的话,在处理文件的时候也经常用到,[编程喵当时有讲 - 4、**面试指南**: 获取针对校招、社招的 40 万+字面试求职攻略《[Java 面试指南](https://javabetter.cn/zhishixingqiu/mianshi.html)》,以及二哥的 LeetCode 刷题笔记、一灰的职场进阶之路、华为 OD 题库 - 5、**学习环境:** 打造一个沉浸式的学习环境,有一种高考冲刺、大学考研的氛围 -截止到 2025 年 10 月 13 日,马上 10000 球友加入星球了,很多小伙伴在认真学习项目之后,都成功拿到了心仪的校招或者社招 offer,我就随便举两个例子。 +截止到 2025 年 07 月 31 日,已经有 9000+ 球友加入星球了,很多小伙伴在认真学习项目之后,都成功拿到了心仪的校招或者社招 offer,我就随便举两个例子。 ![美团快手 TP-LINK 拼多多](https://cdn.tobebetterjavaer.com/stutymore/readme-20250703180225.png) @@ -404,7 +385,7 @@ MinIO 的话,在处理文件的时候也经常用到,[编程喵当时有讲 > 步骤 ②:访问星球置顶帖球友必看:[https://t.zsxq.com/11rEo9Pdu](https://t.zsxq.com/11rEo9Pdu),获取项目的源码和配套教程 -加入星球需要多少钱呢?星球目前定价 159 元,限时优惠 30 元,目前只需要 129 元就可以加入(10000 人这个里程碑要涨价到 169 元了)。 +加入星球需要多少钱呢?星球目前定价 159 元,限时优惠 30 元,目前只需要 129 元就可以加入。 0 人的时候优惠完 69 元,1000 人的时候 79 元,2000 人的时候 89 元,3000 人的时候 99 元,5000 人的时候是 119 元,后面肯定还会继续涨。 @@ -423,7 +404,7 @@ MinIO 的话,在处理文件的时候也经常用到,[编程喵当时有讲 之前就有球友反馈说,“**二哥,你这套教程如果让培训机构来卖,1999 元都算少!** -讲真心话,这个价格也不会持续很久,星球马上 10000 人了,会迎来一波新的涨价(169 元),所以早买早享受,不要等,想好了就去冲,错过不能说后悔一辈子,但至少会有遗憾。 +讲真心话,这个价格也不会持续很久,星球已经 9000 人了,马上 10000 人会迎来一波新的涨价(169 元),所以早买早享受,不要等,想好了就去冲,错过不能说后悔一辈子,但至少会有遗憾。 ![球友们加入星球后的真实反馈](https://cdn.tobebetterjavaer.com/paicoding/0d2b52387576b0884e832c05594fc9de.png) diff --git a/docs/src/zhishixingqiu/readme.md b/docs/src/zhishixingqiu/readme.md index 1d71bb76f6..666ed029b8 100644 --- a/docs/src/zhishixingqiu/readme.md +++ b/docs/src/zhishixingqiu/readme.md @@ -19,7 +19,7 @@ head: 一个好的学习圈子,不仅可以提升你的学习效率,带动你的学习热情,还能够让你在遇到困难的时候,及时得到帮助,从而让你的进阶之路走得更快、更稳、更远。 -二哥的编程星球正是这样一个学习圈子,一个**简历精修 + 编程项目实战 + Java 面试指南 + LeetCode 刷题**的私密圈子,你不仅可以阅读高质量的星球专栏、不限次数地向二哥提问,还能和球友一起打卡成长,除此之外,二哥还会帮你制定学习计划、提供情绪价值(很重要,真的)。 +二哥的编程星球正是这样一个学习圈子,一个**Java面试指南 + 编程项目实战 + 简历精修 + LeetCode 刷题的私密圈子**,你不仅可以阅读高质量的星球专栏、不限次数地向二哥提问,还能和球友一起打卡成长,除此之外,二哥还会帮你制定学习计划、提供情绪价值(很重要,真的)。 也是得到了很多球友的认可,才敢“信誓旦旦”地推荐给大家。下面这些截图应该能代表很多球友的心声:球友们很热心,二哥很耐心,在这个优秀的编程圈子里,进步是肉眼可见的、内心更是温暖的。 @@ -32,7 +32,7 @@ head: ![少走弯路](https://cdn.tobebetterjavaer.com/stutymore/readme-20250415090742.png) -学习的路上最缺的就是清晰的学习路线、优质的学习资料和良好的学习氛围,二哥的编程星球恰好就能给你提供这样的服务,目前优惠完仅需 129 块钱(马上要涨价到 169 元),对比一下别的付费社群,你就知道这个价格有多诚意!甚至之前有碰到过,有球友在加入前不敢相信这个价格是真的。 +学习的路上最缺的就是清晰的学习路线、优质的学习资料和良好的学习氛围,二哥的编程星球恰好就能给你提供这样的服务,目前优惠完仅需 129 块钱,对比一下别的付费社群,你就知道这个价格有多诚意!甚至之前有碰到过,有球友在加入前不敢相信这个价格是真的。 ![球友不敢相信这是真的](https://cdn.tobebetterjavaer.com/stutymore/readme-20250722171412.png) @@ -94,7 +94,7 @@ head: ![面渣逆袭](https://cdn.tobebetterjavaer.com/stutymore/readme-20250227224345.png) -星球最开始的定价是 99 元,第二波是 109 元,第三波是 119 元,后来是 129 元、149 元,现在也涨到了 159 元(10000 人会涨价到 169 元,还差 10 人)。 +星球最开始的定价是 99 元,第二波是 109 元,第三波是 119 元,后来是 129 元、149 元,现在也涨到了 159 元(10000 人会涨价到 169 元)。 老读者看到这就真的不要再犹豫了,立马加入就对了,每天不到 0.35 元,但对你的学习和求职帮助远大于这个门票的价值。就像这位球友说的,这是他人生中花的最值的 100 多块钱。 @@ -118,7 +118,7 @@ head: ![球友对二哥编程星球的认可,真感动了](https://cdn.tobebetterjavaer.com/stutymore/readme-20250227104300.png) -星球从 0 人,到马上 10000 人,也能印证这一点。毫不夸张的说,二哥的编程星球就是付费圈子里性价比最高的社群,没有之一。 +星球从 0 人,到现在的 9000+人,也能印证这一点。毫不夸张的说,二哥的编程星球就是付费圈子里性价比最高的社群,没有之一。 之所以没有定价 299、365,是因为二哥是从农村走出来的孩子,我知道钱来之不易,我希望大家把不多的零花钱都花在最值的地方。 @@ -210,7 +210,7 @@ head: 说句良心话,单纯简历修改的服务,就能值回票价。 -如果你也需要二哥帮你修改简历,打造一份投了就有面试,亮瞎 HR 的简历,请扫下面的优惠券付费加入[二哥的编程星球](https://javabetter.cn/zhishixingqiu/),已经有 10000 位球友加入了,所以别再犹豫了,价格已经从 99 元一路涨到了 159 元(马上上调至 169 元),我真的不希望你等到 179、189 元的时候再加入。 +如果你也需要二哥帮你修改简历,打造一份投了就有面试,亮瞎 HR 的简历,请扫下面的优惠券付费加入[二哥的编程星球](https://javabetter.cn/zhishixingqiu/),已经有 9000+位球友加入了,所以别再犹豫了,价格已经从 99 元一路涨到了 159 元,我真的不希望你等到 169、179 元的时候再加入。 ![微信扫码或者长按识别](https://cdn.tobebetterjavaer.com/stutymore/readme-2025-zsxq-4.png) @@ -241,7 +241,6 @@ head: 多说一句,任何时候,**技术都是我们程序员的安身立命之本**,如果你能认认真真跟完星球原创项目的源码和教程,相信你的编程功底会提升一大截。 - 《[AI 知识库项目派聪明(RAG)](https://javabetter.cn/zhishixingqiu/paismart.html)》 -- 《[Go 版本的派聪明RAG知识库项目](https://javabetter.cn/zhishixingqiu/paismart-go.html)》 - 《[微服务项目 PmHub 教程](https://javabetter.cn/zhishixingqiu/paicoding.html)》 - 《[前后端分离项目技术派实战教程](https://javabetter.cn/zhishixingqiu/paicoding.html)》 - 《编程喵实战项目》 @@ -251,8 +250,6 @@ head: 看看项目上线后球友们的反馈吧: -![派聪明上线后,球友们一致好评](https://cdn.tobebetterjavaer.com/paicoding/c460dcb29244ec470106763f48c1d087.png) - ![技术派上线后的壮观场景](https://cdn.tobebetterjavaer.com/stutymore/readme-20231221220431.png) ![PmHub 上线后的盛况](https://cdn.tobebetterjavaer.com/stutymore/readme-20250704141800.png) @@ -261,8 +258,6 @@ head: 派聪明作为一个基于 RAG 架构的企业级 AI 知识库系统,其核心意义在于解决现代企业知识管理的痛点,推动组织智能化转型。在信息爆炸的时代,企业积累了大量文档资料,但传统的文件管理方式导致知识孤岛现象严重,员工难以快速获取所需信息,严重影响工作效率。 -Go 版本和 Java 版本均已上线。 - ![派聪明,一个 AI 项目](https://cdn.tobebetterjavaer.com/stutymore/readme-20250704144334.png) ![派聪明如何写到简历上](https://cdn.tobebetterjavaer.com/paicoding/3a9a163a5e8f68ef3a87047999aa92b5.png) @@ -398,7 +393,7 @@ admin 端星球白名单: 如果你在准备日常实习/暑期实习/秋招/春招/社招的过程中,缺少项目经历,那这些项目可以说是救命药。可以问问你的直系学长或者学姐,就知道这些项目有多香了。 -马上 10000 位球友加入二哥的编程星球了,所以别再犹豫,价格已经从 99 元一路涨到了 159 元(还差 10 人就将上调至 169 元),我真的不希望你等到 179 元的时候再加入。 +已经有 9000+位球友加入二哥的编程星球了,所以别再犹豫,价格已经从 99 元一路涨到了 159 元,我真的不希望你等到 179 元的时候再加入。 ![微信扫码或者长按识别](https://cdn.tobebetterjavaer.com/stutymore/readme-2025-zsxq-4.png) @@ -463,7 +458,7 @@ admin 端星球白名单: ![微信扫码或者长按识别](https://cdn.tobebetterjavaer.com/stutymore/readme-2025-zsxq-4.png) -随着时间的推移,加入二哥编程星球的球友会越来越多,二哥投入的时间也会越来越多,星球能够提供给大家的价值也会越来越多,所以星球也会涨价到 179 元、189 元,以此类推,所以,想要改变自己的小伙伴还是要趁早加入,**早就是优势**是继牛顿万有引力定律之后最永恒的真理(😁)。 +随着时间的推移,加入二哥编程星球的球友会越来越多,二哥投入的时间也会越来越多,星球能够提供给大家的价值也会越来越多,所以星球也会涨价到 169 元、179 元,以此类推,所以,想要改变自己的小伙伴还是要趁早加入,**早就是优势**是继牛顿万有引力定律之后最永恒的真理(😁)。 ## 四、星球还提供什么服务? @@ -523,6 +518,10 @@ admin 端星球白名单: ![操作系统内存管理的思维导图](https://cdn.tobebetterjavaer.com/paicoding/6e2e1083253db23bc9bf40af15916578.png) +**5)无限期使用技术派的 AI 助手「派聪明」**,目前已经开通了 DeepSeek、豆包、通义千问、讯飞星火和 OpenAI 多通道供大家学习使用。 + +![派聪明 AI 助手](https://cdn.tobebetterjavaer.com/stutymore/paicoding-20241012162807.png) + 二哥会在这个圈子里持续沉淀自己,同时也希望能帮助到更多想要进步的小伙伴: - **如果你家庭条件一般,学历一般,但不甘心命运就这样安排**; @@ -694,9 +693,9 @@ admin 端星球白名单: 星球目前定价**159 元/年,并且会送你 30 元的优惠券**,但提供的服务远远超出了 129 元这个价格可以承载的价值。 -随着时间推移,星球积累的干货会越来越多,加入星球的小伙伴也会越来越多,我花在星球上的时间也会越来越多。所以星球也会逐步提高门槛,下一次就是 **179 元/年**!再下一次就是 **189 元/年**。 +随着时间推移,星球积累的干货会越来越多,加入星球的小伙伴也会越来越多,我花在星球上的时间也会越来越多。所以星球也会逐步提高门槛,下一次就是 **169 元/年**!再下一次就是 **179 元/年**。 -加入后记得添加我的微信(扫下面的二维码),备注「**星球编号**」我会拉你进星球的 VIP 交流群,目前已经是第 14 群了。 +加入后记得添加我的微信(扫下面的二维码),备注「**星球编号**」我会拉你进星球的 VIP 交流群,目前已经是第 12 群了。 ![二哥编程星球-VIP 交流群](https://cdn.tobebetterjavaer.com/paicoding/image-c043ce76c05f421fb8422fb6a704740d.png)