1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-08-01 16:28:03 +08:00

Merge branch 'Snailclimb:main' into main

This commit is contained in:
WTT 2023-09-11 10:19:17 +08:00 committed by GitHub
commit 1067e9ac4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 336 additions and 31 deletions

View File

@ -294,6 +294,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
**重要知识点详解**
- [IoC & AOP详解快速搞懂](./docs/system-design/framework/spring/ioc-and-aop.md)
- [Spring 事务详解](./docs/system-design/framework/spring/spring-transaction.md)
- [Spring 中的设计模式详解](./docs/system-design/framework/spring/spring-design-patterns-summary.md)
- [SpringBoot 自动装配原理详解](./docs/system-design/framework/spring/spring-boot-auto-assembly-principles.md)

View File

@ -401,6 +401,7 @@ export default sidebar({
icon: "star",
collapsible: true,
children: [
"ioc-and-aop",
"spring-transaction",
"spring-design-patterns-summary",
"spring-boot-auto-assembly-principles",

View File

@ -51,7 +51,14 @@ export default hopeTheme({
plugins: {
blog: true,
copyright: true,
copyright: {
author: "JavaGuide(javaguide.cn)",
license: "MIT",
triggerLength: 100,
maxLength: 700,
canonical: "https://javaguide.cn/",
global:true
},
mdEnhance: {
align: true,
codetabs: true,

View File

@ -176,8 +176,8 @@ public void postOrder(TreeNode root){
if(root == null){
return;
}
postOrder(root.right);
postOrder(root.left);
postOrder(root.right);
system.out.println(root.data);
}
```

View File

@ -196,11 +196,17 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
![HTTP/1.0 和 HTTP/1.1 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.1-vs-http2.0.png)
- **IO 多路复用Multiplexing**HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。
- **多路复用Multiplexing**HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本),互不干扰。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接,而浏览器为了控制资源会有 6-8 个 TCP 连接都限制。。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。
- **二进制帧Binary Frames**HTTP/2.0 使用二进制帧进行数据传输,而 HTTP/1.1 则使用文本格式的报文。二进制帧更加紧凑和高效,减少了传输的数据量和带宽消耗。
- **头部压缩Header Compression**HTTP/1.1 支持`Body`压缩,`Header`不支持压缩。HTTP/2.0 支持对`Header`压缩,减少了网络开销。
- **头部压缩Header Compression**HTTP/1.1 支持`Body`压缩,`Header`不支持压缩。HTTP/2.0 支持对`Header`压缩,使用了专门为`Header`压缩而设计的 HPACK 算法,减少了网络开销。
- **服务器推送Server Push**HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数和延迟。而 HTTP/1.1 需要客户端自己发送请求来获取相关资源。
HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://blog.cloudflare.com/http-2-for-web-developers/)
![HTTP/2 Multiplexing](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-multiplexing.png)
可以看到HTTP/2.0 的多路复用使得不同的请求可以共用一个 TCP 连接,避免建立多个连接带来不必要的额外开销,而 HTTP/1.1 中的每个请求都会建立一个单独的连接
### HTTP/2.0 和 HTTP/3.0 有什么区别?
![HTTP/2.0 和 HTTP/3.0 对比](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-vs-http3.0.png)
@ -211,6 +217,12 @@ HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被
- **错误恢复**HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。
- **安全性**HTTP/2.0 和 HTTP/3.0 在安全性上都有较高的要求支持加密通信但在实现上有所不同。HTTP/2.0 使用 TLS 协议进行加密,而 HTTP/3.0 基于 QUIC 协议,包含了内置的加密和身份验证机制,可以提供更强的安全性。
HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较:
![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png)
关于 HTTP/1.0 -> HTTP/3.0 更详细的演进介绍,推荐阅读[HTTP1 到 HTTP3 的工程优化](https://dbwu.tech/posts/http_evolution/)。
### HTTP 是不保存状态的协议, 如何保存用户状态?
HTTP 是一种不保存状态即无状态stateless协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢Session 机制的存在就是为了解决这个问题Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session过了时间限制就会销毁这个 Session

View File

@ -7,7 +7,7 @@ tag:
> 作者: 听风 原文地址: <https://www.cnblogs.com/huchong/p/10219318.html>
>
> JavaGuide 已获得作者授权,并对原文内容进行了完善。
> JavaGuide 已获得作者授权,并对原文内容进行了完善补充
## 数据库命名规范
@ -29,10 +29,7 @@ InnoDB 支持事务,支持行级锁,更好的恢复性,高并发下性能
兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。
参考文章:
- [MySQL 字符集不一致导致索引失效的一个真实案例](https://blog.csdn.net/horses/article/details/107243447)
- [MySQL 字符集详解](../character-set.md)
推荐阅读一下我写的这篇文章:[MySQL 字符集详解](../character-set.md) 。
### 所有表和字段都需要添加注释
@ -135,18 +132,19 @@ MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查
相关阅读:[技术分享 | MySQL 默认值选型(是空,还是 NULL](https://opensource.actionsky.com/20190710-mysql/) 。
### 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间
### 一定不要用字符串存储日期
TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07
对于日期类型来说, 一定不要用字符串存储日期。可以考虑 DATETIME、TIMESTAMP 和 数值型时间戳。
TIMESTAMP 占用 4 字节和 INT 相同,但比 INT 可读性高
这三种种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储
| 类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
| ------------ | -------- | ------------------------------ | ------------------------------------------------------------ | -------------- |
| DATETIME | 5~8字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] 9999-12-31 23:59:59[.999999] | 否 |
| TIMESTAMP | 4~7字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] 2038-01-19 03:14:07[.999999] | 是 |
| 数值型时间戳 | 4字节 | 全数字如1578707612 | 1970-01-01 00:00:01之后的时间 | 否 |
**经常会有人用字符串存储日期型的数据(不正确的做法)**
- 缺点 1无法用日期函数进行计算和比较
- 缺点 2用字符串存储日期要占用更多的空间
MySQL 时间类型选择的详细介绍请看这篇:[MySQL时间类型数据存储建议](https://javaguide.cn/database/mysql/some-thoughts-on-database-storage-time.html)。
### 同财务相关的金额类数据必须使用 decimal 类型
@ -230,9 +228,13 @@ InnoDB 是按照主键索引的顺序来组织表的
## 数据库 SQL 开发规范
### 尽量不在数据库做运算,复杂运算需移到业务应用里完成
尽量不在数据库做运算,复杂运算需移到业务应用里完成。这样可以避免数据库的负担过重,影响数据库的性能和稳定性。数据库的主要作用是存储和管理数据,而不是处理数据。
### 优化对性能影响较大的 SQL 语句
要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句;
要找到最需要优化的 SQL 语句。要么是使用最频繁的语句,要么是优化后提高最明显的语句,可以通过查询 MySQL 的慢查询日志来发现需要进行优化的 SQL 语句
### 充分利用表上已经存在的索引
@ -244,9 +246,10 @@ InnoDB 是按照主键索引的顺序来组织表的
### 禁止使用 SELECT \* 必须使用 SELECT <字段列表> 查询
- `SELECT *` 消耗更多的 CPU 和 IO 以网络带宽资源
- `SELECT *` 无法使用覆盖索引
- `SELECT <字段列表>` 可减少表结构变更带来的影响
- `SELECT *` 会消耗更多的 CPU。
- `SELECT *` 无用字段增加网络带宽资源消耗,增加数据传输时间,尤其是大字段(如 varchar、blob、text
- `SELECT *` 无法使用 MySQL 优化器覆盖索引的优化(基于 MySQL 优化器的“覆盖索引”策略又是速度极快,效率极高,业界极为推荐的查询优化方式)
- `SELECT <字段列表>` 可减少表结构变更带来的影响、
### 禁止使用不含字段列表的 INSERT 语句
@ -378,4 +381,9 @@ pt-online-schema-change 它会首先建立一个与原表结构相同的新表
- 程序使用数据库账号只能在一个 DB 下使用,不准跨库
- 程序使用的账号原则上不准有 drop 权限
## 推荐阅读
- [技术同学必会的MySQL设计规约都是惨痛的教训 - 阿里开发者](https://mp.weixin.qq.com/s/XC8e5iuQtfsrEOERffEZ-Q)
- [聊聊数据库建表的15个小技巧](https://mp.weixin.qq.com/s/NM-aHaW6TXrnO6la6Jfl5A)
<!-- @include: @article-footer.snippet.md -->

View File

@ -21,7 +21,7 @@ category: 分布式
## 网关能提供哪些功能?
绝大部分网关可以提供下面这些功能:
绝大部分网关可以提供下面这些功能(有一些功能需要借助其他框架或者中间件)
- **请求转发**:将请求转发到目标微服务。
- **负载均衡**:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。
@ -37,6 +37,7 @@ category: 分布式
- **异常处理**:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。
- **API 文档:** 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。
- **协议转换**:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。
- **证书管理**:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。
下图来源于[百亿规模 API 网关服务 Shepherd 的设计与实现 - 美团技术团队 - 2021](https://mp.weixin.qq.com/s/iITqdIiHi3XGKq6u6FRVdg)这篇文章。

View File

@ -50,11 +50,20 @@ category: 高可用
重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。
### 常见的重试策略有哪些?
常见的重试策略有两种:
1. **固定间隔时间重试**每次重试之间都使用相同的时间间隔比如每隔1.5秒进行一次重试。这种重试策略的优点是实现起来比较简单,不需要考虑重试次数和时间的关系,也不需要维护额外的状态信息。但是这种重试策略的缺点是可能会导致重试过于频繁或过于稀疏,从而影响系统的性能和效率。如果重试间隔太短,可能会对目标系统造成过大的压力,导致雪崩效应;如果重试间隔太长,可能会导致用户等待时间过长,影响用户体验。
2. **梯度间隔重试**根据重试次数的增加去延长下次重试时间比如第一次重试间隔为1秒第二次为2秒第三次为4秒以此类推。这种重试策略的优点是能够有效提高重试成功的几率随着重试次数增加但是重试依然不成功说明目标系统恢复时间比较长因此可以根据重试次数延长下次重试时间也能通过柔性化的重试避免对下游系统造成更大压力。但是这种重试策略的缺点是实现起来比较复杂需要考虑重试次数和时间的关系以及设置合理的上限和下限值。另外这种重试策略也可能会导致用户等待时间过长影响用户体验。
这两种适合的场景各不相同。固定间隔时间重试适用于目标系统恢复时间比较稳定和可预测的场景,比如网络波动或服务重启。梯度间隔重试适用于目标系统恢复时间比较长或不可预测的场景,比如网络故障和服务故障。
### 重试的次数如何设置?
重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。
重试的次数通常建议设为 3 次。并且,我们通常还会设置重试的间隔,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。
重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略,比如说我们要重试 3 次的话,第 1 次请求失败后,等待 1 秒再进行重试,第 2 次请求失败后,等待 2 秒再进行重试,第 3 次请求失败后,等待 3 秒再进行重试。
### 重试幂等

View File

@ -123,6 +123,12 @@ Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱
最小连接法可以尽可能最大地使请求分配更加合理化,提高服务器的利用率。不过,这种方法实现起来也最复杂,需要监控每一台服务器处理的请求连接数。
### 两次随机法
两次随机法在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。
两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。如果只使用一次随机法,可能会导致某些服务器过载,而某些服务器空闲。
## 七层负载均衡可以怎么做?
简单介绍两种项目中常用的七层负载均衡解决方案DNS 解析和反向代理。

View File

@ -290,6 +290,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
**重要知识点详解**
- [IoC & AOP详解快速搞懂](./system-design/framework/spring/ioc-and-aop.md)
- [Spring 事务详解](./system-design/framework/spring/spring-transaction.md)
- [Spring 中的设计模式详解](./system-design/framework/spring/spring-design-patterns-summary.md)
- [SpringBoot 自动装配原理详解](./system-design/framework/spring/spring-boot-auto-assembly-principles.md)

View File

@ -273,6 +273,7 @@ protected Class<?> loadClass(String name, boolean resolve)
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 `BootstrapClassLoader` 中。
- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 `findClass()` 方法来加载类)。
- 如果子类加载器也无法加载这个类,那么它会抛出一个 `ClassNotFoundException` 异常。
🌈 拓展一下:
@ -294,13 +295,48 @@ protected Class<?> loadClass(String name, boolean resolve)
> 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 `loadClass()`方法来加载类)。
重写 `loadClass()`方法之后,我们就可以改变传统双亲委派模型的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。
我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 `WebAppClassLoader` 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。
Tomcat 的类加载器的层次结构如下:
![Tomcat 的类加载器的层次结构](https://oss.javaguide.cn/github/javaguide/java/jvm/tomcat-class-loader-parents-delegation-model.png)
感兴趣的小伙伴可以自行研究一下 Tomcat 类加载器的层次结构,这有助于我们搞懂 Tomcat 隔离 Web 应用的原理,推荐资料是[《深入拆解 Tomcat & Jetty》](http://gk.link/a/10Egr)。
Tomcat 这四个自定义的类加载器对应的目录如下:
- `CommonClassLoader`对应`<Tomcat>/common/*`
- `CatalinaClassLoader`对应`<Tomcat >/server/*`
- `SharedClassLoader`对应 `<Tomcat >/shared/*`
- `WebAppClassloader`对应 `<Tomcat >/webapps/<app>/WEB-INF/*`
从图中的委派关系中可以看出:
- `CommonClassLoader`作为 `CatalinaClassLoader``SharedClassLoader` 的父加载器。`CommonClassLoader` 能加载的类都可以被 `CatalinaClassLoader``SharedClassLoader` 使用。因此,`CommonClassLoader` 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。
- `CatalinaClassLoader``SharedClassLoader` 能加载的类则与对方相互隔离。`CatalinaClassLoader` 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。`SharedClassLoader` 作为 `WebAppClassLoader` 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。
- 每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。各个 `WebAppClassLoader` 实例之间相互隔离,进而实现 Web 应用之间的类隔。
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
比如SPI 中SPI 的接口(如 `java.sql.Driver`)是由 Java 核心库提供的,由`BootstrapClassLoader` 加载。而 SPI 的实现(如`com.mysql.cj.jdbc.Driver`是由第三方供应商提供的它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下一个类及其依赖类由同一个类加载器加载。所以加载SPI 的接口的类加载器(`BootstrapClassLoader`也会用来加载SPI 的实现。按照双亲委派模型,`BootstrapClassLoader` 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。
再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 `SharedClassLoader` 加载Web 服务器是 Tomcat。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 `SharedClassLoader`)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 `SharedClassLoader` 的加载路径下,所以 `SharedClassLoader` 无法找到业务类,也就无法加载它们。
如何解决这个问题呢? 这个时候就需要用到 **线程上下文类加载器(`ThreadContextClassLoader`** 了。
拿 Spring 这个例子来说,当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 `WebAppClassLoader`,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 `WebAppClassLoader`。这样就可以让高层的类加载器(`SharedClassLoader`)借助子类加载器( `WebAppClassLoader`)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。
线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat设置的。
`Java.lang.Thread` 中的`getContextClassLoader()``setContextClassLoader(ClassLoader cl)`分别用来获取和设置线程的上下文类加载器。如果没有通过`setContextClassLoader(ClassLoader cl)`进行设置的话,线程将继承其父线程的上下文类加载器。
Spring 获取线程线程上下文类加载器的代码如下:
```java
cl = Thread.currentThread().getContextClassLoader();
```
感兴趣的小伙伴可以自行深入研究一下 Tomcat 打破双亲委派模型的原理,推荐资料:[《深入拆解 Tomcat & Jetty》](http://gk.link/a/10Egr)。
## 推荐阅读

View File

@ -16,6 +16,8 @@ head:
<!-- @include: @small-advertisement.snippet.md -->
> 本篇文章由 JavaGuide 收集自网络,原出处不明。
>
> 比起这些枯燥的面试题,我更建议你看看文末推荐的 MyBatis 优质好文。
### #{} 和 \${} 的区别是什么?
@ -299,3 +301,11 @@ MyBatis 提供了 9 种动态 sql 标签:
面试题看似都很简单,但是想要能正确回答上来,必定是研究过源码且深入的人,而不是仅会使用的人或者用的很熟的人,以上所有面试题及其答案所涉及的内容,在我的 MyBatis 系列博客中都有详细讲解和原理分析。
<!-- @include: @article-footer.snippet.md -->
### 文章推荐
- [2W字全面剖析Mybatis中的9种设计模式](https://juejin.cn/post/7273516671574687759)
- [从零开始实现一个MyBatis加解密插件](https://mp.weixin.qq.com/s/WUEAdFDwZsZ4EKO8ix0ijg)
- [MyBatis最全使用指南](https://juejin.cn/post/7051910683264286750)
- [脑洞打开第一次看到这样使用MyBatis的看得我一愣一愣的。](https://juejin.cn/post/7269390456530190376)
- [MyBatis居然也有并发问题](https://juejin.cn/post/7264921613551730722)

View File

@ -0,0 +1,213 @@
---
title: IoC & AOP详解快速搞懂
category: 框架
tag:
- Spring
---
这篇文章会从下面从以下几个问题展开对 IoC & AOP 的解释
- 什么是 IoC
- IoC 解决了什么问题?
- IoC 和 DI 的区别?
- 什么是 AOP
- AOP 解决了什么问题?
- AOP 的应用场景有哪些?
- AOP 为什么叫做切面编程?
- AOP 实现方式有哪些?
首先声明IoC & AOP 不是 Spring 提出来的,它们在 Spring 之前其实已经存在了只不过当时更加偏向于理论。Spring 在技术层次将这两个思想进行了很好的实现。
## IoC Inversion of control
### 什么是 IoC?
IoC Inversion of Control )即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是Java 开发领域对象的创建以及管理的问题。
例如:现有类 A 依赖于类 B
- **传统的开发方式** :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
- **使用 IoC 思想的开发方式** :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面过去即可。
从以上两种开发方式的对比来看:我们 “丧失了一个权力” (创建、管理对象的权力),从而也得到了一个好处(不用再考虑对象的创建、管理等一系列的事情)
**为什么叫控制反转?**
- **控制** :指的是对象创建(实例化、管理)的权力
- **反转** 控制权交给外部环境IoC 容器)
![IoC 图解](https://oss.javaguide.cn/java-guide-blog/frc-365faceb5697f04f31399937c059c162.png)
### IoC 解决了什么问题?
IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
1. 对象之间的耦合度或者说依赖程度降低;
2. 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
例如:现有一个针对 User 的操作,利用 Service 和 Dao 两层结构进行开发
在没有使用 IoC 思想的情况下Service 层想要使用 Dao 层的具体实现的话,需要通过 new 关键字在`UserServiceImpl` 中手动 new 出 `IUserDao` 的具体实现类 `UserDaoImpl`(不能直接 new 接口类)。
![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/ioc-kfji3.png)
很完美,这种方式也是可以实现的,但是我们想象一下如下场景:
开发过程中突然接到一个新的需求,针对对`IUserDao` 接口开发出另一个具体实现类。因为 Server 层依赖了`IUserDao`的具体实现,所以我们需要修改`UserServiceImpl`中 new 的对象。如果只有一个类引用了`IUserDao`的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了`IUserDao`的具体实现的话,一旦需要更换`IUserDao` 的实现方式,那修改起来将会非常的头疼。
![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/why-ioc.png)
使用 IoC 的思想,我们将对象的控制权(创建、管理)交有 IoC 容器去管理,我们在使用的时候直接向 IoC 容器 “要” 就可以了
![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/why-ioc-2.png)
### IoC 和 DI 有区别吗?
IoCInverse of Control:控制反转)是一种设计思想或者说是某种模式。这个设计思想就是 **将原本在程序中手动创建对象的控制权交给第三方比如 IoC 容易。** 对于我们常用的 Spring 框架来说, IoC 容器实际上就是个 Mapkeyvalue,Map 中存放的是各种对象。不过IoC 在其他语言中也有应用,并非 Spring 特有。
IoC 最常见以及最合理的实现方式叫做依赖注入Dependency Injection简称 DI
老马Martin Fowler在一篇文章中提到将 IoC 改名为 DI原文如下原文地址<https://martinfowler.com/articles/injection.html>
![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/martin-fowler-injection.png)
老马的大概意思是 IoC 太普遍并且不表意,很多人会因此而迷惑,所以,使用 DI 来精确指名这个模式比较好。
## AOPAspect oriented programming
这里不会涉及太多专业的术语,核心目的是将 AOP 的思想说清楚。
### 什么是 AOP
AOPAspect Oriented Programming即面向切面编程AOP 是 OOP面向对象编程的一种延续二者互补并不对立。
AOP 的目的是将横切关注点如日志记录、事务管理、权限控制、接口限流、接口幂等等从核心业务逻辑中分离出来通过动态代理、字节码操作等技术实现代码的复用和解耦提高代码的可维护性和可扩展性。OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。
### AOP 为什么叫面向切面编程?
AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的**切面Aspect**。
这里顺带总结一下 AOP 关键术语(不理解也没关系,可以继续往下看):
- **横切关注点cross-cutting concerns** :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
- **切面Aspect**:对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
- **连接点JoinPoint**:连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
- **通知Advice**通知就是切面在某个连接点要执行的操作。通知有五种类型分别是前置通知Before、后置通知After、返回通知AfterReturning、异常通知AfterThrowing和环绕通知Around。前四种通知都是在目标方法的前后执行而环绕通知可以控制目标方法的执行过程。
- **切点Pointcut**:一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如 `execution(* com.xyz.service..*(..))`匹配 `com.xyz.service` 包及其子包下的类或接口。
- **织入Weaving**织入是将切面和目标对象连接起来的过程也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种分别是编译期织入AspectJ和运行期织入AspectJ
### AOP 解决了什么问题?
OOP 不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为 **横切关注点cross-cutting concerns** 。如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
AOP 可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从**核心业务逻辑core concerns核心关注点**中分离出来,实现关注点的分离。
![](https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/crosscut-logic-and-businesslogic-separation%20%20%20%20%20%20.png)
以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的的逻辑。
```java
public CommonResponse<Object> method1() {
// 业务逻辑
xxService.method1();
// 省略具体的业务处理逻辑
// 日志记录
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...
return CommonResponse.success();
}
public CommonResponse<Object> method2() {
// 业务逻辑
xxService.method2();
// 省略具体的业务处理逻辑
// 日志记录
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...
return CommonResponse.success();
}
// ...
```
使用 AOP 技术之后,我们可以日志记录的逻辑封装成一个切面,然后通过切入点和通知来指定在哪些方法需要执行日志记录的操作。
```java
// 日志注解
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 描述
*/
String description() default "";
/**
* 方法类型 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}
// 日志切面
@Component
@Aspect
public class LogAspect {
// 切入点,所有被 Log 注解标注的方法
@Pointcut("@annotation(cn.javaguide.annotation.Log)")
public void webLog() {
}
/**
* 环绕通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 省略具体的处理逻辑
}
// 省略其他代码
}
```
这样的话,我们一行注解即可实现日志记录:
```java
@Log(description = "method1",methodType = MethodType.INSERT)
public CommonResponse<Object> method1() {
// 业务逻辑
xxService.method1();
// 省略具体的业务处理逻辑
return CommonResponse.success();
}
```
### AOP 的应用场景有哪些?
- 日志记录:自定义日志记录注解,利用 AOP一行代码即可实现日志记录。
- 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
- 事务管理:`@Transactional` 注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。`@Transactional`注解就是基于 AOP 实现的。
- 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限如果具备就执行目标方法否则就不执行。例如SpringSecurity 利用`@PreAuthorize` 注解一行代码即可自定义权限校验。
- 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
- 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
- ......
### AOP 实现方式有哪些?
AOP 的常见实现方式有动态代理、字节码操作等方式。
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 框架了。
**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

View File

@ -132,7 +132,7 @@ Spring 时代我们一般通过 XML 文件来配置 Bean后来开发人员觉
相关阅读:
- [IoC 源码阅读](https://javadoop.com/post/spring-ioc)
- [面试被问了几百遍的 IoC 和 AOP ,还在傻傻搞不清楚?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486938&idx=1&sn=c99ef0233f39a5ffc1b98c81e02dfcd4&chksm=cea24211f9d5cb07fa901183ba4d96187820713a72387788408040822ffb2ed575d28e953ce7&token=1736772241&lang=zh_CN#rd)
- [IoC & AOP详解快速搞懂](./ioc-and-aop.md)
### 什么是 Spring Bean
@ -359,7 +359,7 @@ Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某
当然你也可以使用 **AspectJ** Spring AOP 已经集成了 AspectJ AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
AOP 切面编程设计到的一些专业术语:
AOP 切面编程涉及到的一些专业术语:
| 术语 | 含义 |
| :---------------- | :-------------------------------------------------------------------: |
@ -444,7 +444,7 @@ MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心
- Model:系统涉及的数据,也就是 dao 和 bean。
- View展示模型中的数据只是用来展示。
- Controller处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。
- Controller接受用户请求,并将请求发送至 Model最后返回数据给 JSP 并展示给用户
![](https://oss.javaguide.cn/java-guide-blog/mvc-model2.png)

View File

@ -22,6 +22,7 @@
"packageManager": "pnpm@8.6.0",
"dependencies": {
"@vuepress/client": "2.0.0-beta.67",
"@vuepress/plugin-search": "2.0.0-beta.67",
"@vuepress/utils": "2.0.0-beta.67",
"husky": "8.0.3",
"markdownlint-cli": "0.34.0",
@ -30,7 +31,6 @@
"prettier": "2.8.8",
"vue": "3.3.4",
"vuepress": "2.0.0-beta.67",
"@vuepress/plugin-search": "2.0.0-beta.67",
"vuepress-theme-hope": "2.0.0-beta.236"
}
}

2
pnpm-lock.yaml generated
View File

@ -2270,7 +2270,7 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
dependencies:
'@gar/promisify': registry.npmmirror.com/@gar/promisify@1.1.3
semver: registry.npmmirror.com/semver@7.5.1
semver: registry.npmmirror.com/semver@7.5.4
dev: false
registry.npmmirror.com/@npmcli/move-file@2.0.1: