From 3b1767d6e353c3202b92eb4ea69cedf63c213efb Mon Sep 17 00:00:00 2001 From: Guide Date: Fri, 25 Apr 2025 07:00:55 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20update]=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=92=8C=E7=BD=91=E7=BB=9C=E9=83=A8=E5=88=86=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E7=AD=94=E6=A1=88=E4=BC=98=E5=8C=96=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../network/other-network-questions2.md | 68 +- docs/database/mysql/mysql-questions-01.md | 39 +- .../mysql/transaction-isolation-level.md | 53 +- docs/java/basis/serialization.md | 8 +- .../spring/spring-common-annotations.md | 823 ++++++++++-------- 5 files changed, 543 insertions(+), 448 deletions(-) diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index fac525d7..9d193f49 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -11,31 +11,61 @@ tag: ### TCP 与 UDP 的区别(重要) -1. **是否面向连接**:UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。 -2. **是否是可靠传输**:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。 -3. **是否有状态**:这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(**这很渣男!**)。 -4. **传输效率**:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。 -5. **传输形式**:TCP 是面向字节流的,UDP 是面向报文的。 -6. **首部开销**:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。 -7. **是否提供广播或多播服务**:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多; +1. **是否面向连接**: + - TCP 是面向连接的。在传输数据之前,必须先通过“三次握手”建立连接;数据传输完成后,还需要通过“四次挥手”来释放连接。这保证了双方都准备好通信。 + - UDP 是无连接的。发送数据前不需要建立任何连接,直接把数据包(数据报)扔出去。 +2. **是否是可靠传输**: + - TCP 提供可靠的数据传输服务。它通过序列号、确认应答 (ACK)、超时重传、流量控制、拥塞控制等一系列机制,来确保数据能够无差错、不丢失、不重复且按顺序地到达目的地。 + - UDP 提供不可靠的传输。它尽最大努力交付 (best-effort delivery),但不保证数据一定能到达,也不保证到达的顺序,更不会自动重传。收到报文后,接收方也不会主动发确认。 +3. **是否有状态**: + - TCP 是有状态的。因为要保证可靠性,TCP 需要在连接的两端维护连接状态信息,比如序列号、窗口大小、哪些数据发出去了、哪些收到了确认等。 + - UDP 是无状态的。它不维护连接状态,发送方发出数据后就不再关心它是否到达以及如何到达,因此开销更小(**这很“渣男”!**)。 +4. **传输效率**: + - TCP 因为需要建立连接、发送确认、处理重传等,其开销较大,传输效率相对较低。 + - UDP 结构简单,没有复杂的控制机制,开销小,传输效率更高,速度更快。 +5. **传输形式**: + - TCP 是面向字节流 (Byte Stream) 的。它将应用程序交付的数据视为一连串无结构的字节流,可能会对数据进行拆分或合并。 + - UDP 是面向报文 (Message Oriented) 的。应用程序交给 UDP 多大的数据块,UDP 就照样发送,既不拆分也不合并,保留了应用程序消息的边界。 +6. **首部开销**: + - TCP 的头部至少需要 20 字节,如果包含选项字段,最多可达 60 字节。 + - UDP 的头部非常简单,固定只有 8 字节。 +7. **是否提供广播或多播服务**: + - TCP 只支持点对点 (Point-to-Point) 的单播通信。 + - UDP 支持一对一 (单播)、一对多 (多播/Multicast) 和一对所有 (广播/Broadcast) 的通信方式。 8. …… -我把上面总结的内容通过表格形式展示出来了!确定不点个赞嘛? +为了更直观地对比,可以看下面这个表格: -| | TCP | UDP | -| ---------------------- | -------------- | ---------- | -| 是否面向连接 | 是 | 否 | -| 是否可靠 | 是 | 否 | -| 是否有状态 | 是 | 否 | -| 传输效率 | 较慢 | 较快 | -| 传输形式 | 字节流 | 数据报文段 | -| 首部开销 | 20 ~ 60 bytes | 8 bytes | -| 是否提供广播或多播服务 | 否 | 是 | +| 特性 | TCP | UDP | +| ------------ | -------------------------- | ----------------------------------- | +| **连接性** | 面向连接 | 无连接 | +| **可靠性** | 可靠 | 不可靠 (尽力而为) | +| **状态维护** | 有状态 | 无状态 | +| **传输效率** | 较低 | 较高 | +| **传输形式** | 面向字节流 | 面向数据报 (报文) | +| **头部开销** | 20 - 60 字节 | 8 字节 | +| **通信模式** | 点对点 (单播) | 单播、多播、广播 | +| **常见应用** | HTTP/HTTPS, FTP, SMTP, SSH | DNS, DHCP, SNMP, TFTP, VoIP, 视频流 | ### 什么时候选择 TCP,什么时候选 UDP? -- **UDP 一般用于即时通信**,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。 -- **TCP 用于对传输准确性要求特别高的场景**,比如文件传输、发送和接收邮件、远程登录等等。 +选择 TCP 还是 UDP,主要取决于你的应用**对数据传输的可靠性要求有多高,以及对实时性和效率的要求有多高**。 + +当**数据准确性和完整性至关重要,一点都不能出错**时,通常选择 TCP。因为 TCP 提供了一整套机制(三次握手、确认应答、重传、流量控制等)来保证数据能够可靠、有序地送达。典型应用场景如下: + +- **Web 浏览 (HTTP/HTTPS):** 网页内容、图片、脚本必须完整加载才能正确显示。 +- **文件传输 (FTP, SCP):** 文件内容不允许有任何字节丢失或错序。 +- **邮件收发 (SMTP, POP3, IMAP):** 邮件内容需要完整无误地送达。 +- **远程登录 (SSH, Telnet):** 命令和响应需要准确传输。 +- ...... + +当**实时性、速度和效率优先,并且应用能容忍少量数据丢失或乱序**时,通常选择 UDP。UDP 开销小、传输快,没有建立连接和保证可靠性的复杂过程。典型应用场景如下: + +- **实时音视频通信 (VoIP, 视频会议, 直播):** 偶尔丢失一两个数据包(可能导致画面或声音短暂卡顿)通常比因为等待重传(TCP 机制)导致长时间延迟更可接受。应用层可能会有自己的补偿机制。 +- **在线游戏:** 需要快速传输玩家位置、状态等信息,对实时性要求极高,旧的数据很快就没用了,丢失少量数据影响通常不大。 +- **DHCP (动态主机配置协议):** 客户端在请求 IP 时自身没有 IP 地址,无法满足 TCP 建立连接的前提条件,并且 DHCP 有广播需求、交互模式简单以及自带可靠性机制。 +- **物联网 (IoT) 数据上报:** 某些场景下,传感器定期上报数据,丢失个别数据点可能不影响整体趋势分析。 +- ...... ### HTTP 基于 TCP 还是 UDP? diff --git a/docs/database/mysql/mysql-questions-01.md b/docs/database/mysql/mysql-questions-01.md index 4878eee5..b1493a64 100644 --- a/docs/database/mysql/mysql-questions-01.md +++ b/docs/database/mysql/mysql-questions-01.md @@ -553,31 +553,26 @@ MVCC 在 MySQL 中实现所依赖的手段主要是: **隐藏字段、read view ### SQL 标准定义了哪些事务隔离级别? -SQL 标准定义了四个隔离级别: +SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是: -- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 +- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。 +- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。 +- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。 - **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 ---- - -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | - -### MySQL 的隔离级别是基于锁实现的吗? - -MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 - -SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 +| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | +| ---------------- | ----------------- | -------------------------------- | ---------------------- | +| READ UNCOMMITTED | √ | √ | √ | +| READ COMMITTED | × | √ | √ | +| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) | +| SERIALIZABLE | × | × | × | ### MySQL 的默认隔离级别是什么? -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` +MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看: + +- MySQL 8.0 之前:`SELECT @@tx_isolation;` +- MySQL 8.0 及之后:`SELECT @@transaction_isolation;` ```sql mysql> SELECT @@tx_isolation; @@ -590,6 +585,12 @@ mysql> SELECT @@tx_isolation; 关于 MySQL 事务隔离级别的详细介绍,可以看看我写的这篇文章:[MySQL 事务隔离级别详解](./transaction-isolation-level.md)。 +### MySQL 的隔离级别是基于锁实现的吗? + +MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。 + +SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。 + ## MySQL 锁 锁是一种常见的并发事务的控制方式。 diff --git a/docs/database/mysql/transaction-isolation-level.md b/docs/database/mysql/transaction-isolation-level.md index 52ad40f4..8b706640 100644 --- a/docs/database/mysql/transaction-isolation-level.md +++ b/docs/database/mysql/transaction-isolation-level.md @@ -11,43 +11,46 @@ tag: ## 事务隔离级别总结 -SQL 标准定义了四个隔离级别: +SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是: -- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 +- **READ-UNCOMMITTED(读取未提交)** :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。 +- **READ-COMMITTED(读取已提交)** :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。 +- **REPEATABLE-READ(可重复读)** :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。 - **SERIALIZABLE(可串行化)** :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。 ---- +| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | +| ---------------- | ----------------- | -------------------------------- | ---------------------- | +| READ UNCOMMITTED | √ | √ | √ | +| READ COMMITTED | × | √ | √ | +| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) | +| SERIALIZABLE | × | × | × | -| 隔离级别 | 脏读 | 不可重复读 | 幻读 | -| :--------------: | :--: | :--------: | :--: | -| READ-UNCOMMITTED | √ | √ | √ | -| READ-COMMITTED | × | √ | √ | -| REPEATABLE-READ | × | × | √ | -| SERIALIZABLE | × | × | × | +**默认级别查询:** -MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;` +MySQL InnoDB 存储引擎的默认隔离级别是 **REPEATABLE READ**。可以通过以下命令查看: -```sql -MySQL> SELECT @@tx_isolation; -+-----------------+ -| @@tx_isolation | -+-----------------+ -| REPEATABLE-READ | -+-----------------+ +- MySQL 8.0 之前:`SELECT @@tx_isolation;` +- MySQL 8.0 及之后:`SELECT @@transaction_isolation;` + +```bash +mysql> SELECT @@transaction_isolation; ++-------------------------+ +| @@transaction_isolation | ++-------------------------+ +| REPEATABLE-READ | ++-------------------------+ ``` -从上面对 SQL 标准定义了四个隔离级别的介绍可以看出,标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。 +**InnoDB 的 REPEATABLE READ 对幻读的处理:** -但是!InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况: +标准的 SQL 隔离级别定义里,REPEATABLE READ 是无法防止幻读的。但 InnoDB 的实现通过以下机制很大程度上避免了幻读: -- **快照读**:由 MVCC 机制来保证不出现幻读。 -- **当前读**:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。 +- **快照读 (Snapshot Read)**:普通的 SELECT 语句,通过 **MVCC** 机制实现。事务启动时创建一个数据快照,后续的快照读都读取这个版本的数据,从而避免了看到其他事务新插入的行(幻读)或修改的行(不可重复读)。 +- **当前读 (Current Read)**:像 `SELECT ... FOR UPDATE`, `SELECT ... LOCK IN SHARE MODE`, `INSERT`, `UPDATE`, `DELETE` 这些操作。InnoDB 使用 **Next-Key Lock** 来锁定扫描到的索引记录及其间的范围(间隙),防止其他事务在这个范围内插入新的记录,从而避免幻读。Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的组合。 -因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED** ,但是你要知道的是 InnoDB 存储引擎默认使用 **REPEATABLE-READ** 并不会有任何性能损失。 +值得注意的是,虽然通常认为隔离级别越高、并发性越差,但 InnoDB 存储引擎通过 MVCC 机制优化了 REPEATABLE READ 级别。对于许多常见的只读或读多写少的场景,其性能**与 READ COMMITTED 相比可能没有显著差异**。不过,在写密集型且并发冲突较高的场景下,RR 的间隙锁机制可能会比 RC 带来更多的锁等待。 -InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE 隔离级别。 +此外,在某些特定场景下,如需要严格一致性的分布式事务(XA Transactions),InnoDB 可能要求或推荐使用 SERIALIZABLE 隔离级别来确保全局数据的一致性。 《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到: diff --git a/docs/java/basis/serialization.md b/docs/java/basis/serialization.md index fb7d3b69..f6ab9071 100644 --- a/docs/java/basis/serialization.md +++ b/docs/java/basis/serialization.md @@ -83,7 +83,11 @@ public class RpcRequest implements Serializable { ~~`static` 修饰的变量是静态变量,位于方法区,本身是不会被序列化的。 `static` 变量是属于类的而不是对象。你反序列之后,`static` 变量的值就像是默认赋予给了对象一样,看着就像是 `static` 变量被序列化,实际只是假象罢了。~~ -**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**:`static` 修饰的变量是静态变量,属于类而非类的实例,本身是不会被序列化的。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中;在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。 +**🐛 修正(参见:[issue#2174](https://github.com/Snailclimb/JavaGuide/issues/2174))**: + +通常情况下,`static` 变量是属于类的,不属于任何单个对象实例,所以它们本身不会被包含在对象序列化的数据流里。序列化保存的是对象的状态(也就是实例变量的值)。然而,`serialVersionUID` 是一个特例,`serialVersionUID` 的序列化做了特殊处理。关键在于,`serialVersionUID` 不是作为对象状态的一部分被序列化的,而是被序列化机制本身用作一个特殊的“指纹”或“版本号”。 + +当一个对象被序列化时,`serialVersionUID` 会被写入到序列化的二进制流中(像是在保存一个版本号,而不是保存 `static` 变量本身的状态);在反序列化时,也会解析它并做一致性判断,以此来验证序列化对象的版本一致性。如果两者不匹配,反序列化过程将抛出 `InvalidClassException`,因为这通常意味着序列化的类的定义已经发生了更改,可能不再兼容。 官方说明如下: @@ -91,7 +95,7 @@ public class RpcRequest implements Serializable { > > 如果想显式指定 `serialVersionUID` ,则需要在类中使用 `static` 和 `final` 关键字来修饰一个 `long` 类型的变量,变量名字必须为 `"serialVersionUID"` 。 -也就是说,`serialVersionUID` 只是用来被 JVM 识别,实际并没有被序列化。 +也就是说,`serialVersionUID` 本身(作为 static 变量)确实不作为对象状态被序列化。但是,它的值被 Java 序列化机制特殊处理了——作为一个版本标识符被读取并写入序列化流中,用于在反序列化时进行版本兼容性检查。 **如果有些字段不想进行序列化怎么办?** diff --git a/docs/system-design/framework/spring/spring-common-annotations.md b/docs/system-design/framework/spring/spring-common-annotations.md index 57ec89ca..e51fb4b9 100644 --- a/docs/system-design/framework/spring/spring-common-annotations.md +++ b/docs/system-design/framework/spring/spring-common-annotations.md @@ -6,21 +6,19 @@ tag: - Spring --- -### 0.前言 - -可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了! +可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解本文都提供了具体用法,掌握这些内容后,使用 Spring Boot 来开发项目基本没啥大问题了! **为什么要写这篇文章?** -最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。 +最近看到网上有一篇关于 Spring Boot 常用注解的文章被广泛转载,但文章内容存在一些误导性,可能对没有太多实际使用经验的开发者不太友好。于是我花了几天时间总结了这篇文章,希望能够帮助大家更好地理解和使用 Spring 注解。 -**因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 感激不尽!** +**因为个人能力和精力有限,如果有任何错误或遗漏,欢迎指正!非常感激!** -### 1. `@SpringBootApplication` +## Spring Boot 基础注解 -这里先单独拎出`@SpringBootApplication` 注解说一下,虽然我们一般不会主动去使用它。 +`@SpringBootApplication` 是 Spring Boot 应用的核心注解,通常用于标注主启动类。 -_Guide:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。_ +示例: ```java @SpringBootApplication @@ -31,7 +29,13 @@ public class SpringSecurityJwtGuideApplication { } ``` -我们可以把 `@SpringBootApplication`看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合。 +我们可以把 `@SpringBootApplication`看作是下面三个注解的组合: + +- **`@EnableAutoConfiguration`**:启用 Spring Boot 的自动配置机制。 +- **`@ComponentScan`**:扫描 `@Component`、`@Service`、`@Repository`、`@Controller` 等注解的类。 +- **`@Configuration`**:允许注册额外的 Spring Bean 或导入其他配置类。 + +源码如下: ```java package org.springframework.boot.autoconfigure; @@ -58,87 +62,233 @@ public @interface SpringBootConfiguration { } ``` -根据 SpringBoot 官网,这三个注解的作用分别是: +## Spring Bean -- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制 -- `@ComponentScan`:扫描被`@Component` (`@Repository`,`@Service`,`@Controller`)注解的 bean,注解默认会扫描该类所在的包下所有的类。 -- `@Configuration`:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类 +### 依赖注入(Dependency Injection, DI) -### 2. Spring Bean 相关 +`@Autowired` 用于自动注入依赖项(即其他 Spring Bean)。它可以标注在构造器、字段、Setter 方法或配置方法上,Spring 容器会自动查找匹配类型的 Bean 并将其注入。 -#### 2.1. `@Autowired` +```java +@Service +public class UserServiceImpl implements UserService { + // ... +} -自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。 +@RestController +public class UserController { + // 字段注入 + @Autowired + private UserService userService; + // ... +} +``` + +当存在多个相同类型的 Bean 时,`@Autowired` 默认按类型注入可能产生歧义。此时,可以与 `@Qualifier` 结合使用,通过指定 Bean 的名称来精确选择需要注入的实例。 + +```java +@Repository("userRepositoryA") +public class UserRepositoryA implements UserRepository { /* ... */ } + +@Repository("userRepositoryB") +public class UserRepositoryB implements UserRepository { /* ... */ } + +@Service +public class UserService { + @Autowired + @Qualifier("userRepositoryA") // 指定注入名为 "userRepositoryA" 的 Bean + private UserRepository userRepository; + // ... +} +``` + +`@Primary`同样是为了解决同一类型存在多个 Bean 实例的注入问题。在 Bean 定义时(例如使用 `@Bean` 或类注解)添加 `@Primary` 注解,表示该 Bean 是**首选**的注入对象。当进行 `@Autowired` 注入时,如果没有使用 `@Qualifier` 指定名称,Spring 将优先选择带有 `@Primary` 的 Bean。 + +```java +@Primary // 将 UserRepositoryA 设为首选注入对象 +@Repository("userRepositoryA") +public class UserRepositoryA implements UserRepository { /* ... */ } + +@Repository("userRepositoryB") +public class UserRepositoryB implements UserRepository { /* ... */ } + +@Service +public class UserService { + @Autowired // 会自动注入 UserRepositoryA,因为它是 @Primary + private UserRepository userRepository; + // ... +} +``` + +`@Resource(name="beanName")`是 JSR-250 规范定义的注解,也用于依赖注入。它默认按**名称 (by Name)** 查找 Bean 进行注入,而 `@Autowired`默认按**类型 (by Type)** 。如果未指定 `name` 属性,它会尝试根据字段名或方法名查找,如果找不到,则回退到按类型查找(类似 `@Autowired`)。 + +`@Resource`只能标注在字段 和 Setter 方法上,不支持构造器注入。 ```java @Service public class UserService { - ...... -} - -@RestController -@RequestMapping("/users") -public class UserController { - @Autowired - private UserService userService; - ...... + @Resource(name = "userRepositoryA") + private UserRepository userRepository; + // ... } ``` -#### 2.2. `@Component`,`@Repository`,`@Service`, `@Controller` +### Bean 作用域 -我们一般使用 `@Autowired` 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 `@Autowired` 注解自动装配的 bean 的类,可以采用以下注解实现: +`@Scope("scopeName")` 定义 Spring Bean 的作用域,即 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。 + +```java +@Component +// 每次获取都会创建新的 PrototypeBean 实例 +@Scope("prototype") +public class PrototypeBean { + // ... +} +``` + +### Bean 注册 + +Spring 容器需要知道哪些类需要被管理为 Bean。除了使用 `@Bean` 方法显式声明(通常在 `@Configuration` 类中),更常见的方式是使用 Stereotype(构造型) 注解标记类,并配合组件扫描(Component Scanning)机制,让 Spring 自动发现并注册这些类作为 Bean。这些 Bean 后续可以通过 `@Autowired` 等方式注入到其他组件中。 + +下面是常见的一些注册 Bean 的注解: - `@Component`:通用的注解,可标注任意类为 `Spring` 组件。如果一个 Bean 不知道属于哪个层,可以使用`@Component` 注解标注。 - `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 - `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。 - `@Controller` : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 +- `@RestController`:一个组合注解,等效于 `@Controller` + `@ResponseBody`。它专门用于构建 RESTful Web 服务的控制器。标注了 `@RestController` 的类,其所有处理器方法(handler methods)的返回值都会被自动序列化(通常为 JSON)并写入 HTTP 响应体,而不是被解析为视图名称。 -#### 2.3. `@RestController` +`@Controller` vs `@RestController`: -`@RestController`注解是`@Controller`和`@ResponseBody`的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。 - -_Guide:现在都是前后端分离,说实话我已经很久没有用过`@Controller`。如果你的项目太老了的话,就当我没说。_ - -单独使用 `@Controller` 不加 `@ResponseBody`的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。`@Controller` +`@ResponseBody` 返回 JSON 或 XML 形式数据 +- `@Controller`:主要用于传统的 Spring MVC 应用,方法返回值通常是逻辑视图名,需要视图解析器配合渲染页面。如果需要返回数据(如 JSON),则需要在方法上额外添加 `@ResponseBody` 注解。 +- `@RestController`:专为构建返回数据的 RESTful API 设计。类上使用此注解后,所有方法的返回值都会默认被视为响应体内容(相当于每个方法都隐式添加了 `@ResponseBody`),通常用于返回 JSON 或 XML 数据。在现代前后端分离的应用中,`@RestController` 是更常用的选择。 关于`@RestController` 和 `@Controller`的对比,请看这篇文章:[@RestController vs @Controller](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485544&idx=1&sn=3cc95b88979e28fe3bfe539eb421c6d8&chksm=cea247a3f9d5ceb5e324ff4b8697adc3e828ecf71a3468445e70221cce768d1e722085359907&token=1725092312&lang=zh_CN#rd)。 -#### 2.4. `@Scope` +## 配置 -声明 Spring Bean 的作用域,使用方法: +### 声明配置类 -```java -@Bean -@Scope("singleton") -public Person personSingleton() { - return new Person(); -} -``` - -**四种常见的 Spring Bean 的作用域:** - -- singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 -- prototype : 每次请求都会创建一个新的 bean 实例。 -- request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。 -- session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。 - -#### 2.5. `@Configuration` - -一般用来声明配置类,可以使用 `@Component`注解替代,不过使用`@Configuration`注解声明配置类更加语义化。 +`@Configuration` 主要用于声明一个类是 Spring 的配置类。虽然也可以用 `@Component` 注解替代,但 `@Configuration` 能够更明确地表达该类的用途(定义 Bean),语义更清晰,也便于 Spring 进行特定的处理(例如,通过 CGLIB 代理确保 `@Bean` 方法的单例行为)。 ```java @Configuration public class AppConfig { + + // @Bean 注解用于在配置类中声明一个 Bean @Bean public TransferService transferService() { return new TransferServiceImpl(); } + // 配置类中可以包含一个或多个 @Bean 方法。 } ``` -### 3. 处理常见的 HTTP 请求类型 +### 读取配置信息 + +在应用程序开发中,我们经常需要管理一些配置信息,例如数据库连接细节、第三方服务(如阿里云 OSS、短信服务、微信认证)的密钥或地址等。通常,这些信息会**集中存放在配置文件**(如 `application.yml` 或 `application.properties`)中,方便管理和修改。 + +Spring 提供了多种便捷的方式来读取这些配置信息。假设我们有如下 `application.yml` 文件: + +```yaml +wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! + +my-profile: + name: Guide哥 + email: koushuangbwcx@163.com + +library: + location: 湖北武汉加油中国加油 + books: + - name: 天才基本法 + description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 + - name: 时间的秩序 + description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 + - name: 了不起的我 + description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? +``` + +下面介绍几种常用的读取配置的方式: + +1、`@Value("${property.key}")` 注入配置文件(如 `application.properties` 或 `application.yml`)中的单个属性值。它还支持 Spring 表达式语言 (SpEL),可以实现更复杂的注入逻辑。 + +```java +@Value("${wuhan2020}") +String wuhan2020; +``` + +2、`@ConfigurationProperties`可以读取配置信息并与 Bean 绑定,用的更多一些。 + +```java +@Component +@ConfigurationProperties(prefix = "library") +class LibraryProperties { + @NotEmpty + private String location; + private List books; + + @Setter + @Getter + @ToString + static class Book { + String name; + String description; + } + 省略getter/setter + ...... +} +``` + +你可以像使用普通的 Spring Bean 一样,将其注入到类中使用。 + +```java +@Service +public class LibraryService { + + private final LibraryProperties libraryProperties; + + @Autowired + public LibraryService(LibraryProperties libraryProperties) { + this.libraryProperties = libraryProperties; + } + + public void printLibraryInfo() { + System.out.println(libraryProperties); + } +} +``` + +### 加载指定的配置文件 + +`@PropertySource` 注解允许加载自定义的配置文件。适用于需要将部分配置信息独立存储的场景。 + +```java +@Component +@PropertySource("classpath:website.properties") + +class WebSite { + @Value("${url}") + private String url; + + 省略getter/setter + ...... +} +``` + +**注意**:当使用 `@PropertySource` 时,确保外部文件路径正确,且文件在类路径(classpath)中。 + +更多内容请查看我的这篇文章:[10 分钟搞定 SpringBoot 如何优雅读取配置文件?](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。 + +## MVC + +### HTTP 请求 **5 种常见的请求类型:** @@ -148,9 +298,9 @@ public class AppConfig { - **DELETE**:从服务器删除特定的资源。举个例子:`DELETE /users/12`(删除编号为 12 的学生) - **PATCH**:更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。 -#### 3.1. GET 请求 +#### GET 请求 -`@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)` +`@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)`。 ```java @GetMapping("/users") @@ -159,11 +309,11 @@ public ResponseEntity> getAllUsers() { } ``` -#### 3.2. POST 请求 +#### POST 请求 -`@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)` +`@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)`。 -关于`@RequestBody`注解的使用,在下面的“前后端传值”这块会讲到。 +`@PostMapping` 通常与 `@RequestBody` 配合,用于接收 JSON 数据并映射为 Java 对象。 ```java @PostMapping("/users") @@ -172,9 +322,9 @@ public ResponseEntity createUser(@Valid @RequestBody UserCreateRequest use } ``` -#### 3.3. PUT 请求 +#### PUT 请求 -`@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)` +`@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)`。 ```java @PutMapping("/users/{userId}") @@ -184,7 +334,7 @@ public ResponseEntity updateUser(@PathVariable(value = "userId") Long user } ``` -#### 3.4. **DELETE 请求** +#### DELETE 请求 `@DeleteMapping("/users/{userId}")`等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)` @@ -195,7 +345,7 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ } ``` -#### 3.5. **PATCH 请求** +#### PATCH 请求 一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。 @@ -207,32 +357,40 @@ public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){ } ``` -### 4. 前后端传值 +### 参数绑定 -**掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!** +在处理 HTTP 请求时,Spring MVC 提供了多种注解用于绑定请求参数到方法参数中。以下是常见的参数绑定方式: -#### 4.1. `@PathVariable` 和 `@RequestParam` +#### 从 URL 路径中提取参数 -`@PathVariable`用于获取路径参数,`@RequestParam`用于获取查询参数。 - -举个简单的例子: +`@PathVariable` 用于从 URL 路径中提取参数。例如: ```java @GetMapping("/klasses/{klassId}/teachers") -public List getKlassRelatedTeachers( - @PathVariable("klassId") Long klassId, - @RequestParam(value = "type", required = false) String type ) { -... +public List getTeachersByClass(@PathVariable("klassId") Long klassId) { + return teacherService.findTeachersByClass(klassId); } ``` -如果我们请求的 url 是:`/klasses/123456/teachers?type=web` +若请求 URL 为 `/klasses/123/teachers`,则 `klassId = 123`。 -那么我们服务获取到的数据就是:`klassId=123456,type=web`。 +#### 绑定查询参数 -#### 4.2. `@RequestBody` +`@RequestParam` 用于绑定查询参数。例如: -用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 +```java +@GetMapping("/klasses/{klassId}/teachers") +public List getTeachersByClass(@PathVariable Long klassId, + @RequestParam(value = "type", required = false) String type) { + return teacherService.findTeachersByClassAndType(klassId, type); +} +``` + +若请求 URL 为 `/klasses/123/teachers?type=web`,则 `klassId = 123`,`type = web`。 + +#### 绑定请求体中的 JSON 数据 + +`@RequestBody` 用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且**Content-Type 为 application/json** 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用`HttpMessageConverter`或者自定义的`HttpMessageConverter`将请求的 body 中的 json 字符串转换为 java 对象。 我用一个简单的例子来给演示一下基本使用! @@ -272,91 +430,14 @@ public class UserRegisterRequest { ![](./images/spring-annotations/@RequestBody.png) -👉 需要注意的是:**一个请求方法只可以有一个`@RequestBody`,但是可以有多个`@RequestParam`和`@PathVariable`**。 如果你的方法必须要用两个 `@RequestBody`来接受数据的话,大概率是你的数据库设计或者系统设计出问题了! +**注意**: -### 5. 读取配置信息 +- 一个方法只能有一个 `@RequestBody` 参数,但可以有多个 `@PathVariable` 和 `@RequestParam`。 +- 如果需要接收多个复杂对象,建议合并成一个单一对象。 -**很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。** +## 数据校验 -**下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。** - -我们的数据源`application.yml`内容如下: - -```yaml -wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油! - -my-profile: - name: Guide哥 - email: koushuangbwcx@163.com - -library: - location: 湖北武汉加油中国加油 - books: - - name: 天才基本法 - description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。 - - name: 时间的秩序 - description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。 - - name: 了不起的我 - description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻? -``` - -#### 5.1. `@Value`(常用) - -使用 `@Value("${property}")` 读取比较简单的配置信息: - -```java -@Value("${wuhan2020}") -String wuhan2020; -``` - -#### 5.2. `@ConfigurationProperties`(常用) - -通过`@ConfigurationProperties`读取配置信息并与 bean 绑定。 - -```java -@Component -@ConfigurationProperties(prefix = "library") -class LibraryProperties { - @NotEmpty - private String location; - private List books; - - @Setter - @Getter - @ToString - static class Book { - String name; - String description; - } - 省略getter/setter - ...... -} -``` - -你可以像使用普通的 Spring bean 一样,将其注入到类中使用。 - -#### 5.3. `@PropertySource`(不常用) - -`@PropertySource`读取指定 properties 文件 - -```java -@Component -@PropertySource("classpath:website.properties") - -class WebSite { - @Value("${url}") - private String url; - - 省略getter/setter - ...... -} -``` - -更多内容请查看我的这篇文章:[《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486181&idx=2&sn=10db0ae64ef501f96a5b0dbc4bd78786&chksm=cea2452ef9d5cc384678e456427328600971180a77e40c13936b19369672ca3e342c26e92b50&token=816772476&lang=zh_CN#rd) 。 - -### 6. 参数校验 - -**数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。** +数据校验是保障系统稳定性和安全性的关键环节。即使在用户界面(前端)已经实施了数据校验,**后端服务仍必须对接收到的数据进行再次校验**。这是因为前端校验可以被轻易绕过(例如,通过开发者工具修改请求或使用 Postman、curl 等 HTTP 工具直接调用 API),恶意或错误的数据可能直接发送到后端。因此,后端校验是防止非法数据、维护数据一致性、确保业务逻辑正确执行的最后一道,也是最重要的一道防线。 Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 349, 380),它提供了一系列注解,可以直接用于 JavaBean 的属性上,从而实现便捷的参数校验。 @@ -364,46 +445,58 @@ Bean Validation 是一套定义 JavaBean 参数校验标准的规范 (JSR 303, 3 - **JSR 349 (Bean Validation 1.1):** 在 1.0 基础上进行扩展,例如引入了对方法参数和返回值校验的支持、增强了对分组校验(Group Validation)的处理。 - **JSR 380 (Bean Validation 2.0):** 拥抱 Java 8 的新特性,并进行了一些改进,例如支持 `java.time` 包中的日期和时间类型、引入了一些新的校验注解(如 `@NotEmpty`, `@NotBlank`等)。 -校验的时候我们实际用的是 **Hibernate Validator** 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。 +Bean Validation 本身只是一套**规范(接口和注解)**,我们需要一个实现了这套规范的**具体框架**来执行校验逻辑。目前,**Hibernate Validator** 是 Bean Validation 规范最权威、使用最广泛的参考实现。 -SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成): +- Hibernate Validator 4.x 实现了 Bean Validation 1.0 (JSR 303)。 +- Hibernate Validator 5.x 实现了 Bean Validation 1.1 (JSR 349)。 +- Hibernate Validator 6.x 及更高版本实现了 Bean Validation 2.0 (JSR 380)。 -**注**:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 `spring-boot-starter-validation` 依赖。 +在 Spring Boot 项目中使用 Bean Validation 非常方便,这得益于 Spring Boot 的自动配置能力。关于依赖引入,需要注意: + +- 在较早版本的 Spring Boot(通常指 2.3.x 之前)中,`spring-boot-starter-web` 依赖默认包含了 hibernate-validator。因此,只要引入了 Web Starter,就无需额外添加校验相关的依赖。 +- 从 Spring Boot 2.3.x 版本开始,为了更精细化的依赖管理,校验相关的依赖被移出了 spring-boot-starter-web。如果你的项目使用了这些或更新的版本,并且需要 Bean Validation 功能,那么你需要显式地添加 `spring-boot-starter-validation` 依赖: + +```xml + + org.springframework.boot + spring-boot-starter-validation + +``` ![](https://oss.javaguide.cn/2021/03/c7bacd12-1c1a-4e41-aaaf-4cad840fc073.png) -非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》。 +非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)。 -👉 需要注意的是:**所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints`** +👉 需要注意的是:所有的注解,推荐使用 JSR 注解,即`javax.validation.constraints`,而不是`org.hibernate.validator.constraints` -#### 6.1. 一些常用的字段验证的注解 +### 一些常用的字段验证的注解 -- `@NotEmpty` 被注释的字符串的不能为 null 也不能为空 -- `@NotBlank` 被注释的字符串非 null,并且必须包含一个非空白字符 -- `@Null` 被注释的元素必须为 null -- `@NotNull` 被注释的元素必须不为 null -- `@AssertTrue` 被注释的元素必须为 true -- `@AssertFalse` 被注释的元素必须为 false -- `@Pattern(regex=,flag=)`被注释的元素必须符合指定的正则表达式 -- `@Email` 被注释的元素必须是 Email 格式。 -- `@Min(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@Max(value)`被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@DecimalMin(value)`被注释的元素必须是一个数字,其值必须大于等于指定的最小值 -- `@DecimalMax(value)` 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 -- `@Size(max=, min=)`被注释的元素的大小必须在指定的范围内 -- `@Digits(integer, fraction)`被注释的元素必须是一个数字,其值必须在可接受的范围内 -- `@Past`被注释的元素必须是一个过去的日期 -- `@Future` 被注释的元素必须是一个将来的日期 -- …… +Bean Validation 规范及其实现(如 Hibernate Validator)提供了丰富的注解,用于声明式地定义校验规则。以下是一些常用的注解及其说明: -#### 6.2. 验证请求体(RequestBody) +- `@NotNull`: 检查被注解的元素(任意类型)不能为 `null`。 +- `@NotEmpty`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)不能为 `null` 且其大小/长度不能为 0。注意:对于字符串,`@NotEmpty` 允许包含空白字符的字符串,如 `" "`。 +- `@NotBlank`: 检查被注解的 `CharSequence`(如 `String`)不能为 `null`,并且去除首尾空格后的长度必须大于 0。(即,不能为空白字符串)。 +- `@Null`: 检查被注解的元素必须为 `null`。 +- `@AssertTrue` / `@AssertFalse`: 检查被注解的 `boolean` 或 `Boolean` 类型元素必须为 `true` / `false`。 +- `@Min(value)` / `@Max(value)`: 检查被注解的数字类型(或其字符串表示)的值必须大于等于 / 小于等于指定的 `value`。适用于整数类型(`byte`、`short`、`int`、`long`、`BigInteger` 等)。 +- `@DecimalMin(value)` / `@DecimalMax(value)`: 功能类似 `@Min` / `@Max`,但适用于包含小数的数字类型(`BigDecimal`、`BigInteger`、`CharSequence`、`byte`、`short`、`int`、`long`及其包装类)。 `value` 必须是数字的字符串表示。 +- `@Size(min=, max=)`: 检查被注解的元素(如 `CharSequence`、`Collection`、`Map`、`Array`)的大小/长度必须在指定的 `min` 和 `max` 范围之内(包含边界)。 +- `@Digits(integer=, fraction=)`: 检查被注解的数字类型(或其字符串表示)的值,其整数部分的位数必须 ≤ `integer`,小数部分的位数必须 ≤ `fraction`。 +- `@Pattern(regexp=, flags=)`: 检查被注解的 `CharSequence`(如 `String`)是否匹配指定的正则表达式 (`regexp`)。`flags` 可以指定匹配模式(如不区分大小写)。 +- `@Email`: 检查被注解的 `CharSequence`(如 `String`)是否符合 Email 格式(内置了一个相对宽松的正则表达式)。 +- `@Past` / `@Future`: 检查被注解的日期或时间类型(`java.util.Date`、`java.util.Calendar`、JSR 310 `java.time` 包下的类型)是否在当前时间之前 / 之后。 +- `@PastOrPresent` / `@FutureOrPresent`: 类似 `@Past` / `@Future`,但允许等于当前时间。 +- ...... + +### 验证请求体(RequestBody) + +当 Controller 方法使用 `@RequestBody` 注解来接收请求体并将其绑定到一个对象时,可以在该参数前添加 `@Valid` 注解来触发对该对象的校验。如果验证失败,它将抛出`MethodArgumentNotValidException`。 ```java @Data @AllArgsConstructor @NoArgsConstructor public class Person { - @NotNull(message = "classId 不能为空") private String classId; @@ -418,17 +511,12 @@ public class Person { @Email(message = "email 格式不正确") @NotNull(message = "email 不能为空") private String email; - } -``` -我们在需要验证的参数上加上了`@Valid`注解,如果验证失败,它将抛出`MethodArgumentNotValidException`。 -```java @RestController @RequestMapping("/api") public class PersonController { - @PostMapping("/person") public ResponseEntity getPerson(@RequestBody @Valid Person person) { return ResponseEntity.ok().body(person); @@ -436,26 +524,45 @@ public class PersonController { } ``` -#### 6.3. 验证请求参数(Path Variables 和 Request Parameters) +### 验证请求参数(Path Variables 和 Request Parameters) -**一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。** +对于直接映射到方法参数的简单类型数据(如路径变量 `@PathVariable` 或请求参数 `@RequestParam`),校验方式略有不同: + +1. **在 Controller 类上添加 `@Validated` 注解**:这个注解是 Spring 提供的(非 JSR 标准),它使得 Spring 能够处理方法级别的参数校验注解。**这是必需步骤。** +2. **将校验注解直接放在方法参数上**:将 `@Min`, `@Max`, `@Size`, `@Pattern` 等校验注解直接应用于对应的 `@PathVariable` 或 `@RequestParam` 参数。 + +一定一定不要忘记在类上加上 `@Validated` 注解了,这个参数可以告诉 Spring 去校验方法参数。 ```java @RestController @RequestMapping("/api") -@Validated +@Validated // 关键步骤 1: 必须在类上添加 @Validated public class PersonController { @GetMapping("/person/{id}") - public ResponseEntity getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) { + public ResponseEntity getPersonByID( + @PathVariable("id") + @Max(value = 5, message = "ID 不能超过 5") // 关键步骤 2: 校验注解直接放在参数上 + Integer id + ) { + // 如果传入的 id > 5,Spring 会在进入方法体前抛出 ConstraintViolationException 异常。 + // 全局异常处理器同样需要处理此异常。 return ResponseEntity.ok().body(id); } + + @GetMapping("/person") + public ResponseEntity findPersonByName( + @RequestParam("name") + @NotBlank(message = "姓名不能为空") // 同样适用于 @RequestParam + @Size(max = 10, message = "姓名长度不能超过 10") + String name + ) { + return ResponseEntity.ok().body("Found person: " + name); + } } ``` -更多关于如何在 Spring 项目中进行参数校验的内容,请看《[如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485783&idx=1&sn=a407f3b75efa17c643407daa7fb2acd6&chksm=cea2469cf9d5cf8afbcd0a8a1c9cc4294d6805b8e01bee6f76bb2884c5bc15478e91459def49&token=292197051&lang=zh_CN#rd)》这篇文章。 - -### 7. 全局处理 Controller 层异常 +## 全局异常处理 介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。 @@ -486,34 +593,61 @@ public class GlobalExceptionHandler { 1. [SpringBoot 处理异常的几种常见姿势](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485568&idx=2&sn=c5ba880fd0c5d82e39531fa42cb036ac&chksm=cea2474bf9d5ce5dcbc6a5f6580198fdce4bc92ef577579183a729cb5d1430e4994720d59b34&token=2133161636&lang=zh_CN#rd) 2. [使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486379&idx=2&sn=48c29ae65b3ed874749f0803f0e4d90e&chksm=cea24460f9d5cd769ed53ad7e17c97a7963a89f5350e370be633db0ae8d783c3a3dbd58c70f8&token=1054498516&lang=zh_CN#rd) -### 8. JPA 相关 +## 事务 -#### 8.1. 创建表 +在要开启事务的方法上使用`@Transactional`注解即可! -`@Entity`声明一个类对应一个数据库实体。 +```java +@Transactional(rollbackFor = Exception.class) +public void save() { + ...... +} -`@Table` 设置表名 +``` + +我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 + +`@Transactional` 注解一般可以作用在`类`或者`方法`上。 + +- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 +- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 + +更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 + +## JPA + +Spring Data JPA 提供了一系列注解和功能,帮助开发者轻松实现 ORM(对象关系映射)。 + +### 创建表 + +`@Entity` 用于声明一个类为 JPA 实体类,与数据库中的表映射。`@Table` 指定实体对应的表名。 ```java @Entity @Table(name = "role") public class Role { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; private String description; - 省略getter/setter...... + + // 省略 getter/setter } ``` -#### 8.2. 创建主键 +### 主键生成策略 -`@Id`:声明一个字段为主键。 +`@Id`声明字段为主键。`@GeneratedValue` 指定主键的生成策略。 -使用`@Id`声明之后,我们还需要定义主键的生成策略。我们可以使用 `@GeneratedValue` 指定主键生成策略。 +JPA 提供了 4 种主键生成策略: -**1.通过 `@GeneratedValue`直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。** +- **`GenerationType.TABLE`**:通过数据库表生成主键。 +- **`GenerationType.SEQUENCE`**:通过数据库序列生成主键(适用于 Oracle 等数据库)。 +- **`GenerationType.IDENTITY`**:主键自增长(适用于 MySQL 等数据库)。 +- **`GenerationType.AUTO`**:由 JPA 自动选择合适的生成策略(默认策略)。 ```java @Id @@ -521,51 +655,7 @@ public class Role { private Long id; ``` -JPA 使用枚举定义了 4 种常见的主键生成策略,如下: - -_Guide:枚举替代常量的一种用法_ - -```java -public enum GenerationType { - - /** - * 使用一个特定的数据库表格来保存主键 - * 持久化引擎通过关系数据库的一张特定的表格来生成主键, - */ - TABLE, - - /** - *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键 - */ - SEQUENCE, - - /** - * 主键自增长 - */ - IDENTITY, - - /** - *把主键生成策略交给持久化引擎(persistence engine), - *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种 - */ - AUTO -} - -``` - -`@GeneratedValue`注解默认使用的策略是`GenerationType.AUTO` - -```java -public @interface GeneratedValue { - - GenerationType strategy() default AUTO; - String generator() default ""; -} -``` - -一般使用 MySQL 数据库的话,使用`GenerationType.IDENTITY`策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。 - -**2.通过 `@GenericGenerator`声明一个主键策略,然后 `@GeneratedValue`使用这个策略** +通过 `@GenericGenerator` 声明自定义主键生成策略: ```java @Id @@ -582,7 +672,7 @@ private Long id; private Long id; ``` -jpa 提供的主键生成策略有如下几种: +JPA 提供的主键生成策略有如下几种: ```java public class DefaultIdentifierGeneratorFactory @@ -617,148 +707,112 @@ public class DefaultIdentifierGeneratorFactory } ``` -#### 8.3. 设置字段类型 +### 字段映射 -`@Column` 声明字段。 +`@Column` 用于指定实体字段与数据库列的映射关系。 -**示例:** - -设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空 +- **`name`**:指定数据库列名。 +- **`nullable`**:指定是否允许为 `null`。 +- **`length`**:设置字段的长度(仅适用于 `String` 类型)。 +- **`columnDefinition`**:指定字段的数据库类型和默认值。 ```java -@Column(name = "user_name", nullable = false, length=32) +@Column(name = "user_name", nullable = false, length = 32) private String userName; -``` -设置字段类型并且加默认值,这个还是挺常用的。 - -```java @Column(columnDefinition = "tinyint(1) default 1") private Boolean enabled; ``` -#### 8.4. 指定不持久化特定字段 +### 忽略字段 -`@Transient`:声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。 - -如果我们想让`secrect` 这个字段不被持久化,可以使用 `@Transient`关键字声明。 +`@Transient` 用于声明不需要持久化的字段。 ```java -@Entity(name="USER") +@Entity public class User { - ...... @Transient - private String secrect; // not persistent because of @Transient - + private String temporaryField; // 不会映射到数据库表中 } ``` -除了 `@Transient`关键字声明, 还可以采用下面几种方法: +其他不被持久化的字段方式: -```java -static String secrect; // not persistent because of static -final String secrect = "Satish"; // not persistent because of final -transient String secrect; // not persistent because of transient -``` +- **`static`**:静态字段不会被持久化。 +- **`final`**:最终字段不会被持久化。 +- **`transient`**:使用 Java 的 `transient` 关键字声明的字段不会被序列化或持久化。 -一般使用注解的方式比较多。 +### 大字段存储 -#### 8.5. 声明大字段 - -`@Lob`:声明某个字段为大字段。 +`@Lob` 用于声明大字段(如 `CLOB` 或 `BLOB`)。 ```java @Lob -private String content; -``` - -更详细的声明: - -```java -@Lob -//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟加载,而 FetchType.LAZY 表示延迟加载 ; -@Basic(fetch = FetchType.EAGER) -//columnDefinition 属性指定数据表对应的 Lob 字段类型 @Column(name = "content", columnDefinition = "LONGTEXT NOT NULL") private String content; ``` -#### 8.6. 创建枚举类型的字段 +### 枚举类型映射 -可以使用枚举类型的字段,不过枚举字段要用`@Enumerated`注解修饰。 +`@Enumerated` 用于将枚举类型映射为数据库字段。 + +- **`EnumType.ORDINAL`**:存储枚举的序号(默认)。 +- **`EnumType.STRING`**:存储枚举的名称(推荐)。 ```java public enum Gender { - MALE("男性"), - FEMALE("女性"); - - private String value; - Gender(String str){ - value=str; - } + MALE, + FEMALE } -``` -```java @Entity -@Table(name = "role") -public class Role { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; - private String description; +public class User { + @Enumerated(EnumType.STRING) private Gender gender; - 省略getter/setter...... } ``` -数据库里面对应存储的是 MALE/FEMALE。 +数据库中存储的值为 `MALE` 或 `FEMALE`。 -#### 8.7. 增加审计功能 +### 审计功能 -只要继承了 `AbstractAuditBase`的类都会默认加上下面四个字段。 +通过 JPA 的审计功能,可以在实体中自动记录创建时间、更新时间、创建人和更新人等信息。 + +审计基类: ```java @Data -@AllArgsConstructor -@NoArgsConstructor @MappedSuperclass -@EntityListeners(value = AuditingEntityListener.class) +@EntityListeners(AuditingEntityListener.class) public abstract class AbstractAuditBase { @CreatedDate @Column(updatable = false) - @JsonIgnore private Instant createdAt; @LastModifiedDate - @JsonIgnore private Instant updatedAt; @CreatedBy @Column(updatable = false) - @JsonIgnore private String createdBy; @LastModifiedBy - @JsonIgnore private String updatedBy; } - ``` -我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目): +配置审计功能: ```java - @Configuration @EnableJpaAuditing -public class AuditSecurityConfiguration { +public class AuditConfig { + @Bean - AuditorAware auditorAware() { + public AuditorAware auditorProvider() { return () -> Optional.ofNullable(SecurityContextHolder.getContext()) .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) @@ -770,101 +824,101 @@ public class AuditSecurityConfiguration { 简单介绍一下上面涉及到的一些注解: 1. `@CreatedDate`: 表示该字段为创建时间字段,在这个实体被 insert 的时候,会设置值 -2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 +2. `@CreatedBy` :表示该字段为创建人,在这个实体被 insert 的时候,会设置值 `@LastModifiedDate`、`@LastModifiedBy`同理。 +3. `@EnableJpaAuditing`:开启 JPA 审计功能。 - `@LastModifiedDate`、`@LastModifiedBy`同理。 +### 修改和删除操作 -`@EnableJpaAuditing`:开启 JPA 审计功能。 - -#### 8.8. 删除/修改数据 - -`@Modifying` 注解提示 JPA 该操作是修改操作,注意还要配合`@Transactional`注解使用。 +`@Modifying` 注解用于标识修改或删除操作,必须与 `@Transactional` 一起使用。 ```java @Repository -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository { @Modifying - @Transactional(rollbackFor = Exception.class) + @Transactional void deleteByUserName(String userName); } ``` -#### 8.9. 关联关系 +### 关联关系 -- `@OneToOne` 声明一对一关系 -- `@OneToMany` 声明一对多关系 -- `@ManyToOne` 声明多对一关系 -- `@ManyToMany` 声明多对多关系 +JPA 提供了 4 种关联关系的注解: -更多关于 Spring Boot JPA 的文章请看我的这篇文章:[一文搞懂如何在 Spring Boot 正确中使用 JPA](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485689&idx=1&sn=061b32c2222869932be5631fb0bb5260&chksm=cea24732f9d5ce24a356fb3675170e7843addbfcc79ee267cfdb45c83fc7e90babf0f20d22e1&token=292197051&lang=zh_CN#rd) 。 - -### 9. 事务 `@Transactional` - -在要开启事务的方法上使用`@Transactional`注解即可! +- **`@OneToOne`**:一对一关系。 +- **`@OneToMany`**:一对多关系。 +- **`@ManyToOne`**:多对一关系。 +- **`@ManyToMany`**:多对多关系。 ```java -@Transactional(rollbackFor = Exception.class) -public void save() { - ...... -} +@Entity +public class User { + @OneToOne + private Profile profile; + + @OneToMany(mappedBy = "user") + private List orders; +} ``` -我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事务只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事务在遇到非运行时异常时也回滚。 +## JSON 数据处理 -`@Transactional` 注解一般可以作用在`类`或者`方法`上。 +在 Web 开发中,经常需要处理 Java 对象与 JSON 格式之间的转换。Spring 通常集成 Jackson 库来完成此任务,以下是一些常用的 Jackson 注解,可以帮助我们定制化 JSON 的序列化(Java 对象转 JSON)和反序列化(JSON 转 Java 对象)过程。 -- **作用于类**:当把`@Transactional` 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。 -- **作用于方法**:当类配置了`@Transactional`,方法也配置了`@Transactional`,方法的事务会覆盖类的事务配置信息。 +### 过滤 JSON 字段 -更多关于 Spring 事务的内容请查看我的这篇文章:[可能是最漂亮的 Spring 事务管理详解](./spring-transaction.md) 。 +有时我们不希望 Java 对象的某些字段被包含在最终生成的 JSON 中,或者在将 JSON 转换为 Java 对象时不处理某些 JSON 属性。 -### 10. json 数据处理 - -#### 10.1. 过滤 json 数据 - -**`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。** +`@JsonIgnoreProperties` 作用在类上用于过滤掉特定字段不返回或者不解析。 ```java -//生成json时将userRoles属性过滤 +// 在生成 JSON 时忽略 userRoles 属性 +// 如果允许未知属性(即 JSON 中有而类中没有的属性),可以添加 ignoreUnknown = true @JsonIgnoreProperties({"userRoles"}) public class User { - private String userName; private String fullName; private String password; private List userRoles = new ArrayList<>(); + // getters and setters... } ``` -**`@JsonIgnore`一般用于类的属性上,作用和上面的`@JsonIgnoreProperties` 一样。** +`@JsonIgnore`作用于字段或` getter/setter` 方法级别,用于指定在序列化或反序列化时忽略该特定属性。 ```java - public class User { - private String userName; private String fullName; private String password; - //生成json时将userRoles属性过滤 + + // 在生成 JSON 时忽略 userRoles 属性 @JsonIgnore private List userRoles = new ArrayList<>(); + // getters and setters... } ``` -#### 10.2. 格式化 json 数据 +`@JsonIgnoreProperties` 更适用于在类定义时明确排除多个字段,或继承场景下的字段排除;`@JsonIgnore` 则更直接地用于标记单个具体字段。 -`@JsonFormat`一般用来格式化 json 数据。 +### 格式化 JSON 数据 + +`@JsonFormat` 用于指定属性在序列化和反序列化时的格式。常用于日期时间类型的格式化。 比如: ```java -@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT") +// 指定 Date 类型序列化为 ISO 8601 格式字符串,并设置时区为 GMT +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "GMT") private Date date; ``` -#### 10.3. 扁平化对象 +### 扁平化 JSON 对象 + +`@JsonUnwrapped` 注解作用于字段上,用于在序列化时将其嵌套对象的属性“提升”到当前对象的层级,反序列化时执行相反操作。这可以使 JSON 结构更扁平。 + +假设有 `Account` 类,包含 `Location` 和 `PersonInfo` 两个嵌套对象。 ```java @Getter @@ -892,7 +946,7 @@ public class Account { ``` -未扁平化之前: +未扁平化之前的 JSON 结构: ```json { @@ -907,7 +961,7 @@ public class Account { } ``` -使用`@JsonUnwrapped` 扁平对象之后: +使用`@JsonUnwrapped` 扁平对象: ```java @Getter @@ -922,6 +976,8 @@ public class Account { } ``` +扁平化后的 JSON 结构: + ```json { "provinceName": "湖北", @@ -931,36 +987,37 @@ public class Account { } ``` -### 11. 测试相关 +## 测试 -**`@ActiveProfiles`一般作用于测试类上, 用于声明生效的 Spring 配置文件。** +`@ActiveProfiles`一般作用于测试类上, 用于声明生效的 Spring 配置文件。 ```java -@SpringBootTest(webEnvironment = RANDOM_PORT) +// 指定在 RANDOM_PORT 上启动应用上下文,并激活 "test" profile +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @Slf4j public abstract class TestBase { - ...... + // Common test setup or abstract methods... } ``` -**`@Test`声明一个方法为测试方法** +`@Test` 是 JUnit 框架(通常是 JUnit 5 Jupiter)提供的注解,用于标记一个方法为测试方法。虽然不是 Spring 自身的注解,但它是执行单元测试和集成测试的基础。 -**`@Transactional`被声明的测试方法的数据会回滚,避免污染测试数据。** +`@Transactional`被声明的测试方法的数据会回滚,避免污染测试数据。 -**`@WithMockUser` Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。** +`@WithMockUser` 是 Spring Security Test 模块提供的注解,用于在测试期间模拟一个已认证的用户。可以方便地指定用户名、密码、角色(authorities)等信息,从而测试受安全保护的端点或方法。 ```java +public class MyServiceTest extends TestBase { // Assuming TestBase provides Spring context + @Test - @Transactional - @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER") - void should_import_student_success() throws Exception { - ...... + @Transactional // 测试数据将回滚 + @WithMockUser(username = "test-user", authorities = { "ROLE_TEACHER", "read" }) // 模拟一个名为 "test-user",拥有 TEACHER 角色和 read 权限的用户 + void should_perform_action_requiring_teacher_role() throws Exception { + // ... 测试逻辑 ... + // 这里可以调用需要 "ROLE_TEACHER" 权限的服务方法 } +} ``` -_暂时总结到这里吧!虽然花了挺长时间才写完,不过可能还是会一些常用的注解的被漏掉,所以,我将文章也同步到了 Github 上去,Github 地址: 欢迎完善!_ - -本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:[https://github.com/Snailclimb/JavaGuide](https://github.com/Snailclimb/JavaGuide)。 -