diff --git a/README.md b/README.md index 3d967a6..1698ef0 100755 --- a/README.md +++ b/README.md @@ -1,17 +1,38 @@ +## 项目介绍 + - 这是 [JavaGuide](https://javaguide.cn/) 面试突击版本,适合突击面试的小伙伴。并且,提供了 PDF 下载,方便大家离线阅读/打印,阅读体验非常高。 -- 如果你准备面试的时间比较充足的话,建议阅读完整版,针对重要的知识点有更详细的讲解。地址:[javaguide.cn](https://javaguide.cn/) -- 专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./docs/about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 +- 如果你准备面试的时间比较充足的话,建议阅读完整版,针对重要的知识点有更详细的讲解。地址:**[javaguide.cn](https://javaguide.cn/)**。 +- 专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 ## Java - [Java基础常见面试题总结](./docs/java/java-basis.md) +- [Java集合常见面试题总结](./docs/java/java-collection.md) +- [Java并发常见面试题总结](./docs/java/java-concurrent.md) +- [JVM常见面试题总结](./docs/java/java-jvm.md) + +## 计算机基础 + +- [计算机网络常见面试题总结](./docs/cs-basics/network.md) +- [操作系统常见面试题总结](./docs/cs-basics/operating-system.md) +- [数据结构常见面试题总结](./docs/cs-basics/data-structure.md) +- [算法常见面试题总结](./docs/cs-basics/algorithms.md) + +## 数据库和缓存 +- [MySQL常见面试题总结](./docs/database/mysql.md) +- [Redis常见面试题总结](./docs/database/redis.md) +## 系统设计 + +- [Spring和Spring Boot常见面试题总结](./docs/system-design/spring.md) +- [设计模式常见面试题总结](./docs/system-design/design-pattern.md) ## 公众号 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 -![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) +JavaGuide 公众号 + + - diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 7f88f56..1a6cf7e 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -7,7 +7,7 @@ export default defineUserConfig({ title: "JavaGuide(面试突击版)", description: - "Java 学习&面试指南(Go、Python 后端面试通用,计算机基础面试总结) ", + "Java 学习&面试指南(Go、Python 后端面试通用,计算机基础面试总结)", lang: "zh-CN", head: [ diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts index a7110f1..3b6f817 100644 --- a/docs/.vuepress/navbar.ts +++ b/docs/.vuepress/navbar.ts @@ -2,5 +2,6 @@ import { navbar } from "vuepress-theme-hope"; export default navbar([ { text: "Java 面试", icon: "java", link: "/home.md" }, - { text: "PDF 下载", icon: "java", link: "https://mp.weixin.qq.com/s/q14qXzdM4KTmawyMi5mFpg" }, + { text: "PDF 下载", icon: "pdf", link: "https://mp.weixin.qq.com/s/q14qXzdM4KTmawyMi5mFpg" }, + // { text: "后端面经", icon: "interview", link: "/system-design/design-pattern.md" }, ]); diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index 6987ec7..c7d04f7 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -6,7 +6,7 @@ export default sidebar({ text: "项目介绍", icon: "star", collapsible: true, - prefix: "javaguide/", + prefix: "intro/", children: ["faq"], }, { @@ -38,7 +38,7 @@ export default sidebar({ }, { text: "计算机基础", - icon: "interview", + icon: "computer", collapsible: false, prefix: "cs-basics/", children: [ @@ -50,17 +50,17 @@ export default sidebar({ }, { text: "数据库和缓存", - icon: "interview", + icon: "database", collapsible: false, prefix: "database/", children: ["mysql", "redis"], }, { text: "系统设计", - icon: "interview", + icon: "design", collapsible: false, prefix: "system-design/", - children: ["design-pattern"], + children: ["spring", "design-pattern"], }, ], }); diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts index 8ced6ac..3fc88fd 100644 --- a/docs/.vuepress/theme.ts +++ b/docs/.vuepress/theme.ts @@ -33,18 +33,6 @@ export default hopeTheme({ align: true, codeTabs: true, gfm: true, - include: { - resolvePath: (file, cwd) => { - if (file.startsWith("@")) - return path.resolve( - __dirname, - "../snippets", - file.replace("@", "./"), - ); - - return path.resolve(cwd, file); - }, - }, tasklist: true, }, diff --git a/docs/README.md b/docs/README.md index 114779b..b32223c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,14 +3,14 @@ home: true icon: home title: Java 面试指南 heroImage: /logo.svg -heroText: JavaGuide -tagline: 「Java学习 + 面试指南」涵盖 Java 程序员需要掌握的核心知识 +heroText: JavaGuide 面试突击版 +tagline: Java 学习&面试指南(Go、Python 后端面试通用,计算机基础面试总结) actions: - text: 开始阅读 link: /home.md type: primary - text: 知识星球 - link: /about-the-author/zhishixingqiu-two-years.md + link: https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html type: default footer: |- 鄂ICP备2020015769号-1 | 主题: VuePress Theme Hope @@ -22,14 +22,18 @@ JavaGuide 已经持续维护 6 年多了,累计提交了接近 **6000** commit 如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star(绝不强制点 Star,觉得内容不错有收获再点赞就好),这是对我最大的鼓励,感谢各位一路同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 +这是 [JavaGuide](https://javaguide.cn/) 面试突击版本,适合突击面试的小伙伴。并且,提供了 PDF 下载,方便大家离线阅读/打印,阅读体验非常高。 +如果你准备面试的时间比较充足的话,建议阅读完整版,针对重要的知识点有更详细的讲解。地址:**[javaguide.cn](https://javaguide.cn/)**。 + +专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](./docs/about-the-author/zhishixingqiu-two-years.md)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 ## 关于作者 -- [我曾经也是网瘾少年](./about-the-author/internet-addiction-teenager.md) -- [害,毕业三年了!](./about-the-author/my-college-life.md) -- [我的知识星球快 3 岁了!](./about-the-author/zhishixingqiu-two-years.md) -- [坚持写技术博客六年了](./about-the-author/writing-technology-blog-six-years.md) +- [我曾经也是网瘾少年](https://javaguide.cn/about-the-author/internet-addiction-teenager.html) +- [害,毕业三年了!](https://javaguide.cn/about-the-author/my-college-life.html) +- [我的知识星球 4 岁了!](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) +- [坚持写技术博客六年了](https://javaguide.cn/about-the-author/writing-technology-blog-six-years.html) ## 公众号 diff --git a/docs/database/mysql.md b/docs/database/mysql.md index 6e28328..7e285df 100755 --- a/docs/database/mysql.md +++ b/docs/database/mysql.md @@ -323,7 +323,7 @@ MyISAM 不提供事务支持。 InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。 -关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 +关于 MySQL 事务的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](https://javaguide.cn/database/mysql/transaction-isolation-level.html)。 **3、是否支持外键** @@ -357,7 +357,7 @@ MyISAM 不支持,而 InnoDB 支持。 InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。 -详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](./mysql-index.md)。 +详细区别,推荐你看看我写的这篇文章:[MySQL 索引详解](https://javaguide.cn/database/mysql/mysql-index.html)。 **7、性能有差别。** diff --git a/docs/database/redis.md b/docs/database/redis.md index 7865f0d..69a4e16 100644 --- a/docs/database/redis.md +++ b/docs/database/redis.md @@ -927,8 +927,6 @@ maxmemory-policy noeviction 关于淘汰策略的详细说明可以参考 Redis 官方文档:。 - - ## Redis 事务 ### 什么是 Redis 事务? @@ -1214,7 +1212,7 @@ bigkey 通常是由于下面这些原因产生的: bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。 -在 [Redis 常见阻塞原因总结](./redis-common-blocking-problems-summary.md) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面: +在 [Redis 常见阻塞原因总结](https://javaguide.cn/database/redis/redis-common-blocking-problems-summary.html) 这篇文章中我们提到:大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面: 1. 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 2. 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 diff --git a/docs/home.md b/docs/home.md index 2dfdbf6..cc1f61d 100644 --- a/docs/home.md +++ b/docs/home.md @@ -1,19 +1,42 @@ --- icon: creative -title: JavaGuide(Java学习&面试指南) +title: Java 学习&面试指南(Go、Python 后端面试通用,计算机基础面试总结) --- -::: tip 友情提示 +## 项目介绍 -- **知识星球**:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 -- **求个 Star**:如果觉得 JavaGuide 的内容对你有帮助的话,还请点个免费的 Star,这是对我最大的鼓励,感谢各位一起同行,共勉!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 +- 这是 [JavaGuide](https://javaguide.cn/) 面试突击版本,适合突击面试的小伙伴。并且,提供了 PDF 下载,方便大家离线阅读/打印,阅读体验非常高。 +- 如果你准备面试的时间比较充足的话,建议阅读完整版,针对重要的知识点有更详细的讲解。地址:**[javaguide.cn](https://javaguide.cn/)**。 +- 专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 **[JavaGuide 知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(点击链接即可查看星球的详细介绍,一定确定自己真的需要再加入)。 -::: +## Java +- [Java基础常见面试题总结](./java/java-basis.md) +- [Java集合常见面试题总结](./java/java-collection.md) +- [Java并发常见面试题总结](./java/java-concurrent.md) +- [JVM常见面试题总结](./java/java-jvm.md) +## 计算机基础 + +- [计算机网络常见面试题总结](./cs-basics/network.md) +- [操作系统常见面试题总结](./cs-basics/operating-system.md) +- [数据结构常见面试题总结](./cs-basics/data-structure.md) +- [算法常见面试题总结](./cs-basics/algorithms.md) + +## 数据库和缓存 + +- [MySQL常见面试题总结](./database/mysql.md) +- [Redis常见面试题总结](./database/redis.md) + +## 系统设计 + +- [Spring和Spring Boot常见面试题总结](./system-design/spring.md) +- [设计模式常见面试题总结](./system-design/design-pattern.md) ## 公众号 -如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号"**JavaGuide**"。 +如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 ![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) + + diff --git a/docs/interview-preparation/interview-experience.md b/docs/interview-preparation/interview-experience.md index a2f32fd..84a2400 100644 --- a/docs/interview-preparation/interview-experience.md +++ b/docs/interview-preparation/interview-experience.md @@ -26,5 +26,3 @@ icon: experience 1. 参考资料解释的要更详细一些,还可以顺便让你把相关的知识点复习一下。 2. 给出的参考资料基本都是我的原创,假如后续我想对面试问题的答案进行完善,就不需要挨个把之前的面经写的答案给修改了(面试中的很多问题都是比较类似的)。当然了,我的原创文章也不太可能覆盖到面试的每个点,部分面试问题的答案,我是精选的其他技术博主写的优质文章,文章质量都很高。 - - diff --git a/docs/interview-preparation/project-experience-guide.md b/docs/interview-preparation/project-experience-guide.md index 13465ef..3fd2423 100644 --- a/docs/interview-preparation/project-experience-guide.md +++ b/docs/interview-preparation/project-experience-guide.md @@ -26,7 +26,7 @@ icon: project 我面试过很多求职者,简历上看着有微服务的项目经验,结果随便问两个问题就知道根本不是自己做的或者说做的时候压根没认真思考。这种情况会给我留下非常不好的印象。 -我在 **[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中也说过: +我在 **[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)** 的「面试准备篇」中也说过: > 个人认为也没必要非要去做微服务或者分布式项目,不一定对你面试有利。微服务或者分布式项目涉及的知识点太多,一般人很难吃透。并且,这类项目其实对于校招生来说稍微有一点超标了。即使你做出来,很多面试官也会认为不是你独立完成的。 > @@ -72,7 +72,7 @@ GitHub 或者码云上面有很多实战类别项目,你可以选择一个来 ## 有没有还不错的项目推荐? -**[《Java 面试指北》](../zhuanlan/java-mian-shi-zhi-bei.md)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。 +**[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)** 的「面试准备篇」中有一篇文章专门整理了一些比较高质量的实战项目,包含业务项目、轮子项目、国外公开课 Lab 和视频类实战项目教程推荐,非常适合用来学习或者作为项目经验。 ![优质 Java 实战项目推荐](https://oss.javaguide.cn/javamianshizhibei/project-experience-guide.png) diff --git a/docs/interview-preparation/resume-guide.md b/docs/interview-preparation/resume-guide.md index e0123ae..319d37f 100644 --- a/docs/interview-preparation/resume-guide.md +++ b/docs/interview-preparation/resume-guide.md @@ -288,7 +288,7 @@ FAB 法则由下面 3 个单词组成(FAB 法则的名字就是由它们的首 下面是星球提供的部分服务(点击下方图片即可获取知识星球的详细介绍): -[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](../about-the-author/zhishixingqiu-two-years.md) +[![星球服务](https://oss.javaguide.cn/xingqiu/xingqiufuwu.png)](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) 这里再提供一份限时专属优惠卷: diff --git a/docs/interview-preparation/self-test-of-common-interview-questions.md b/docs/interview-preparation/self-test-of-common-interview-questions.md index 9a454c8..fb3cf13 100644 --- a/docs/interview-preparation/self-test-of-common-interview-questions.md +++ b/docs/interview-preparation/self-test-of-common-interview-questions.md @@ -15,5 +15,3 @@ icon: security-fill 在面试中如果你实在没有头绪的话,一个好的面试官也是会给你提示的。 ![](https://oss.javaguide.cn/xingqiu/image-20220628102848236.png) - - diff --git a/docs/intro/faq.md b/docs/intro/faq.md index f89cdbc..2511046 100644 --- a/docs/intro/faq.md +++ b/docs/intro/faq.md @@ -13,11 +13,10 @@ JavaGuide 已经有了在线阅读版本(地址:https://javaguide.cn/), ## 如何获取最新版本? -你可以通过我的公众号获取到 **《JavaGuide 面试突击版》** 的最新版本。 +你可以通过我的公众号获取到 **《JavaGuide 面试突击版》** 的最新版本,后台回复"**PDF**"即可! + +JavaGuide 公众号 -
- -
## 如何学习本项目? 不论是在线版本还是 PDF 版本都提供了非常详细的目录,建议可以从头到尾看一遍,如果基础不错的话也可以挑自己需要的章节查看。看的过程中自己要多思考,碰到不懂的地方,自己记得要勤搜索,需要记忆的地方也不要吝啬自己的脑子。 diff --git a/docs/java/java-basis.md b/docs/java/java-basis.md index d4396a7..153c7aa 100755 --- a/docs/java/java-basis.md +++ b/docs/java/java-basis.md @@ -9,7 +9,7 @@ head: content: Java特点,Java SE,Java EE,Java ME,Java虚拟机,JVM,JDK,JRE,字节码,Java编译与解释,AOT编译,云原生,AOT与JIT对比,GraalVM,Oracle JDK与OpenJDK区别,OpenJDK,LTS支持,多线程支持,静态变量,成员变量与局部变量区别,包装类型缓存机制,自动装箱与拆箱,浮点数精度丢失,BigDecimal,Java基本数据类型,Java标识符与关键字,移位运算符,Java注释,静态方法与实例方法,方法重载与重写,可变长参数,Java性能优化 - - meta - name: description - content: 全网质量最高的Java基础常见知识点和面试题总结,希望对你有帮助! + content: 系统梳理 Java 面试中最常考的基础知识与高频问题:涵盖 JVM、JDK、JRE 区别,字节码与编译/解释执行机制,AOT 与 JIT 对比及 GraalVM,Oracle JDK 与 OpenJDK 区别,8 种基本数据类型与自动装箱、包装类型缓存机制,浮点数精度与 BigDecimal,成员变量和局部变量、静态变量与方法,重载和重写,自增自减与移位运算符,String 不可变性与常量池,异常体系与 try-with-resources,泛型、反射、SPI、序列化及 I/O 等核心考点,适合作为 Java 基础面试突击与复习笔记。 --- ------ @@ -22,13 +22,13 @@ head: 由于很多读者都有突击面试的需求,所以我在几年前就弄了 **JavaGuide 面试突击版本**(JavaGuide 内容精简版,只保留重点),并持续完善跟进。对于喜欢纸质阅读的朋友来说,也可以打印出来,整体阅读体验非常高! -除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了**⭐️**标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 +除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了⭐️标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 对于时间比较充裕的朋友,我个人还是更推荐 [JavaGuide](https://javaguide.cn/) 网站系统学习,内容更全面,更深入。 JavaGuide 已经持续维护 6 年多了,累计提交了接近 **6000** commit ,共有 **570+** 多位贡献者共同参与维护和完善。用心做原创优质内容,如果觉得有帮助的话,欢迎点赞分享!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 -对于需要更进一步面试辅导服务的读者,欢迎加入**[JavaGuide 官方知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(技术专栏/一对一提问/简历修改/求职指南/面试打卡),绝对物超所值! +对于需要更进一步面试辅导服务的读者,欢迎加入 **[JavaGuide 官方知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(技术专栏/一对一提问/简历修改/求职指南/面试打卡),绝对物超所值! 面试突击最新版本可以在我的公众号回复"**PDF**"获取([JavaGuide 官方知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)会提前同步最新版,针对球友的一个小福利)。 @@ -1644,6 +1644,14 @@ public static String getStr() { ![](https://oss.javaguide.cn/github/javaguide/java/basis/unchecked-exception.png) +### 你更倾向于使用 Checked Exception 还是 Unchecked Exception? + +默认使用 Unchecked Exception,只在必要时才用 Checked Exception。 + +我们可以把 Unchecked Exception(比如 `NullPointerException`)看作是代码 Bug。对待 Bug,最好的方式是让它暴露出来然后去修复代码,而不是用 `try-catch` 去掩盖它。 + +一般来说,只在一种情况下使用 Checked Exception:当这个异常是业务逻辑的一部分,并且调用方必须处理它时。比如说,一个余额不足异常。这不是 bug,而是一个正常的业务分支,我需要用 Checked Exception 来强制调用者去处理这种情况,比如提示用户去充值。这样就能在保证关键业务逻辑完整性的同时,让代码尽可能保持简洁。 + ### try-catch-finally 如何使用? - `try`块:用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 @@ -1911,6 +1919,56 @@ public class DebugInvocationHandler implements InvocationHandler { 像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。 +## 代理 + +关于 Java 代理的详细介绍,可以看看笔者写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html "Java 代理模式详解")这篇文章。 + +### 如何实现动态代理? + +动态代理是一种非常强大的设计模式,它允许我们在**不修改源代码**的情况下,对一个类或对象的方法进行**功能增强(Enhancement)**。 + +在 Java 中,实现动态代理最主流的方式有两种:**JDK 动态代理** 和 **CGLIB 动态代理**。 + +**第一种:JDK 动态代理** + +Java 官方提供的,其核心要求是目标类必须实现一个或多个接口。JDK 动态代理在运行时,会利用 `Proxy.newProxyInstance()` 方法,动态地创建一个实现了这些接口的代理类的实例。这个代理类在内存中生成,你看不到它的 `.java` 或 `.class` 文件。 + +当你调用代理对象的任何一个方法时,这个调用都会被转发到我们提供的一个 `InvocationHandler` 接口的 `invoke` 方法中。在 `invoke` 方法里,我们就可以在调用原始方法(目标方法)之前或之后,加入我们自己的增强逻辑。 + +**第二种:CGLIB 动态代理** + +CGLIB 是一个第三方的代码生成库。它的原理与 JDK 完全不同,它不要求被代理的类实现接口。它在运行时,动态生成目标类的子类作为代理类(通过 ASM 字节码操作技术)。然后,它会重写父类(也就是被代理类)中所有非 `final`、`private` 和 `static` 的方法。 + +当你调用代理对象的任何一个方法时,这个调用会被 CGLIB 的 `MethodInterceptor` 接口的 `intercept` 方法拦截。和 `InvocationHandler` 的 `invoke` 方法一样,我们可以在 `intercept` 方法里,在调用原始的父类方法之前或之后,加入我们的增强逻辑。 + +### 静态代理和动态代理有什么区别? + +静态代理和动态代理的核心差异在于 **代理关系的确定时机、实现灵活性及维护成本** 。 + +| 对比维度 | 静态代理 (Static Proxy) | 动态代理 (Dynamic Proxy) | +| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 代理关系确定时机 | 编译期(编译后生成固定的 `.class` 字节码文件) | 运行时(动态生成代理类字节码并加载到 JVM) | +| 实现方式 | 手动编写代理类,需与目标类实现同一接口,一对一绑定 | 无需手动编写代理类,通过 `Handler`/`Interceptor` 封装增强逻辑,一对多复用 | +| 接口依赖 | 必须实现接口(代理类与目标类遵循同一接口规范) | 支持代理接口或直接代理实现类 | +| 代码量与维护性 | 代码量大(目标类越多,代理类越多),维护成本高;接口新增方法时,目标类与代理类需同步修改 | 代码量极少(通用增强逻辑可复用),维护性好;与接口解耦,接口变更不影响代理逻辑 | +| 核心优势 | 实现简单、逻辑直观,无额外框架依赖 | 灵活性强、复用性高,降低重复编码,适配复杂场景 | +| 典型应用场景 | 简单的装饰器模式、少量固定类的增强需求 | Spring AOP、RPC 框架(如 Dubbo)、ORM 框架 | + +### ⭐️JDK 动态代理和 CGLIB 动态代理有什么区别? + +1. JDK 动态代理是官方的,它要求被代理的类必须实现接口。它的原理是动态生成一个接口的实现类来作为代理。CGLIB 是第三方的,它不需要接口。它的原理是动态生成一个被代理类的子类来作为代理。但也正因为是继承,所以它不能代理 `final` 的类,被代理的方法也不能是 `final` 或 `private` 。 +2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。 + +### ⭐️介绍一下动态代理在框架中的实际应用场景 + +动态代理最典型的应用场景就是**Spring AOP**。 + +AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 + +Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示: + +![SpringAOPProcess](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/230ae587a322d6e4d09510161987d346.jpeg) + ## 注解 ### 何谓注解? @@ -2073,7 +2131,7 @@ Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来 参考答案:[Java IO 设计模式总结](https://javaguide.cn/java/io/io-design-patterns.html) -### BIO、NIO 和 AIO 的区别? +### ⭐️BIO、NIO 和 AIO 的区别? 参考答案:[Java IO 模型详解](https://javaguide.cn/java/io/io-model.html) diff --git a/docs/java/java-collection.md b/docs/java/java-collection.md index 171530f..d364238 100755 --- a/docs/java/java-collection.md +++ b/docs/java/java-collection.md @@ -6,10 +6,10 @@ tag: head: - - meta - name: keywords - content: Collection,List,Set,Queue,Deque,PriorityQueue,HashMap,ConcurrentHashMap,Hashtable + content: Java集合,Collection,List,ArrayList,LinkedList,Set,HashSet,TreeSet,Queue,Deque,ArrayDeque,PriorityQueue,BlockingQueue,ArrayBlockingQueue,LinkedBlockingQueue,HashMap,TreeMap,ConcurrentHashMap,Hashtable,fail-fast,fail-safe - - meta - name: description - content: Java集合常见知识点和面试题总结,希望对你有帮助! + content: 系统梳理 Java 集合框架常见知识点与高频面试题,覆盖 List、Set、Queue、Map 及其典型实现(如 ArrayList、LinkedList、HashSet、HashMap、ConcurrentHashMap、BlockingQueue 等),并结合源码讲解扩容机制、时间复杂度、线程安全与 fail-fast/fail-safe 等关键细节。 --- ------ @@ -22,7 +22,7 @@ head: 由于很多读者都有突击面试的需求,所以我在几年前就弄了 **JavaGuide 面试突击版本**(JavaGuide 内容精简版,只保留重点),并持续完善跟进。对于喜欢纸质阅读的朋友来说,也可以打印出来,整体阅读体验非常高! -除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了**⭐️**标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 +除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了⭐️标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 对于时间比较充裕的朋友,我个人还是更推荐 [JavaGuide](https://javaguide.cn/) 网站系统学习,内容更全面,更深入。 diff --git a/docs/java/java-concurrent.md b/docs/java/java-concurrent.md index 35917b5..4bb1853 100755 --- a/docs/java/java-concurrent.md +++ b/docs/java/java-concurrent.md @@ -22,7 +22,7 @@ head: 由于很多读者都有突击面试的需求,所以我在几年前就弄了 **JavaGuide 面试突击版本**(JavaGuide 内容精简版,只保留重点),并持续完善跟进。对于喜欢纸质阅读的朋友来说,也可以打印出来,整体阅读体验非常高! -除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了**⭐️**标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 +除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了⭐️标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 对于时间比较充裕的朋友,我个人还是更推荐 [JavaGuide](https://javaguide.cn/) 网站系统学习,内容更全面,更深入。 diff --git a/docs/java/java-jvm.md b/docs/java/java-jvm.md index a544891..e7983ba 100755 --- a/docs/java/java-jvm.md +++ b/docs/java/java-jvm.md @@ -22,7 +22,7 @@ head: 由于很多读者都有突击面试的需求,所以我在几年前就弄了 **JavaGuide 面试突击版本**(JavaGuide 内容精简版,只保留重点),并持续完善跟进。对于喜欢纸质阅读的朋友来说,也可以打印出来,整体阅读体验非常高! -除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了**⭐️**标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 +除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了⭐️标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 对于时间比较充裕的朋友,我个人还是更推荐 [JavaGuide](https://javaguide.cn/) 网站系统学习,内容更全面,更深入。 diff --git a/docs/real-interview-experience/2025-alibaba-taotian-1.md b/docs/real-interview-experience/2025-alibaba-taotian-1.md new file mode 100644 index 0000000..2b3779b --- /dev/null +++ b/docs/real-interview-experience/2025-alibaba-taotian-1.md @@ -0,0 +1,430 @@ +过去两年,阿里的招聘策略正在悄悄发生结构性变化。 + +一方面,阿里整体调整组织架构,形成淘天、阿里云、夸克等多个相对独立的业务方向;另一方面,不少应届生明显感受到:**阿里开始"卡学历"了。** + +从我这段时间收到的反馈来看—— + +- 淘天、高德、蚂蚁、阿里云等核心业务线 **对双非学历的过滤明显增强**,进入难度比前几年高很多; +- 社招相对宽松,但学历同样会影响初筛通过率; +- 部门之间的招聘标准差异极大: **有的部门卡得很严,有的部门依旧相对友好。** + +还有一个投递小技巧:阿里不同部门之间的投递 **是可以分开算的**。 同学们别傻傻只投一个 JD,被拒了连第二次机会都没有。 + +至于面试流程,阿里整体依然保持"技术主导",一般是: + +- 两轮或三轮技术面(极少出现四轮) +- 技术通过后才会约 HR 面 +- HR 面依旧"玄学",也需要重视 + +**千万不要以为:技术面过了 = 稳了。** + +这是一位来自四川大学的同学分享的 **阿里淘天一面** 面经。从整体感受来看,这次面试偏轻松,面试官提问比较随意(有点像是 KPI 面试),主要分为三个部分: + +1. 非技术类问题(自我介绍、实习经历等) +2. 小型笔试题(在线编程,不是 LeetCode 偏难题) +3. 基础技术八股(如 GET/POST 区别、反射应用场景、SQL 优化等) + +面试时长约 **一个半小时**。整体难度比较简单,最让我意外的事竟然考察了三道笔试题,不是那种纯粹的 LeetCode 问题,偏向于考察对 Java 语言的掌握,挺简单的! + +![](https://static001.geekbang.org/infoq/6a/6a07333e392e0b710fb5bf2b3ae28652.png) + +> 这篇是24届同学的面经,当时分享过,但笔试题的答案需要重新完善一下。根据我的观察来看,阿里的面试一般不会考察这么多笔试题,所以说有点像是 KPI 面试。 + +## 非技术问题 + +### 自我介绍 + +面试时的自我介绍,其实是你给面试官的"第一印象浓缩版"。它不需要面面俱到,但要精准、自信地展现你的核心价值和与岗位的匹配度。通常控制在 1-2 分钟内比较合适。一个好的自我介绍应该包含这几点要素: + +1. 用简单的话说清楚自己主要的技术栈于擅长的领域,例如 Java 后端开发、分布式系统开发; +2. 把重点放在自己的优势上,重点突出自己的能力,最好能用一个简短的例子支撑,例如:我比较擅长定位和解决复杂问题。在[某项目/实习]中,我曾通过[简述方法,如日志分析、源码追踪、压力测试]成功解决了[某个具体问题,如一个棘手的性能瓶颈/一个偶现的 Bug],将[某个指标]提升了[百分比/具体数值]。 +3. 简要提及 1-2 个最能体现你能力和与岗位要求匹配的项目经历、实习经历或竞赛成绩。不需要展开细节,目的是引出面试官后续的提问。 +4. 如果时间允许,可以非常简短地表达对所申请岗位的兴趣和对公司的向往,表明你是有备而来。 + +### 讲一下实习经历以及遇到的难点 + +实习经历的描述一定要避免空谈,尽量列举出你在实习期间取得的成就和具体贡献,使用具体的数据和指标来量化你的工作成果。 + +示例(这里假设项目细节放在实习经历这里介绍,你也可以选择将实习经历参与的项目放到项目经历中): + +1. 参与项目订单模块的开发,负责订单创建、删除、查询等功能。 +2. 排查并解决扣费模块由于扣费父任务和反作弊子任务使用同一个线程池导致的死锁问题。 +3. 使用 CompletableFuture 并行加载后台用户统计模块的数据信息,平均相应时间从 3.5s 降低到 1s。 +4. 使用 Redis+Caffeine 多级缓存优化热门数据(如首页、热门商品)的访问,解决了缓存击穿和穿透问题,查询速度毫秒级,QPS 30w+。 +5. 在实习期间,共完成了 10 个需求开发和 5 个问题修复,编写了 2000 行代码和 100 个测试用例,通过了代码评审和测试验收,上线运行稳定无故障。 + +关于实习经历这块再多提一点。很多同学实习期间可能接触不到什么实际的开发任务,大部分时间可能都是在熟悉和维护项目。对于这种情况,你可以适当润色这段实习经历,找一些简单的功能研究透,包装成自己做的,很多同学都是这么做的。不过,我更建议你在实习期间主动去承担一些开发任务,甚至说对原系统进行优化改造。常见的性能优化方向实践(涉及到多线程、JVM、数据库/缓存、数据结构优化这 4 个常见的性能优化方向)总结请看:https://t.zsxq.com/0c1uS7q2Y (这块内容分享在 [知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html) 里了,你也可以自己按照我的思路总结,效果是一样的)。 + +![](https://mmbiz.qpic.cn/mmbiz_png/iaIdQfEric9TxXGicjSaF6UyjV4csrgaupfKjoAicvzudEdsneGxSVXKpZWHJ89sEcABibf318JJb1qyhu8joLibzicAg/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +### 说一下自己以后的发展发向 + +> [工作五年之后,对技术和业务的思考](https://javaguide.cn/high-quality-technical-articles/advanced-programmer/thinking-about-technology-and-business-after-five-years-of-work.html) 这篇文章是我在两年前看到的一篇对我触动比较深的文章,介绍了作者工作五年之后,对于技术和业务的深度思考。 + +建议: + +- 如果你的想法是干个两三年就跳槽或者换行业的话,尽量不要直说,一定要体现出自己的稳定性。 +- 绝大部分人的职业目标都可以从技术精进、项目管理和个人影响力三个方面来回答。 + +参考回答: + +在接下来的五年里,我的职业目标主要集中在技术精进、项目管理和个人影响力三个方面。 + +首先,技术上,我会深入专研 Java 后端开发,争取早日成为 Java 后端开发领域的技术专家。为此,我将不断深入学习 Java 的核心技术和最新技术进展。 + +其次,项目管理上,我会慢慢尝试着在工作中承担更多的项目管理职责,积累项目管理经验,争取早日能够拥有独立带领中小型项目的能力。 + +最后,个人影响力上,我希望通过我的专业技能对公司的核心产品做出重大贡献,解决技术难题,提升产品性能和用户体验。同时,我也计划积极参与贡献开源项目和技术社区。 + +## 笔试题 + +笔试的形式是给你的邮箱发个链接,点进去就是一个在线的编辑器。 + +### 写三种单例模式的实现方式 + +**1、枚举(推荐)**: + +```java +public enum Singleton { + INSTANCE; + public void doSomething(String str) { + System.out.println(str); + } +} +``` + +《Effective Java》作者推荐的一种单例实现方式,简单高效,无需加锁,线程安全,可以避免通过反射破坏枚举单例。 + +**2、静态内部类(推荐)**: + +```java +public class Singleton { + // 私有化构造方法 + private Singleton() { + } + + // 对外提供获取实例的公共方法 + public static Singleton getInstance() { + return SingletonInner.INSTANCE; + } + + // 定义静态内部类 + private static class SingletonInner{ + private final static Singleton INSTANCE = new Singleton(); + } + +} +``` + +当外部类 `Singleton` 被加载的时候,并不会创建静态内部类 `SingletonInner` 的实例对象。只有当调用 `getInstance()` 方法时,`SingletonInner` 才会被加载,这个时候才会创建单例对象 `INSTANCE`。`INSTANCE` 的唯一性、创建过程的线程安全性,都由 JVM 来保证。 + +这种方式同样简单高效,无需加锁,线程安全,并且支持延时加载。 + +**3、双重校验锁**: + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + // 私有化构造方法 + private Singleton() { + } + + public static Singleton getUniqueInstance() { + //先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + //类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: + +1. 为 `uniqueInstance` 分配内存空间 +2. 初始化 `uniqueInstance` +3. 将 `uniqueInstance` 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 + +这种方式实现起来较麻烦,但同样线程安全,支持延时加载。 + +推荐阅读:[Java 并发常见面试题总结(中)](https://javaguide.cn/java/concurrent/java-concurrent-questions-02.html)。 + +### 编号为 1-n 的循环报 1-3,报道 3 的出列,求最后一人的编号 + +问题描述:编号为 1-n 的循环报 1-3,报道 3 的出列,求最后一人的编号 + +标准的约瑟夫环问题。有 n 个人围成一个圈,从某个人开始报数,报到某个特定数字(本题中为 3 )时该人出圈,直到只剩下一个人为止。 + +解决约瑟夫环问题,可以分两种情况: + +1. 我们要求出最后留下的那个人的编号(本题要求)。 +2. 求全过程,即要算出每轮出局的人。 + +有多种方法可以解决约瑟夫环问题,其中一种是使用递归的方式。 + +本题的约瑟夫环问题的公式为: **(f(n - 1, k) + k - 1) % n + 1** 。f(n,k) 表示 n 个人报数,每次报数报到 k 的人出局,最终最后一个人的编号。 + +假设 n 为 10,k 为 3 ,逆推过程如下: + +- f(1, 3) = 1(当 n = 1 时,只有一个人,最后一人的编号就为 1); +- f(2,3) =(f(1,3) + 3 -1)%2 + 1 = 3%2 + 1 = 2(当 n = 2 时,最后一人的编号为 2); +- f(3,3) = (f(2,3) + 3 - 1))%3 + 1 = 4%3 + 1 = 2(当 n = 3 时,最后一人的编号为 2); +- f(4,3) = (f(3,3) + 3 - 1) % 4 + 1 = 4%4 + 1 = 1(当 n = 4 时,最后一人的编号为 1); +- ... +- f(10,3) = 3 (当 n = 10 时,最后一人的编号为 4); + +这个问题对应[剑指 Offer 62. 圆圈中最后剩下的数字](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/) ,两者意思是类似的,比较简单。 + +```java +public class Josephus { + + // 定义递归函数 + public static int f(int n, int k) { + // 如果只有一个人,则返回 1 + if (n == 1) { + return 1; + } + return (f(n - 1, k) + k - 1) % n + 1; + } + + public static void main(String[] args) { + int n = 10; + int k = 3; + System.out.println("最后留下的那个人的编号是:" + f(n, k)); + } +} +``` + +输出: + +```plain +最后留下的那个人的编号是:4 +``` + +### 写两个线程打印 1-n,一个线程打印奇数,一个线程打印偶数 + +问题描述:写两个线程打印 1-100,一个线程打印奇数,一个线程打印偶数。 + +这道题的实现方式还是挺多的,线程的等待/通知机制(`wait()`和`notify()`)、信号量 `Semaphore`等都可以实现。 + +#### synchronized+wait/notify 实现 + +我们先定义一个类 `ParityPrinter` 用于打印奇数和偶数。 + +```java +public class ParityPrinter { + private final int max; + // 从1开始计数 + private int count = 1; + private final Object lock = new Object(); + + public ParityPrinter(int max) { + this.max = max; + } + + public void printOdd() { + print(true); + } + + public void printEven() { + print(false); + } + + private void print(boolean isOdd) { + for (int i = 1; i <= max; i += 2) { + // 确保同一时间只有一个线程可以执行内部代码块 + synchronized (lock) { + // 等待直到轮到当前线程打印 + // count为奇数时奇数线程打印,count为偶数时偶数线程打印 + while (isOdd == (count % 2 == 0)) { + try { + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + System.out.println(Thread.currentThread().getName() + " : " + count++); + // 通知等待的线程 + lock.notify(); + } + } + } +} +``` + +`ParityPrinter`类中的变量和方法介绍: + +- `max`: 最大打印数值,由构造函数传入。 +- `count`: 从 1 开始的计数器,用于追踪当前打印到的数字。 +- `lock`: 一个对象锁,用于线程间的同步控制。 +- `printOdd()`和`printEven()`: 分别启动打印奇数和偶数的逻辑,实际上调用了私有的`print()`方法,并传入线程名称前缀和一个布尔值表示打印奇数(`true`)还是偶数(`false`)。 + +接着,我们创建两个线程,一个负责打印奇数,一个负责打印偶数。 + +```java + // 打印 1-100 + ParityPrinter printer = new ParityPrinter(100); + // 创建打印奇数和偶数的线程 + Thread t1 = new Thread(printer::printOdd, "Odd"); + Thread t2 = new Thread(printer::printEven, "Even"); + t1.start(); + t2.start(); +``` + +输出: + +```plain +Odd : 1 +Even : 2 +Odd : 3 +Even : 4 +Odd : 5 +... +Odd : 95 +Even : 96 +Odd : 97 +Even : 98 +Odd : 99 +Even : 100 +``` + +#### Semaphore 实现 + +如果想要把上面的代码修改为基于 `Semaphore`实现也挺简单的。 + +```java +public class ParityPrinter { + private final int max; + private int count = 1; + // 初始为1,奇数线程先获取 + private final Semaphore oddSemaphore = new Semaphore(1); + // 初始为0,偶数线程等待 + private final Semaphore evenSemaphore = new Semaphore(0); + + public ParityPrinter(int max) { + this.max = max; + } + + public void printOdd() { + print(oddSemaphore, evenSemaphore); + } + + public void printEven() { + print(evenSemaphore, oddSemaphore); + } + + private void print(Semaphore currentSemaphore, Semaphore nextSemaphore) { + for (int i = 1; i <= max; i += 2) { + try { + // 获取当前信号量 + currentSemaphore.acquire(); + System.out.println(Thread.currentThread().getName() + " : " + count++); + // 释放下一个信号量 + nextSemaphore.release(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } +} +``` + +可以看到,我们这里使用两个信号量 `oddSemaphore` 和 `evenSemaphore` 来确保两个线程交替执行。`oddSemaphore` 信号量先获取,也就是先执行奇数输出。一个线程执行完之后,就释放下一个信号量。 + +## 技术问题 + +### GET 和 POST 的区别 + +这个问题在知乎上被讨论的挺火热的,地址: 。 + +![](https://static001.geekbang.org/infoq/04/0454a5fff1437c32754f1dfcc3881148.png) + +GET 和 POST 是 HTTP 协议中两种常用的请求方法,它们在不同的场景和目的下有不同的特点和用法。一般来说,可以从以下几个方面来区分它们(重点搞清两者在语义上的区别即可): + +- 语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。 +- 幂等:GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。 +- 格式:GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。不过,实际上 GET 请求也可以用 body 传输数据,只是并不推荐这样做,因为这样可能会导致一些兼容性或者语义上的问题。 +- 缓存:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。 +- 安全性:GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中。 + +再次提示,重点搞清两者在语义上的区别即可,实际使用过程中,也是通过语义来区分使用 GET 还是 POST。不过,也有一些项目所有的请求都用 POST,这个并不是固定的,项目组达成共识即可。 + +### 如何优化 MySQL 查询 + +回答这个问题的核心是先提到开启慢查询日志和使用 EXPLAIN 进行执行计划分析。 + +慢查询日志捕获那些执行时间超过阈值的SQL语句,这是发现问题的起点。拿到慢SQL后,用 `EXPLAIN` 关键字分析这条SQL的执行计划,分析原因。 + +基于 `EXPLAIN` 的分析结果,进行针对性优化。比较常见的 SQL优化手段如下: + +1. 索引优化(最常用) +2. 避免 `SELECT *` +3. 深度分页优化 +4. 尽量避免多表做 join +5. 选择合适的字段类型 +6. ...... + +[《Java 面试指北》](https://mp.weixin.qq.com/s/JNJIKnUMc0MU_i2VNXb50A)的技术面试题篇总结了常见的高并发面试问题,其中包含常见的 SQL 优化手段,内容非常全面。 + +![img](https://oss.javaguide.cn/javamianshizhibei/sql-optimization.png) + +推荐顺带看看下面这两篇文章: + +- [MySQL 高性能优化规范建议总结](https://javaguide.cn/database/mysql/mysql-high-performance-optimization-specification-recommendations.html) +- [MySQL 执行计划分析](https://javaguide.cn/database/mysql/mysql-query-execution-plan.html) + +### 反射及应用场景 + +简单来说,Java 反射 (Reflection) 是一种**在程序运行时,动态地获取类的信息并操作类或对象(方法、属性)的能力**。 + +通常情况下,我们写的代码在编译时类型就已经确定了,要调用哪个方法、访问哪个字段都是明确的。但反射允许我们在**运行时**才去探知一个类有哪些方法、哪些属性、它的构造函数是怎样的,甚至可以动态地创建对象、调用方法或修改属性,哪怕这些方法或属性是私有的。 + +正是这种在运行时"反观自身"并进行操作的能力,使得反射成为许多**通用框架和库的基石**。它让代码更加灵活,能够处理在编译时未知的类型。 + +我们平时写业务代码可能很少直接跟 Java 的反射(Reflection)打交道。但你可能没意识到,你天天都在享受反射带来的便利!**很多流行的框架,比如 Spring/Spring Boot、MyBatis 等,底层都大量运用了反射机制**,这才让它们能够那么灵活和强大。 + +下面简单列举几个最场景的场景帮助大家理解。 + +**1.依赖注入与控制反转(IoC)** + +以 Spring/Spring Boot 为代表的 IoC 框架,会在启动时扫描带有特定注解(如 `@Component`, `@Service`, `@Repository`, `@Controller`)的类,利用反射实例化对象(Bean),并通过反射注入依赖(如 `@Autowired`、构造器注入等)。 + +**2.注解处理** + +注解本身只是个"标记",得有人去读这个标记才知道要做什么。反射就是那个"读取器"。框架通过反射检查类、方法、字段上有没有特定的注解,然后根据注解信息执行相应的逻辑。比如,看到 `@Value`,就用反射读取注解内容,去配置文件找对应的值,再用反射把值设置给字段。 + +**3.动态代理与 AOP** + +想在调用某个方法前后自动加点料(比如打日志、开事务、做权限检查)?AOP(面向切面编程)就是干这个的,而动态代理是实现 AOP 的常用手段。JDK 自带的动态代理(Proxy 和 InvocationHandler)就离不开反射。代理对象在内部调用真实对象的方法时,就是通过反射的 `Method.invoke` 来完成的。 + +```java +public class DebugInvocationHandler implements InvocationHandler { + private final Object target; // 真实对象 + + public DebugInvocationHandler(Object target) { this.target = target; } + + // proxy: 代理对象, method: 被调用的方法, args: 方法参数 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("切面逻辑:调用方法 " + method.getName() + " 之前"); + // 通过反射调用真实对象的同名方法 + Object result = method.invoke(target, args); + System.out.println("切面逻辑:调用方法 " + method.getName() + " 之后"); + return result; + } +} +``` + +**4.对象关系映射(ORM)** + +像 MyBatis、Hibernate 这种框架,能帮你把数据库查出来的一行行数据,自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的?还是靠反射。它通过反射获取 Java 类的属性列表,然后把查询结果按名字或配置对应起来,再用反射调用 setter 或直接修改字段值。反过来,保存对象到数据库时,也是用反射读取属性值来拼 SQL。 \ No newline at end of file diff --git a/docs/system-design/spring.md b/docs/system-design/spring.md new file mode 100644 index 0000000..9b79d8c --- /dev/null +++ b/docs/system-design/spring.md @@ -0,0 +1,1242 @@ +--- +title: Spring和Spring Boot常见面试题总结 +category: 系统设计 +tag: + - 常见框架 +head: + - - meta + - name: keywords + content: Spring面试题,Spring Boot面试题,Spring IoC,依赖注入,Spring AOP,Spring MVC 工作原理,Spring 事务传播行为,Spring 循环依赖,Spring Bean 生命周期,Spring Security 权限控制,Java 面试指南,JavaGuide + - - meta + - name: description + content: 本文系统梳理 Spring 和 Spring Boot 常见面试题与高频知识点,包括 IoC 与依赖注入原理、Bean 的作用域与生命周期、AOP 核心概念及通知类型、Spring MVC 核心组件和请求处理流程、事务传播行为与隔离级别、循环依赖及三级缓存、@Transactional 回滚规则、Spring Security 权限控制与密码加密、Spring Boot 自动配置和配置文件加载优先级等,适合作为 Java 后端面试突击与复习笔记。 +--- + +------ + +![面试突击-Spring](https://oss.javaguide.cn/github/javaguide-interview/cover/spring.png) + +------ + +## 前言 + +由于很多读者都有突击面试的需求,所以我在几年前就弄了 **JavaGuide 面试突击版本**(JavaGuide 内容精简版,只保留重点),并持续完善跟进。对于喜欢纸质阅读的朋友来说,也可以打印出来,整体阅读体验非常高! + +除了只保留最常问的面试题之外,我还进一步对重点中的重点进行了⭐️标注。并且,有亮色(白天)和暗色(夜间)两个主题选择,需要打印出来的朋友记得选择亮色版本。 + +对于时间比较充裕的朋友,我个人还是更推荐 [JavaGuide](https://javaguide.cn/) 网站系统学习,内容更全面,更深入。 + +JavaGuide 已经持续维护 6 年多了,累计提交了接近 **6000** commit ,共有 **570+** 多位贡献者共同参与维护和完善。用心做原创优质内容,如果觉得有帮助的话,欢迎点赞分享!传送门:[GitHub](https://github.com/Snailclimb/JavaGuide) | [Gitee](https://gitee.com/SnailClimb/JavaGuide)。 + +对于需要更进一步面试辅导服务的读者,欢迎加入 **[JavaGuide 官方知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)**(技术专栏/一对一提问/简历修改/求职指南/面试打卡),绝对物超所值! + +面试突击最新版本可以在我的公众号回复"**PDF**"获取([JavaGuide 官方知识星球](https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html)会提前同步最新版,针对球友的一个小福利)。 + +![JavaGuide 官方公众号](https://oss.javaguide.cn/github/javaguide/gongzhonghaoxuanchuan.png) + +这部分内容摘自 [JavaGuide](https://javaguide.cn/) 下面几篇文章中的重点: + +- [Spring 常见面试题总结](https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html)(Spring 基础、IoC、AOP、MVC、事务、循环依赖等) +- [SpringBoot 常见面试题总结](https://javaguide.cn/system-design/framework/spring/springboot-knowledge-and-questions-summary.html) +- [Spring&SpringBoot常用注解总结](https://javaguide.cn/system-design/framework/spring/spring-common-annotations.html) +- [IoC & AOP详解(快速搞懂)](https://javaguide.cn/system-design/framework/spring/ioc-and-aop.html) +- [Spring 事务详解](https://javaguide.cn/system-design/framework/spring/spring-transaction.html) +- [Spring 中的设计模式详解](https://javaguide.cn/system-design/framework/spring/spring-design-patterns-summary.html) +- [SpringBoot 自动装配原理详解](https://javaguide.cn/system-design/framework/spring/spring-boot-auto-assembly-principles.html) +- [Async 注解原理分析](https://javaguide.cn/system-design/framework/spring/async.html) + +## Spring IoC + +### ⭐️什么是 IoC? + +IoC (Inversion of Control )即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是:Java 开发领域对象的创建以及管理的问题。 + +例如:现有类 A 依赖于类 B + +- **传统的开发方式** :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来 +- **使用 IoC 思想的开发方式** :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面去取即可。 + +从以上两种开发方式的对比来看:我们 "丧失了一个权力" (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情) + +**为什么叫控制反转?** + +- **控制** :指的是对象创建(实例化、管理)的权力 +- **反转** :控制权交给外部环境(IoC 容器) + +![IoC 图解](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration.png) + +### ⭐️IoC 解决了什么问题? + +IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢? + +1. 对象之间的耦合度或者说依赖程度降低; +2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。 + +例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发 + +在没有使用 IoC 思想的情况下,Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在`UserServiceImpl` 中手动 new 出 `IUserDao` 的具体实现类 `UserDaoImpl`(不能直接 new 接口类)。 + +很完美,这种方式也是可以实现的,但是我们想象一下如下场景: + +开发过程中突然接到一个新的需求,针对`IUserDao` 接口开发出另一个具体实现类。因为 Server 层依赖了`IUserDao`的具体实现,所以我们需要修改`UserServiceImpl`中 new 的对象。如果只有一个类引用了`IUserDao`的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了`IUserDao`的具体实现的话,一旦需要更换`IUserDao` 的实现方式,那修改起来将会非常的头疼。 + +![IoC&Aop-ioc-illustration-dao-service](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration-dao-service.png) + +使用 IoC 的思想,我们将对象的控制权(创建、管理)交由 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 "要" 就可以了 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/IoC&Aop-ioc-illustration-dao.png) + +### 什么是 Spring Bean? + +简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。 + +我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。 + +```xml + + + + +``` + +下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/062b422bd7ac4d53afd28fb74b2bc94d.png) + +`org.springframework.beans`和 `org.springframework.context` 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看 + +### 将一个类声明为 Bean 的注解有哪些? + +- `@Component`:通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 +- `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 +- `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 +- `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 `Service` 层返回数据给前端页面。 + +### @Component 和 @Bean 的区别是什么? + +- `@Component` 注解作用于类,而`@Bean`注解作用于方法。 +- `@Component`通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。`@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 bean,`@Bean`告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。 +- `@Bean` 注解比 `@Component` 注解的自定义性更强,而且很多地方我们只能通过 `@Bean` 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean`来实现。 + +`@Bean`注解使用示例: + +```java +@Configuration +public class AppConfig { + @Bean + public TransferService transferService() { + return new TransferServiceImpl(); + } + +} +``` + +上面的代码相当于下面的 xml 配置 + +```xml + + + +``` + +下面这个例子是通过 `@Component` 无法实现的。 + +```java +@Bean +public OneService getService(status) { + case (status) { + when 1: + return new serviceImpl1(); + when 2: + return new serviceImpl2(); + when 3: + return new serviceImpl3(); + } +} +``` + +### 注入 Bean 的注解有哪些? + +Spring 内置的 `@Autowired` 以及 JDK 内置的 `@Resource` 和 `@Inject` 都可以用于注入 Bean。 + +| Annotation | Package | Source | +| ------------ | ---------------------------------- | ------------ | +| `@Autowired` | `org.springframework.bean.factory` | Spring 2.5+ | +| `@Resource` | `javax.annotation` | Java JSR-250 | +| `@Inject` | `javax.inject` | Java JSR-330 | + +`@Autowired` 和`@Resource`使用的比较多一些。 + +### ⭐️@Autowired 和 @Resource 的区别是什么? + +`@Autowired` 是 Spring 内置的注解,默认注入逻辑为**先按类型(byType)匹配,若存在多个同类型 Bean,则再尝试按名称(byName)筛选**。 + +具体来说: + +1. 优先根据接口 / 类的类型在 Spring 容器中查找匹配的 Bean。若只找到一个符合类型的 Bean,直接注入,无需考虑名称; +2. 若找到多个同类型的 Bean(例如一个接口有多个实现类),则会尝试通过**属性名或参数名**与 Bean 的名称进行匹配(默认 Bean 名称为类名首字母小写,除非通过 `@Bean(name = "...")` 或 `@Component("...")` 显式指定)。 + +当一个接口存在多个实现类时: + +- 若属性名与某个 Bean 的名称一致,则注入该 Bean; +- 若属性名与所有 Bean 名称都不匹配,会抛出 `NoUniqueBeanDefinitionException`,此时需要通过 `@Qualifier` 显式指定要注入的 Bean 名称。 + +举例说明: + +```java +// SmsService 接口有两个实现类:SmsServiceImpl1、SmsServiceImpl2(均被 Spring 管理) + +// 报错:byType 匹配到多个 Bean,且属性名 "smsService" 与两个实现类的默认名称(smsServiceImpl1、smsServiceImpl2)都不匹配 +@Autowired +private SmsService smsService; + +// 正确:属性名 "smsServiceImpl1" 与实现类 SmsServiceImpl1 的默认名称匹配 +@Autowired +private SmsService smsServiceImpl1; + +// 正确:通过 @Qualifier 显式指定 Bean 名称 "smsServiceImpl1" +@Autowired +@Qualifier(value = "smsServiceImpl1") +private SmsService smsService; +``` + +实际开发实践中,我们还是建议通过 `@Qualifier` 注解来显式指定名称而不是依赖变量的名称。 + +`@Resource`属于 JDK 提供的注解,默认注入逻辑为**先按名称(byName)匹配,若存在多个同类型 Bean,则再尝试按类型(byType)筛选**。 + +`@Resource` 有两个比较重要且日常开发常用的属性:`name`(名称)、`type`(类型)。 + +```java +public @interface Resource { + String name() default ""; + Class type() default Object.class; +} +``` + +如果仅指定 `name` 属性则注入方式为`byName`,如果仅指定`type`属性则注入方式为`byType`,如果同时指定`name` 和`type`属性(不建议这么做)则注入方式为`byType`+`byName`。 + +```java +// 报错,byName 和 byType 都无法匹配到 bean +@Resource +private SmsService smsService; +// 正确注入 SmsServiceImpl1 对象对应的 bean +@Resource +private SmsService smsServiceImpl1; +// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式) +@Resource(name = "smsServiceImpl1") +private SmsService smsService; +``` + +**简单总结一下**: + +- `@Autowired` 是 Spring 提供的注解,`@Resource` 是 JDK 提供的注解。 +- `Autowired` 默认的注入方式为`byType`(根据类型进行匹配),`@Resource`默认注入方式为 `byName`(根据名称进行匹配)。 +- 当一个接口存在多个实现类的情况下,`@Autowired` 和`@Resource`都需要通过名称才能正确匹配到对应的 Bean。`Autowired` 可以通过 `@Qualifier` 注解来显式指定名称,`@Resource`可以通过 `name` 属性来显式指定名称。 +- `@Autowired` 支持在构造函数、方法、字段和参数上使用。`@Resource` 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。 + +考虑到 `@Resource` 的语义更清晰(名称优先),并且是 Java 标准,能减少对 Spring 框架的强耦合,我们通常**更推荐使用 `@Resource`**,尤其是在需要按名称注入的场景下。而 `@Autowired` 配合构造器注入,在实现依赖注入的不可变性和强制性方面有优势,也是一种非常好的实践。 + +### 注入 Bean 的方式有哪些? + +依赖注入 (Dependency Injection, DI) 的常见方式: + +1. 构造函数注入:通过类的构造函数来注入依赖项。 +1. Setter 注入:通过类的 Setter 方法来注入依赖项。 +1. Field(字段) 注入:直接在类的字段上使用注解(如 `@Autowired` 或 `@Resource`)来注入依赖项。 + +构造函数注入示例: + +```java +@Service +public class UserService { + + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + //... +} +``` + +Setter 注入示例: + +```java +@Service +public class UserService { + + private UserRepository userRepository; + + // 在 Spring 4.3 及以后的版本,特定情况下 @Autowired 可以省略不写 + @Autowired + public void setUserRepository(UserRepository userRepository) { + this.userRepository = userRepository; + } + + //... +} +``` + +Field 注入示例: + +```java +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + //... +} +``` + +### ⭐️构造函数注入还是 Setter 注入? + +Spring 官方有对这个问题的回答:。 + +我这里主要提取总结完善一下 Spring 官方的建议。 + +**Spring 官方推荐构造函数注入**,这种注入方式的优势如下: + +1. 依赖完整性:确保所有必需依赖在对象创建时就被注入,避免了空指针异常的风险。 +2. 不可变性:有助于创建不可变对象,提高了线程安全性。 +3. 初始化保证:组件在使用前已完全初始化,减少了潜在的错误。 +4. 测试便利性:在单元测试中,可以直接通过构造函数传入模拟的依赖项,而不必依赖 Spring 容器进行注入。 + +构造函数注入适合处理**必需的依赖项**,而 **Setter 注入** 则更适合**可选的依赖项**,这些依赖项可以有默认值或在对象生命周期中动态设置。虽然 `@Autowired` 可以用于 Setter 方法来处理必需的依赖项,但构造函数注入仍然是更好的选择。 + +在某些情况下(例如第三方类不提供 Setter 方法),构造函数注入可能是**唯一的选择**。 + +### ⭐️Bean 的作用域有哪些? + +Spring 中 Bean 的作用域通常有下面几种: + +- **singleton** : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。 +- **prototype** : 每次获取都会创建一个新的 bean 实例。也就是说,连续 `getBean()` 两次,得到的是不同的 Bean 实例。 +- **request** (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。 +- **session** (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。 +- **application/global-session** (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。 +- **websocket** (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。 + +**如何配置 bean 的作用域呢?** + +xml 方式: + +```xml + +``` + +注解方式: + +```java +@Bean +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public Person personPrototype() { + return new Person(); +} +``` + +### ⭐️Bean 是线程安全的吗? + +Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。 + +我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。 + +prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。 + +有状态 Bean 示例: + +```java +// 定义了一个购物车类,其中包含一个保存用户的购物车里商品的 List +@Component +public class ShoppingCart { + private List items = new ArrayList(); + + public void addItem(String item) { + items.add(item); + } + + public List getItems() { + return items; + } +} +``` + +不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。 + +无状态 Bean 示例: + +```java +// 定义了一个用户服务,它仅包含业务逻辑而不保存任何状态。 +@Component +public class UserService { + + public User findUserById(Long id) { + //... + } + //... +} +``` + +对于有状态单例 Bean 的线程安全问题,常见的三种解决办法是: + +1. **避免可变成员变量**: 尽量设计 Bean 为无状态。 +2. **使用`ThreadLocal`**: 将可变成员变量保存在 `ThreadLocal` 中,确保线程独立。 +3. **使用同步机制**: 利用 `synchronized` 或 `ReentrantLock` 来进行同步控制,确保线程安全。 + +这里以 `ThreadLocal`为例,演示一下`ThreadLocal` 保存用户登录信息的场景: + +```java +public class UserThreadLocal { + + private UserThreadLocal() {} + + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(() -> null); + + public static void put(SysUser sysUser) { + LOCAL.set(sysUser); + } + + public static SysUser get() { + return LOCAL.get(); + } + + public static void remove() { + LOCAL.remove(); + } +} +``` + +### ⭐️Bean 的生命周期了解么? + +1. **创建 Bean 的实例**:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。 +2. **Bean 属性赋值/填充**:为 Bean 设置相关属性和依赖,例如`@Autowired` 等注解注入的对象、`@Value` 注入的值、`setter`方法或构造函数注入依赖和值、`@Resource`注入的各种资源。 +3. **Bean 初始化**: + - 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入 Bean 的名字。 + - 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 + - 如果 Bean 实现了 `BeanFactoryAware` 接口,调用 `setBeanFactory()`方法,传入 `BeanFactory`对象的实例。 + - 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 + - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 + - 如果 Bean 实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 + - 如果 Bean 在配置文件中的定义包含 `init-method` 属性,执行指定的方法。 + - 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法。 +4. **销毁 Bean**:销毁并不是说要立马把 Bean 给销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。 + - 如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 + - 如果 Bean 在配置文件中的定义包含 `destroy-method` 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过`@PreDestroy` 注解标记 Bean 销毁之前执行的方法。 + +`AbstractAutowireCapableBeanFactory` 的 `doCreateBean()` 方法中能看到依次执行了这 4 个阶段: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // 1. 创建 Bean 的实例 + BeanWrapper instanceWrapper = null; + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + Object exposedObject = bean; + try { + // 2. Bean 属性赋值/填充 + populateBean(beanName, mbd, instanceWrapper); + // 3. Bean 初始化 + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + + // 4. 销毁 Bean-注册回调接口 + try { + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + + return exposedObject; +} +``` + +`Aware` 接口能让 Bean 能拿到 Spring 容器资源。 + +Spring 中提供的 `Aware` 接口主要有: + +1. `BeanNameAware`:注入当前 bean 对应 beanName; +2. `BeanClassLoaderAware`:注入加载当前 bean 的 ClassLoader; +3. `BeanFactoryAware`:注入当前 `BeanFactory` 容器的引用。 + +`BeanPostProcessor` 接口是 Spring 为修改 Bean 提供的强大扩展点。 + +```java +public interface BeanPostProcessor { + + // 初始化前置处理 + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + // 初始化后置处理 + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + +} +``` + +- `postProcessBeforeInitialization`:Bean 实例化、属性注入完成后,`InitializingBean#afterPropertiesSet`方法以及自定义的 `init-method` 方法之前执行; +- `postProcessAfterInitialization`:类似于上面,不过是在 `InitializingBean#afterPropertiesSet`方法以及自定义的 `init-method` 方法之后执行。 + +`InitializingBean` 和 `init-method` 是 Spring 为 Bean 初始化提供的扩展点。 + +```java +public interface InitializingBean { + // 初始化逻辑 + void afterPropertiesSet() throws Exception; +} +``` + +指定 `init-method` 方法,指定初始化方法: + +```xml + + + + + + +``` + +**如何记忆呢?** + +1. 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。 +2. 初始化这一步涉及到的步骤比较多,包含 `Aware` 接口的依赖注入、`BeanPostProcessor` 在初始化前后的处理以及 `InitializingBean` 和 `init-method` 的初始化操作。 +3. 销毁这一步会注册相关销毁回调接口,最后通过`DisposableBean` 和 `destory-method` 进行销毁。 + +最后,再分享一张清晰的图解(图源:[如何记忆 Spring Bean 的生命周期](https://chaycao.github.io/2020/02/15/如何记忆Spring-Bean的生命周期.html))。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/spring-bean-lifestyle.png) + +## Spring AOP + +### ⭐️谈谈自己对于 AOP 的了解 + +AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 + +Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 **JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示: + +![SpringAOPProcess](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/230ae587a322d6e4d09510161987d346.jpeg) + +当然你也可以使用 **AspectJ** !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。 + +AOP 切面编程涉及到的一些专业术语: + +| 术语 | 含义 | +| :---------------- | :----------------------------------------------------------: | +| 目标(Target) | 被通知的对象 | +| 代理(Proxy) | 向目标对象应用通知之后创建的代理对象 | +| 连接点(JoinPoint) | 目标对象的所属类中,定义的所有方法均为连接点 | +| 切入点(Pointcut) | 被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点) | +| 通知(Advice) | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情 | +| 切面(Aspect) | 切入点(Pointcut)+通知(Advice) | +| Weaving(织入) | 将通知应用到目标对象,进而生成代理对象的过程动作 | + +### ⭐️Spring AOP 和 AspectJ AOP 有什么区别? + +| 特性 | Spring AOP | AspectJ | +| -------------- | -------------------------------------------------------- | ------------------------------------------ | +| **增强方式** | 运行时增强(基于动态代理) | 编译时增强、类加载时增强(直接操作字节码) | +| **切入点支持** | 方法级(Spring Bean 范围内,不支持 final 和 staic 方法) | 方法级、字段、构造器、静态方法等 | +| **性能** | 运行时依赖代理,有一定开销,切面多时性能较低 | 运行时无代理开销,性能更高 | +| **复杂性** | 简单,易用,适合大多数场景 | 功能强大,但相对复杂 | +| **使用场景** | Spring 应用下比较简单的 AOP 需求 | 高性能、高复杂度的 AOP 需求 | + +**如何选择?** + +- **功能考量**:AspectJ 支持更复杂的 AOP 场景,Spring AOP 更简单易用。如果你需要增强 `final` 方法、静态方法、字段访问、构造器调用等,或者需要在非 Spring 管理的对象上应用增强逻辑,AspectJ 是唯一的选择。 +- **性能考量**:切面数量较少时两者性能差异不大,但切面较多时 AspectJ 性能更优。 + +**一句话总结**:简单场景优先使用 Spring AOP;复杂场景或高性能需求时,选择 AspectJ。 + +### ⭐️AOP 常见的通知类型有哪些? + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/aspectj-advice-types.jpg) + +- **Before**(前置通知):目标对象的方法调用之前触发 +- **After** (后置通知):目标对象的方法调用之后触发 +- **AfterReturning**(返回通知):目标对象的方法调用完成,在返回结果值之后触发 +- **AfterThrowing**(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。 +- **Around** (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法 + +### 多个切面的执行顺序如何控制? + +1、通常使用`@Order` 注解直接定义切面顺序 + +```java +// 值越小优先级越高 +@Order(3) +@Component +@Aspect +public class LoggingAspect implements Ordered { +``` + +**2、实现`Ordered` 接口重写 `getOrder` 方法。** + +```java +@Component +@Aspect +public class LoggingAspect implements Ordered { + + // .... + + @Override + public int getOrder() { + // 返回值越小优先级越高 + return 1; + } +} +``` + +## Spring MVC + +### 说说自己对于 Spring MVC 了解? + +MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。 + +![](https://oss.javaguide.cn/java-guide-blog/image-20210809181452421.png) + +网上有很多人说 MVC 不是设计模式,只是软件设计规范,我个人更倾向于 MVC 同样是众多设计模式中的一种。**[java-design-patterns](https://github.com/iluwatar/java-design-patterns)** 项目中就有关于 MVC 的相关介绍。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/159b3d3e70dd45e6afa81bf06d09264e.png) + +想要真正理解 Spring MVC,我们先来看看 Model 1 和 Model 2 这两个没有 Spring MVC 的时代。 + +**Model 1 时代** + +很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。 + +这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。 + +![mvc-mode1](https://oss.javaguide.cn/java-guide-blog/mvc-mode1.png) + +**Model 2 时代** + +学过 Servlet 并做过相关 Demo 的朋友应该了解"Java Bean(Model)+ JSP(View)+Servlet(Controller) "这种开发模式,这就是早期的 JavaWeb MVC 开发模式。 + +- Model:系统涉及的数据,也就是 dao 和 bean。 +- View:展示模型中的数据,只是用来展示。 +- Controller:接受用户请求,并将请求发送至 Model,最后返回数据给 JSP 并展示给用户 + +![](https://oss.javaguide.cn/java-guide-blog/mvc-model2.png) + +Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。 + +于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。 + +**Spring MVC 时代** + +随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。 + +MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。 + +### Spring MVC 的核心组件有哪些? + +记住了下面这些组件,也就记住了 SpringMVC 的工作原理。 + +- **`DispatcherServlet`**:**核心的中央处理器**,负责接收请求、分发,并给予客户端响应。 +- **`HandlerMapping`**:**处理器映射器**,根据 URL 去匹配查找能处理的 `Handler` ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 +- **`HandlerAdapter`**:**处理器适配器**,根据 `HandlerMapping` 找到的 `Handler` ,适配执行对应的 `Handler`; +- **`Handler`**:**请求处理器**,处理实际请求的处理器。 +- **`ViewResolver`**:**视图解析器**,根据 `Handler` 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 `DispatcherServlet` 响应客户端 + +### ⭐️SpringMVC 工作原理了解吗? + +**Spring MVC 原理如下图所示:** + +> SpringMVC 工作原理的图解我没有自己画,直接图省事在网上找了一个非常清晰直观的,原出处不明。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/de6d2b213f112297298f3e223bf08f28.png) + +**流程说明(重要):** + +1. 客户端(浏览器)发送请求, `DispatcherServlet`拦截请求。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping` 。`HandlerMapping` 根据 URL 去匹配查找能处理的 `Handler`(也就是我们平常说的 `Controller` 控制器) ,并会将请求涉及到的拦截器和 `Handler` 一起封装。 +3. `DispatcherServlet` 调用 `HandlerAdapter`适配器执行 `Handler` 。 +4. `Handler` 完成对用户请求的处理后,会返回一个 `ModelAndView` 对象给`DispatcherServlet`,`ModelAndView` 顾名思义,包含了数据模型以及相应的视图的信息。`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +5. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +6. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +7. 把 `View` 返回给请求者(浏览器) + +上述流程是传统开发模式(JSP,Thymeleaf 等)的工作原理。然而现在主流的开发方式是前后端分离,这种情况下 Spring MVC 的 `View` 概念发生了一些变化。由于 `View` 通常由前端框架(Vue, React 等)来处理,后端不再负责渲染页面,而是只负责提供数据,因此: + +- 前后端分离时,后端通常不再返回具体的视图,而是返回**纯数据**(通常是 JSON 格式),由前端负责渲染和展示。 +- `View` 的部分在前后端分离的场景下往往不需要设置,Spring MVC 的控制器方法只需要返回数据,不再返回 `ModelAndView`,而是直接返回数据,Spring 会自动将其转换为 JSON 格式。相应的,`ViewResolver` 也将不再被使用。 + +怎么做到呢? + +- 使用 `@RestController` 注解代替传统的 `@Controller` 注解,这样所有方法默认会返回 JSON 格式的数据,而不是试图解析视图。 +- 如果你使用的是 `@Controller`,可以结合 `@ResponseBody` 注解来返回 JSON。 + +### 统一异常处理怎么做? + +推荐使用注解的方式统一异常处理,具体会使用到 `@ControllerAdvice` + `@ExceptionHandler` 这两个注解 。 + +```java +@ControllerAdvice +@ResponseBody +public class GlobalExceptionHandler { + + @ExceptionHandler(BaseException.class) + public ResponseEntity handleAppException(BaseException ex, HttpServletRequest request) { + //...... + } + + @ExceptionHandler(value = ResourceNotFoundException.class) + public ResponseEntity handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) { + //...... + } +} +``` + +这种异常处理方式下,会给所有或者指定的 `Controller` 织入异常处理的逻辑(AOP),当 `Controller` 中的方法抛出异常的时候,由被`@ExceptionHandler` 注解修饰的方法进行处理。 + +`ExceptionHandlerMethodResolver` 中 `getMappedMethod` 方法决定了异常具体被哪个被 `@ExceptionHandler` 注解修饰的方法处理异常。 + +```java +@Nullable + private Method getMappedMethod(Class exceptionType) { + List> matches = new ArrayList(); + //找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系 + for (Class mappedException : this.mappedMethods.keySet()) { + if (mappedException.isAssignableFrom(exceptionType)) { + matches.add(mappedException); + } + } + // 不为空说明有方法处理异常 + if (!matches.isEmpty()) { + // 按照匹配程度从小到大排序 + matches.sort(new ExceptionDepthComparator(exceptionType)); + // 返回处理异常的方法 + return this.mappedMethods.get(matches.get(0)); + } + else { + return null; + } + } +``` + +从源代码看出:**`getMappedMethod()`会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。** + +## Spring 框架中用到了哪些设计模式? + +> 关于下面这些设计模式的详细介绍,可以看我写的 [Spring 中的设计模式详解](https://javaguide.cn/system-design/framework/spring/spring-design-patterns-summary.html) 这篇文章。 + +- **工厂设计模式** : Spring 使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 +- **代理设计模式** : Spring AOP 功能的实现。 +- **单例设计模式** : Spring 中的 Bean 默认都是单例的。 +- **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 +- **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 +- **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 +- **适配器模式** : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 +- ...... + +## ⭐️Spring 的循环依赖 + +### Spring 循环依赖了解吗,怎么解决? + +循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。 + +```java +@Component +public class CircularDependencyA { + @Autowired + private CircularDependencyB circB; +} + +@Component +public class CircularDependencyB { + @Autowired + private CircularDependencyA circA; +} +``` + +单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。 + +```java +@Component +public class CircularDependencyA { + @Autowired + private CircularDependencyA circA; +} +``` + +Spring 框架通过使用三级缓存来解决这个问题,确保即使在循环依赖的情况下也能正确创建 Bean。 + +Spring 中的三级缓存其实就是三个 Map,如下: + +```java +// 一级缓存 +/** Cache of singleton objects: bean name to bean instance. */ +private final Map singletonObjects = new ConcurrentHashMap(256); + +// 二级缓存 +/** Cache of early singleton objects: bean name to bean instance. */ +private final Map earlySingletonObjects = new HashMap(16); + +// 三级缓存 +/** Cache of singleton factories: bean name to ObjectFactory. */ +private final Map> singletonFactories = new HashMap(16); +``` + +简单来说,Spring 的三级缓存包括: + +1. **一级缓存(singletonObjects)**:存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为"Spring 的单例属性"而生。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。 +2. **二级缓存(earlySingletonObjects)**:存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中`ObjectFactory`产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用`ObjectFactory#getObject()`都是会产生新的代理对象的。 +3. **三级缓存(singletonFactories)**:存放`ObjectFactory`,`ObjectFactory`的`getObject()`方法(最终调用的是`getEarlyBeanReference()`方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。 + +接下来说一下 Spring 创建 Bean 的流程: + +1. 先去 **一级缓存 `singletonObjects`** 中获取,存在就返回; +2. 如果不存在或者对象正在创建中,于是去 **二级缓存 `earlySingletonObjects`** 中获取; +3. 如果还没有获取到,就去 **三级缓存 `singletonFactories`** 中获取,通过执行 `ObjectFacotry` 的 `getObject()` 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。 + +在三级缓存中存储的是 `ObjectFacoty` : + +```java +public interface ObjectFactory { + T getObject() throws BeansException; +} +``` + +Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 `addSingletonFactory` 方法,向三级缓存中添加一个 `ObjectFactory` 对象: + +```java +// AbstractAutowireCapableBeanFactory # doCreateBean # +public abstract class AbstractAutowireCapableBeanFactory ... { + protected Object doCreateBean(...) { + //... + + // 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } +} +``` + +那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 `ObjectFactory` 的 `getObject` 方法获取对象。 + +```java +class A { + // 使用了 B + private B b; +} +class B { + // 使用了 A + private A a; +} +``` + +以上面的循环依赖代码为例,整个解决循环依赖的流程如下: + +- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A; +- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 **一二级缓存** 中肯定没有 A; +- 那么此时就去三级缓存中调用 `getObject()` 方法去获取 A 的 **前期暴露的对象** ,也就是调用上边加入的 `getEarlyBeanReference()` 方法,生成一个 A 的 **前期暴露对象**; +- 然后就将这个 `ObjectFactory` 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。 + +**只用两级缓存够吗?** 在没有 AOP 的情况下,确实可以只使用一级和二级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,三级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。 + +**最后总结一下 Spring 如何解决三级缓存**: + +在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 **三级缓存 `singletonFactories`** 中拿到三级缓存中存储的 `ObjectFactory` 并调用它的 `getObject()` 方法来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了! + +不过,这种机制也有一些缺点,比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和`@Async`注解的 bean 无法支持循环依赖。 + +### @Lazy 能解决循环依赖吗? + +`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 + +Spring Boot 2.2 新增了**全局懒加载属性**,开启后全局 bean 被设置为懒加载,需要时再去创建。 + +配置文件配置全局懒加载: + +```properties +#默认false +spring.main.lazy-initialization=true +``` + +编码的方式设置全局懒加载: + +```java +SpringApplication springApplication=new SpringApplication(Start.class); +springApplication.setLazyInitialization(false); +springApplication.run(args); +``` + +如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。 + +如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。 + +循环依赖问题是如何通过`@Lazy` 解决的呢?这里举一个例子,比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加 `@Lazy` 注解之后(延迟 Bean B 的实例化),加载的流程如下: + +- 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性; +- 由于在 A 上标注了 `@Lazy` 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性; +- 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。 + +从上面的加载流程可以看出: `@Lazy` 解决循环依赖的关键点在于代理对象的使用。 + +- **没有 `@Lazy` 的情况下**:在 Spring 容器初始化 `A` 时会立即尝试创建 `B`,而在创建 `B` 的过程中又会尝试创建 `A`,最终导致循环依赖(即无限递归,最终抛出异常)。 +- **使用 `@Lazy` 的情况下**:Spring 不会立即创建 `B`,而是会注入一个 `B` 的代理对象。由于此时 `B` 仍未被真正初始化,`A` 的初始化可以顺利完成。等到 `A` 实例实际调用 `B` 的方法时,代理对象才会触发 `B` 的真正初始化。 + +`@Lazy` 能够在一定程度上打破循环依赖链,允许 Spring 容器顺利地完成 Bean 的创建和注入。但这并不是一个根本性的解决方案,尤其是在构造函数注入、复杂的多级依赖等场景中,`@Lazy` 无法有效地解决问题。因此,最佳实践仍然是尽量避免设计上的循环依赖。 + +### SpringBoot 允许循环依赖发生么? + +SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。 + +SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法: + +- 在全局配置文件中设置允许循环依赖存在:`spring.main.allow-circular-references=true`。最简单粗暴的方式,不太推荐。 +- 在导致循环依赖的 Bean 上添加 `@Lazy` 注解,这是一种比较推荐的方式。`@Lazy` 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。 +- ...... + +## ⭐️Spring 事务 + +关于 Spring 事务的详细介绍,可以看我写的 [Spring 事务详解](https://javaguide.cn/system-design/framework/spring/spring-transaction.html) 这篇文章。 + +### Spring 管理事务的方式有几种? + +- **编程式事务**:在代码中硬编码(在分布式系统中推荐使用) : 通过 `TransactionTemplate`或者 `TransactionManager` 手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。 +- **声明式事务**:在 XML 配置文件中配置或者直接基于注解(单体应用或者简单业务系统推荐使用) : 实际是通过 AOP 实现(基于`@Transactional` 的全注解方式使用最多) + +### Spring 事务中哪几种事务传播行为? + +**事务传播行为是为了解决业务层方法之间互相调用的事务问题**。 + +当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。 + +正确的事务传播行为可能的值如下: + +**1.`TransactionDefinition.PROPAGATION_REQUIRED`** + +使用的最多的一个事务传播行为,我们平时经常使用的`@Transactional`注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 + +**`2.TransactionDefinition.PROPAGATION_REQUIRES_NEW`** + +创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,`Propagation.REQUIRES_NEW`修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 + +**3.`TransactionDefinition.PROPAGATION_NESTED`** + +如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于`TransactionDefinition.PROPAGATION_REQUIRED`。 + +**4.`TransactionDefinition.PROPAGATION_MANDATORY`** + +如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性) + +这个使用的很少。 + +若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚: + +- **`TransactionDefinition.PROPAGATION_SUPPORTS`**: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 +- **`TransactionDefinition.PROPAGATION_NOT_SUPPORTED`**: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 +- **`TransactionDefinition.PROPAGATION_NEVER`**: 以非事务方式运行,如果当前存在事务,则抛出异常。 + +### Spring 事务中的隔离级别有哪几种? + +和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:`Isolation` + +```java +public enum Isolation { + + DEFAULT(TransactionDefinition.ISOLATION_DEFAULT), + READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED), + READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED), + REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ), + SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE); + + private final int value; + + Isolation(int value) { + this.value = value; + } + + public int value() { + return this.value; + } + +} +``` + +下面我依次对每一种事务隔离级别进行介绍: + +- **`TransactionDefinition.ISOLATION_DEFAULT`** :使用后端数据库默认的隔离级别,MySQL 默认采用的 `REPEATABLE_READ` 隔离级别 Oracle 默认采用的 `READ_COMMITTED` 隔离级别. +- **`TransactionDefinition.ISOLATION_READ_UNCOMMITTED`** :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读** +- **`TransactionDefinition.ISOLATION_READ_COMMITTED`** : 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生** +- **`TransactionDefinition.ISOLATION_REPEATABLE_READ`** : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。** +- **`TransactionDefinition.ISOLATION_SERIALIZABLE`** : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 + +### @Transactional(rollbackFor = Exception.class)注解了解吗? + +`Exception` 分为运行时异常 `RuntimeException` 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。 + +当 `@Transactional` 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。 + +`@Transactional` 注解默认回滚策略是只有在遇到`RuntimeException`(运行时异常) 或者 `Error` 时才会回滚事务,而不会回滚 `Checked Exception`(受检查异常)。这是因为 Spring 认为`RuntimeException`和 Error 是不可预期的错误,而受检异常是可预期的错误,可以通过业务逻辑来处理。 + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/spring-transactional-rollbackfor.png) + +如果想要修改默认的回滚策略,可以使用 `@Transactional` 注解的 `rollbackFor` 和 `noRollbackFor` 属性来指定哪些异常需要回滚,哪些异常不需要回滚。例如,如果想要让所有的异常都回滚事务,可以使用如下的注解: + +```java +@Transactional(rollbackFor = Exception.class) +public void someMethod() { +// some business logic +} +``` + +如果想要让某些特定的异常不回滚事务,可以使用如下的注解: + +```java +@Transactional(noRollbackFor = CustomException.class) +public void someMethod() { +// some business logic +} +``` + +## Spring Security + +Spring Security 重要的是实战,这里仅对小部分知识点进行总结。 + +### 有哪些控制请求访问权限的方法? + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/image-20220728201854641.png) + +- `permitAll()`:无条件允许任何形式访问,不管你登录还是没有登录。 +- `anonymous()`:允许匿名访问,也就是没有登录才可以访问。 +- `denyAll()`:无条件决绝任何形式的访问。 +- `authenticated()`:只允许已认证的用户访问。 +- `fullyAuthenticated()`:只允许已经登录或者通过 remember-me 登录的用户访问。 +- `hasRole(String)` : 只允许指定的角色访问。 +- `hasAnyRole(String)` : 指定一个或者多个角色,满足其一的用户即可访问。 +- `hasAuthority(String)`:只允许具有指定权限的用户访问 +- `hasAnyAuthority(String)`:指定一个或者多个权限,满足其一的用户即可访问。 +- `hasIpAddress(String)` : 只允许指定 ip 的用户访问。 + +### hasRole 和 hasAuthority 有区别吗? + +可以看看松哥的这篇文章:[Spring Security 中的 hasRole 和 hasAuthority 有区别吗?](https://mp.weixin.qq.com/s/GTNOa2k9_n_H0w24upClRw),介绍的比较详细。 + +### ⭐️如何对密码进行加密? + +如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。 + +Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的接口是 `PasswordEncoder` ,如果你想要自己实现一个加密算法的话,也需要实现 `PasswordEncoder` 接口。 + +`PasswordEncoder` 接口一共也就 3 个必须实现的方法。 + +```java +public interface PasswordEncoder { + // 加密也就是对原始密码进行编码 + String encode(CharSequence var1); + // 比对原始密码和数据库中保存的密码 + boolean matches(CharSequence var1, String var2); + // 判断加密密码是否需要再次进行加密,默认返回 false + default boolean upgradeEncoding(String encodedPassword) { + return false; + } +} +``` + +![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/image-20220728183540954.png) + +官方推荐使用基于 bcrypt 强哈希函数的加密算法实现类。 + +### 如何优雅更换系统使用的加密算法? + +如果我们在开发过程中,突然发现现有的加密算法无法满足我们的需求,需要更换成另外一个加密算法,这个时候应该怎么办呢? + +推荐的做法是通过 `DelegatingPasswordEncoder` 兼容多种不同的密码加密方案,以适应不同的业务需求。 + +从名字也能看出来,`DelegatingPasswordEncoder` 其实就是一个代理类,并非是一种全新的加密算法,它做的事情就是代理上面提到的加密算法实现类。在 Spring Security 5.0 之后,默认就是基于 `DelegatingPasswordEncoder` 进行密码加密的。 + +## SpringBoot + +### ⭐️Spring,Spring MVC,Spring Boot 之间什么关系? + +很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。 + +Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。 + +下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。 + +![Spring主要模块](https://oss.javaguide.cn/github/javaguide/jvme0c60b4606711fc4a0b6faf03230247a.png) + +Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。 + +![](https://oss.javaguide.cn/java-guide-blog/image-20210809181452421.png) + +使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了! + +Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。 + +Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用! + +### ⭐️Spring Boot 支持哪些内嵌 Servlet 容器?如何选择? + +Spring Boot 提供了三种内嵌 Web 容器,分别为 **Tomcat**、**Jetty** 和 **Undertow** 。 + +当你在项目中引入 `spring-boot-starter-web` 这个起步依赖时,Spring Boot 默认会包含并启用 Tomcat 作为内嵌 Servlet 容器。 + +如果你想使用 Jetty 或 Undertow,需要在构建文件(如 Maven 的 `pom.xml`或 Gradle 的 `build.gradle`)中,从 `spring-boot-starter-web` 中排除默认的 Tomcat 依赖 (`spring-boot-starter-tomcat`),添加你想使用的容器对应的 Starter 依赖(例如 `spring-boot-starter-jetty` 或 `spring-boot-starter-undertow`)。 + +```xml + + org.springframework.boot + spring-boot-starter-web + + + + spring-boot-starter-tomcat + org.springframework.boot + + + + + + + + spring-boot-starter-jetty + org.springframework.boot + +``` + +在 Spring Boot 项目中,我们可以根据具体应用场景和性能需求,灵活地选择不同的嵌入式 Servlet 容器来提供 HTTP 服务: + +1. **Tomcat**:适用于大多数常规 Web 应用程序和 RESTful 服务,易于使用和配置,但在高并发场景下确实可能不如 Undertow 表现出色。 +2. **Undertow**:Undertow 具有极低的启动时间和资源占用,支持非阻塞 IO(NIO),在高并发场景下表现出色,性能优于 Tomcat。 +3. **Jetty**:如果应用程序涉及即时通信、聊天系统或其他需要保持长连接的场景,Jetty 是一个更好的选择。它在处理长连接和 WebSocket 时表现优越。另外。Jetty 在性能和内存使用方面通常优于 Tomcat,虽然在极端高并发场景中可能略逊于 Undertow。 + +**⚠️** **注意** : + +Spring Boot 4.0 完全移除了对 Undertow 的内嵌支持——不仅删掉了 **spring-boot-starter-undertow**,也不再提供任何 Undertow 相关的自动配置。移除的根本原因是:Spring Boot 4.0 基线升级到 Servlet 6.1(也就是说必须支持 Servlet 6.1 才能留在 starter 列表里),而截至 2025-10 官方发布说明时,Undertow 尚未兼容该版本。 + +### Spring Boot 默认使用的日志框架是什么? + +Spring Boot 默认选用 SLF4J (Simple Logging Facade for Java) 作为其日志门面 (Facade) / 日志抽象层,并搭配 Logback 作为默认的具体日志实现库 (Implementation)。 + +### ⭐️Spring Boot 的自动配置是如何实现的? + +Spring Boot 的自动配置机制是通过 `@SpringBootApplication` 注解启动的,这个注解本质上是几个关键注解的组合。我们可以将 `@SpringBootApplication` 看作是 `@Configuration`、`@EnableAutoConfiguration` 和 `@ComponentScan` 注解的集合。 + +- **`@EnableAutoConfiguration`**: 启用 Spring Boot 的自动配置机制。它是自动配置的核心,允许 Spring Boot 根据项目的依赖和配置自动配置 Spring 应用的各个部分。 +- **`@ComponentScan`**: 启用组件扫描,扫描被 `@Component`(以及 `@Service`、`@Controller` 等)注解的类,并将这些类注册为 Spring 容器中的 Bean。默认情况下,它会扫描该类所在包及其子包下的所有类。 +- **`@Configuration`**: 允许在上下文中注册额外的 Bean 或导入其他配置类。它相当于一个具有 `@Bean` 方法的 Spring 配置类。 + +`@EnableAutoConfiguration`是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程): + +```java +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.Import; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import({AutoConfigurationImportSelector.class}) +public @interface EnableAutoConfiguration { + String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; + + Class[] exclude() default {}; + + String[] excludeName() default {}; +} +``` + +这个注解通过 `@Import` 导入了 `AutoConfigurationImportSelector` 类,而 `AutoConfigurationImportSelector` 是自动配置的核心类之一。`@Import` 注解的作用是将指定的配置类或 Bean 导入到当前的配置类中。 + +`AutoConfigurationImportSelector` 类的 `getCandidateConfigurations` 方法会加载所有可用的自动配置类,并将这些类的信息以 `List` 的形式返回。这些配置类会被 Spring 容器管理为 Bean,从而实现自动配置。 + +```java + protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { + List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), + getBeanClassLoader()); + Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + + "are using a custom packaging, make sure that file is correct."); + return configurations; + } +``` + +这里使用了 `SpringFactoriesLoader` 来加载位于 `META-INF/spring.factories` 文件中的自动配置类。这些配置类会根据应用的具体条件(例如类路径中的依赖)自动配置相应的组件。 + +**自动配置信息有了,那么自动配置还差什么呢?** + +`@Conditional` 注解!在自动配置类中,Spring Boot 使用了一系列条件注解(如 `@Conditional`、`@ConditionalOnClass`、`@ConditionalOnBean` 等)来判断某些配置是否应该生效。这些注解是 `@Conditional` 注解的扩展,用于在特定条件满足时才启用相应的配置。 + +例如,在 Spring Security 的自动配置中,有一个名为 `SecurityAutoConfiguration` 的自动配置类,它导入了 `WebSecurityEnablerConfiguration` 类。 + +`WebSecurityEnablerConfiguration` 类的源码如下: + +```java +@Configuration +@ConditionalOnBean(WebSecurityConfigurerAdapter.class) +@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@EnableWebSecurity +public class WebSecurityEnablerConfiguration { + +} +``` + +`WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。 + +最后,简单总结一下:Spring Boot 的自动配置机制通过 `@EnableAutoConfiguration` 启动。该注解利用 `@Import` 注解导入了 `AutoConfigurationImportSelector` 类,而 `AutoConfigurationImportSelector` 类则负责加载并管理所有的自动配置类。这些自动配置类通常在`META-INF/spring.factories` 文件中声明,并根据项目的依赖和配置条件,通过条件注解(如 `@ConditionalOnClass`、`@ConditionalOnBean` 等)判断是否应该生效。 + +⭐自动配置是详细的源码解读可以参考 [JavaGuide](https://javaguide.cn/) 上这篇文章:[SpringBoot 自动装配原理详解](https://javaguide.cn/system-design/framework/spring/spring-boot-auto-assembly-principles.html)。 + +### Spring Boot 如何加载配置文件?如果两种配置文件同时存在,会怎样处理? + +Spring Boot 会自动从类路径的根目录(通常是项目的 `src/main/resources/` 目录)下查找并加载名为 `application.properties` 或 `application.yml` (包括 `.yaml` 扩展名) 的文件。 + +如果在同一目录下同时存在 `application.properties` 和 `application.yml` 文件,`application.properties` 文件中的配置项优先级更高,会覆盖 `application.yml` 中相同的配置项。为了避免配置冲突和混淆,建议在一个项目中只使用一种格式。 + +如果开发者没有提供任何 `application.properties` 或 `application.yml` 文件,或者文件中没有定义某个特定的配置项,Spring Boot 将会使用其内置的默认配置值(如果该配置项有默认值的话)。 + +### Spring Boot 加载配置文件的优先级了解么? + +Spring Boot 加载配置文件的优先级设计得非常灵活,主要是为了方便我们在不同环境(开发、测试、生产)下覆盖或指定配置。它的原则是:**后加载的覆盖先加载的,而且离用户(或部署环境)越近的优先级越高**。 + +**加载顺序如下**: + +1. 当前项目根目录下 `config/` 子目录的配置文件 (`./config/application.yml` 或 `./config/application.properties`):优先级最高,通常放在运行 Jar 包同级的 `config` 目录里。 + +2. 当前项目根目录下的配置文件 (`./application.yml` 或 `./application.properties`): 直接放在运行 Jar 包同级目录里,优先级次之。 + +3. 类路径内 `config/` 子目录的配置文件 (`classpath:/config/application.yml` 或 `classpath:/config/application.properties`): 对应项目中的 `src/main/resources/config/` 下的文件,优先级再次之。 + +4. 类路径下的配置文件 (`classpath:/application.yml` 或 `classpath:/application.properties`): 对应项目中的 `src/main/resources/` 根目录下的文件,在这些位置里优先级最低。 + +总结:Jar 包外 `config/`> Jar 包外根目录> Jar 包内 `config/`> Jar 包内根目录。 + + + +**简单记忆规则**: + +- **包外> 包内**(方便部署时覆盖配置)。 +- **`config/` 目录> 根目录**(无论包内还是包外,`config` 目录里的配置优先级更高)。 + +如果某个 Profile 文件(如 `application-dev.yml`)被激活(通过 `spring.profiles.active=dev` 指定),那么,**在同一个目录下**,Profile 文件的优先级高于通用文件。例如: + +- `src/main/resources/application-dev.yml` 的配置会覆盖 `src/main/resources/application.yml` 中的同名配置。 +- 同样地,Jar 包外的 `config/application-dev.yml` 会覆盖 `config/application.yml`。 + +通过这样的灵活设计,Spring Boot 能很好地适应各种环境的配置需求,同时确保配置文件的覆盖和管理清晰有序。 + +### ⭐️更多 SpringBoot 面试题 + +更多 **Spring Boot** 相关的面试题欢迎加入我的知识星球,已经整理到了[《Java 面试指北》](https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html)中。 + +很多 Spring Boot 重要的新特性都已经同步到了这篇文章中,质量很高,保证内容与时俱进! + +![SpringBoot 面试题](https://oss.javaguide.cn/javamianshizhibei/springboot-questions.png)

AltStyle によって変換されたページ (->オリジナル) /