diff --git a/2020/08/18/install-Docker/index.html b/2020/08/18/install-Docker/index.html index c89850cc..a7afbcb7 100644 --- a/2020/08/18/install-Docker/index.html +++ b/2020/08/18/install-Docker/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index bce4a23f..4373c26e 100644 --- a/2020/08/19/docker-frequently-used-commands/index.html +++ b/2020/08/19/docker-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2020/08/27/Linux-frequently-used-commands/index.html b/2020/08/27/Linux-frequently-used-commands/index.html index c3110b63..d27fb035 100644 --- a/2020/08/27/Linux-frequently-used-commands/index.html +++ b/2020/08/27/Linux-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2020/09/04/MySQL-frequently-used-commands/index.html b/2020/09/04/MySQL-frequently-used-commands/index.html index a54ad660..94596739 100644 --- a/2020/09/04/MySQL-frequently-used-commands/index.html +++ b/2020/09/04/MySQL-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2023/05/27/how-to-install-clash-on-ubuntu/index.html b/2023/05/27/how-to-install-clash-on-ubuntu/index.html index 8b1bdff4..2622a0bf 100644 --- a/2023/05/27/how-to-install-clash-on-ubuntu/index.html +++ b/2023/05/27/how-to-install-clash-on-ubuntu/index.html @@ -27,10 +27,10 @@ - + - + @@ -230,7 +230,7 @@

更新于 - + @@ -317,8 +317,8 @@

- +
diff --git a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html index 181e882b..5238b1cb 100644 --- a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html @@ -27,10 +27,10 @@ - + - + @@ -230,7 +230,7 @@

更新于 - + @@ -325,8 +325,8 @@

- +
diff --git a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html index cb8712ec..006e3150 100644 --- a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html @@ -27,10 +27,10 @@ - + - + @@ -230,7 +230,7 @@

更新于 - + @@ -308,8 +308,8 @@

- +
diff --git a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html index b0da9f22..7df4bf23 100644 --- a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html +++ b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html @@ -27,10 +27,10 @@ - + - + @@ -230,7 +230,7 @@

更新于 - + @@ -334,8 +334,8 @@

- +
diff --git a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html index 7da3c41e..1afaa4a6 100644 --- a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html +++ b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html index e5ac1254..507282d7 100644 --- a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html +++ b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html index 4d6e7050..1eb2cda9 100644 --- a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html +++ b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html index 27124afe..6c245f9e 100644 --- a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html +++ b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2023/06/29/tmux-frequently-used-commands/index.html b/2023/06/29/tmux-frequently-used-commands/index.html index 37e6be1b..cd85e5cd 100644 --- a/2023/06/29/tmux-frequently-used-commands/index.html +++ b/2023/06/29/tmux-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2023/07/13/Java-class-loader-source-code-analysis/index.html b/2023/07/13/Java-class-loader-source-code-analysis/index.html index 5354343b..9ff072c6 100644 --- a/2023/07/13/Java-class-loader-source-code-analysis/index.html +++ b/2023/07/13/Java-class-loader-source-code-analysis/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/08/04/Spring-application-context-refresh-process/index.html b/2023/08/04/Spring-application-context-refresh-process/index.html index af0318af..b33fff3e 100644 --- a/2023/08/04/Spring-application-context-refresh-process/index.html +++ b/2023/08/04/Spring-application-context-refresh-process/index.html @@ -29,7 +29,7 @@ - + @@ -233,7 +233,7 @@

更新于 - + diff --git a/2023/08/10/how-does-Spring-load-beans/index.html b/2023/08/10/how-does-Spring-load-beans/index.html index acf17954..886ef770 100644 --- a/2023/08/10/how-does-Spring-load-beans/index.html +++ b/2023/08/10/how-does-Spring-load-beans/index.html @@ -28,7 +28,7 @@ - + @@ -232,7 +232,7 @@

更新于 - + diff --git a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html index 7f59101a..a8e7747f 100644 --- a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html +++ b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/11/03/testing-and-analysis-of-StringTable/index.html b/2023/11/03/testing-and-analysis-of-StringTable/index.html index f0d7cb76..d701583b 100644 --- a/2023/11/03/testing-and-analysis-of-StringTable/index.html +++ b/2023/11/03/testing-and-analysis-of-StringTable/index.html @@ -31,7 +31,7 @@ - + @@ -235,7 +235,7 @@

更新于 - + diff --git a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html index 073cee66..c1af55a2 100644 --- a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html +++ b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html @@ -32,7 +32,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2023/11/07/garbage-collection-in-Java/index.html b/2023/11/07/garbage-collection-in-Java/index.html index a1e1dc10..5c4dc8c5 100644 --- a/2023/11/07/garbage-collection-in-Java/index.html +++ b/2023/11/07/garbage-collection-in-Java/index.html @@ -34,7 +34,7 @@ - + @@ -238,7 +238,7 @@

更新于 - + diff --git a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html index a0b05bcf..d38651bc 100644 --- a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html +++ b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html @@ -28,7 +28,7 @@ - + @@ -232,7 +232,7 @@

更新于 - + diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html index 7b9bdd3c..cdbb7465 100644 --- a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html +++ b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html @@ -33,7 +33,7 @@ - + @@ -238,7 +238,7 @@

更新于 - + diff --git a/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html b/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html index 1f9ccf18..08781d9d 100644 --- a/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html +++ b/2023/11/18/setup-monitoring-using-grafana-and-prometheus/index.html @@ -32,7 +32,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html b/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html index b8fcb1c1..22104382 100644 --- a/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html +++ b/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html @@ -28,7 +28,7 @@ - + @@ -233,7 +233,7 @@

更新于 - + diff --git a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html index 7773d97e..65ec530b 100644 --- a/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html +++ b/2023/11/19/how-does-Spring-AOP-create-proxy-beans/index.html @@ -32,7 +32,7 @@ - + @@ -237,7 +237,7 @@

更新于 - + diff --git a/2023/11/22/circular-dependencies-in-Spring/index.html b/2023/11/22/circular-dependencies-in-Spring/index.html index 2ea8e1a7..e898a1f7 100644 --- a/2023/11/22/circular-dependencies-in-Spring/index.html +++ b/2023/11/22/circular-dependencies-in-Spring/index.html @@ -31,7 +31,7 @@ - + @@ -235,7 +235,7 @@

更新于 - + diff --git a/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/index.html b/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/index.html index be1fb5aa..aae19b22 100644 --- a/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/index.html +++ b/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/index.html @@ -31,7 +31,7 @@ - + @@ -235,7 +235,7 @@

更新于 - + diff --git a/2023/11/28/how-does-Dubbo-SPI-works/index.html b/2023/11/28/how-does-Dubbo-SPI-works/index.html index ab304019..b7e8d02d 100644 --- a/2023/11/28/how-does-Dubbo-SPI-works/index.html +++ b/2023/11/28/how-does-Dubbo-SPI-works/index.html @@ -31,7 +31,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/index.html b/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/index.html index 85d29f02..2999e09a 100644 --- a/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/index.html +++ b/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/index.html @@ -27,7 +27,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + diff --git a/2023/12/01/Nginx-reverse-proxy-for-home-networks/index.html b/2023/12/01/Nginx-reverse-proxy-for-home-networks/index.html index 9e89712e..b1dab03c 100644 --- a/2023/12/01/Nginx-reverse-proxy-for-home-networks/index.html +++ b/2023/12/01/Nginx-reverse-proxy-for-home-networks/index.html @@ -30,7 +30,7 @@ - + @@ -234,7 +234,7 @@

更新于 - + diff --git a/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/index.html b/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/index.html index 18060390..2740a97e 100644 --- a/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/index.html +++ b/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/index.html @@ -27,7 +27,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + diff --git a/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/index.html b/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/index.html index 06a8af86..e70e22cc 100644 --- a/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/index.html +++ b/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/index.html @@ -28,7 +28,7 @@ - + @@ -232,7 +232,7 @@

更新于 - + diff --git a/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/index.html b/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/index.html index bf44dc40..70903d6c 100644 --- a/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/index.html +++ b/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/index.html @@ -30,7 +30,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2023/12/06/how-does-Spring-Boot-SPI-works/index.html b/2023/12/06/how-does-Spring-Boot-SPI-works/index.html index 5df16d27..769aaeaa 100644 --- a/2023/12/06/how-does-Spring-Boot-SPI-works/index.html +++ b/2023/12/06/how-does-Spring-Boot-SPI-works/index.html @@ -30,7 +30,7 @@ - + @@ -235,7 +235,7 @@

更新于 - + diff --git a/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/index.html b/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/index.html index 8644cef2..db45033f 100644 --- a/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/index.html +++ b/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/index.html @@ -29,7 +29,7 @@ - + @@ -233,7 +233,7 @@

更新于 - + diff --git a/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/index.html b/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/index.html index f717f283..622f5f11 100644 --- a/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/index.html +++ b/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/index.html b/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/index.html index 3585316f..f98953f9 100644 --- a/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/index.html +++ b/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/index.html @@ -27,7 +27,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + diff --git a/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/index.html b/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/index.html index bb99f446..572f2123 100644 --- a/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/index.html +++ b/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/index.html @@ -27,7 +27,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + diff --git a/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/index.html b/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/index.html index 069f0dc1..63ff79d1 100644 --- a/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/index.html +++ b/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/index.html @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

更新于 - + diff --git a/2023/12/14/install-ELK-using-Docker-Compose/index.html b/2023/12/14/install-ELK-using-Docker-Compose/index.html index 8b3a5a99..1a32563f 100644 --- a/2023/12/14/install-ELK-using-Docker-Compose/index.html +++ b/2023/12/14/install-ELK-using-Docker-Compose/index.html @@ -33,7 +33,7 @@ - + @@ -238,7 +238,7 @@

更新于 - + diff --git a/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/index.html b/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/index.html index f40ea5e2..e7c6a89b 100644 --- a/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/index.html +++ b/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/index.html @@ -28,7 +28,7 @@ - + @@ -233,7 +233,7 @@

更新于 - + diff --git a/2023/12/25/Unsafe-an-anti-Java-class/index.html b/2023/12/25/Unsafe-an-anti-Java-class/index.html index dd814ad6..1912e4c2 100644 --- a/2023/12/25/Unsafe-an-anti-Java-class/index.html +++ b/2023/12/25/Unsafe-an-anti-Java-class/index.html @@ -28,7 +28,7 @@ - + @@ -231,7 +231,7 @@

更新于 - + diff --git a/2023/12/27/source-code-analysis-of-Java-class-Reference/index.html b/2023/12/27/source-code-analysis-of-Java-class-Reference/index.html index 3ba4b44a..fabfa901 100644 --- a/2023/12/27/source-code-analysis-of-Java-class-Reference/index.html +++ b/2023/12/27/source-code-analysis-of-Java-class-Reference/index.html @@ -33,7 +33,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/index.html b/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/index.html index f3ee86d9..bb31396d 100644 --- a/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/index.html +++ b/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/index.html @@ -31,7 +31,7 @@ - + @@ -234,7 +234,7 @@

更新于 - + diff --git a/2024/01/06/talk-about-isolation-of-MySQL-transactions/index.html b/2024/01/06/talk-about-isolation-of-MySQL-transactions/index.html index 99742271..789d19c2 100644 --- a/2024/01/06/talk-about-isolation-of-MySQL-transactions/index.html +++ b/2024/01/06/talk-about-isolation-of-MySQL-transactions/index.html @@ -33,7 +33,7 @@ - + @@ -236,7 +236,7 @@

更新于 - + diff --git a/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/index.html b/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/index.html index e94032a5..1bdfa76f 100644 --- a/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/index.html +++ b/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/index.html @@ -33,7 +33,7 @@ - + @@ -237,7 +237,7 @@

更新于 - + diff --git a/2024/01/18/use-vim/index.html b/2024/01/18/use-vim/index.html index 663887d8..53ecba75 100644 --- a/2024/01/18/use-vim/index.html +++ b/2024/01/18/use-vim/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/2024/01/30/installation-and-use-of-k3s/index.html b/2024/01/30/installation-and-use-of-k3s/index.html index 0ba18900..d53092c3 100644 --- a/2024/01/30/installation-and-use-of-k3s/index.html +++ b/2024/01/30/installation-and-use-of-k3s/index.html @@ -27,7 +27,7 @@ - + @@ -230,7 +230,7 @@

更新于 - + diff --git a/index.html b/index.html index fbdacb58..b97bd036 100644 --- a/index.html +++ b/index.html @@ -224,7 +224,7 @@

更新于 - + @@ -329,7 +329,7 @@

更新于 - + @@ -434,7 +434,7 @@

更新于 - + @@ -539,7 +539,7 @@

更新于 - + @@ -644,7 +644,7 @@

更新于 - + @@ -749,7 +749,7 @@

更新于 - + @@ -854,7 +854,7 @@

更新于 - + @@ -959,7 +959,7 @@

更新于 - + @@ -1064,7 +1064,7 @@

更新于 - + @@ -1169,7 +1169,7 @@

更新于 - + diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 14dac751..6b590ede 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"Docker 常用命令","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"安装 Docker","url":"/2020/08/18/install-Docker/"},{"title":"Linux 常用命令和快捷键","url":"/2020/08/27/Linux-frequently-used-commands/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Windows 10 上安装 OpenVPN 服务端","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"MySQL 常用命令","url":"/2020/09/04/MySQL-frequently-used-commands/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"Spring 应用 context 刷新流程","url":"/2023/08/04/Spring-application-context-refresh-process/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"使用 Grafana 和 Prometheus 搭建监控","url":"/2023/11/18/setup-monitoring-using-grafana-and-prometheus/"},{"title":"JDK 动态代理和 CGLib","url":"/2023/11/19/JDK-dynamic-proxy-and-CGLib/"},{"title":"Spring AOP 如何创建代理 beans","url":"/2023/11/19/how-does-Spring-AOP-create-proxy-beans/"},{"title":"Spring 中的循环依赖","url":"/2023/11/22/circular-dependencies-in-Spring/"},{"title":"Dubbo SPI 自适应拓展的工作原理","url":"/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/"},{"title":"Spring @Configuration 注解的源码分析","url":"/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/"},{"title":"Nginx 反向代理在家庭网络中的应用","url":"/2023/12/01/Nginx-reverse-proxy-for-home-networks/"},{"title":"使用 logrotate 滚动 Docker 容器内的 Nginx 的日志","url":"/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/"},{"title":"Spring 中 @Import 注解的使用和源码分析","url":"/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/"},{"title":"Spring Boot 自定义 starter 和自动配置的工作原理","url":"/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/"},{"title":"Spring Boot SPI 的工作原理","url":"/2023/12/06/how-does-Spring-Boot-SPI-works/"},{"title":"Dubbo SPI 的工作原理","url":"/2023/11/28/how-does-Dubbo-SPI-works/"},{"title":"Spring 中 @PropertySource 注解的使用和源码分析","url":"/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/"},{"title":"Spring AutowiredAnnotationBeanPostProcessor 的源码分析","url":"/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/"},{"title":"ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗","url":"/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/"},{"title":"当 MySQL 以 skip-name-resolve 模式启动时如何使用 grant 命令","url":"/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/"},{"title":"使用 Docker Compose 安装 ELK","url":"/2023/12/14/install-ELK-using-Docker-Compose/"},{"title":"Unsafe,一个“反 Java”的 class","url":"/2023/12/25/Unsafe-an-anti-Java-class/"},{"title":"ComponentScan 扫描路径覆盖的真相","url":"/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"谈谈 MySQL 事务的隔离性","url":"/2024/01/06/talk-about-isolation-of-MySQL-transactions/"},{"title":"不使用 GParted 的情况下为 VMware 中的 Ubuntu Server 增大磁盘空间","url":"/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/"},{"title":"使用 Vim","url":"/2024/01/18/use-vim/"},{"title":"k3s 的安装和使用","url":"/2024/01/30/installation-and-use-of-k3s/"},{"title":"探索 Java 类 Cleaner 和 Finalizer","url":"/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/"},{"title":"Java 类 Reference 的源码分析","url":"/2023/12/27/source-code-analysis-of-Java-class-Reference/"},{"title":"synchronized 锁机制的分析和验证","url":"/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/"}] \ No newline at end of file +[{"title":"安装 Docker","url":"/2020/08/18/install-Docker/"},{"title":"Docker 常用命令","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"Linux 常用命令和快捷键","url":"/2020/08/27/Linux-frequently-used-commands/"},{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"MySQL 常用命令","url":"/2020/09/04/MySQL-frequently-used-commands/"},{"title":"在 Windows 10 上安装 OpenVPN 服务端","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"Spring 应用 context 刷新流程","url":"/2023/08/04/Spring-application-context-refresh-process/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"使用 Grafana 和 Prometheus 搭建监控","url":"/2023/11/18/setup-monitoring-using-grafana-and-prometheus/"},{"title":"JDK 动态代理和 CGLib","url":"/2023/11/19/JDK-dynamic-proxy-and-CGLib/"},{"title":"Spring AOP 如何创建代理 beans","url":"/2023/11/19/how-does-Spring-AOP-create-proxy-beans/"},{"title":"Spring 中的循环依赖","url":"/2023/11/22/circular-dependencies-in-Spring/"},{"title":"Spring @Configuration 注解的源码分析","url":"/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/"},{"title":"Dubbo SPI 的工作原理","url":"/2023/11/28/how-does-Dubbo-SPI-works/"},{"title":"Nginx 反向代理在家庭网络中的应用","url":"/2023/12/01/Nginx-reverse-proxy-for-home-networks/"},{"title":"Dubbo SPI 自适应拓展的工作原理","url":"/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/"},{"title":"使用 logrotate 滚动 Docker 容器内的 Nginx 的日志","url":"/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/"},{"title":"Spring 中 @Import 注解的使用和源码分析","url":"/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/"},{"title":"Spring Boot 自定义 starter 和自动配置的工作原理","url":"/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/"},{"title":"Spring Boot SPI 的工作原理","url":"/2023/12/06/how-does-Spring-Boot-SPI-works/"},{"title":"Spring 中 @PropertySource 注解的使用和源码分析","url":"/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/"},{"title":"ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗","url":"/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/"},{"title":"Spring AutowiredAnnotationBeanPostProcessor 的源码分析","url":"/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/"},{"title":"当 MySQL 以 skip-name-resolve 模式启动时如何使用 grant 命令","url":"/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/"},{"title":"使用 Docker Compose 安装 ELK","url":"/2023/12/14/install-ELK-using-Docker-Compose/"},{"title":"synchronized 锁机制的分析和验证","url":"/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/"},{"title":"Unsafe,一个“反 Java”的 class","url":"/2023/12/25/Unsafe-an-anti-Java-class/"},{"title":"Java 类 Reference 的源码分析","url":"/2023/12/27/source-code-analysis-of-Java-class-Reference/"},{"title":"探索 Java 类 Cleaner 和 Finalizer","url":"/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/"},{"title":"ComponentScan 扫描路径覆盖的真相","url":"/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/"},{"title":"谈谈 MySQL 事务的隔离性","url":"/2024/01/06/talk-about-isolation-of-MySQL-transactions/"},{"title":"不使用 GParted 的情况下为 VMware 中的 Ubuntu Server 增大磁盘空间","url":"/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/"},{"title":"使用 Vim","url":"/2024/01/18/use-vim/"},{"title":"k3s 的安装和使用","url":"/2024/01/30/installation-and-use-of-k3s/"}] \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index 72efa96e..e03ef86e 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -224,7 +224,7 @@

更新于 - + @@ -329,7 +329,7 @@

更新于 - + @@ -434,7 +434,7 @@

更新于 - + @@ -539,7 +539,7 @@

更新于 - + @@ -644,7 +644,7 @@

更新于 - + @@ -749,7 +749,7 @@

更新于 - + @@ -854,7 +854,7 @@

更新于 - + @@ -959,7 +959,7 @@

更新于 - + @@ -1064,7 +1064,7 @@

更新于 - + @@ -1169,7 +1169,7 @@

更新于 - + diff --git a/page/3/index.html b/page/3/index.html index b57b6665..7eeebe23 100644 --- a/page/3/index.html +++ b/page/3/index.html @@ -224,7 +224,7 @@

更新于 - + @@ -329,7 +329,7 @@

更新于 - + @@ -434,7 +434,7 @@

更新于 - + @@ -539,7 +539,7 @@

更新于 - + @@ -767,7 +767,7 @@

更新于 - + @@ -969,7 +969,7 @@

更新于 - + @@ -1074,7 +1074,7 @@

更新于 - + @@ -1228,7 +1228,7 @@

更新于 - + @@ -1396,7 +1396,7 @@

更新于 - + @@ -1829,7 +1829,7 @@

更新于 - + diff --git a/page/4/index.html b/page/4/index.html index fccc4db0..9e4a6d47 100644 --- a/page/4/index.html +++ b/page/4/index.html @@ -224,7 +224,7 @@

更新于 - + @@ -424,7 +424,7 @@

更新于 - + @@ -586,7 +586,7 @@

更新于 - + @@ -746,7 +746,7 @@

更新于 - + @@ -951,7 +951,7 @@

更新于 - + @@ -1237,7 +1237,7 @@

更新于 - + @@ -1342,7 +1342,7 @@

更新于 - + @@ -1447,7 +1447,7 @@

更新于 - + @@ -1552,7 +1552,7 @@

更新于 - + @@ -1702,7 +1702,7 @@

更新于 - + diff --git a/page/5/index.html b/page/5/index.html index 81bf6a71..969b1049 100644 --- a/page/5/index.html +++ b/page/5/index.html @@ -224,7 +224,7 @@

更新于 - + @@ -329,7 +329,7 @@

更新于 - + @@ -434,7 +434,7 @@

更新于 - + @@ -539,7 +539,7 @@

更新于 - + @@ -644,7 +644,7 @@

更新于 - + @@ -749,7 +749,7 @@

更新于 - + @@ -854,7 +854,7 @@

更新于 - + @@ -959,7 +959,7 @@

更新于 - + diff --git a/search.xml b/search.xml index 08c8a6a2..aa0026ea 100644 --- a/search.xml +++ b/search.xml @@ -1,5 +1,34 @@ + + 安装 Docker + /2020/08/18/install-Docker/ + 记录安装 Docker 的过程用于备忘,主要在新建虚拟机或重装云服务器系统时使用。

+ + +

在 Ubuntu 上安装 Docker

官方文档:Install Docker Engine on Ubuntu

+
    +
  1. 卸载旧版本
    for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
  2. +
  3. 设置 apt 仓库
    # Add Docker's official GPG key:
    sudo apt-get update
    sudo apt-get install ca-certificates curl gnupg
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    # Add the repository to Apt sources:
    echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
  4. +
  5. 安装最新版本
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  6. +
  7. 通过运行 hello-world 镜像验证安装成功
    sudo docker run hello-world
  8. +
+

在 Linux 上安装 Docker 后的步骤

官方文档:Linux post-installation steps for Docker Engine

+

以非 root 用户身份管理 Docker

    +
  1. 创建 docker 群组
    sudo groupadd docker
  2. +
  3. 将当前用户添加到 docker 群组
    sudo usermod -aG docker $USER
  4. +
  5. 登出再登录,或者通过以下命令切换群组登录
    newgrp docker
  6. +
  7. 验证可以不通过 sudo 执行 docker 命令
    docker run hello-world
  8. +
+

配置 Docker 通过 systemd 启动

    +
  1. 设置开机自动启动 Docker
    sudo systemctl enable docker.service
    sudo systemctl enable containerd.service
  2. +
  3. 关闭开机自动启动
    sudo systemctl disable docker.service
    sudo systemctl disable containerd.service
  4. +
+]]>
+ + docker + +
Docker 常用命令 /2020/08/19/docker-frequently-used-commands/ @@ -317,35 +346,6 @@

参考链接

Docker 命令行参考
深入探究docker attach的退出方式

-]]> - - docker - -
- - 安装 Docker - /2020/08/18/install-Docker/ - 记录安装 Docker 的过程用于备忘,主要在新建虚拟机或重装云服务器系统时使用。

- - -

在 Ubuntu 上安装 Docker

官方文档:Install Docker Engine on Ubuntu

-
    -
  1. 卸载旧版本
    for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
  2. -
  3. 设置 apt 仓库
    # Add Docker's official GPG key:
    sudo apt-get update
    sudo apt-get install ca-certificates curl gnupg
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg

    # Add the repository to Apt sources:
    echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
  4. -
  5. 安装最新版本
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  6. -
  7. 通过运行 hello-world 镜像验证安装成功
    sudo docker run hello-world
  8. -
-

在 Linux 上安装 Docker 后的步骤

官方文档:Linux post-installation steps for Docker Engine

-

以非 root 用户身份管理 Docker

    -
  1. 创建 docker 群组
    sudo groupadd docker
  2. -
  3. 将当前用户添加到 docker 群组
    sudo usermod -aG docker $USER
  4. -
  5. 登出再登录,或者通过以下命令切换群组登录
    newgrp docker
  6. -
  7. 验证可以不通过 sudo 执行 docker 命令
    docker run hello-world
  8. -
-

配置 Docker 通过 systemd 启动

    -
  1. 设置开机自动启动 Docker
    sudo systemctl enable docker.service
    sudo systemctl enable containerd.service
  2. -
  3. 关闭开机自动启动
    sudo systemctl disable docker.service
    sudo systemctl disable containerd.service
  4. -
]]>
docker @@ -924,111 +924,6 @@ linux
- - 在 iOS 和 macOS 上安装 OpenVPN 客户端 - /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ - 本文记录了如何在 iOSmacOS 上安装和配置 OpenVPN 客户端,主要介绍如何编写客户端配置文件 client.ovpn 并导入。

- - - -

安装 OpenVPN Connect

-

配置 OpenVPN Connect

-

客户端配置文件模板 client.ovpn 以及 ca.crtca),client.crtcert),client.keykey)等文件均在安装 OpenVPN 服务端时获得。

-
-

客户端提供了两种方式导入配置文件:

-
    -
  1. 通过 URL,建议 URL 仅限在私有网络内访问。
  2. -
  3. 通过其他方式例如邮件(安全性降低),下载为本地文件再导入。本人使用 OneDrive 共享到 iPhone
  4. -
-

对于客户端配置而言,iOS 的困难点在于其文件系统封闭,ca.crtca),client.crtcert),client.keykey)不能放置到指定位置。因此配置文件分为两种形式:

-
    -
  1. CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件。这种方式简单方便,推荐!。
  2. -
  3. 使用 opensslCA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先导入 client.ovpn12 再导入 client.ovpn不推荐的原因在于本人导入失败,最终放弃。
  4. -
-

单一 client.ovpn

    -
  1. 从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改配置
  2. -
  3. remote your-server 1194 中的地址和端口替换成你的 OpenVPN 服务端对外的地址和端口
  4. -
  5. ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,再将各自文件中的内容以类 XML 的形式粘贴到 client.ovpn
  6. -
  7. 将修改好的客户端配置文件导入到客户端中
  8. -
-
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
- -

client.ovpn + client.opvn12(失败)

    -
  1. 使用 openssl 命令将客户端的证书和密钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。
    openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
  2. -
  3. 由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA根证书的配置。
    既可以用传统的引用文件的方式:
    ca ca.crt
    -也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:
    <ca>
    paste contents of ca.crt here
    </ca>
  4. -
  5. 先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn
  6. -
-
-

但是我失败了……导入 client.ovpn12 时密码一直错误,没有解决。推荐使用第一种方式。

-
-

NAT 和 DDNS

本人的设置如下,仅供参考:

-
    -
  • OpenVPN 服务端配置的端口号为默认的 1194
  • -
  • 在路由器管理后台的 NAT 设置中,配置一个自定义的高位端口号如 49999 作为对外端口号映射到 Windows 10 主机的 1194 端口号。这既是为了安全,也是为了避免不必要的检测风险(有没有用我也不知道啊 =_= )。
  • -
  • 由于公网 IP 是动态的,一旦 IP 发生变化,就需要修改配置文件。因此使用 ddns-go 配合 Cloudflare 实现动态域名解析。
  • -
-

参考文章

-]]>
- - openvpn - proxy - -
- - 在 Windows 10 上安装 OpenVPN 服务端 - /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ - 本文记录了如何在 Windows 10 上安装和配置 OpenVPN 服务端。

- - - -

环境

    -
  • Windows 10 专业版
  • -
  • OpenVPN 2.6.4
  • -
  • 拥有动态公网 IP
  • -
-

安装 OpenVPN server

    -
  1. OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4
  2. -
  3. 在选择安装类型时,选择 Customize 而不要选择 Install Now
      -
    • 勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive
    • -
    • 勾选 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive
    • -
    -
  4. -
  5. 安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。此时在“控制面板\网络和 Internet\网络连接”中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6OpenVPN Wintun
  6. -
-

配置 OpenVPN server

    -
  1. 打开 Windows 10 终端程序。
  2. -
  3. 进入 OpenVPN 默认安装目录中的 easy-rsa 目录。
    cd 'C:\Program Files\OpenVPN\easy-rsa'
  4. -
  5. 执行命令进入 Easy-RSA 3 Shell
    .\EasyRSA-Start.bat
  6. -
  7. 初始化公钥基础设施目录 pki
    ./easyrsa init-pki
  8. -
  9. 构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-ca nopass
  10. -
  11. 构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-server-full server nopass
  12. -
  13. 构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-client-full client nopass
  14. -
  15. 生成 Diffie-Hellman 参数
    ./easyrsa gen-dh
  16. -
  17. 从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:端口号按需修改,默认为 1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0
    port 1194
    dh dh.pem
    duplicate-cn
    ;tls-auth ta.key 0
  18. -
  19. 复制 ca.crtdh.pemserver.crtserver.key 到目录 C:\Program Files\OpenVPN\config 中。
  20. -
-

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1

-

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

-]]>
- - openvpn - proxy - -
在 Ubuntu 上安装 Clash /2023/05/27/how-to-install-clash-on-ubuntu/ @@ -1078,77 +973,7 @@ ]]> - proxy clash - - - - 使用 OpenVPN 访问家庭内网 - /2023/06/07/how-to-use-OpenVPN-to-access-home-network/ - 本文记录了如何使用 OpenVPN 访问家庭内网。

- - - -

网络概况

    -
  • 宽带是电信宽带,分配了动态的公网 IP
  • -
  • 光猫使用桥接模式,通过路由器拨号上网(PPPoE),路由器局域网为 192.168.3.0/24
  • -
  • 一台 Windows 10 主机,在路由器局域网上的 IP 192.168.3.120
  • -
  • Windows 10 主机上运行 Vmware 虚拟机,网络采用 NAT 模式。子网为 192.168.46.0/24。运行了 Linux 主机,IP 192.168.46.128Windows 10 主机在子网中的 IP192.168.3.1
  • -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
子网Windows 10 主机Linux 主机客户端
路由器 192.168.3.0/24192.168.3.120--
Vmware NAT 192.168.46.0/24192.168.46.1192.168.46.128-
VPN 10.8.0.0/2410.8.0.1-10.8.0.6
-

目标和考虑因素

    -
  1. 能从公网访问家庭内网,包括 Windows 10 主机和虚拟机上的 Linux 主机。
  2. -
  3. 不想通过路由器的 NAT 功能直接将局域网上的设备映射到公网。一是为了安全,二是为了避免运营商审查。
  4. -
  5. 已经使用 ZeroTier 将所需设备组建在一个局域网当中作为备选方案。
  6. -
  7. 想利用公网 IP 以及上行带宽尝尝鲜。
  8. -
  9. 想要能直连虚拟机上的 Linux 主机,而不是通过 VmwareNAT 映射。否则每次新增服务都要设置 NAT,修改 Windows Defender 的端口暴露规则。
  10. -
-

实现过程

在 Windows 10 上安装 OpenVPN 服务器

在 Windows 10 上安装 OpenVPN 服务器

-

在 iOS 和 macOS 上安装 OpenVPN 客户端

在 iOS 和 macOS 上安装 OpenVPN 客户端

-

客户端访问服务端其他的私有子网

如果在所需的每一个设备上都安装 OpenVPN,将它们连接在 VPN 的子网 10.8.0.0/24 中,也是可以满足需求的,但是这样略有些麻烦。

-

server.ovpn 新增配置

server.ovpn 配置文件中新增一行配置,这个配置的意思是将该路由配置统一推送给客户端,让它们可以访问服务端的其他私有子网。相当于将服务端的其他私有子网的情况告知客户端,这样客户端就知道发往 192.168.46.128Packet 是发向哪里的。

-
push "route 192.168.46.0 255.255.255.0"
- -

打开 Windows 10 主机的路由转发功能

    -
  1. Win + R 输入 regedit 打开注册表。
  2. -
  3. 找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,修改 IPEnableRouter1
  4. -
  5. 重启主机
  6. -
-

为虚拟机上的 Linux 主机新增路由

在终端中输入命令,这是为了让 OpenVPN 服务端的其他私有子网上的设备知道来自 10.8.0.0/24Packet 应该路由回 OpenVPN 服务端。

-
route add -net 10.8.0.0/24 gw 192.168.46.1
- - -

参考链接

透过openvpn来访问内网资源
OpenVPN 路由详解
OpenVPN中的另一个路由问题 - 在VPN上无法访问本地计算机
openvpn添加本地路由表
windows开启路由转发
linux route命令的使用详解
Windows命令行route命令使用图解

-]]>
- - openvpn proxy
@@ -1424,25 +1249,200 @@ - 如何为终端、docker 和容器设置代理 - /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ - Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

-

为终端设置代理

有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wgetcurl

-

设置 Shell 环境变量

这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxyhttps_proxyno_proxy
仅为当前会话设置,执行命令:

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc~/.zshrc)添加:

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

重新启动一个会话或者执行命令 source ~/.bashrc 使其在当前会话立即生效。

-

修改 wget 配置文件

在搜索过程中发现还可以在 wget 的配置文件 ~/.wgetrc 中添加:

-
use_proxy = on

http_proxy = http://proxyAddress:port
https_proxy = http://proxyAddress:port
- -

为 docker 设置代理

如果你以为为终端设置代理后 docker 就会使用代理,那你就错了。在从官方的镜像仓库 pull 镜像反复出错后并收到类似 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.3.140:59460->44.205.64.79:443: read: connection reset by peer 这样的报错信息后,我才开始怀疑我并没有真正给 docker 设置好代理。
在执行 docker pull 命令时,实际上命令是由守护进程 docker daemon 执行的。

-

通过 systemd 设置

如果你的 docker daemon 是通过 systemd 管理的,那么你可以通过设置 docker.service 服务的环境变量来设置代理。
执行命令查看 docker.service 信息,得知配置文件位置 /lib/systemd/system/docker.service

-
~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-06-13 00:52:54 CST; 22h ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 387690 (dockerd)
Tasks: 139
Memory: 89.6M
CPU: 1min 26.512s
CGroup: /system.slice/docker.service
-

docker.service[Service] 模块添加:

-
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
-

重新加载配置文件并重启服务:

-
systemctl daemon-reload
systemctl restart docker.service
+ 在 Windows 10 上安装 OpenVPN 服务端 + /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ + 本文记录了如何在 Windows 10 上安装和配置 OpenVPN 服务端。

+ + + +

环境

    +
  • Windows 10 专业版
  • +
  • OpenVPN 2.6.4
  • +
  • 拥有动态公网 IP
  • +
+

安装 OpenVPN server

    +
  1. OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4
  2. +
  3. 在选择安装类型时,选择 Customize 而不要选择 Install Now
      +
    • 勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive
    • +
    • 勾选 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive
    • +
    +
  4. +
  5. 安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。此时在“控制面板\网络和 Internet\网络连接”中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6OpenVPN Wintun
  6. +
+

配置 OpenVPN server

    +
  1. 打开 Windows 10 终端程序。
  2. +
  3. 进入 OpenVPN 默认安装目录中的 easy-rsa 目录。
    cd 'C:\Program Files\OpenVPN\easy-rsa'
  4. +
  5. 执行命令进入 Easy-RSA 3 Shell
    .\EasyRSA-Start.bat
  6. +
  7. 初始化公钥基础设施目录 pki
    ./easyrsa init-pki
  8. +
  9. 构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-ca nopass
  10. +
  11. 构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-server-full server nopass
  12. +
  13. 构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。
    ./easyrsa build-client-full client nopass
  14. +
  15. 生成 Diffie-Hellman 参数
    ./easyrsa gen-dh
  16. +
  17. 从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:端口号按需修改,默认为 1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0
    port 1194
    dh dh.pem
    duplicate-cn
    ;tls-auth ta.key 0
  18. +
  19. 复制 ca.crtdh.pemserver.crtserver.key 到目录 C:\Program Files\OpenVPN\config 中。
  20. +
+

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1

+

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

+]]>
+ + proxy + openvpn + +
+ + 在 iOS 和 macOS 上安装 OpenVPN 客户端 + /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ + 本文记录了如何在 iOSmacOS 上安装和配置 OpenVPN 客户端,主要介绍如何编写客户端配置文件 client.ovpn 并导入。

+ + + +

安装 OpenVPN Connect

+

配置 OpenVPN Connect

+

客户端配置文件模板 client.ovpn 以及 ca.crtca),client.crtcert),client.keykey)等文件均在安装 OpenVPN 服务端时获得。

+
+

客户端提供了两种方式导入配置文件:

+
    +
  1. 通过 URL,建议 URL 仅限在私有网络内访问。
  2. +
  3. 通过其他方式例如邮件(安全性降低),下载为本地文件再导入。本人使用 OneDrive 共享到 iPhone
  4. +
+

对于客户端配置而言,iOS 的困难点在于其文件系统封闭,ca.crtca),client.crtcert),client.keykey)不能放置到指定位置。因此配置文件分为两种形式:

+
    +
  1. CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件。这种方式简单方便,推荐!。
  2. +
  3. 使用 opensslCA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先导入 client.ovpn12 再导入 client.ovpn不推荐的原因在于本人导入失败,最终放弃。
  4. +
+

单一 client.ovpn

    +
  1. 从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改配置
  2. +
  3. remote your-server 1194 中的地址和端口替换成你的 OpenVPN 服务端对外的地址和端口
  4. +
  5. ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,再将各自文件中的内容以类 XML 的形式粘贴到 client.ovpn
  6. +
  7. 将修改好的客户端配置文件导入到客户端中
  8. +
+
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
+ +

client.ovpn + client.opvn12(失败)

    +
  1. 使用 openssl 命令将客户端的证书和密钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。
    openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
  2. +
  3. 由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA根证书的配置。
    既可以用传统的引用文件的方式:
    ca ca.crt
    +也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:
    <ca>
    paste contents of ca.crt here
    </ca>
  4. +
  5. 先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn
  6. +
+
+

但是我失败了……导入 client.ovpn12 时密码一直错误,没有解决。推荐使用第一种方式。

+
+

NAT 和 DDNS

本人的设置如下,仅供参考:

+
    +
  • OpenVPN 服务端配置的端口号为默认的 1194
  • +
  • 在路由器管理后台的 NAT 设置中,配置一个自定义的高位端口号如 49999 作为对外端口号映射到 Windows 10 主机的 1194 端口号。这既是为了安全,也是为了避免不必要的检测风险(有没有用我也不知道啊 =_= )。
  • +
  • 由于公网 IP 是动态的,一旦 IP 发生变化,就需要修改配置文件。因此使用 ddns-go 配合 Cloudflare 实现动态域名解析。
  • +
+

参考文章

+]]>
+ + proxy + openvpn + +
+ + 使用 OpenVPN 访问家庭内网 + /2023/06/07/how-to-use-OpenVPN-to-access-home-network/ + 本文记录了如何使用 OpenVPN 访问家庭内网。

+ + + +

网络概况

    +
  • 宽带是电信宽带,分配了动态的公网 IP
  • +
  • 光猫使用桥接模式,通过路由器拨号上网(PPPoE),路由器局域网为 192.168.3.0/24
  • +
  • 一台 Windows 10 主机,在路由器局域网上的 IP 192.168.3.120
  • +
  • Windows 10 主机上运行 Vmware 虚拟机,网络采用 NAT 模式。子网为 192.168.46.0/24。运行了 Linux 主机,IP 192.168.46.128Windows 10 主机在子网中的 IP192.168.3.1
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
子网Windows 10 主机Linux 主机客户端
路由器 192.168.3.0/24192.168.3.120--
Vmware NAT 192.168.46.0/24192.168.46.1192.168.46.128-
VPN 10.8.0.0/2410.8.0.1-10.8.0.6
+

目标和考虑因素

    +
  1. 能从公网访问家庭内网,包括 Windows 10 主机和虚拟机上的 Linux 主机。
  2. +
  3. 不想通过路由器的 NAT 功能直接将局域网上的设备映射到公网。一是为了安全,二是为了避免运营商审查。
  4. +
  5. 已经使用 ZeroTier 将所需设备组建在一个局域网当中作为备选方案。
  6. +
  7. 想利用公网 IP 以及上行带宽尝尝鲜。
  8. +
  9. 想要能直连虚拟机上的 Linux 主机,而不是通过 VmwareNAT 映射。否则每次新增服务都要设置 NAT,修改 Windows Defender 的端口暴露规则。
  10. +
+

实现过程

在 Windows 10 上安装 OpenVPN 服务器

在 Windows 10 上安装 OpenVPN 服务器

+

在 iOS 和 macOS 上安装 OpenVPN 客户端

在 iOS 和 macOS 上安装 OpenVPN 客户端

+

客户端访问服务端其他的私有子网

如果在所需的每一个设备上都安装 OpenVPN,将它们连接在 VPN 的子网 10.8.0.0/24 中,也是可以满足需求的,但是这样略有些麻烦。

+

server.ovpn 新增配置

server.ovpn 配置文件中新增一行配置,这个配置的意思是将该路由配置统一推送给客户端,让它们可以访问服务端的其他私有子网。相当于将服务端的其他私有子网的情况告知客户端,这样客户端就知道发往 192.168.46.128Packet 是发向哪里的。

+
push "route 192.168.46.0 255.255.255.0"
+ +

打开 Windows 10 主机的路由转发功能

    +
  1. Win + R 输入 regedit 打开注册表。
  2. +
  3. 找到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,修改 IPEnableRouter1
  4. +
  5. 重启主机
  6. +
+

为虚拟机上的 Linux 主机新增路由

在终端中输入命令,这是为了让 OpenVPN 服务端的其他私有子网上的设备知道来自 10.8.0.0/24Packet 应该路由回 OpenVPN 服务端。

+
route add -net 10.8.0.0/24 gw 192.168.46.1
+ + +

参考链接

透过openvpn来访问内网资源
OpenVPN 路由详解
OpenVPN中的另一个路由问题 - 在VPN上无法访问本地计算机
openvpn添加本地路由表
windows开启路由转发
linux route命令的使用详解
Windows命令行route命令使用图解

+]]>
+ + proxy + openvpn + +
+ + 如何为终端、docker 和容器设置代理 + /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ + Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

+

为终端设置代理

有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wgetcurl

+

设置 Shell 环境变量

这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxyhttps_proxyno_proxy
仅为当前会话设置,执行命令:

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc~/.zshrc)添加:

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

重新启动一个会话或者执行命令 source ~/.bashrc 使其在当前会话立即生效。

+

修改 wget 配置文件

在搜索过程中发现还可以在 wget 的配置文件 ~/.wgetrc 中添加:

+
use_proxy = on

http_proxy = http://proxyAddress:port
https_proxy = http://proxyAddress:port
+ +

为 docker 设置代理

如果你以为为终端设置代理后 docker 就会使用代理,那你就错了。在从官方的镜像仓库 pull 镜像反复出错后并收到类似 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.3.140:59460->44.205.64.79:443: read: connection reset by peer 这样的报错信息后,我才开始怀疑我并没有真正给 docker 设置好代理。
在执行 docker pull 命令时,实际上命令是由守护进程 docker daemon 执行的。

+

通过 systemd 设置

如果你的 docker daemon 是通过 systemd 管理的,那么你可以通过设置 docker.service 服务的环境变量来设置代理。
执行命令查看 docker.service 信息,得知配置文件位置 /lib/systemd/system/docker.service

+
~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-06-13 00:52:54 CST; 22h ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 387690 (dockerd)
Tasks: 139
Memory: 89.6M
CPU: 1min 26.512s
CGroup: /system.slice/docker.service
+

docker.service[Service] 模块添加:

+
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
+

重新加载配置文件并重启服务:

+
systemctl daemon-reload
systemctl restart docker.service

修改 dockerd 配置文件

还可以修改 dockerd 配置文件,添加:

export http_proxy="http://proxyAddress:port"
@@ -1475,6 +1475,29 @@ proxy
+ + Ubuntu server 20.04 安装后没有分配全部磁盘空间 + /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ + 使用 VMware 安装 Ubuntu server 20.04,注意到实际文件系统的总空间大小仅占设置的虚拟磁盘空间大小的一半左右。本文介绍了如何解决该问题。

+ + +
+

最近在本地测试 KubesphereMinikube,使用 Ubuntu server 20.04 搭建了多个虚拟机,磁盘空间紧张。注意到在安装后,实际文件系统的总空间大小仅占设置的虚拟磁盘空间大小的一半左右。如果 Ubuntu server 20.04 安装时使用默认的 LVM 选项,就会出现这种情况。

+
+

解决步骤

    +
  1. 使用 df -h 命令显示文件系统的总空间和可用空间信息。分配了 40G 磁盘空间,可用仅 19G
    $ df -h
    Filesystem Size Used Avail Use% Mounted on
    udev 3.9G 0 3.9G 0% /dev
    tmpfs 792M 7.5M 785M 1% /run
    /dev/mapper/ubuntu--vg-ubuntu--lv 19G 17G 995M 95% /
    tmpfs 3.9G 0 3.9G 0% /dev/shm
    tmpfs 5.0M 0 5.0M 0% /run/lock
    tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
    /dev/sda2 2.0G 108M 1.7G 6% /boot
    /dev/loop0 64M 64M 0 100% /snap/core20/1828
    /dev/loop2 50M 50M 0 100% /snap/snapd/18357
    /dev/loop1 92M 92M 0 100% /snap/lxd/24061
    tmpfs 792M 0 792M 0% /run/user/1000
    /dev/loop3 54M 54M 0 100% /snap/snapd/19457
  2. +
  3. 使用 sudo vgdisplay 命令查看发现 Free PE / Size 还有 19G
    $ sudo vgdisplay
    --- Volume group ---
    VG Name ubuntu-vg
    System ID
    Format lvm2
    Metadata Areas 1
    Metadata Sequence No 2
    VG Access read/write
    VG Status resizable
    MAX LV 0
    Cur LV 1
    Open LV 1
    Max PV 0
    Cur PV 1
    Act PV 1
    VG Size <38.00 GiB
    PE Size 4.00 MiB
    Total PE 9727
    Alloc PE / Size 4863 / <19.00 GiB
    Free PE / Size 4864 / 19.00 GiB
    VG UUID NuEjzH-CKXm-W6lA-gqzj-4bds-IR1Y-dTZ8IP
  4. +
  5. 使用 sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 调整逻辑卷的大小。
    $ sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
    Size of logical volume ubuntu-vg/ubuntu-lv changed from <19.00 GiB (4863 extents) to <38.00 GiB (9727 extents).
    Logical volume ubuntu-vg/ubuntu-lv successfully resized.
  6. +
  7. 使用 sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv 调整文件系统的大小。
    $ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
    resize2fs 1.45.5 (07-Jan-2020)
    Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
    old_desc_blocks = 3, new_desc_blocks = 5
    The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 9960448 (4k) blocks long.
  8. +
  9. 使用 df -h 命令再次查看,确认文件系统的总空间大小调整为 38G
    df -h
    Filesystem Size Used Avail Use% Mounted on
    udev 3.9G 0 3.9G 0% /dev
    tmpfs 792M 7.5M 785M 1% /run
    /dev/mapper/ubuntu--vg-ubuntu--lv 38G 17G 19G 47% /
    tmpfs 3.9G 0 3.9G 0% /dev/shm
    tmpfs 5.0M 0 5.0M 0% /run/lock
    tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
    /dev/sda2 2.0G 108M 1.7G 6% /boot
    /dev/loop0 64M 64M 0 100% /snap/core20/1828
    /dev/loop2 50M 50M 0 100% /snap/snapd/18357
    /dev/loop1 92M 92M 0 100% /snap/lxd/24061
    tmpfs 792M 0 792M 0% /run/user/1000
    /dev/loop3 54M 54M 0 100% /snap/snapd/19457
    /dev/loop4 64M 64M 0 100% /snap/core20/1950
  10. +
+

参考链接

ubuntu20.04 server 安装后磁盘空间只有一半的处理
Ubuntu Server 20.04.1 LTS, not all disk space was allocated during installation?

+]]>
+ + linux + ubuntu + +
如何在 Ubuntu 20.04 上安装 Minikube /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ @@ -1672,151 +1695,34 @@ - Spring 应用 context 刷新流程 - /2023/08/04/Spring-application-context-refresh-process/ - context 刷新流程简单图解

刷新流程

- -

刷新流程中的组件

+ Java 类加载器源码分析 + /2023/07/13/Java-class-loader-source-code-analysis/ + 组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

+
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
-

上下文刷新 AbstractApplicationContext#refresh

public void refresh() throws BeansException, IllegalStateException {
// 刷新和销毁的同步监视器。
synchronized (this.startupShutdownMonitor) {
// 1. 准备 context 以供刷新。
prepareRefresh();
// 2. 告诉 context 子类刷新内部 beanFactory 并返回。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. 准备 beanFactory 以供在此 context 中使用。
prepareBeanFactory(beanFactory);
try {
// 4. 允许 context 子类对 bean 工厂进行后处理。
postProcessBeanFactory(beanFactory);
// 5. 调用在 context 中注册为 bean 的工厂后处理器。
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册拦截 Bean 创建的 Bean 后处理器。
registerBeanPostProcessors(beanFactory);
// 7. 初始化 context 的消息源。
initMessageSource();
// 8. 为 context 初始化事件多播器。
initApplicationEventMulticaster();
// 9. 在特定 context 子类中初始化其他特殊 bean。
onRefresh();
// 10. 检查监听器 beans 并注册。
registerListeners();
// 11. 实例化所有剩余的(非惰性初始化)单例。
finishBeanFactoryInitialization(beanFactory);
// 12. 最后一步:发布相应的事件。
finishRefresh();
}
catch (BeansException ex) {
// ...
// 销毁已经创建的单例,以防止资源未正常释放。
destroyBeans();
// 重置 'active' flag.
cancelRefresh(ex);
throw ex;
}
finally {
resetCommonCaches();
}
}
}
+

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

+
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
-

准备 context 以供刷新 prepareRefresh

准备此 context 以供刷新,设置其启动日期和活动标志以及执行属性源的初始化。

+

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

+
    +
  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. +
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. +
  5. 调用 findClass(String) 方法来查找该类。
  6. +
+

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

-

编写管理资源的容器时,可以参考。

+

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

-
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}
// 初始化 PropertySource,默认什么都不做。
initPropertySources();
// 校验所有被标记为 required 的 properties 都可以被解析。
getEnvironment().validateRequiredProperties();
// 允许收集早期的 ApplicationEvents,当事件多播器可用就发送。
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}
+

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

+
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
-

obtainFreshBeanFactory

告诉子类刷新内部 bean 工厂并返回,返回的实例类型为 DefaultListableBeanFactory。在这里完成了配置文件的读取,初步注册了 bean 定义。

-
-

我大概这辈子都不会想理清楚这里面关于 XML 文件的解析过程,但是我知道可以在这里观察到 beanFactory 因为配置文件注册了哪些 bean。

-
-
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
// 刷新 beanFactory
// 使用 XmlBeanDefinitionReader 加载配置文件中的 bean 定义
refreshBeanFactory();
// 返回 beanFactory
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
+
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

+
static {
ClassLoader.registerAsParallelCapable();
}
-

准备 beanFactory

配置 BeanFactory 以供在此 context 中使用,例如 context 的类加载器和一些后处理器,手动注册一些单例。

+

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

    -
  1. 为 beanFactory 配置 context 相关的资源,如类加载器
  2. -
  3. 添加 Bean 后处理器
      -
    • ApplicationContextAwareProcessor,context 回调,注入特定类型时可触发自定义逻辑
    • -
    • ApplicationListenerDetector,检测 ApplicationListener
    • -
    -
  4. -
  5. 手动注册单例
  6. -
-
-

ignoreDependencyInterface 和 registerResolvableDependency 在理解之后比单纯地记忆它们有趣许多。

-
-
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 告诉内部 beanFactory 使用 context 的类加载器等等。
beanFactory.setBeanClassLoader(getClassLoader());
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 为内部 beanFactory 配置 context 回调。
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 如果一个 bean 的依赖实现了以下接口,忽略该依赖的检查和自动装配。
// 例如在 populateBean 时,如果 bena 的依赖存在 set 方法,就会去解析,调用 getBean
// 被设置 ignoreDependencyInterface 的依赖,仍然可以通过后置处理器进行依赖注入,例如以下的类型会使用上面那个后置处理器的回调方法注入。
// 因此 @Autowire 这些通过后置处理器实现依赖注入的注解,也不会受影响
// 这样设计的一个可能是往往注入这些类型时,希望触发某些事件。
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// BeanFactory 之类的接口没有在普通工厂中注册为可解析类型,直接为它们指定 bean。
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 提前注册后处理器以检测内部 beans 是否是一个 ApplicationListener。
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 检测是否有 LoadTimeWeaver,如果存在就准备编织。
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// 手动注册默认的环境 beans。
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}
- -
-

在创建 Bean 开始前注册的单例,都属于手动注册的单例 manualSingletonNames

-
-
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
super.registerSingleton(beanName, singletonObject);

if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
if (!this.beanDefinitionMap.containsKey(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames.size() + 1);
updatedSingletons.addAll(this.manualSingletonNames);
updatedSingletons.add(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// Still in startup registration phase
if (!this.beanDefinitionMap.containsKey(beanName)) {
this.manualSingletonNames.add(beanName);
}
}

clearByTypeCache();
}
- -

postProcessBeanFactory

在标准初始化后修改内部 beanFactory,默认什么都不做。

-

invokeBeanFactoryPostProcessors

实例化并调用所有在 context 中注册的 beanFactory 后处理器,需遵循顺序规则。具体的处理被委托给 PostProcessorRegistrationDelegate。

-
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
// ...
}
- -

invokeBeanFactoryPostProcessors 方法堪比裹脚布。

-

关于调用顺序的规则

-
    -
  1. BeanFactoryPostProcessor 分为 context 添加的和 beanFactory 注册的,前者优于后者
  2. -
  3. BeanFactoryPostProcessor 又可分为常规的和 BeanDefinitionRegistryPostProcessor,后者优于前者
  4. -
  5. PriorityOrdered 优于 Ordered 优于剩余的
  6. -
-

可能新增 beanDefinition 的情况:

-
    -
  1. BeanDefinitionRegistryPostProcessor 可能在 beanFactory 中引入新的 beanDefinition
  2. -
-
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

// 存储已处理过的后处理器
Set<String> processedBeans = new HashSet<String>();

// 第一阶段:BeanDefinitionRegistryPostProcessor
if (beanFactory instanceof BeanDefinitionRegistry) {
// 如果 beanFactory 同时是 BeanDefinitionRegistry 类型
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
// 存储常规的 BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
// 存储 BeanDefinitionRegistryPostProcessor
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new LinkedList<BeanDefinitionRegistryPostProcessor>();

// 第 0 轮,先对 context 注册的 BeanFactoryPostProcessor 进行分类
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
// 分类的同时,直接调用 BeanDefinitionRegistryPostProcessor
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// 将 BeanDefinitionRegistryPostProcessors 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();

// 第 1 轮,先处理 beanFactory 中实现了 PriorityOrdered 的 BeanDefinitionRegistryPostProcessor
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
// getBean 并添加到当前的后处理器集合
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
// 排序后添加
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// 第 2 轮,Ordered
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// 第 3 轮, 调用剩余的 BeanDefinitionRegistryPostProcessors 直到没有新的出现。
// 后出现的 PriorityOrdered 不比前面的 Ordered 更早被处理
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// 调用目前出现的 BeanFactoryPostProcessors
// 仍遵循 PriorityOrdered、Ordered、Regular(registry)、Regular(context 添加的) 的顺序
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// 否则,直接调用 context 注册的 beanFactoryPostProcessors
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 第二阶段:BeanFactoryPostProcessor
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// 将 BeanFactoryPostProcessor 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// 第一阶段已处理,跳过
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// 第 1 轮,BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// 第 2 轮,BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// 第 3 轮,剩余的 BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// 清理缓存的 merged bean definitions 因为 post-processors 可能已经修改了原来的 metadata
beanFactory.clearMetadataCache();
}
- -

registerBeanPostProcessors

注册拦截 bean 创建的 bean 后处理器。具体的处理被委托给 PostProcessorRegistrationDelegate。

-
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}
- -

registerBeanPostProcessors 相比之下是一条清新的裹脚布。这里特别区分 3 种类型的 Bean 后处理器:

- -

ApplicationListenerDetector 既是 MergedBeanDefinitionPostProcessor,又是 DestructionAwareBeanPostProcessor,在初始化后将 listener 加入,在销毁前将 listener 移除。

-
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
// 获取 BeanPostProcessor 的名称
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

// 注册 BeanPostProcessorChecker,在 BeanPostProcessor 实例化期间创建 bean 时,记录一条消息。
// 即,当一个 bean 不能被所有 BeanPostProcessors 处理时,记录。
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

// 将 BeanPostProcessors 按 implement PriorityOrdered,Ordered,和剩余的进行分类。
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<BeanPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// 第 1 轮,注册 BeanPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

// 第 2 轮,注册 BeanPostProcessors that implement Ordered.
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);

// 第 3 轮,注册剩余的 regular BeanPostProcessors.
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

// 最后(第 4 轮), 排序并注册 internal BeanPostProcessors.
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);

// Re-register post-processor for detecting inner beans as ApplicationListeners,
// moving it to the end of the processor chain (for picking up proxies etc).
// 重新注册用于检测 ApplicationListeners 的 Bean 后处理器,将其移动到处理器链的最后(用于获取代理)。
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}
- -

添加 BeanPostProcessor 时

-
    -
  1. 先移除
  2. -
  3. 再添加
  4. -
  5. 判断类型并记录标记
      -
    • 感知实例化的后处理器
    • -
    • 感知销毁的后处理器
    • -
    -
  6. -
-
public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
this.beanPostProcessors.remove(beanPostProcessor);
this.beanPostProcessors.add(beanPostProcessor);
if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) {
this.hasInstantiationAwareBeanPostProcessors = true;
}
if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) {
this.hasDestructionAwareBeanPostProcessors = true;
}
}
- -

initMessageSource

初始化消息源。 如果在此 context 中未定义,则使用父级的。

-
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
// 设置 parent MessageSource.
if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
if (hms.getParentMessageSource() == null) {
// 只有当 parent MessageSource 尚未注册才将 parent context 设置为 parent MessageSource
hms.setParentMessageSource(getInternalParentMessageSource());
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using MessageSource [" + this.messageSource + "]");
}
}
else {
// 使用代理 messageSource,以此接收 getMessage 调用。
DelegatingMessageSource dms = new DelegatingMessageSource();
dms.setParentMessageSource(getInternalParentMessageSource());
this.messageSource = dms;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME +
"': using default [" + this.messageSource + "]");
}
}
}
- -

initApplicationEventMulticaster

初始化 ApplicationEventMulticaster。 如果上下文中未定义,则使用 SimpleApplicationEventMulticaster。可以看得出代码的结构和 initMessageSource 是类似的。

-
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isDebugEnabled()) {
logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
"': using default [" + this.applicationEventMulticaster + "]");
}
}
}
- -

onRefresh

可以重写模板方法来添加特定 context 的刷新工作。默认情况下什么都不做。

-

registerListeners

获取侦听器 bean 并注册。无需初始化即可添加

-
protected void registerListeners() {
// 注册静态指定的 ApplicationListener,和 beanFactoryPostProcessor 类似,context 可以提前添加好。
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
// 这段注释看到不止一次,但是不太理解,感觉和代码联系不起来?
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}

// 现在我们终于有了多播器,发布早期的应用事件。
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
- -

添加 ApplicationListener。

-
-

后处理器 ApplicationListenerDetector 在 processor chain 的最后,最终会将创建的代理添加为监听器。什么情况下会出现代码中预防的情况呢?

-
-
public void addApplicationListener(ApplicationListener<?> listener) {
synchronized (this.retrievalMutex) {
// 如果已经注册,需要显式地删除代理,以避免同一监听器的双重调用。
Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
if (singletonTarget instanceof ApplicationListener) {
this.defaultRetriever.applicationListeners.remove(singletonTarget);
}
this.defaultRetriever.applicationListeners.add(listener);
this.retrieverCache.clear();
}
}
- -

finishBeanFactoryInitialization

实例化所有剩余的(非惰性初始化)单例。以 context 视角,是完成内部 beanFactory 的初始化。

-

几乎可以只关注最后的 beanFactory.preInstantiateSingletons()

-
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 为 context 初始化转换服务
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

// 如果之前没有任何 bean 后处理器(例如 PropertyPlaceholderConfigurer)注册,则注册默认的嵌入值解析器,主要用于解析注释属性值。
// 接口 ConfigurableEnvironment 继承自 ConfigurablePropertyResolver
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
return getEnvironment().resolvePlaceholders(strVal);
}
});
}

// 尽早初始化 LoadTimeWeaverAware,以便尽早注册其转换器。
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// 停止使用临时类加载器进行类型匹配。
beanFactory.setTempClassLoader(null);

// 允许缓存所有 bean 定义的元数据,而不期望进一步更改。
beanFactory.freezeConfiguration();

// 实例化所有剩余的(非惰性初始化)单例。
beanFactory.preInstantiateSingletons();
}
- -

确保所有非惰性初始化单例都已实例化,同时还要考虑 FactoryBeans。 如果需要,通常在工厂设置结束时调用。

-加载 Bean 的流程分析在此。 - -
-

先对集合进行 Copy 再迭代是很常见的处理方式,可以有效保证迭代时不受原集合影响,也不会影响到原集合。

-
-
@Override
public void preInstantiateSingletons() throws BeansException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Pre-instantiating singletons in " + this);
}

// 拷贝一份 beanDefinitionNames
List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);

// 触发所有非惰性初始化单例的实例化
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
// FactoryBean
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return ((SmartFactoryBean<?>) factory).isEagerInit();
}
}, getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
// 是否立即初始化
getBean(beanName);
}
}
else {
// 常规 Bean(重要)
getBean(beanName);
}
}
}

// 触发所有适用 bean 的初始化后回调
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
smartSingleton.afterSingletonsInstantiated();
return null;
}
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}
- -

finishRefresh

最后一步,完成 context 刷新,比如发布相应的事件。

-
protected void finishRefresh() {
// 1. 为此 context 初始化生命周期处理器。
initLifecycleProcessor();
// 2. 将刷新传播到生命周期处理器。
getLifecycleProcessor().onRefresh();
// 3. 发布 ContextRefreshedEvent。
publishEvent(new ContextRefreshedEvent(this));
// 4. 参与 LiveBeansView MBean(如果处于活动状态)。
LiveBeansView.registerApplicationContext(this);
}
]]>
- - java - spring - - - - Java 类加载器源码分析 - /2023/07/13/Java-class-loader-source-code-analysis/ - 组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

-
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
- -

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

-
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
- -

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

-
    -
  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. -
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. -
  5. 调用 findClass(String) 方法来查找该类。
  6. -
-

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

-
-

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

-
-

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

-
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
- -
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

-
static {
ClassLoader.registerAsParallelCapable();
}
- -

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

-
    -
  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. -
  3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
  4. +
  5. 尚未创建调用者的实例(类加载器尚未实例化)
  6. +
  7. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
怎么保证这两个条件成立呢?
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. @@ -1986,6 +1892,78 @@ class loader + + Spring Bean 加载过程 + /2023/08/10/how-does-Spring-load-beans/ + Spring Bean 生命周期 + +

    获取 Bean

    获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

    +
    public Object getBean(String name) throws BeansException {
    // 具体工作由 doGetBean 完成
    return doGetBean(name, null, null, false);
    }
    + +

    deGetBean

    作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

    +
    protected <T> T doGetBean(
    final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
    throws BeansException {
    // 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
    final String beanName = transformedBeanName(name);
    Object bean;

    // 检查单例缓存中是否已存在
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
    // ...
    // 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }
    else {
    // 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
    // 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
    if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
    }

    // 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
    BeanFactory parentBeanFactory = getParentBeanFactory();
    if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    String nameToLookup = originalBeanName(name);
    if (args != null) {
    return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
    return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
    }

    if (!typeCheckOnly) {
    // 标记为至少创建过一次
    markBeanAsCreated(beanName);
    }

    try {
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    checkMergedBeanDefinition(mbd, beanName, args);

    // 确保 bean 依赖的 bean(构造器参数) 都已实例化
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
    for (String dep : dependsOn) {
    if (isDependent(beanName, dep)) {
    // 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
    }
    // 注册依赖关系,确保先销毁被依赖的 bean
    registerDependentBean(dep, beanName);
    // 递归,获取依赖的 bean
    getBean(dep);
    }
    }
    }

    if (mbd.isSingleton()) {
    // 如果是单例类型(绝大多数都是此类型)
    // 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
    {
    @Override
    public Object getObject() throws BeansException {
    try {
    // 创建 bean
    return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
    // 由于可能已经提前暴露,需要显示地销毁
    destroySingleton(beanName);
    throw ex;
    }
    }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    else if (mbd.isPrototype()) {
    // 如果是原型类型,每次都新创建一个
    // ...
    }
    else {
    // 如果是其他 scope 类型
    // ...
    }
    }
    catch (BeansException ex) {
    cleanupAfterBeanCreationFailure(beanName);
    throw ex;
    }
    }
    + +

    getSingleton

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    // 加锁
    synchronized (this.singletonObjects) {
    // 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    if (this.singletonsCurrentlyInDestruction) {
    // 如果正在销毁单例,则抛异常
    // 注意:不要在销毁方法中调用获取 bean 方法
    }
    // 创建前,先注册到正在创建中的集合
    // 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
    beforeSingletonCreation(beanName);
    boolean newSingleton = false;
    // ...
    try {
    // 使用传入的单例工厂创建对象
    singletonObject = singletonFactory.getObject();
    newSingleton = true;
    }
    catch (IllegalStateException ex) {
    // 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
    singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
    throw ex;
    }
    }
    catch (BeanCreationException ex) {
    // ...
    }
    finally {
    // ...
    // 创建后,从正在创建中集合移除
    afterSingletonCreation(beanName);
    }
    if (newSingleton) {
    // 添加单例到缓存
    addSingleton(beanName, singletonObject);
    }
    }
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
    }
    + +

    创建 Bean

    createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

    +
    protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    // ...
    try {
    // 给 Bean 后置处理器一个返回代理的机会
    Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
    if (bean != null) {
    return bean;
    }
    }
    // ...
    // 常规的创建 Bean
    Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    return beanInstance;
    }
    + +

    doCreateBean

    常规的创建 Bean 的具体工作是由 doCreateBean 完成的。

    +
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
    // 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
    instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
    Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
    mbd.resolvedTargetType = beanType;

    // ...

    // 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
    boolean earlySingletonExposure = (mbd.isSingleton()
    && this.allowCircularReferences
    && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
    addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override
    public Object getObject() throws BeansException {
    return getEarlyBeanReference(beanName, mbd, bean);
    }
    });
    }

    Object exposedObject = bean;
    try {
    // 填充属性(包括解析依赖的 bean)
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    // 初始化 bean
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    // ...

    // 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
    try {
    registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    // ...

    return exposedObject;
    }
    + +

    createBeanInstance

    创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

    +
      +
    1. 工厂方法
    2. +
    3. 构造器方法
        +
      1. 有参
      2. +
      3. 无参
      4. +
      +
    4. +
    +

    populateBean

    为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

    +
    protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
    PropertyValues pvs = mbd.getPropertyValues();
    // ...

    // 给 InstantiationAwareBeanPostProcessors 一个机会,
    // 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
    boolean continueWithPropertyPopulation = true;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
    continueWithPropertyPopulation = false;
    break;
    }
    }
    }
    }

    // 是否继续填充属性的流程
    if (!continueWithPropertyPopulation) {
    return;
    }

    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
    || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
    // 根据名称注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
    autowireByName(beanName, mbd, bw, newPvs);
    }

    // 根据类型注入
    if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
    autowireByType(beanName, mbd, bw, newPvs);
    }
    pvs = newPvs;
    }

    // 是否存在 InstantiationAwareBeanPostProcessors
    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    // 是否需要检查依赖
    boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

    if (hasInstAwareBpps || needsDepCheck) {
    PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    if (hasInstAwareBpps) {
    // 后置处理 PropertyValues
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof InstantiationAwareBeanPostProcessor) {
    InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
    pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
    if (pvs == null) {
    return;
    }
    }
    }
    }
    if (needsDepCheck) {
    checkDependencies(beanName, mbd, filteredPds, pvs);
    }
    }
    // 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
    applyPropertyValues(beanName, mbd, bw, pvs);
    }
    + +

    initializeBean

    在填充完属性后,实例就可以进行初始化工作:

    +
      +
    1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
    2. +
    3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    4. +
    5. invokeInitMethods,调用初始化方法
    6. +
    7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    8. +
    +
    protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
    // 处理 Aware 接口的相应方法
    if (System.getSecurityManager() != null) {
    AccessController.doPrivileged(new PrivilegedAction<Object>() {
    @Override
    public Object run() {
    invokeAwareMethods(beanName, bean);
    return null;
    }
    }, getAccessControlContext());
    }
    else {
    invokeAwareMethods(beanName, bean);
    }

    // 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    try {
    // 调用初始化方法
    invokeInitMethods(beanName, wrappedBean, mbd);
    }
    catch (Throwable ex) {
    throw new BeanCreationException(
    (mbd != null ? mbd.getResourceDescription() : null),
    beanName, "Invocation of init method failed", ex);
    }

    if (mbd == null || !mbd.isSynthetic()) {
    // 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
    }
    + +
    处理 Aware 接口的相应方法

    让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

    +
    private void invokeAwareMethods(final String beanName, final Object bean) {
    if (bean instanceof Aware) {
    if (bean instanceof BeanNameAware) {
    ((BeanNameAware) bean).setBeanName(beanName);
    }
    if (bean instanceof BeanClassLoaderAware) {
    ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
    }
    if (bean instanceof BeanFactoryAware) {
    ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
    }
    }
    }
    + +
    调用初始化方法
      +
    1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
    2. +
    3. 如果自定义 init 方法且满足调用条件,同样进行调用
    4. +
    +
    protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
    // 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
    // 给 bean 一个感知属性已设置并做出反应的机会
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean
    && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
    if (System.getSecurityManager() != null) {
    try {
    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
    @Override
    public Object run() throws Exception {
    ((InitializingBean) bean).afterPropertiesSet();
    return null;
    }
    }, getAccessControlContext());
    }
    catch (PrivilegedActionException pae) {
    throw pae.getException();
    }
    }
    else {
    ((InitializingBean) bean).afterPropertiesSet();
    }
    }

    // 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
    if (mbd != null) {
    String initMethodName = mbd.getInitMethodName();
    if (initMethodName != null
    && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
    && !mbd.isExternallyManagedInitMethod(initMethodName)) {
    invokeCustomInitMethod(beanName, bean, mbd);
    }
    }
    }
    + +
    BeanPostProcessor 处理

    在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

    +
      +
    1. 获取 Processor 列表
    2. +
    3. 判断 Processor 类型是否是当前需要的
    4. +
    5. 对 bean 进行处理
    6. +
    +
    public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessBeforeInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }

    public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
    result = beanProcessor.postProcessAfterInitialization(result, beanName);
    if (result == null) {
    return result;
    }
    }
    return result;
    }
    + +

    再思 Bean 的初始化

    以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
    在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

    +
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
    populateBean(beanName, mbd, instanceWrapper);
    if (exposedObject != null) {
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    }
    + +

    在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

    +
      +
    1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
    2. +
    3. Bean 自身的初始化。
    4. +
    +

    在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
    在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

    +]]>
    + + java + spring + +
    JVM GC 的测试和分析 /2023/11/01/testing-and-analysis-of-jvm-gc/ @@ -2173,326 +2151,146 @@ - JVM 内存区域的测试和分析 - /2023/11/04/testing-and-analysis-of-jvm-memory-area/ - 内存区域

    JVM 内存区域划分为:

    -
      -
    • 程序计数器
    • -
    • 虚拟机栈
    • -
    • 本地方法栈
    • -
    • -
    • 方法区
    • -
    - + Spring 应用 context 刷新流程 + /2023/08/04/Spring-application-context-refresh-process/ + context 刷新流程简单图解

    刷新流程

    -

    程序计数器

    虚拟机栈

    Java 虚拟机栈(Java Virtual Machine Stack),线程私有,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

    -

    可以使用 -Xss1024k 设置虚拟机栈的大小。默认情况下都是 1024k,只有 Windows 中取决于虚拟内存。

    -

    栈内存溢出

      -
    1. 栈帧过多导致栈内存溢出
    2. -
    3. 栈帧过大导致栈内存溢出(难复现)
    4. -
    -

    不正确的递归调用

    public class StackTest_4 {

    private static int count = 0;

    // 改变栈的大小限制 -Xss256k,观察调用次数的变化
    public static void main(String[] args) {
    try {
    method1();
    } catch (Throwable t) {
    t.printStackTrace();
    } finally {
    // 默认情况下经过 20000+ 次,改变参数后 3000+ 次
    System.out.println(count);
    }
    }

    private static void method1() {
    count++;
    method1();
    }
    }
    +

    刷新流程中的组件

    +

    上下文刷新 AbstractApplicationContext#refresh

    public void refresh() throws BeansException, IllegalStateException {
    // 刷新和销毁的同步监视器。
    synchronized (this.startupShutdownMonitor) {
    // 1. 准备 context 以供刷新。
    prepareRefresh();
    // 2. 告诉 context 子类刷新内部 beanFactory 并返回。
    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    // 3. 准备 beanFactory 以供在此 context 中使用。
    prepareBeanFactory(beanFactory);
    try {
    // 4. 允许 context 子类对 bean 工厂进行后处理。
    postProcessBeanFactory(beanFactory);
    // 5. 调用在 context 中注册为 bean 的工厂后处理器。
    invokeBeanFactoryPostProcessors(beanFactory);
    // 6. 注册拦截 Bean 创建的 Bean 后处理器。
    registerBeanPostProcessors(beanFactory);
    // 7. 初始化 context 的消息源。
    initMessageSource();
    // 8. 为 context 初始化事件多播器。
    initApplicationEventMulticaster();
    // 9. 在特定 context 子类中初始化其他特殊 bean。
    onRefresh();
    // 10. 检查监听器 beans 并注册。
    registerListeners();
    // 11. 实例化所有剩余的(非惰性初始化)单例。
    finishBeanFactoryInitialization(beanFactory);
    // 12. 最后一步:发布相应的事件。
    finishRefresh();
    }
    catch (BeansException ex) {
    // ...
    // 销毁已经创建的单例,以防止资源未正常释放。
    destroyBeans();
    // 重置 'active' flag.
    cancelRefresh(ex);
    throw ex;
    }
    finally {
    resetCommonCaches();
    }
    }
    }
    -

    循环引用导致 JSON 解析无限循环

    并非只有自己写的递归方法可能引发栈内存溢出,有可能第三方库也会引发栈内存溢出。

    -
    public class StackTest_5 {

    public static void main(String[] args) throws JsonProcessingException {
    Department department = new Department();
    department.setName("Tech");

    Employee employee1 = new Employee();
    employee1.setName("Tom");
    employee1.setDepartment(department);

    Employee employee2 = new Employee();
    employee2.setName("Tim");
    employee2.setDepartment(department);

    department.setEmployees(Arrays.asList(employee1, employee2));

    ObjectMapper objectMapper = new ObjectMapper();
    System.out.println(objectMapper.writeValueAsString(department));
    }

    static class Department {
    private String name;
    private List<Employee> employees;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public List<Employee> getEmployees() {
    return employees;
    }

    public void setEmployees(List<Employee> employees) {
    this.employees = employees;
    }
    }

    static class Employee {
    private String name;
    private Department department;

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Department getDepartment() {
    return department;
    }

    public void setDepartment(Department department) {
    this.department = department;
    }
    }
    }
    +

    准备 context 以供刷新 prepareRefresh

    准备此 context 以供刷新,设置其启动日期和活动标志以及执行属性源的初始化。

    +
    +

    编写管理资源的容器时,可以参考。

    +
    +
    protected void prepareRefresh() {
    this.startupDate = System.currentTimeMillis();
    this.closed.set(false);
    this.active.set(true);
    if (logger.isInfoEnabled()) {
    logger.info("Refreshing " + this);
    }
    // 初始化 PropertySource,默认什么都不做。
    initPropertySources();
    // 校验所有被标记为 required 的 properties 都可以被解析。
    getEnvironment().validateRequiredProperties();
    // 允许收集早期的 ApplicationEvents,当事件多播器可用就发送。
    this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
    }
    -

    局部变量的线程安全问题

      -
    1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
    2. -
    3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
    4. -
    -
    public class StackTest_3 {

    public static void main(String[] args) {
    method1();
    }

    // 线程安全
    private static void method1() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb);
    }

    // 线程不安全
    private static void method2(StringBuilder sb) {
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb);
    }

    // 线程不安全,看到一个说法:发生指令重排,sb 的 append 操作发生在返回之后(有待确认)
    private static StringBuilder method3() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    return sb;
    }
    }
    +

    obtainFreshBeanFactory

    告诉子类刷新内部 bean 工厂并返回,返回的实例类型为 DefaultListableBeanFactory。在这里完成了配置文件的读取,初步注册了 bean 定义。

    +
    +

    我大概这辈子都不会想理清楚这里面关于 XML 文件的解析过程,但是我知道可以在这里观察到 beanFactory 因为配置文件注册了哪些 bean。

    +
    +
    protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    // 刷新 beanFactory
    // 使用 XmlBeanDefinitionReader 加载配置文件中的 bean 定义
    refreshBeanFactory();
    // 返回 beanFactory
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    if (logger.isDebugEnabled()) {
    logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
    }
    return beanFactory;
    }
    -

    线程问题排查

    CPU 占用率居高不下

    public class ThreadTest_1 {

    public static void main(String[] args) {
    new Thread(null, () -> {
    System.out.println("t1...");
    while (true) {

    }
    }, "thread1").start();

    new Thread(null, () -> {
    System.out.println("t2...");
    try {
    TimeUnit.SECONDS.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }, "thread2").start();

    new Thread(null, () -> {
    System.out.println("t3...");
    try {
    TimeUnit.SECONDS.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }, "thread3").start();
    }
    }
    -

    当发现 CPU 占用率居高不下时,可以尝试以下步骤:

    +

    准备 beanFactory

    配置 BeanFactory 以供在此 context 中使用,例如 context 的类加载器和一些后处理器,手动注册一些单例。

      -
    1. top,定位 cpu 占用高的进程 id。
    2. -
    3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
    4. -
    5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
    6. +
    7. 为 beanFactory 配置 context 相关的资源,如类加载器
    8. +
    9. 添加 Bean 后处理器
        +
      • ApplicationContextAwareProcessor,context 回调,注入特定类型时可触发自定义逻辑
      • +
      • ApplicationListenerDetector,检测 ApplicationListener
      • +
      +
    10. +
    11. 手动注册单例
    -
    "thread1" #8 prio=5 os_prio=0 tid=0x00007f9bd0162800 nid=0x1061ad runnable [0x00007f9bd56eb000]
    java.lang.Thread.State: RUNNABLE
    at com.moralok.jvm.thread.ThreadTest_1.lambda$main$0(ThreadTest_1.java:10)
    at com.moralok.jvm.thread.ThreadTest_1$$Lambda$1/250421012.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:750)
    +
    +

    ignoreDependencyInterface 和 registerResolvableDependency 在理解之后比单纯地记忆它们有趣许多。

    +
    +
    protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    // 告诉内部 beanFactory 使用 context 的类加载器等等。
    beanFactory.setBeanClassLoader(getClassLoader());
    beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
    beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

    // 为内部 beanFactory 配置 context 回调。
    beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
    // 如果一个 bean 的依赖实现了以下接口,忽略该依赖的检查和自动装配。
    // 例如在 populateBean 时,如果 bena 的依赖存在 set 方法,就会去解析,调用 getBean
    // 被设置 ignoreDependencyInterface 的依赖,仍然可以通过后置处理器进行依赖注入,例如以下的类型会使用上面那个后置处理器的回调方法注入。
    // 因此 @Autowire 这些通过后置处理器实现依赖注入的注解,也不会受影响
    // 这样设计的一个可能是往往注入这些类型时,希望触发某些事件。
    beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
    beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
    beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
    beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
    beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
    beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

    // BeanFactory 之类的接口没有在普通工厂中注册为可解析类型,直接为它们指定 bean。
    beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
    beanFactory.registerResolvableDependency(ResourceLoader.class, this);
    beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
    beanFactory.registerResolvableDependency(ApplicationContext.class, this);

    // 提前注册后处理器以检测内部 beans 是否是一个 ApplicationListener。
    beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

    // 检测是否有 LoadTimeWeaver,如果存在就准备编织。
    if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
    beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
    // Set a temporary ClassLoader for type matching.
    beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    }

    // 手动注册默认的环境 beans。
    if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
    beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
    }
    if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
    beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
    }
    if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
    beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
    }
    }
    -

    死锁,迟迟未返回结果

    public class ThreadTest_2 {

    private static final Object A = new Object();
    private static final Object B = new Object();

    public static void main(String[] args) {
    new Thread(null, () -> {
    System.out.println("t1...");
    synchronized (A) {
    System.out.println(Thread.currentThread().getName() + " get A");
    try {
    TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    synchronized (B) {
    System.out.println(Thread.currentThread().getName() + " get B");
    }
    }
    }, "thread1").start();

    new Thread(null, () -> {
    System.out.println("t2...");
    synchronized (B) {
    System.out.println(Thread.currentThread().getName() + " get B");
    try {
    TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }
    synchronized (A) {
    System.out.println(Thread.currentThread().getName() + " get A");
    }
    }
    }, "thread2").start();
    }
    }
    +
    +

    在创建 Bean 开始前注册的单例,都属于手动注册的单例 manualSingletonNames

    +
    +
    public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
    super.registerSingleton(beanName, singletonObject);

    if (hasBeanCreationStarted()) {
    // Cannot modify startup-time collection elements anymore (for stable iteration)
    synchronized (this.beanDefinitionMap) {
    if (!this.beanDefinitionMap.containsKey(beanName)) {
    Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames.size() + 1);
    updatedSingletons.addAll(this.manualSingletonNames);
    updatedSingletons.add(beanName);
    this.manualSingletonNames = updatedSingletons;
    }
    }
    }
    else {
    // Still in startup registration phase
    if (!this.beanDefinitionMap.containsKey(beanName)) {
    this.manualSingletonNames.add(beanName);
    }
    }

    clearByTypeCache();
    }
    -
      -
    1. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
    2. -
    3. 其他工具如 jconsole 也具有检测死锁的功能。
    4. -
    -

    本地方法栈

    堆(Heap)的特点:

    -
      -
    1. 线程共享,需要考虑线程安全问题。
    2. -
    3. 存在垃圾回收机制。
    4. -
    5. 使用 -Xmx8m 设置大小。
    6. -
    -

    堆内存溢出

    既然堆有垃圾回收机制,为什么还会发生内存溢出呢?最开始的时候,我也有这样的困惑。
    后来我才认识到,还在使用中的对象是不能被强制回收的,不再使用的对象不是立刻回收的。当创建对象却没有足够的内存空间时,如果清理掉那些不再使用的对象就有足够的内存空间,就不会发生内存溢出,程序只是表现为卡顿。

    -
    public class HeapTest_1 {  

    // -Xmx8m
    // 不设置可能不提示 Java heap space,出错地方不同,报错信息不同
    public static void main(String[] args) {
    int i = 0;
    try {
    List<String> list = new ArrayList<>();
    String s = "hello";
    while (true) {
    list.add(s);
    s = s + s;
    i++;
    }
    } catch (Throwable t) {
    t.printStackTrace();
    } finally {
    System.out.println("运行次数 " + i);
    }
    }
    }
    - -
    java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
    at java.lang.StringBuilder.append(StringBuilder.java:141)
    at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
    运行次数 17
    - -

    堆内存溢出的发生往往需要长时间的运行,因此在排查相关问题时,可以适当调小堆内存。

    -

    监测堆内存

      -
    1. 使用 jps 查看 Java 进程列表
    2. -
    3. 使用 jmap -heap pid 查看堆内存信息
    4. -
    5. 还可以使用 jconsole 观察堆内存变化曲线
    6. -
    7. 还可以使用 VisualVM 查看堆信息
    8. -
    -
    public class HeapTest_2 {

    public static void main(String[] args) throws InterruptedException {
    System.out.println("1...");
    TimeUnit.SECONDS.sleep(30);
    // 堆空间占用上升 10MB
    byte[] bytes = new byte[1024 * 1024 * 10];
    System.out.println("2...");
    TimeUnit.SECONDS.sleep(30);
    bytes = null;
    // 堆空间占用下降
    System.gc();
    System.out.println("3...");
    TimeUnit.SECONDS.sleep(3000);
    }
    }
    - -

    使用 jmap -heap pid 查看堆内存信息:

    -
    Eden Space:
    capacity = 268435456 (256.0MB)
    used = 32212360 (30.72010040283203MB)

    used = 42698136 (40.720115661621094MB)

    used = 5368728 (5.120018005371094MB)
    - -

    使用 jconsole 查看堆内存信息:

    - - -

    堆内存占用居高不下

    当你发现堆内存占用居高不下,经过 GC,下降也不明显,如果你想查看一下堆内的具体情况,可以将其 dump 查看。

    -
    public class HeapTest_3 {  

    // jps 查进程,jmap 看堆内存,jconsole 执行GC,堆内存占用没有明显下降
    // 使用 VisualVM 的堆 dump 功能,观察大对象
    public static void main(String[] args) throws IOException {
    List<Student> students = new ArrayList<>();
    for (int i = 0; i < 200; i++) {
    students.add(new Student());
    }
    System.in.read();
    }

    static class Student {
    private byte[] score = new byte[1024 * 1024];
    }
    }
    -

    可使用 VisualVM 的 Heap Dump 功能:

    - +

    postProcessBeanFactory

    在标准初始化后修改内部 beanFactory,默认什么都不做。

    +

    invokeBeanFactoryPostProcessors

    实例化并调用所有在 context 中注册的 beanFactory 后处理器,需遵循顺序规则。具体的处理被委托给 PostProcessorRegistrationDelegate。

    +
    protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
    // ...
    }
    -

    也可使用 jmap -dump:format=b,file=filename.hprof pid,需要其他分析工具搭配。

    -

    方法区

    根据《Java虚拟机规范》,方法区在逻辑上是堆的一部分,但是在具体实现上,各个虚拟机厂商并不相同。对于 Hotspot 而言:

    -
      -
    • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
    • -
    • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。
    • -
    -

    方法区溢出

    public class MethodAreaTest_1 extends ClassLoader {

    // -XX:MaxMetaspaceSize=8m MaxMetaspaceSize is too small.
    // -XX:MaxMetaspaceSize=10m java.lang.OutOfMemoryError: Compressed class space
    // 不是 Metaspace 应该是某个参数设置的问题
    // JDK 6: -XX:MaxPermSize=8m PermGen space
    public static void main(String[] args) {
    int j = 0;
    try {
    MethodAreaTest_1 methodAreaTest1 = new MethodAreaTest_1();
    for (int i = 0; i < 20000; i++, j++) {
    ClassWriter classWriter = new ClassWriter(0);
    // 版本号,public,类名,包名,父类,接口
    classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
    // 返回二进制字节码
    byte[] code = classWriter.toByteArray();
    // 加载类
    methodAreaTest1.defineClass("Class" + i, code, 0, code.length);
    }
    } catch (ClassFormatError e) {
    e.printStackTrace();
    } finally {
    System.out.println("次数 " + j);
    }
    }
    }
    +

    invokeBeanFactoryPostProcessors 方法堪比裹脚布。

    +

    关于调用顺序的规则

      -
    1. 当设置的值太小时 -XX:MaxMetaspaceSize=8m,提示 MaxMetaspaceSize is too small。
    2. -
    3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
    4. -
    5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
    6. -
    7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
    8. +
    9. BeanFactoryPostProcessor 分为 context 添加的和 beanFactory 注册的,前者优于后者
    10. +
    11. BeanFactoryPostProcessor 又可分为常规的和 BeanDefinitionRegistryPostProcessor,后者优于前者
    12. +
    13. PriorityOrdered 优于 Ordered 优于剩余的
    -

    不要认为自己不会写动态生成字节码相关的代码就忽略这方面的问题,如今很多框架使用字节码技术大量地动态生成类。

    -

    运行时常量池

    二进制字节码文件主要包含三类信息:

    +

    可能新增 beanDefinition 的情况:

      -
    1. 类的基本信息
    2. -
    3. 类的常量池(Constant Pool)
    4. -
    5. 类的方法信息
    6. +
    7. BeanDefinitionRegistryPostProcessor 可能在 beanFactory 中引入新的 beanDefinition
    -

    使用 javap 反编译

    public class MethodAreaTest_2 {  

    public static void main(String[] args) {
    System.out.println("hello world");
    }
    }
    +
    public static void invokeBeanFactoryPostProcessors(
    ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

    // 存储已处理过的后处理器
    Set<String> processedBeans = new HashSet<String>();

    // 第一阶段:BeanDefinitionRegistryPostProcessor
    if (beanFactory instanceof BeanDefinitionRegistry) {
    // 如果 beanFactory 同时是 BeanDefinitionRegistry 类型
    BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
    // 存储常规的 BeanFactoryPostProcessor
    List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
    // 存储 BeanDefinitionRegistryPostProcessor
    List<BeanDefinitionRegistryPostProcessor> registryProcessors = new LinkedList<BeanDefinitionRegistryPostProcessor>();

    // 第 0 轮,先对 context 注册的 BeanFactoryPostProcessor 进行分类
    for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
    if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
    BeanDefinitionRegistryPostProcessor registryProcessor =
    (BeanDefinitionRegistryPostProcessor) postProcessor;
    // 分类的同时,直接调用 BeanDefinitionRegistryPostProcessor
    registryProcessor.postProcessBeanDefinitionRegistry(registry);
    registryProcessors.add(registryProcessor);
    }
    else {
    regularPostProcessors.add(postProcessor);
    }
    }

    // 将 BeanDefinitionRegistryPostProcessors 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
    List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();

    // 第 1 轮,先处理 beanFactory 中实现了 PriorityOrdered 的 BeanDefinitionRegistryPostProcessor
    String[] postProcessorNames =
    beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
    for (String ppName : postProcessorNames) {
    if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
    // getBean 并添加到当前的后处理器集合
    currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
    processedBeans.add(ppName);
    }
    }
    // 排序后添加
    sortPostProcessors(currentRegistryProcessors, beanFactory);
    registryProcessors.addAll(currentRegistryProcessors);
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    currentRegistryProcessors.clear();

    // 第 2 轮,Ordered
    postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
    for (String ppName : postProcessorNames) {
    if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
    currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
    processedBeans.add(ppName);
    }
    }
    sortPostProcessors(currentRegistryProcessors, beanFactory);
    registryProcessors.addAll(currentRegistryProcessors);
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    currentRegistryProcessors.clear();

    // 第 3 轮, 调用剩余的 BeanDefinitionRegistryPostProcessors 直到没有新的出现。
    // 后出现的 PriorityOrdered 不比前面的 Ordered 更早被处理
    boolean reiterate = true;
    while (reiterate) {
    reiterate = false;
    postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
    for (String ppName : postProcessorNames) {
    if (!processedBeans.contains(ppName)) {
    currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
    processedBeans.add(ppName);
    reiterate = true;
    }
    }
    sortPostProcessors(currentRegistryProcessors, beanFactory);
    registryProcessors.addAll(currentRegistryProcessors);
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    currentRegistryProcessors.clear();
    }

    // 调用目前出现的 BeanFactoryPostProcessors
    // 仍遵循 PriorityOrdered、Ordered、Regular(registry)、Regular(context 添加的) 的顺序
    invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
    invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
    }

    else {
    // 否则,直接调用 context 注册的 beanFactoryPostProcessors
    invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
    }

    // 第二阶段:BeanFactoryPostProcessor
    String[] postProcessorNames =
    beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

    // 将 BeanFactoryPostProcessor 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
    List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
    List<String> orderedPostProcessorNames = new ArrayList<String>();
    List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
    for (String ppName : postProcessorNames) {
    if (processedBeans.contains(ppName)) {
    // 第一阶段已处理,跳过
    }
    else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
    priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
    }
    else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
    orderedPostProcessorNames.add(ppName);
    }
    else {
    nonOrderedPostProcessorNames.add(ppName);
    }
    }

    // 第 1 轮,BeanFactoryPostProcessors that implement PriorityOrdered.
    sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
    invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

    // 第 2 轮,BeanFactoryPostProcessors that implement Ordered.
    List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
    for (String postProcessorName : orderedPostProcessorNames) {
    orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
    }
    sortPostProcessors(orderedPostProcessors, beanFactory);
    invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

    // 第 3 轮,剩余的 BeanFactoryPostProcessors.
    List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
    for (String postProcessorName : nonOrderedPostProcessorNames) {
    nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
    }
    invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

    // 清理缓存的 merged bean definitions 因为 post-processors 可能已经修改了原来的 metadata
    beanFactory.clearMetadataCache();
    }
    -
    Classfile /C:/Users/username/Documents/github/jvm-study/target/classes/com/moralok/jvm/memory/methodarea/MethodAreaTest_2.class
    Last modified 2023-11-4; size 619 bytes
    MD5 checksum 0ed10a8f0a03be54fd4159958ee7446c
    Compiled from "MethodAreaTest_2.java"
    public class com.moralok.jvm.memory.methodarea.MethodAreaTest_2
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
    #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
    #3 = String #23 // hello world
    #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
    #5 = Class #26 // com/moralok/jvm/memory/methodarea/MethodAreaTest_2
    #6 = Class #27 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 LocalVariableTable
    #12 = Utf8 this
    #13 = Utf8 Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;
    #14 = Utf8 main
    #15 = Utf8 ([Ljava/lang/String;)V
    #16 = Utf8 args
    #17 = Utf8 [Ljava/lang/String;
    #18 = Utf8 SourceFile
    #19 = Utf8 MethodAreaTest_2.java
    #20 = NameAndType #7:#8 // "<init>":()V
    #21 = Class #28 // java/lang/System
    #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
    #23 = Utf8 hello world
    #24 = Class #31 // java/io/PrintStream
    #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
    #26 = Utf8 com/moralok/jvm/memory/methodarea/MethodAreaTest_2
    #27 = Utf8 java/lang/Object
    #28 = Utf8 java/lang/System
    #29 = Utf8 out
    #30 = Utf8 Ljava/io/PrintStream;
    #31 = Utf8 java/io/PrintStream
    #32 = Utf8 println
    #33 = Utf8 (Ljava/lang/String;)V
    {
    public com.moralok.jvm.memory.methodarea.MethodAreaTest_2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
    stack=1, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    LineNumberTable:
    line 3: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=2, locals=1, args_size=1
    0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc #3 // String hello world
    5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return
    LineNumberTable:
    line 6: 0
    line 7: 8
    LocalVariableTable:
    Start Length Slot Name Signature
    0 9 0 args [Ljava/lang/String;
    }
    SourceFile: "MethodAreaTest_2.java"
    +

    registerBeanPostProcessors

    注册拦截 bean 创建的 bean 后处理器。具体的处理被委托给 PostProcessorRegistrationDelegate。

    +
    protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
    }
    -
      -
    1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
    2. -
    3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
    4. -
    5. 这个过程其实就是信息从文件进入了内存。
    6. -
    -

    虚拟机解释器(interpreter)需要解释的字节码指令如下:

    -
    0: getstatic     #2
    3: ldc #3
    5: invokevirtual #4
    -

    索引 #2 的意思就是去常量表里查找对应项代表的事物。

    -

    直接内存

      -
    • 常见于 NIO 操作中的数据缓冲区。
    • -
    • 分配和回收的成本较高,但读写性能更高。
    • -
    • 不由 JVM 进行内存释放
    • +

      registerBeanPostProcessors 相比之下是一条清新的裹脚布。这里特别区分 3 种类型的 Bean 后处理器:

      +
        +
      • MergedBeanDefinitionPostProcessor
      • +
      • InstantiationAwareBeanPostProcessor 感知实例化
      • +
      • DestructionAwareBeanPostProcessor 感知销毁
      -

      NIO 和 IO 的拷贝性能

      public class DirectMemoryTest_1 {  

      private static final String FROM = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY.mp4";
      private static final String TO = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY-copy.mp4";
      private static final int _1Mb = 1024 * 1024;

      public static void main(String[] args) {
      io();
      directBuffer();
      }

      private static void directBuffer() {
      long start = System.nanoTime();
      try (FileChannel from = new FileInputStream(FROM).getChannel();
      FileChannel to = new FileOutputStream(TO).getChannel()) {
      ByteBuffer buffer = ByteBuffer.allocateDirect(_1Mb);
      while (true) {
      int len = from.read(buffer);
      if (len == -1) {
      break;
      }
      buffer.flip();
      to.write(buffer);
      buffer.clear();
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      long end = System.nanoTime();
      System.out.println("directBuffer 用时 " + (end - start) / 1000_000.0);
      }

      private static void io() {
      long start = System.nanoTime();
      try (FileInputStream from = new FileInputStream(FROM);
      FileOutputStream to = new FileOutputStream(TO)) {
      byte[] buffer = new byte[_1Mb];
      while (true) {
      int len = from.read(buffer);
      if (len == -1) {
      break;
      }
      to.write(buffer);
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      long end = System.nanoTime();
      System.out.println("io 用时 " + (end - start) / 1000_000.0);
      }
      }
      - -
      io 用时 1676.9797
      directBuffer 用时 836.4796
      - - - - - - -

      直接内存溢出

      public class DirectMemoryTest_2 {  

      private static final int _100Mb = 1024 * 1024 * 100;

      public static void main(String[] args) {
      List<ByteBuffer> list = new ArrayList<>();
      int i = 0;
      try {
      while (true) {
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
      list.add(byteBuffer);
      i++;
      }
      } catch (Throwable t) {
      t.printStackTrace();
      } System.out.println(i);
      }
      }
      - -
      java.lang.OutOfMemoryError: Direct buffer memory
      at java.nio.Bits.reserveMemory(Bits.java:695)
      at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
      at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
      at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
      145
      - -

      这似乎是代码中抛出的异常,而不是真正的直接内存溢出?

      -

      直接内存释放的原理

      演示直接内存的释放受 GC 影响

      public class DirectMemoryTest_3 {

      private static final int _1GB = 1024 * 1024 * 1024;

      public static void main(String[] args) throws IOException {
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
      System.out.println("分配完毕");
      System.in.read();
      System.out.println("开始释放");
      byteBuffer = null;
      // 随着 ByteBuffer 的释放,从任务管理器界面看到程序的内存的占用迅速下降 1GB。
      System.gc();
      System.in.read();
      }
      }
      - -

      手动进行直接内存的分配和释放

      在代码中实现手动进行直接内存的分配和释放。

      -
      public class DirectMemoryTest_4 {

      private static final int _1GB = 1024 * 1024 * 1024;

      public static void main(String[] args) throws IOException {
      Unsafe unsafe = getUnsafe();

      // 分配内存
      long base = unsafe.allocateMemory(_1GB);
      unsafe.setMemory(base, _1GB, (byte) 0);
      System.in.read();

      // 释放内存
      unsafe.freeMemory(base);
      System.in.read();
      }

      private static Unsafe getUnsafe() {
      try {
      Field f = Unsafe.class.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      Unsafe unsafe = (Unsafe) f.get(null);
      return unsafe;
      } catch (NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
      }
      }
      }
      - -

      如何将 GC 和直接内存的分配和释放关联

      本质上,直接内存的自动释放是利用了虚引用的机制,间接调用了 unsafe 的分配和释放直接内存的方法。

      -

      DirectByteBuffer 就是使用 unsafe.allocateMemory(size) 分配直接内存。DirectByteBuffer 对象以及一个 Deallocator 对象(Runnable 类型)一起用于创建了一个虚引用类型的 Cleaner 对象。

      -
      DirectByteBuffer(int cap) {

      // 省略
      try {
      base = unsafe.allocateMemory(size);
      } catch (OutOfMemoryError x) {
      Bits.unreserveMemory(size, cap);
      throw x;
      }
      // 省略
      cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
      att = null;
      }
      - -

      根据虚引用的机制,如果 DirectByteBuffer 对象被回收,虚引用对象会被加入到 Cleanner 的引用队列,ReferenceHandler 线程会处理引用队列中的 Cleaner 对象,进而调用 Deallocator 对象的 run 方法。

      -
      public void run() {
      if (address == 0) {
      // Paranoia
      return;
      }
      unsafe.freeMemory(address);
      address = 0;
      Bits.unreserveMemory(size, capacity);
      }
      -]]> - - java - jvm - - - - Spring Bean 加载过程 - /2023/08/10/how-does-Spring-load-beans/ - Spring Bean 生命周期 - -

      获取 Bean

      获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

      -
      public Object getBean(String name) throws BeansException {
      // 具体工作由 doGetBean 完成
      return doGetBean(name, null, null, false);
      }
      - -

      deGetBean

      作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

      -
      protected <T> T doGetBean(
      final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
      throws BeansException {
      // 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
      final String beanName = transformedBeanName(name);
      Object bean;

      // 检查单例缓存中是否已存在
      Object sharedInstance = getSingleton(beanName);
      if (sharedInstance != null && args == null) {
      // ...
      // 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
      }
      else {
      // 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
      // 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
      if (isPrototypeCurrentlyInCreation(beanName)) {
      throw new BeanCurrentlyInCreationException(beanName);
      }

      // 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
      BeanFactory parentBeanFactory = getParentBeanFactory();
      if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
      String nameToLookup = originalBeanName(name);
      if (args != null) {
      return (T) parentBeanFactory.getBean(nameToLookup, args);
      }
      else {
      return parentBeanFactory.getBean(nameToLookup, requiredType);
      }
      }

      if (!typeCheckOnly) {
      // 标记为至少创建过一次
      markBeanAsCreated(beanName);
      }

      try {
      final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
      checkMergedBeanDefinition(mbd, beanName, args);

      // 确保 bean 依赖的 bean(构造器参数) 都已实例化
      String[] dependsOn = mbd.getDependsOn();
      if (dependsOn != null) {
      for (String dep : dependsOn) {
      if (isDependent(beanName, dep)) {
      // 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
      }
      // 注册依赖关系,确保先销毁被依赖的 bean
      registerDependentBean(dep, beanName);
      // 递归,获取依赖的 bean
      getBean(dep);
      }
      }
      }

      if (mbd.isSingleton()) {
      // 如果是单例类型(绝大多数都是此类型)
      // 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
      sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
      {
      @Override
      public Object getObject() throws BeansException {
      try {
      // 创建 bean
      return createBean(beanName, mbd, args);
      }
      catch (BeansException ex) {
      // 由于可能已经提前暴露,需要显示地销毁
      destroySingleton(beanName);
      throw ex;
      }
      }
      });
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
      }
      else if (mbd.isPrototype()) {
      // 如果是原型类型,每次都新创建一个
      // ...
      }
      else {
      // 如果是其他 scope 类型
      // ...
      }
      }
      catch (BeansException ex) {
      cleanupAfterBeanCreationFailure(beanName);
      throw ex;
      }
      }
      - -

      getSingleton

      public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
      // 加锁
      synchronized (this.singletonObjects) {
      // 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
      if (this.singletonsCurrentlyInDestruction) {
      // 如果正在销毁单例,则抛异常
      // 注意:不要在销毁方法中调用获取 bean 方法
      }
      // 创建前,先注册到正在创建中的集合
      // 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
      beforeSingletonCreation(beanName);
      boolean newSingleton = false;
      // ...
      try {
      // 使用传入的单例工厂创建对象
      singletonObject = singletonFactory.getObject();
      newSingleton = true;
      }
      catch (IllegalStateException ex) {
      // 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
      singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
      throw ex;
      }
      }
      catch (BeanCreationException ex) {
      // ...
      }
      finally {
      // ...
      // 创建后,从正在创建中集合移除
      afterSingletonCreation(beanName);
      }
      if (newSingleton) {
      // 添加单例到缓存
      addSingleton(beanName, singletonObject);
      }
      }
      return (singletonObject != NULL_OBJECT ? singletonObject : null);
      }
      }
      - -

      创建 Bean

      createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

      -
      protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
      // ...
      try {
      // 给 Bean 后置处理器一个返回代理的机会
      Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
      if (bean != null) {
      return bean;
      }
      }
      // ...
      // 常规的创建 Bean
      Object beanInstance = doCreateBean(beanName, mbdToUse, args);
      return beanInstance;
      }
      - -

      doCreateBean

      常规的创建 Bean 的具体工作是由 doCreateBean 完成的。

      -
      protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
      BeanWrapper instanceWrapper = null;
      if (mbd.isSingleton()) {
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
      }
      if (instanceWrapper == null) {
      // 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
      instanceWrapper = createBeanInstance(beanName, mbd, args);
      }
      final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
      Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
      mbd.resolvedTargetType = beanType;

      // ...

      // 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
      boolean earlySingletonExposure = (mbd.isSingleton()
      && this.allowCircularReferences
      && isSingletonCurrentlyInCreation(beanName));
      if (earlySingletonExposure) {
      addSingletonFactory(beanName, new ObjectFactory<Object>() {
      @Override
      public Object getObject() throws BeansException {
      return getEarlyBeanReference(beanName, mbd, bean);
      }
      });
      }

      Object exposedObject = bean;
      try {
      // 填充属性(包括解析依赖的 bean)
      populateBean(beanName, mbd, instanceWrapper);
      if (exposedObject != null) {
      // 初始化 bean
      exposedObject = initializeBean(beanName, exposedObject, mbd);
      }
      }
      // ...

      // 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
      try {
      registerDisposableBeanIfNecessary(beanName, bean, mbd);
      }
      // ...

      return exposedObject;
      }
      +

      ApplicationListenerDetector 既是 MergedBeanDefinitionPostProcessor,又是 DestructionAwareBeanPostProcessor,在初始化后将 listener 加入,在销毁前将 listener 移除。

      +
      public static void registerBeanPostProcessors(
      ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
      // 获取 BeanPostProcessor 的名称
      String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

      // 注册 BeanPostProcessorChecker,在 BeanPostProcessor 实例化期间创建 bean 时,记录一条消息。
      // 即,当一个 bean 不能被所有 BeanPostProcessors 处理时,记录。
      int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
      beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

      // 将 BeanPostProcessors 按 implement PriorityOrdered,Ordered,和剩余的进行分类。
      List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
      List<BeanPostProcessor> internalPostProcessors = new ArrayList<BeanPostProcessor>();
      List<String> orderedPostProcessorNames = new ArrayList<String>();
      List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
      for (String ppName : postProcessorNames) {
      if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
      BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
      priorityOrderedPostProcessors.add(pp);
      if (pp instanceof MergedBeanDefinitionPostProcessor) {
      internalPostProcessors.add(pp);
      }
      }
      else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
      orderedPostProcessorNames.add(ppName);
      }
      else {
      nonOrderedPostProcessorNames.add(ppName);
      }
      }

      // 第 1 轮,注册 BeanPostProcessors that implement PriorityOrdered.
      sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
      registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

      // 第 2 轮,注册 BeanPostProcessors that implement Ordered.
      List<BeanPostProcessor> orderedPostProcessors = new ArrayList<BeanPostProcessor>();
      for (String ppName : orderedPostProcessorNames) {
      BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
      orderedPostProcessors.add(pp);
      if (pp instanceof MergedBeanDefinitionPostProcessor) {
      internalPostProcessors.add(pp);
      }
      }
      sortPostProcessors(orderedPostProcessors, beanFactory);
      registerBeanPostProcessors(beanFactory, orderedPostProcessors);

      // 第 3 轮,注册剩余的 regular BeanPostProcessors.
      List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
      for (String ppName : nonOrderedPostProcessorNames) {
      BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
      nonOrderedPostProcessors.add(pp);
      if (pp instanceof MergedBeanDefinitionPostProcessor) {
      internalPostProcessors.add(pp);
      }
      }
      registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

      // 最后(第 4 轮), 排序并注册 internal BeanPostProcessors.
      sortPostProcessors(internalPostProcessors, beanFactory);
      registerBeanPostProcessors(beanFactory, internalPostProcessors);

      // Re-register post-processor for detecting inner beans as ApplicationListeners,
      // moving it to the end of the processor chain (for picking up proxies etc).
      // 重新注册用于检测 ApplicationListeners 的 Bean 后处理器,将其移动到处理器链的最后(用于获取代理)。
      beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
      }
      -

      createBeanInstance

      创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

      +

      添加 BeanPostProcessor 时

        -
      1. 工厂方法
      2. -
      3. 构造器方法
          -
        1. 有参
        2. -
        3. 无参
        4. -
        +
      4. 先移除
      5. +
      6. 再添加
      7. +
      8. 判断类型并记录标记
          +
        • 感知实例化的后处理器
        • +
        • 感知销毁的后处理器
        • +
      -

      populateBean

      为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

      -
      protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
      PropertyValues pvs = mbd.getPropertyValues();
      // ...

      // 给 InstantiationAwareBeanPostProcessors 一个机会,
      // 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
      boolean continueWithPropertyPopulation = true;
      if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
      if (bp instanceof InstantiationAwareBeanPostProcessor) {
      InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
      if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
      continueWithPropertyPopulation = false;
      break;
      }
      }
      }
      }

      // 是否继续填充属性的流程
      if (!continueWithPropertyPopulation) {
      return;
      }

      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
      || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
      MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
      // 根据名称注入
      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
      autowireByName(beanName, mbd, bw, newPvs);
      }

      // 根据类型注入
      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
      autowireByType(beanName, mbd, bw, newPvs);
      }
      pvs = newPvs;
      }

      // 是否存在 InstantiationAwareBeanPostProcessors
      boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
      // 是否需要检查依赖
      boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

      if (hasInstAwareBpps || needsDepCheck) {
      PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
      if (hasInstAwareBpps) {
      // 后置处理 PropertyValues
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
      if (bp instanceof InstantiationAwareBeanPostProcessor) {
      InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
      pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
      if (pvs == null) {
      return;
      }
      }
      }
      }
      if (needsDepCheck) {
      checkDependencies(beanName, mbd, filteredPds, pvs);
      }
      }
      // 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
      applyPropertyValues(beanName, mbd, bw, pvs);
      }
      +
      public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
      Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
      this.beanPostProcessors.remove(beanPostProcessor);
      this.beanPostProcessors.add(beanPostProcessor);
      if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) {
      this.hasInstantiationAwareBeanPostProcessors = true;
      }
      if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) {
      this.hasDestructionAwareBeanPostProcessors = true;
      }
      }
      -

      initializeBean

      在填充完属性后,实例就可以进行初始化工作:

      -
        -
      1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
      2. -
      3. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
      4. -
      5. invokeInitMethods,调用初始化方法
      6. -
      7. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
      8. -
      -
      protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
      // 处理 Aware 接口的相应方法
      if (System.getSecurityManager() != null) {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
      @Override
      public Object run() {
      invokeAwareMethods(beanName, bean);
      return null;
      }
      }, getAccessControlContext());
      }
      else {
      invokeAwareMethods(beanName, bean);
      }

      // 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
      Object wrappedBean = bean;
      if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
      }

      try {
      // 调用初始化方法
      invokeInitMethods(beanName, wrappedBean, mbd);
      }
      catch (Throwable ex) {
      throw new BeanCreationException(
      (mbd != null ? mbd.getResourceDescription() : null),
      beanName, "Invocation of init method failed", ex);
      }

      if (mbd == null || !mbd.isSynthetic()) {
      // 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
      }
      return wrappedBean;
      }
      +

      initMessageSource

      初始化消息源。 如果在此 context 中未定义,则使用父级的。

      +
      protected void initMessageSource() {
      ConfigurableListableBeanFactory beanFactory = getBeanFactory();
      if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
      this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
      // 设置 parent MessageSource.
      if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
      HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
      if (hms.getParentMessageSource() == null) {
      // 只有当 parent MessageSource 尚未注册才将 parent context 设置为 parent MessageSource
      hms.setParentMessageSource(getInternalParentMessageSource());
      }
      }
      if (logger.isDebugEnabled()) {
      logger.debug("Using MessageSource [" + this.messageSource + "]");
      }
      }
      else {
      // 使用代理 messageSource,以此接收 getMessage 调用。
      DelegatingMessageSource dms = new DelegatingMessageSource();
      dms.setParentMessageSource(getInternalParentMessageSource());
      this.messageSource = dms;
      beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
      if (logger.isDebugEnabled()) {
      logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME +
      "': using default [" + this.messageSource + "]");
      }
      }
      }
      -
      处理 Aware 接口的相应方法

      让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

      -
      private void invokeAwareMethods(final String beanName, final Object bean) {
      if (bean instanceof Aware) {
      if (bean instanceof BeanNameAware) {
      ((BeanNameAware) bean).setBeanName(beanName);
      }
      if (bean instanceof BeanClassLoaderAware) {
      ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
      }
      if (bean instanceof BeanFactoryAware) {
      ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
      }
      }
      }
      +

      initApplicationEventMulticaster

      初始化 ApplicationEventMulticaster。 如果上下文中未定义,则使用 SimpleApplicationEventMulticaster。可以看得出代码的结构和 initMessageSource 是类似的。

      +
      protected void initApplicationEventMulticaster() {
      ConfigurableListableBeanFactory beanFactory = getBeanFactory();
      if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
      this.applicationEventMulticaster =
      beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
      if (logger.isDebugEnabled()) {
      logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
      }
      }
      else {
      this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
      beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
      if (logger.isDebugEnabled()) {
      logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
      APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
      "': using default [" + this.applicationEventMulticaster + "]");
      }
      }
      }
      -
      调用初始化方法
        -
      1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
      2. -
      3. 如果自定义 init 方法且满足调用条件,同样进行调用
      4. -
      -
      protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
      // 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
      // 给 bean 一个感知属性已设置并做出反应的机会
      boolean isInitializingBean = (bean instanceof InitializingBean);
      if (isInitializingBean
      && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
      if (System.getSecurityManager() != null) {
      try {
      AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
      @Override
      public Object run() throws Exception {
      ((InitializingBean) bean).afterPropertiesSet();
      return null;
      }
      }, getAccessControlContext());
      }
      catch (PrivilegedActionException pae) {
      throw pae.getException();
      }
      }
      else {
      ((InitializingBean) bean).afterPropertiesSet();
      }
      }

      // 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
      if (mbd != null) {
      String initMethodName = mbd.getInitMethodName();
      if (initMethodName != null
      && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
      && !mbd.isExternallyManagedInitMethod(initMethodName)) {
      invokeCustomInitMethod(beanName, bean, mbd);
      }
      }
      }
      +

      onRefresh

      可以重写模板方法来添加特定 context 的刷新工作。默认情况下什么都不做。

      +

      registerListeners

      获取侦听器 bean 并注册。无需初始化即可添加

      +
      protected void registerListeners() {
      // 注册静态指定的 ApplicationListener,和 beanFactoryPostProcessor 类似,context 可以提前添加好。
      for (ApplicationListener<?> listener : getApplicationListeners()) {
      getApplicationEventMulticaster().addApplicationListener(listener);
      }

      // Do not initialize FactoryBeans here: We need to leave all regular beans
      // uninitialized to let post-processors apply to them!
      // 这段注释看到不止一次,但是不太理解,感觉和代码联系不起来?
      String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
      for (String listenerBeanName : listenerBeanNames) {
      getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
      }

      // 现在我们终于有了多播器,发布早期的应用事件。
      Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
      this.earlyApplicationEvents = null;
      if (earlyEventsToProcess != null) {
      for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
      getApplicationEventMulticaster().multicastEvent(earlyEvent);
      }
      }
      }
      -
      BeanPostProcessor 处理

      在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

      -
        -
      1. 获取 Processor 列表
      2. -
      3. 判断 Processor 类型是否是当前需要的
      4. -
      5. 对 bean 进行处理
      6. -
      -
      public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

      Object result = existingBean;
      for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
      result = beanProcessor.postProcessBeforeInitialization(result, beanName);
      if (result == null) {
      return result;
      }
      }
      return result;
      }

      public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

      Object result = existingBean;
      for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
      result = beanProcessor.postProcessAfterInitialization(result, beanName);
      if (result == null) {
      return result;
      }
      }
      return result;
      }
      +

      添加 ApplicationListener。

      +
      +

      后处理器 ApplicationListenerDetector 在 processor chain 的最后,最终会将创建的代理添加为监听器。什么情况下会出现代码中预防的情况呢?

      +
      +
      public void addApplicationListener(ApplicationListener<?> listener) {
      synchronized (this.retrievalMutex) {
      // 如果已经注册,需要显式地删除代理,以避免同一监听器的双重调用。
      Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
      if (singletonTarget instanceof ApplicationListener) {
      this.defaultRetriever.applicationListeners.remove(singletonTarget);
      }
      this.defaultRetriever.applicationListeners.add(listener);
      this.retrieverCache.clear();
      }
      }
      -

      再思 Bean 的初始化

      以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
      在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

      -
      // Initialize the bean instance.
      Object exposedObject = bean;
      try {
      populateBean(beanName, mbd, instanceWrapper);
      if (exposedObject != null) {
      exposedObject = initializeBean(beanName, exposedObject, mbd);
      }
      }
      +

      finishBeanFactoryInitialization

      实例化所有剩余的(非惰性初始化)单例。以 context 视角,是完成内部 beanFactory 的初始化。

      +

      几乎可以只关注最后的 beanFactory.preInstantiateSingletons()

      +
      protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
      // 为 context 初始化转换服务
      if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
      beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
      beanFactory.setConversionService(
      beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
      }

      // 如果之前没有任何 bean 后处理器(例如 PropertyPlaceholderConfigurer)注册,则注册默认的嵌入值解析器,主要用于解析注释属性值。
      // 接口 ConfigurableEnvironment 继承自 ConfigurablePropertyResolver
      if (!beanFactory.hasEmbeddedValueResolver()) {
      beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
      @Override
      public String resolveStringValue(String strVal) {
      return getEnvironment().resolvePlaceholders(strVal);
      }
      });
      }

      // 尽早初始化 LoadTimeWeaverAware,以便尽早注册其转换器。
      String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
      for (String weaverAwareName : weaverAwareNames) {
      getBean(weaverAwareName);
      }

      // 停止使用临时类加载器进行类型匹配。
      beanFactory.setTempClassLoader(null);

      // 允许缓存所有 bean 定义的元数据,而不期望进一步更改。
      beanFactory.freezeConfiguration();

      // 实例化所有剩余的(非惰性初始化)单例。
      beanFactory.preInstantiateSingletons();
      }
      -

      在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

      -
        -
      1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
      2. -
      3. Bean 自身的初始化。
      4. -
      -

      在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
      在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

      -]]>
      +

      确保所有非惰性初始化单例都已实例化,同时还要考虑 FactoryBeans。 如果需要,通常在工厂设置结束时调用。

      +加载 Bean 的流程分析在此。 + +
      +

      先对集合进行 Copy 再迭代是很常见的处理方式,可以有效保证迭代时不受原集合影响,也不会影响到原集合。

      +
      +
      @Override
      public void preInstantiateSingletons() throws BeansException {
      if (this.logger.isDebugEnabled()) {
      this.logger.debug("Pre-instantiating singletons in " + this);
      }

      // 拷贝一份 beanDefinitionNames
      List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);

      // 触发所有非惰性初始化单例的实例化
      for (String beanName : beanNames) {
      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
      if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
      if (isFactoryBean(beanName)) {
      // FactoryBean
      final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
      boolean isEagerInit;
      if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
      isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
      @Override
      public Boolean run() {
      return ((SmartFactoryBean<?>) factory).isEagerInit();
      }
      }, getAccessControlContext());
      }
      else {
      isEagerInit = (factory instanceof SmartFactoryBean &&
      ((SmartFactoryBean<?>) factory).isEagerInit());
      }
      if (isEagerInit) {
      // 是否立即初始化
      getBean(beanName);
      }
      }
      else {
      // 常规 Bean(重要)
      getBean(beanName);
      }
      }
      }

      // 触发所有适用 bean 的初始化后回调
      for (String beanName : beanNames) {
      Object singletonInstance = getSingleton(beanName);
      if (singletonInstance instanceof SmartInitializingSingleton) {
      final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
      if (System.getSecurityManager() != null) {
      AccessController.doPrivileged(new PrivilegedAction<Object>() {
      @Override
      public Object run() {
      smartSingleton.afterSingletonsInstantiated();
      return null;
      }
      }, getAccessControlContext());
      }
      else {
      smartSingleton.afterSingletonsInstantiated();
      }
      }
      }
      }
      + +

      finishRefresh

      最后一步,完成 context 刷新,比如发布相应的事件。

      +
      protected void finishRefresh() {
      // 1. 为此 context 初始化生命周期处理器。
      initLifecycleProcessor();
      // 2. 将刷新传播到生命周期处理器。
      getLifecycleProcessor().onRefresh();
      // 3. 发布 ContextRefreshedEvent。
      publishEvent(new ContextRefreshedEvent(this));
      // 4. 参与 LiveBeansView MBean(如果处于活动状态)。
      LiveBeansView.registerApplicationContext(this);
      }
      ]]> java spring
      - 关于 Java 字节码指令的一些例子分析 - /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ - 演示字节码指令的执行
      public class ByteCodeTest_2 {

      public static void main(String[] args) {
      int a = 10;
      int b = Short.MAX_VALUE + 1;
      int c = a + b;
      System.out.println(c);
      }
      }
      - -

      操作数栈和本地变量表的大小

      在编译期间就可计算得到操作数栈和本地变量表的大小。

      -
      stack=2, locals=4, args_size=1
      - -

      本地变量表

      Slot,即槽位,可理解为索引。

      -
      Start  Length  Slot  Name   Signature
      0 18 0 args [Ljava/lang/String;
      3 15 1 a I
      6 12 2 b I
      10 8 3 c I
      - -

      运行时常量池

      #3 = Integer            32768
      #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
      #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
      - - - -

      字节码指令

       0: bipush        10
      2: istore_1
      3: ldc #3 // int 32768
      5: istore_2
      6: iload_1
      7: iload_2
      8: iadd
      9: istore_3
      10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
      13: iload_3
      14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
      17: return
      + Java 垃圾收集 + /2023/11/07/garbage-collection-in-Java/ + +

      每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
      沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
      尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。

      + +

      概述

      垃圾收集(Garbage Collection,简称 GC)需要考虑什么?

        -
      • bipush,将一个 byte,推入操作数栈。
          -
        • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
        • -
        • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
        • +
        • 哪些内存需要回收?
        • +
        • 什么时候回收?
        • +
        • 如何回收?
        -
      • -
      • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
      • -
      • ldc,从运行时常量池中将指定常量推入操作数栈。
      • -
      • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
      • -
      • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
      • -
      • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
      • -
      • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
      • -
      • getstatic,获取类的静态属性,推入操作数栈。
      • -
      • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
      • -
      • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
      • -
      • return,返回 void
      • +

        为什么要去了解垃圾收集和内存分配?
        当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。

        +

        在 Java 中,垃圾收集需要关注哪些内存区域?
        程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
        但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

        +

        哪些内存需要回收?

        哪些对象是还存活着,哪些已经死亡?

        +
        +

        对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?

        +
        +

        引用计数算法(Reference Counting)

        优点:

        +
          +
        • 原理简单
        • +
        • 判定效率高
        -

        分析 a++ 和 ++a

        public class ByteCodeTest_3 {

        public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
        }
        }
        - -

        字节码指令

         0: bipush        10
        2: istore_1
        3: iload_1
        4: iinc 1, 1
        7: iinc 1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc 1, -1
        16: iadd
        17: istore_2
        - -
          -
        • a++ 和 ++a 的区别是先 load 还是先 iinc。
        • -
        • iinc,将局部变量表指定槽位的数加上一个常数。
        • -
        • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
        • -
        • b = 10 + 12 + 12 = 34
        • -
        • a = 10 + 1 + 1 - 1 = 11
        • -
        -

        分析判断条件

        public class ByteCodeTest_4 {

        public static void main(String[] args) {
        int a = 0;
        // ifeq, goto
        if (a == 0) {
        a = 10;
        } else {
        a = 20;
        }
        }
        }
        - -

        字节码指令

         0: iconst_0
        1: istore_1
        2: iload_1
        3: ifne 12
        6: bipush 10
        8: istore_1
        9: goto 15
        12: bipush 20
        14: istore_1
        15: return
        - -
          -
        • iconst,将一个 int 常量推入操作数栈。
        • -
        • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
        • -
        • goto,总是进入的分支,跳转到指定行号。
        • -
        -

        涉及的字节码指令

          -
        • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
        • -
        • istore,将栈顶的 int,存入局部变量表的指定槽位。
        • -
        • iload,将局部变量表指定槽位的 int,推入操作数栈。
        • -
        • ldc,从运行时常量池将指定常量推入操作数栈。
        • -
        • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
        • -
        • getstatic,获取类的静态属性,推入操作数栈。
        • -
        • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
        • -
        • return,返回 void。
        • -
        • iinc,将局部变量表中指定槽位的数加一个常量。
        • -
        • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
            -
          • ifeq,equals
          • -
          • ifne,not equals
          • -
          • iflt,less than
          • -
          • ifge,greater than or equals
          • -
          • ifgt,great than
          • -
          • ifle,less than or equals
          • -
          -
        • -
        • goto,总是进入的分支,跳转到指定行号。
        • -
        -]]> - - java - bytecode - - - - Java 垃圾收集 - /2023/11/07/garbage-collection-in-Java/ - -

        每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
        沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
        尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。

        - -

        概述

        垃圾收集(Garbage Collection,简称 GC)需要考虑什么?

        -
          -
        • 哪些内存需要回收?
        • -
        • 什么时候回收?
        • -
        • 如何回收?
        • -
        -

        为什么要去了解垃圾收集和内存分配?
        当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。

        -

        在 Java 中,垃圾收集需要关注哪些内存区域?
        程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
        但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

        -

        哪些内存需要回收?

        哪些对象是还存活着,哪些已经死亡?

        -
        -

        对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?

        -
        -

        引用计数算法(Reference Counting)

        优点:

        -
          -
        • 原理简单
        • -
        • 判定效率高
        • -
        -

        缺点:

        +

        缺点:

        • 例外情况多,需要额外处理(比如循环引用)
        @@ -2815,112 +2613,337 @@
        - 基于 Redis 的分布式锁的简单实现 - /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ - 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

        - - -

        很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

        -

        基于 Redis 的分布式锁简单实现

        本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

        -
        > setnx lock:user1 true
        OK

        ... do something

        > del lock:user1
        (integer) 1
        - -

        死锁问题

        问题一:异常引发死锁 1

        如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

        - - -

        改进一:设置超时时间

        对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

        -
        > setnx lock:user1 true
        OK
        > expire lock:user1 5

        ... do something

        > del lock:user1
        (integer) 1
        - -

        问题二:异常引发死锁 2

        事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

        - - -

        本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

        -

        改进二:setnx + expire 的原子指令

        如果 setnx 和 expire 可以用一个原子指令实现就好了。

        - - -
        基于原生指令的实现

        在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

        -
        > set lock:user1 true ex 5 nx
        OK

        ... do something

        > del lock:user1
        (integer) 1
        + 关于 Java 字节码指令的一些例子分析 + /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ + 演示字节码指令的执行
        public class ByteCodeTest_2 {

        public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
        }
        }
        -
        基于 Lua 脚本的实现

        除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

        -
        if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
        redis.call('expire', KEYS[1], ARGV[2])
        return true
        else
        return false
        end
        +

        操作数栈和本地变量表的大小

        在编译期间就可计算得到操作数栈和本地变量表的大小。

        +
        stack=2, locals=4, args_size=1
        -

        超时问题

        基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

        - +

        本地变量表

        Slot,即槽位,可理解为索引。

        +
        Start  Length  Slot  Name   Signature
        0 18 0 args [Ljava/lang/String;
        3 15 1 a I
        6 12 2 b I
        10 8 3 c I
        -

        问题一:其他线程提前进入临界区

        如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

        -

        问题二:错误释放其他线程的锁

        如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

        -

        改进一:不要用于较长时间的任务

        与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

        -

        如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

        -

        改进二:使用 watchdog 实现锁续期

        为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

        -
        private void scheduleRenewal() {
        String value = lockValue.get();
        ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
        () -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
        );
        renewalTask.set(scheduledFuture);
        }
        +

        运行时常量池

        #3 = Integer            32768
        #4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
        #5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
        -

        但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

        -

        改进三:加锁时指定 tag

        可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

        -
        基于原生指令的实现
        tag = random.nextint()
        if redis.set(key, tag, nx= True, ex=5):
        do_something()
        redis.delifequals(key, tag)
        + -

        但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

        - +

        字节码指令

         0: bipush        10
        2: istore_1
        3: ldc #3 // int 32768
        5: istore_2
        6: iload_1
        7: iload_2
        8: iadd
        9: istore_3
        10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
        17: return
        +
          +
        • bipush,将一个 byte,推入操作数栈。
            +
          • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
          • +
          • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
          • +
          +
        • +
        • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
        • +
        • ldc,从运行时常量池中将指定常量推入操作数栈。
        • +
        • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
        • +
        • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
        • +
        • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
        • +
        • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
        • +
        • getstatic,获取类的静态属性,推入操作数栈。
        • +
        • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
        • +
        • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
        • +
        • return,返回 void
        • +
        +

        分析 a++ 和 ++a

        public class ByteCodeTest_3 {

        public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
        }
        }
        -
        基于 Lua 脚本的实现
        if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
        else
        return 0
        end
        +

        字节码指令

         0: bipush        10
        2: istore_1
        3: iload_1
        4: iinc 1, 1
        7: iinc 1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc 1, -1
        16: iadd
        17: istore_2
        -

        可重入性

        可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

        -

        使用 ThreadLocal 实现锁计数

        Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

        -
        private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

        public boolean tryLock() {
        Integer count = lockCount.get();
        if (count != null && count > 0) {
        lockCount.set(count + 1);
        return true;
        }
        String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
        if ("OK".equals(result)) {
        lockCount.set(1);
        scheduleRenewal();
        return true;
        }
        return false;
        }
        +
          +
        • a++ 和 ++a 的区别是先 load 还是先 iinc。
        • +
        • iinc,将局部变量表指定槽位的数加上一个常数。
        • +
        • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
        • +
        • b = 10 + 12 + 12 = 34
        • +
        • a = 10 + 1 + 1 - 1 = 11
        • +
        +

        分析判断条件

        public class ByteCodeTest_4 {

        public static void main(String[] args) {
        int a = 0;
        // ifeq, goto
        if (a == 0) {
        a = 10;
        } else {
        a = 20;
        }
        }
        }
        -

        使用 Redis hash 实现锁计数

        还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

        -
        if (redis.call('exists', KEYS[1]) == 0) then 
        redis.call('hset', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
        end;
        if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
        return redis.call('pttl', KEYS[1]);
        +

        字节码指令

         0: iconst_0
        1: istore_1
        2: iload_1
        3: ifne 12
        6: bipush 10
        8: istore_1
        9: goto 15
        12: bipush 20
        14: istore_1
        15: return
        -

        书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

        -

        代码实现

        redis-lock

        -

        参考文章

          -
        • 《Redis 深度历险,核心原理与应用实践》
        • +
            +
          • iconst,将一个 int 常量推入操作数栈。
          • +
          • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
          • +
          • goto,总是进入的分支,跳转到指定行号。
          • +
          +

          涉及的字节码指令

            +
          • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
          • +
          • istore,将栈顶的 int,存入局部变量表的指定槽位。
          • +
          • iload,将局部变量表指定槽位的 int,推入操作数栈。
          • +
          • ldc,从运行时常量池将指定常量推入操作数栈。
          • +
          • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
          • +
          • getstatic,获取类的静态属性,推入操作数栈。
          • +
          • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
          • +
          • return,返回 void。
          • +
          • iinc,将局部变量表中指定槽位的数加一个常量。
          • +
          • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
              +
            • ifeq,equals
            • +
            • ifne,not equals
            • +
            • iflt,less than
            • +
            • ifge,greater than or equals
            • +
            • ifgt,great than
            • +
            • ifle,less than or equals
            • +
            +
          • +
          • goto,总是进入的分支,跳转到指定行号。
          ]]> - lock - distributed lock - redis + java + bytecode - 使用 Grafana 和 Prometheus 搭建监控 - /2023/11/18/setup-monitoring-using-grafana-and-prometheus/ - 本文介绍如何通过 Dockers Compose 安装 GrafanaPrometheus 在局域网中配合各类 exporter 为主机和诸多内部服务搭建监控。

          - + JVM 内存区域的测试和分析 + /2023/11/04/testing-and-analysis-of-jvm-memory-area/ + 内存区域

          JVM 内存区域划分为:

          +
            +
          • 程序计数器
          • +
          • 虚拟机栈
          • +
          • 本地方法栈
          • +
          • +
          • 方法区
          • +
          + -

          Grafana

          docker-compose 部署

          version: "1.0"
          services:
          grafana:
          image: grafana/grafana:9.5.6
          container_name: grafana
          ports:
          - 3000:3000
          volumes:
          - grafana-storage:/var/lib/grafana
          networks:
          - my-network
          restart: unless-stopped

          volumes:
          grafana-storage:
          driver: local

          networks:
          my-network:
          external: true
          +

          程序计数器

          虚拟机栈

          Java 虚拟机栈(Java Virtual Machine Stack),线程私有,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

          +

          可以使用 -Xss1024k 设置虚拟机栈的大小。默认情况下都是 1024k,只有 Windows 中取决于虚拟内存。

          +

          栈内存溢出

            +
          1. 栈帧过多导致栈内存溢出
          2. +
          3. 栈帧过大导致栈内存溢出(难复现)
          4. +
          +

          不正确的递归调用

          public class StackTest_4 {

          private static int count = 0;

          // 改变栈的大小限制 -Xss256k,观察调用次数的变化
          public static void main(String[] args) {
          try {
          method1();
          } catch (Throwable t) {
          t.printStackTrace();
          } finally {
          // 默认情况下经过 20000+ 次,改变参数后 3000+ 次
          System.out.println(count);
          }
          }

          private static void method1() {
          count++;
          method1();
          }
          }
          -

          问题记录

          在通过 nginx 代理 Grafana 后,出现 "Origin not allowed" 报错信息

          -
          -

          问题的原因参考官方社区:After update to 8.3.5: ‘Origin not allowed’ behind proxy

          -

          nginx 配置文件的 proxy 配置上方添加 proxy_set_header Host $http_host,然后重启 nginx 恢复正常。

          -
          location / {
          proxy_set_header Host $http_host;
          proxy_pass http://grafana:3000;
          }
          +

          循环引用导致 JSON 解析无限循环

          并非只有自己写的递归方法可能引发栈内存溢出,有可能第三方库也会引发栈内存溢出。

          +
          public class StackTest_5 {

          public static void main(String[] args) throws JsonProcessingException {
          Department department = new Department();
          department.setName("Tech");

          Employee employee1 = new Employee();
          employee1.setName("Tom");
          employee1.setDepartment(department);

          Employee employee2 = new Employee();
          employee2.setName("Tim");
          employee2.setDepartment(department);

          department.setEmployees(Arrays.asList(employee1, employee2));

          ObjectMapper objectMapper = new ObjectMapper();
          System.out.println(objectMapper.writeValueAsString(department));
          }

          static class Department {
          private String name;
          private List<Employee> employees;

          public String getName() {
          return name;
          }

          public void setName(String name) {
          this.name = name;
          }

          public List<Employee> getEmployees() {
          return employees;
          }

          public void setEmployees(List<Employee> employees) {
          this.employees = employees;
          }
          }

          static class Employee {
          private String name;
          private Department department;

          public String getName() {
          return name;
          }

          public void setName(String name) {
          this.name = name;
          }

          public Department getDepartment() {
          return department;
          }

          public void setDepartment(Department department) {
          this.department = department;
          }
          }
          }
          -

          Prometheus

          docker-compose 部署

          version: "1.0"
          services:
          prometheus:
          image: bitnami/prometheus:2.45.0
          container_name: prometheus
          ports:
          - 19090:9090
          volumes:
          - prometheus-data:/opt/bitnami/prometheus/data
          - $PWD/conf/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml
          networks:
          - my-network
          restart: unless-stopped

          volumes:
          prometheus-data:
          driver: local

          networks:
          my-network:
          external: true
          +

          局部变量的线程安全问题

            +
          1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
          2. +
          3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
          4. +
          +
          public class StackTest_3 {

          public static void main(String[] args) {
          method1();
          }

          // 线程安全
          private static void method1() {
          StringBuilder sb = new StringBuilder();
          sb.append(1);
          sb.append(2);
          sb.append(3);
          System.out.println(sb);
          }

          // 线程不安全
          private static void method2(StringBuilder sb) {
          sb.append(1);
          sb.append(2);
          sb.append(3);
          System.out.println(sb);
          }

          // 线程不安全,看到一个说法:发生指令重排,sb 的 append 操作发生在返回之后(有待确认)
          private static StringBuilder method3() {
          StringBuilder sb = new StringBuilder();
          sb.append(1);
          sb.append(2);
          sb.append(3);
          return sb;
          }
          }
          -

          配置文件 prometheus.yml

          global:
          scrape_interval: 15s

          scrape_configs:
          # Ubuntu
          - job_name: node
          static_configs:
          - targets: ['192.168.46.135:9100']
          # Windows
          - job_name: windows
          static_configs:
          - targets: ['192.168.46.1:9182']
          # Redis
          - job_name: redis-exporter
          static_configs:
          - targets: ['redis-exporter:9121']
          # MySQL
          - job_name: mysqld-exporter
          static_configs:
          - targets: ['mysqld-exporter:9104']
          +

          线程问题排查

          CPU 占用率居高不下

          public class ThreadTest_1 {

          public static void main(String[] args) {
          new Thread(null, () -> {
          System.out.println("t1...");
          while (true) {

          }
          }, "thread1").start();

          new Thread(null, () -> {
          System.out.println("t2...");
          try {
          TimeUnit.SECONDS.sleep(1000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          }, "thread2").start();

          new Thread(null, () -> {
          System.out.println("t3...");
          try {
          TimeUnit.SECONDS.sleep(1000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          }, "thread3").start();
          }
          }
          +

          当发现 CPU 占用率居高不下时,可以尝试以下步骤:

          +
            +
          1. top,定位 cpu 占用高的进程 id。
          2. +
          3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
          4. +
          5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
          6. +
          +
          "thread1" #8 prio=5 os_prio=0 tid=0x00007f9bd0162800 nid=0x1061ad runnable [0x00007f9bd56eb000]
          java.lang.Thread.State: RUNNABLE
          at com.moralok.jvm.thread.ThreadTest_1.lambda$main$0(ThreadTest_1.java:10)
          at com.moralok.jvm.thread.ThreadTest_1$$Lambda$1/250421012.run(Unknown Source)
          at java.lang.Thread.run(Thread.java:750)
          -

          Ubuntu 监控

          node_exporter

          node_exporter 被设计为监控主机系统,因为它需要访问主机系统,所以不推荐使用 Docker 容器部署。

          - -

          步骤

            -
          1. 下载解压得到 node_exporter 二进制可执行文件。
          2. -
          3. 修改权限,chmod u+x node_exporter
          4. -
          5. ./node_exporter 启动
          6. -
          7. Prometheus 配置文件
            global:
            scrape_interval: 15s

            scrape_configs:
            - job_name: node
            static_configs:
            - targets: ['localhost:9100']
          8. +

            死锁,迟迟未返回结果

            public class ThreadTest_2 {

            private static final Object A = new Object();
            private static final Object B = new Object();

            public static void main(String[] args) {
            new Thread(null, () -> {
            System.out.println("t1...");
            synchronized (A) {
            System.out.println(Thread.currentThread().getName() + " get A");
            try {
            TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            throw new RuntimeException(e);
            }
            synchronized (B) {
            System.out.println(Thread.currentThread().getName() + " get B");
            }
            }
            }, "thread1").start();

            new Thread(null, () -> {
            System.out.println("t2...");
            synchronized (B) {
            System.out.println(Thread.currentThread().getName() + " get B");
            try {
            TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
            throw new RuntimeException(e);
            }
            synchronized (A) {
            System.out.println(Thread.currentThread().getName() + " get A");
            }
            }
            }, "thread2").start();
            }
            }
            + +
              +
            1. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
            2. +
            3. 其他工具如 jconsole 也具有检测死锁的功能。
            -

            设置 node_exporter 自启动

              -
            1. 路径 /etc/systemd/system
            2. -
            3. 创建 node_exporter.service
              [Unit]
              Description=node_exporter
              After=network-online.target

              [Service]
              Restart=on-failure
              ExecStart=/opt/node_exporter

              [Install]
              WantedBy=multi-user.target
            4. +

              本地方法栈

              堆(Heap)的特点:

              +
                +
              1. 线程共享,需要考虑线程安全问题。
              2. +
              3. 存在垃圾回收机制。
              4. +
              5. 使用 -Xmx8m 设置大小。
              -
              +

              堆内存溢出

              既然堆有垃圾回收机制,为什么还会发生内存溢出呢?最开始的时候,我也有这样的困惑。
              后来我才认识到,还在使用中的对象是不能被强制回收的,不再使用的对象不是立刻回收的。当创建对象却没有足够的内存空间时,如果清理掉那些不再使用的对象就有足够的内存空间,就不会发生内存溢出,程序只是表现为卡顿。

              +
              public class HeapTest_1 {  

              // -Xmx8m
              // 不设置可能不提示 Java heap space,出错地方不同,报错信息不同
              public static void main(String[] args) {
              int i = 0;
              try {
              List<String> list = new ArrayList<>();
              String s = "hello";
              while (true) {
              list.add(s);
              s = s + s;
              i++;
              }
              } catch (Throwable t) {
              t.printStackTrace();
              } finally {
              System.out.println("运行次数 " + i);
              }
              }
              }
              -

              Windows 监控

              windows_exporter

                -
              • windows_exporter Github 仓库
              • -
              • windows_exporter 下载地址
              • +
                java.lang.OutOfMemoryError: Java heap space
                at java.util.Arrays.copyOf(Arrays.java:3332)
                at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
                at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
                at java.lang.StringBuilder.append(StringBuilder.java:141)
                at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
                运行次数 17
                + +

                堆内存溢出的发生往往需要长时间的运行,因此在排查相关问题时,可以适当调小堆内存。

                +

                监测堆内存

                  +
                1. 使用 jps 查看 Java 进程列表
                2. +
                3. 使用 jmap -heap pid 查看堆内存信息
                4. +
                5. 还可以使用 jconsole 观察堆内存变化曲线
                6. +
                7. 还可以使用 VisualVM 查看堆信息
                8. +
                +
                public class HeapTest_2 {

                public static void main(String[] args) throws InterruptedException {
                System.out.println("1...");
                TimeUnit.SECONDS.sleep(30);
                // 堆空间占用上升 10MB
                byte[] bytes = new byte[1024 * 1024 * 10];
                System.out.println("2...");
                TimeUnit.SECONDS.sleep(30);
                bytes = null;
                // 堆空间占用下降
                System.gc();
                System.out.println("3...");
                TimeUnit.SECONDS.sleep(3000);
                }
                }
                + +

                使用 jmap -heap pid 查看堆内存信息:

                +
                Eden Space:
                capacity = 268435456 (256.0MB)
                used = 32212360 (30.72010040283203MB)

                used = 42698136 (40.720115661621094MB)

                used = 5368728 (5.120018005371094MB)
                + +

                使用 jconsole 查看堆内存信息:

                + + +

                堆内存占用居高不下

                当你发现堆内存占用居高不下,经过 GC,下降也不明显,如果你想查看一下堆内的具体情况,可以将其 dump 查看。

                +
                public class HeapTest_3 {  

                // jps 查进程,jmap 看堆内存,jconsole 执行GC,堆内存占用没有明显下降
                // 使用 VisualVM 的堆 dump 功能,观察大对象
                public static void main(String[] args) throws IOException {
                List<Student> students = new ArrayList<>();
                for (int i = 0; i < 200; i++) {
                students.add(new Student());
                }
                System.in.read();
                }

                static class Student {
                private byte[] score = new byte[1024 * 1024];
                }
                }
                +

                可使用 VisualVM 的 Heap Dump 功能:

                + + +

                也可使用 jmap -dump:format=b,file=filename.hprof pid,需要其他分析工具搭配。

                +

                方法区

                根据《Java虚拟机规范》,方法区在逻辑上是堆的一部分,但是在具体实现上,各个虚拟机厂商并不相同。对于 Hotspot 而言:

                +
                  +
                • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
                • +
                • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。
                • +
                +

                方法区溢出

                public class MethodAreaTest_1 extends ClassLoader {

                // -XX:MaxMetaspaceSize=8m MaxMetaspaceSize is too small.
                // -XX:MaxMetaspaceSize=10m java.lang.OutOfMemoryError: Compressed class space
                // 不是 Metaspace 应该是某个参数设置的问题
                // JDK 6: -XX:MaxPermSize=8m PermGen space
                public static void main(String[] args) {
                int j = 0;
                try {
                MethodAreaTest_1 methodAreaTest1 = new MethodAreaTest_1();
                for (int i = 0; i < 20000; i++, j++) {
                ClassWriter classWriter = new ClassWriter(0);
                // 版本号,public,类名,包名,父类,接口
                classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回二进制字节码
                byte[] code = classWriter.toByteArray();
                // 加载类
                methodAreaTest1.defineClass("Class" + i, code, 0, code.length);
                }
                } catch (ClassFormatError e) {
                e.printStackTrace();
                } finally {
                System.out.println("次数 " + j);
                }
                }
                }
                +
                  +
                1. 当设置的值太小时 -XX:MaxMetaspaceSize=8m,提示 MaxMetaspaceSize is too small。
                2. +
                3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
                4. +
                5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
                6. +
                7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
                8. +
                +

                不要认为自己不会写动态生成字节码相关的代码就忽略这方面的问题,如今很多框架使用字节码技术大量地动态生成类。

                +

                运行时常量池

                二进制字节码文件主要包含三类信息:

                +
                  +
                1. 类的基本信息
                2. +
                3. 类的常量池(Constant Pool)
                4. +
                5. 类的方法信息
                6. +
                +

                使用 javap 反编译

                public class MethodAreaTest_2 {  

                public static void main(String[] args) {
                System.out.println("hello world");
                }
                }
                + +
                Classfile /C:/Users/username/Documents/github/jvm-study/target/classes/com/moralok/jvm/memory/methodarea/MethodAreaTest_2.class
                Last modified 2023-11-4; size 619 bytes
                MD5 checksum 0ed10a8f0a03be54fd4159958ee7446c
                Compiled from "MethodAreaTest_2.java"
                public class com.moralok.jvm.memory.methodarea.MethodAreaTest_2
                minor version: 0
                major version: 52
                flags: ACC_PUBLIC, ACC_SUPER
                Constant pool:
                #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
                #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
                #3 = String #23 // hello world
                #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
                #5 = Class #26 // com/moralok/jvm/memory/methodarea/MethodAreaTest_2
                #6 = Class #27 // java/lang/Object
                #7 = Utf8 <init>
                #8 = Utf8 ()V
                #9 = Utf8 Code
                #10 = Utf8 LineNumberTable
                #11 = Utf8 LocalVariableTable
                #12 = Utf8 this
                #13 = Utf8 Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;
                #14 = Utf8 main
                #15 = Utf8 ([Ljava/lang/String;)V
                #16 = Utf8 args
                #17 = Utf8 [Ljava/lang/String;
                #18 = Utf8 SourceFile
                #19 = Utf8 MethodAreaTest_2.java
                #20 = NameAndType #7:#8 // "<init>":()V
                #21 = Class #28 // java/lang/System
                #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
                #23 = Utf8 hello world
                #24 = Class #31 // java/io/PrintStream
                #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
                #26 = Utf8 com/moralok/jvm/memory/methodarea/MethodAreaTest_2
                #27 = Utf8 java/lang/Object
                #28 = Utf8 java/lang/System
                #29 = Utf8 out
                #30 = Utf8 Ljava/io/PrintStream;
                #31 = Utf8 java/io/PrintStream
                #32 = Utf8 println
                #33 = Utf8 (Ljava/lang/String;)V
                {
                public com.moralok.jvm.memory.methodarea.MethodAreaTest_2();
                descriptor: ()V
                flags: ACC_PUBLIC
                Code:
                stack=1, locals=1, args_size=1
                0: aload_0
                1: invokespecial #1 // Method java/lang/Object."<init>":()V
                4: return
                LineNumberTable:
                line 3: 0
                LocalVariableTable:
                Start Length Slot Name Signature
                0 5 0 this Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;

                public static void main(java.lang.String[]);
                descriptor: ([Ljava/lang/String;)V
                flags: ACC_PUBLIC, ACC_STATIC
                Code:
                stack=2, locals=1, args_size=1
                0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
                3: ldc #3 // String hello world
                5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
                8: return
                LineNumberTable:
                line 6: 0
                line 7: 8
                LocalVariableTable:
                Start Length Slot Name Signature
                0 9 0 args [Ljava/lang/String;
                }
                SourceFile: "MethodAreaTest_2.java"
                + +
                  +
                1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
                2. +
                3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
                4. +
                5. 这个过程其实就是信息从文件进入了内存。
                6. +
                +

                虚拟机解释器(interpreter)需要解释的字节码指令如下:

                +
                0: getstatic     #2
                3: ldc #3
                5: invokevirtual #4
                +

                索引 #2 的意思就是去常量表里查找对应项代表的事物。

                +

                直接内存

                  +
                • 常见于 NIO 操作中的数据缓冲区。
                • +
                • 分配和回收的成本较高,但读写性能更高。
                • +
                • 不由 JVM 进行内存释放
                • +
                +

                NIO 和 IO 的拷贝性能

                public class DirectMemoryTest_1 {  

                private static final String FROM = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY.mp4";
                private static final String TO = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY-copy.mp4";
                private static final int _1Mb = 1024 * 1024;

                public static void main(String[] args) {
                io();
                directBuffer();
                }

                private static void directBuffer() {
                long start = System.nanoTime();
                try (FileChannel from = new FileInputStream(FROM).getChannel();
                FileChannel to = new FileOutputStream(TO).getChannel()) {
                ByteBuffer buffer = ByteBuffer.allocateDirect(_1Mb);
                while (true) {
                int len = from.read(buffer);
                if (len == -1) {
                break;
                }
                buffer.flip();
                to.write(buffer);
                buffer.clear();
                }
                } catch (IOException e) {
                e.printStackTrace();
                }
                long end = System.nanoTime();
                System.out.println("directBuffer 用时 " + (end - start) / 1000_000.0);
                }

                private static void io() {
                long start = System.nanoTime();
                try (FileInputStream from = new FileInputStream(FROM);
                FileOutputStream to = new FileOutputStream(TO)) {
                byte[] buffer = new byte[_1Mb];
                while (true) {
                int len = from.read(buffer);
                if (len == -1) {
                break;
                }
                to.write(buffer);
                }
                } catch (IOException e) {
                e.printStackTrace();
                }
                long end = System.nanoTime();
                System.out.println("io 用时 " + (end - start) / 1000_000.0);
                }
                }
                + +
                io 用时 1676.9797
                directBuffer 用时 836.4796
                + + + + + + +

                直接内存溢出

                public class DirectMemoryTest_2 {  

                private static final int _100Mb = 1024 * 1024 * 100;

                public static void main(String[] args) {
                List<ByteBuffer> list = new ArrayList<>();
                int i = 0;
                try {
                while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
                list.add(byteBuffer);
                i++;
                }
                } catch (Throwable t) {
                t.printStackTrace();
                } System.out.println(i);
                }
                }
                + +
                java.lang.OutOfMemoryError: Direct buffer memory
                at java.nio.Bits.reserveMemory(Bits.java:695)
                at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
                at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
                at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
                145
                + +

                这似乎是代码中抛出的异常,而不是真正的直接内存溢出?

                +

                直接内存释放的原理

                演示直接内存的释放受 GC 影响

                public class DirectMemoryTest_3 {

                private static final int _1GB = 1024 * 1024 * 1024;

                public static void main(String[] args) throws IOException {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
                System.out.println("分配完毕");
                System.in.read();
                System.out.println("开始释放");
                byteBuffer = null;
                // 随着 ByteBuffer 的释放,从任务管理器界面看到程序的内存的占用迅速下降 1GB。
                System.gc();
                System.in.read();
                }
                }
                + +

                手动进行直接内存的分配和释放

                在代码中实现手动进行直接内存的分配和释放。

                +
                public class DirectMemoryTest_4 {

                private static final int _1GB = 1024 * 1024 * 1024;

                public static void main(String[] args) throws IOException {
                Unsafe unsafe = getUnsafe();

                // 分配内存
                long base = unsafe.allocateMemory(_1GB);
                unsafe.setMemory(base, _1GB, (byte) 0);
                System.in.read();

                // 释放内存
                unsafe.freeMemory(base);
                System.in.read();
                }

                private static Unsafe getUnsafe() {
                try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                Unsafe unsafe = (Unsafe) f.get(null);
                return unsafe;
                } catch (NoSuchFieldException | IllegalAccessException e) {
                throw new RuntimeException(e);
                }
                }
                }
                + +

                如何将 GC 和直接内存的分配和释放关联

                本质上,直接内存的自动释放是利用了虚引用的机制,间接调用了 unsafe 的分配和释放直接内存的方法。

                +

                DirectByteBuffer 就是使用 unsafe.allocateMemory(size) 分配直接内存。DirectByteBuffer 对象以及一个 Deallocator 对象(Runnable 类型)一起用于创建了一个虚引用类型的 Cleaner 对象。

                +
                DirectByteBuffer(int cap) {

                // 省略
                try {
                base = unsafe.allocateMemory(size);
                } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
                }
                // 省略
                cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
                att = null;
                }
                + +

                根据虚引用的机制,如果 DirectByteBuffer 对象被回收,虚引用对象会被加入到 Cleanner 的引用队列,ReferenceHandler 线程会处理引用队列中的 Cleaner 对象,进而调用 Deallocator 对象的 run 方法。

                +
                public void run() {
                if (address == 0) {
                // Paranoia
                return;
                }
                unsafe.freeMemory(address);
                address = 0;
                Bits.unreserveMemory(size, capacity);
                }
                +]]> + + java + jvm + + + + 基于 Redis 的分布式锁的简单实现 + /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ + 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

                + + +

                很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

                +

                基于 Redis 的分布式锁简单实现

                本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

                +
                > setnx lock:user1 true
                OK

                ... do something

                > del lock:user1
                (integer) 1
                + +

                死锁问题

                问题一:异常引发死锁 1

                如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

                + + +

                改进一:设置超时时间

                对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

                +
                > setnx lock:user1 true
                OK
                > expire lock:user1 5

                ... do something

                > del lock:user1
                (integer) 1
                + +

                问题二:异常引发死锁 2

                事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

                + + +

                本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

                +

                改进二:setnx + expire 的原子指令

                如果 setnx 和 expire 可以用一个原子指令实现就好了。

                + + +
                基于原生指令的实现

                在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

                +
                > set lock:user1 true ex 5 nx
                OK

                ... do something

                > del lock:user1
                (integer) 1
                + +
                基于 Lua 脚本的实现

                除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

                +
                if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
                redis.call('expire', KEYS[1], ARGV[2])
                return true
                else
                return false
                end
                + +

                超时问题

                基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

                + + +

                问题一:其他线程提前进入临界区

                如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

                +

                问题二:错误释放其他线程的锁

                如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

                +

                改进一:不要用于较长时间的任务

                与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

                +

                如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

                +

                改进二:使用 watchdog 实现锁续期

                为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

                +
                private void scheduleRenewal() {
                String value = lockValue.get();
                ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
                () -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
                );
                renewalTask.set(scheduledFuture);
                }
                + +

                但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

                +

                改进三:加锁时指定 tag

                可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

                +
                基于原生指令的实现
                tag = random.nextint()
                if redis.set(key, tag, nx= True, ex=5):
                do_something()
                redis.delifequals(key, tag)
                + +

                但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

                + + +
                基于 Lua 脚本的实现
                if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
                else
                return 0
                end
                + +

                可重入性

                可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

                +

                使用 ThreadLocal 实现锁计数

                Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

                +
                private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

                public boolean tryLock() {
                Integer count = lockCount.get();
                if (count != null && count > 0) {
                lockCount.set(count + 1);
                return true;
                }
                String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
                if ("OK".equals(result)) {
                lockCount.set(1);
                scheduleRenewal();
                return true;
                }
                return false;
                }
                + +

                使用 Redis hash 实现锁计数

                还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

                +
                if (redis.call('exists', KEYS[1]) == 0) then 
                redis.call('hset', KEYS[1], ARGV[2], 1);
                redis.call('pexpire', KEYS[1], ARGV[1]);
                return nil;
                end;
                if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
                redis.call('hincrby', KEYS[1], ARGV[2], 1);
                redis.call('pexpire', KEYS[1], ARGV[1]);
                return nil;
                return redis.call('pttl', KEYS[1]);
                + +

                书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

                +

                代码实现

                redis-lock

                +

                参考文章

                  +
                • 《Redis 深度历险,核心原理与应用实践》
                • +
                +]]>
                + + lock + distributed lock + redis + +
                + + 使用 Grafana 和 Prometheus 搭建监控 + /2023/11/18/setup-monitoring-using-grafana-and-prometheus/ + 本文介绍如何通过 Dockers Compose 安装 GrafanaPrometheus 在局域网中配合各类 exporter 为主机和诸多内部服务搭建监控。

                + + +

                Grafana

                docker-compose 部署

                version: "1.0"
                services:
                grafana:
                image: grafana/grafana:9.5.6
                container_name: grafana
                ports:
                - 3000:3000
                volumes:
                - grafana-storage:/var/lib/grafana
                networks:
                - my-network
                restart: unless-stopped

                volumes:
                grafana-storage:
                driver: local

                networks:
                my-network:
                external: true
                + +

                问题记录

                在通过 nginx 代理 Grafana 后,出现 "Origin not allowed" 报错信息

                +
                + +

                问题的原因参考官方社区:After update to 8.3.5: ‘Origin not allowed’ behind proxy

                +

                nginx 配置文件的 proxy 配置上方添加 proxy_set_header Host $http_host,然后重启 nginx 恢复正常。

                +
                location / {
                proxy_set_header Host $http_host;
                proxy_pass http://grafana:3000;
                }
                + +

                Prometheus

                docker-compose 部署

                version: "1.0"
                services:
                prometheus:
                image: bitnami/prometheus:2.45.0
                container_name: prometheus
                ports:
                - 19090:9090
                volumes:
                - prometheus-data:/opt/bitnami/prometheus/data
                - $PWD/conf/prometheus.yml:/opt/bitnami/prometheus/conf/prometheus.yml
                networks:
                - my-network
                restart: unless-stopped

                volumes:
                prometheus-data:
                driver: local

                networks:
                my-network:
                external: true
                + +

                配置文件 prometheus.yml

                global:
                scrape_interval: 15s

                scrape_configs:
                # Ubuntu
                - job_name: node
                static_configs:
                - targets: ['192.168.46.135:9100']
                # Windows
                - job_name: windows
                static_configs:
                - targets: ['192.168.46.1:9182']
                # Redis
                - job_name: redis-exporter
                static_configs:
                - targets: ['redis-exporter:9121']
                # MySQL
                - job_name: mysqld-exporter
                static_configs:
                - targets: ['mysqld-exporter:9104']
                + +

                Ubuntu 监控

                node_exporter

                node_exporter 被设计为监控主机系统,因为它需要访问主机系统,所以不推荐使用 Docker 容器部署。

                + +

                步骤

                  +
                1. 下载解压得到 node_exporter 二进制可执行文件。
                2. +
                3. 修改权限,chmod u+x node_exporter
                4. +
                5. ./node_exporter 启动
                6. +
                7. Prometheus 配置文件
                  global:
                  scrape_interval: 15s

                  scrape_configs:
                  - job_name: node
                  static_configs:
                  - targets: ['localhost:9100']
                8. +
                +

                设置 node_exporter 自启动

                  +
                1. 路径 /etc/systemd/system
                2. +
                3. 创建 node_exporter.service
                  [Unit]
                  Description=node_exporter
                  After=network-online.target

                  [Service]
                  Restart=on-failure
                  ExecStart=/opt/node_exporter

                  [Install]
                  WantedBy=multi-user.target
                4. +
                +
                + +

                Windows 监控

                windows_exporter

                步骤

                  @@ -3362,237 +3385,54 @@ - Dubbo SPI 自适应拓展的工作原理 - /2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/ - 直接展示一个具体的 Dubbo SPI 自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo 的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI 自适应拓展的相关源码和工作原理。

                  + Spring @Configuration 注解的源码分析 + /2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/ + Configuration 注解是 Spring 中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean 注解将创建的 bean 交给 Spring 容器管理。神奇的是,被 Bean 注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug 时注意过配置类的实例,你会发现配置类的 Class 名称中携带 EnhancerBySpringCGLIB。本文将从源码角度,分析 Configuration 注解是如何工作的。

                  +

                  测试用例

                    +
                  • 配置类
                    @Configuration
                    public class BeanConfig {

                    @Bean
                    public Person lisi() {
                    return new Person("lisi", 20);
                    }

                    @Bean(value = "customName")
                    public Person person() {
                    // 测试 lisi() 在配置类拥有注解 @Configuration 时只会真正执行一次
                    lisi();
                    return new Person("wangwu", 30);
                    }
                    }
                  • +
                  • 测试类
                    @Test
                    public void annotationConfigTest() {
                    ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);
                    BeanConfig beanConfig = (BeanConfig) ac.getBean("beanConfig");
                    // 即使通过 beanConfig 调用,也不会执行第二次
                    Person lisi = beanConfig.lisi();
                    }
                  • +
                  +

                  解析配置类

                  什么是配置类?

                  通常情况下,我们称被 Configuration 注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。

                  +
                  ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);

                  public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
                  this();
                  // 注册类,几乎可以说无条件地注册 annotatedClasses 的 bean 定义
                  register(annotatedClasses);
                  refresh();
                  }
                  + +

                  本文不详细介绍配置类本身如何注册到 BeanFactory 中。当 BeanConfig 被传递给 AnnotationConfigApplicationContext,自身会先被解析为 BeanDefinition 注册到 beanFactory 中。有两点需要注意:

                  +
                    +
                  1. annotatedClasses 可以传入多个,意味着一开始静态指定的配置类可以有多个。
                  2. +
                  3. annotatedClasses 除了在命名上提示用户应传入被注解的类外,register(annotatedClasses) 实际上只是将它们视作普通的 Bean 注册到 beanFactory 中。它们是从外界传入的**首批 BeanDefinition**。
                  4. +
                  +

                  之后 Spring 进入 refresh 流程。使用 IDEA Debug 观察此时的 beanDefinitionMap,除了 beanConfig 外,AnnotationConfigApplicationContext 在创建时,已经自动注册了 6bean 定义,其中一个就是我们今天的主角 org.springframework.context.annotation.internalConfigurationAnnotationProcessor -> org.springframework.context.annotation.ConfigurationClassPostProcessor。显而易见,此时配置类还未被处理得到新的 bean 定义。

                  +
                  + +

                  配置类后处理器 ConfigurationClassPostProcessor

                  配置类后处理器 ConfigurationClassPostProcessor 实现了接口 BeanDefinitionRegistryPostProcessor,也因此同时实现了接口 BeanFactoryPostProcessor。在Spring 应用 context 刷新流程中,我们介绍过这两个接口,它们作为工厂后处理器,被用于 refresh 过程的调用工厂后处理器阶段invokeBeanFactoryPostProcessors(beanFactory))。工厂后处理器的作用,一言以蔽之,允许自定义修改应用上下文中的 bean 定义。

                  +

                  配置类后处理器 ConfigurationClassPostProcessor 的具体作用可以概括为两点:

                  +
                    +
                  1. 解析配置类中配置的 Bean,将它们的 bean 定义注册到 BeanFactory 中。
                  2. +
                  3. (如有必要)增强配置类
                  4. +
                  +

                  处理配置类的核心方法 processConfigBeanDefinitions

                  根据之前的介绍,进入 invokeBeanFactoryPostProcessors(beanFactory)ConfigurationClassPostProcessor 会先作为 BeanDefinitionRegistryPostProcessor 被调用。

                  -

                  站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。

                  +

                  个人的理解是,先将 BeanFactory 视作 BeanDefinitionRegistry 注册好 BeanDefinition,再视作 BeanFactory 进行处理,有点预备好原材料再统一处理的意思。

                  -

                  原理

                  Dubbo 中,很多拓展都是通过 SPI 机制动态加载的,比如 ProtocolClusterLoadBalance 等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。

                  -

                  示例

                  定义一个接口 Animal

                  -
                  public interface Animal {
                  void bark();
                  }
                  - -

                  定义两个实现类 DogCat

                  -
                  public class Dog implements Animal {
                  @Override
                  public void bark() {
                  System.out.println("Dog bark...");
                  }
                  }

                  public class Cat implements Animal {
                  @Override
                  public void bark() {
                  System.out.println("Cat bark...");
                  }
                  }
                  +
                  @Override
                  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
                  // ...
                  // 处理配置的 BeanDefinition
                  processConfigBeanDefinitions(registry);
                  }
                  -

                  在运行时根据参数动态地加载拓展。

                  -
                  public void bark(String type) {
                  if (type == null) {
                  throw new IllegalArgumentException("type == null");
                  }
                  // 通过 SPI 动态地加载具体的 Animal
                  Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                  // 调用目标方法
                  animal.bark();
                  }
                  +
                  +

                  核心方法 processConfigBeanDefinitions(registry) 冗长,个人建议无需过度关注细节(但同时个人感受是反复阅读和 Debug 确实有益于加深理解,看个人时间和精力)。

                  +
                  +

                  基于配置类的 BeanDefinition Registry(也就是 BeanFactory),获取配置类,构建和校验配置模型:

                  +
                    +
                  1. BeanDefinition Registry(即 BeanFactory)中查找配置类。
                  2. +
                  3. 解析配置类得到配置模型,从模型中读取 BeanDefinitions 注册到 BeanDefinition Registry
                  4. +
                  5. 新的 BeanDefinitions 可能有新的配置类,回到 1 再来一遍。重复循环直到不再引入新的配置类。
                  6. +
                  +

                  以本文示例进行说明,静态添加的配置类只有 BeanConfig,假如 BeanConfig 不仅被 Configuration 注解标注,还被 ComponentScan 注解标注,并且刚好 Spring 通过扫描获得并添加了新的配置类,那么新的配置类就需要继续被解析。

                  +
                  +

                  应正视配置模型这个概念,它可以理解为配置类到 BeanDefinitions 的中间产物。最初我先入为主,带着解析得到 BeanDefinitions 这样“一阶段”完成的观念,非常不理解 processConfigBeanDefinitions 方法上 Build and validate a configuration model based on the registry of Configuration classes 这句注释。先行强调注意,处理配置类得到 bean 定义分为“两阶段”,解析配置类得到配置模型,从配置模型中读取 bean 定义

                  +
                  +
                  -

                  改进

                  是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:

                  -
                  Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                  animal.bark();

                  WeelMaker weelMaker = ExtensionLoader.getExtensionLoader(WeelMaker.class).getExtension(weelMakerName);
                  weelMaker.makeWeel();

                  LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invocation.getLoadBalanceType());
                  loadBalance.select();

                  // ...
                  - -

                  这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter) 这样重复的代码;引入了 ExtensionLoader 这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo 和 动物 Animal 举例。

                  -

                  在非动态加载情况下,我们可能会这样写:

                  -
                  public class Zoo {
                  private List<Animal> animals;

                  public void bark(String type) {
                  for (Animal animal : animals) {
                  if (type.equals(animal.name)) {
                  animal.bark();
                  }
                  }
                  }
                  }
                  - -

                  在动态加载情况下,我们可能会这样写。在这种情况下,Zoo 没有合适的方式直接持有 Animal,而是通过 ExtensionLoader 间接地持有。

                  -
                  public class Zoo {
                  private ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);

                  public void bark(String type) {
                  Animal animal = extensionLoader.getExtension(type);
                  animal.bark();
                  }
                  }
                  - -

                  我们更想要以下这种直接持有 Animal 的方式,在运行时 animal 可以是 Dog,也可以是 Cat,还可以是其他的动物。

                  -
                  public class Zoo {
                  private Animal animal;

                  public void bark(String type) {
                  animal.bark();
                  }
                  }
                  - -

                  Dubbo 采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal 的自适应拓展,就像下面这样:

                  -
                  public class AdaptiveAnimal implements Animal {
                  public void bark(String type) {
                  if (type == null) {
                  throw new IllegalArgumentException("type == null");
                  }

                  Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                  animal.bark();
                  }
                  }

                  Animal animal = new AdaptiveAnimal();
                  animal.bark(type);
                  - -

                  当然,我们不希望需要手动地为每一个拓展编写 Adaptive 代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark 方法的参数列表中新增了 type 类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo 的另一个设计规避了这一缺点,Dubbo公共契约中提到:所有扩展点参数都包含 URL 参数,URL 作为上下文信息贯穿整个扩展点设计体系。因此围绕着 DubboURL 为中心的拓展体系,你很难设计出 Animal.bark(URL url) 这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo 并未完全抛弃手工编写自适应拓展的方式,而是予以保留。

                  -

                  手工编码的自适应拓展

                  在在 Dubbo 中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL 作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory 的自适应拓展,当你调用它的 getExtension 方法时,它就是将工作全权委托给 factory.getExtension(type, name) 完成的,而 factories 在创建 AdaptiveExtensionFactory 时就已经获取了。

                  -
                  @Adaptive
                  public class AdaptiveExtensionFactory implements ExtensionFactory {

                  private final List<ExtensionFactory> factories;

                  public AdaptiveExtensionFactory() {
                  // 获取 ExtensionFactory 的 ExtensionLoader
                  ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
                  List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
                  // 获取全部支持的(不包含自适应拓展)拓展名称,依次获取拓展加入 factories
                  for (String name : loader.getSupportedExtensions()) {
                  list.add(loader.getExtension(name));
                  }
                  factories = Collections.unmodifiableList(list);
                  }

                  @Override
                  public <T> T getExtension(Class<T> type, String name) {
                  for (ExtensionFactory factory : factories) {
                  // 委托给其他 ExtensionFactory 拓展获取,比如 SpiExtensionFactory
                  T extension = factory.getExtension(type, name);
                  if (extension != null) {
                  return extension;
                  }
                  }
                  return null;
                  }

                  }
                  - -

                  至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。

                  -

                  源码分析

                  Adaptive 注解

                  Adaptive 注解是一个与自适应拓展息息相关的注解,该定义如下:

                  -
                  @Documented
                  @Retention(RetentionPolicy.RUNTIME)
                  @Target({ElementType.TYPE, ElementType.METHOD})
                  public @interface Adaptive {
                  String[] value() default {};
                  }
                  - -

                  根据 Target 注解的 value 可知,Adaptive 注解可标注在类或者方法上。当 Adaptive 注解标注在类上时,Dubbo 不会为该类生成代理类。当 Adaptive 注解标注在接口方法上时,Dubbo 则会为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解标注,分别是 AdaptiveCompilerAdaptiveExtensionFactory。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive 注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。

                  -

                  获取自适应拓展

                  获取自适应拓展的入口方法是 getAdaptiveExtension,使用 getOrCreate 的模式获取。

                  -
                  public T getAdaptiveExtension() {
                  // 双重检查
                  // 从缓存中获取自适应拓展
                  Object instance = cachedAdaptiveInstance.get();
                  if (instance == null) {
                  // 创建自适应拓展失败的结果也会被缓存,避免重复尝试
                  if (createAdaptiveInstanceError != null) {
                  throw new IllegalStateException("Failed to create adaptive instance: " +
                  createAdaptiveInstanceError.toString(),
                  createAdaptiveInstanceError);
                  }

                  synchronized (cachedAdaptiveInstance) {
                  instance = cachedAdaptiveInstance.get();
                  if (instance == null) {
                  try {
                  // 创建自适应拓展
                  instance = createAdaptiveExtension();
                  // 将自适应拓展设置到缓存中
                  cachedAdaptiveInstance.set(instance);
                  } catch (Throwable t) {
                  createAdaptiveInstanceError = t;
                  throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                  }
                  }
                  }
                  }

                  return (T) instance;
                  }
                  - -

                  创建自适应拓展

                  当缓存为空时,就会通过 createAdaptiveExtension 方法创建。方法包含以下三个处理逻辑:

                  -
                    -
                  1. 调用 getAdaptiveExtensionClass 方法获取自适应拓展的 Class 对象。
                  2. -
                  3. 通过反射进行实例化。
                  4. -
                  5. 调用 injectExtension 方法对拓展实例进行依赖注入。
                  6. -
                  -
                  -

                  手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展

                  -
                  -
                  private T createAdaptiveExtension() {
                  try {
                  return injectExtension((T) getAdaptiveExtensionClass().newInstance());
                  } catch (Exception e) {
                  throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
                  }
                  }
                  - -

                  获取自适应拓展类

                  获取自适应拓展类的 getAdaptiveExtensionClass 方法包含以下三个处理逻辑:

                  -
                    -
                  1. 通过 getExtensionClasses 方法获取所有拓展类。
                  2. -
                  3. 检查缓存 cachedAdaptiveClass,如果不为 null,则返回缓存。
                  4. -
                  5. 如果缓存为 null,则调用 createAdaptiveExtensionClass 创建自适应拓展类(代理类)。
                  6. -
                  -

                  Dubbo SPI 的工作原理中我们分析过 getExtensionClasses 方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive 注解标注了,那么该类就会被赋值给 cachedAdaptiveClass 变量。“原理”部分介绍的 AdaptiveExtensionFactory 就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive 注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。

                  -
                  private Class<?> getAdaptiveExtensionClass() {
                  getExtensionClasses();
                  if (cachedAdaptiveClass != null) {
                  return cachedAdaptiveClass;
                  }
                  return cachedAdaptiveClass = createAdaptiveExtensionClass();
                  }
                  - -
                  -

                  到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用 getOrCreate 的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。

                  -
                  -

                  生成自适应拓展类

                  生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。

                  -
                    -
                  1. 通过拼接字符串得到代理类的源码。
                  2. -
                  3. 使用编译器编译得到 Class 对象。
                  4. -
                  -
                  -

                  在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。

                  -
                  -
                  private Class<?> createAdaptiveExtensionClass() {
                  // 区别于旧版本:新版本抽象出一个 AdaptiveClassCodeGenerator 用于生成代码
                  String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
                  ClassLoader classLoader = findClassLoader();
                  // 获取编译器拓展
                  org.apache.dubbo.common.compiler.Compiler compiler =
                  ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
                  // 编译代码,生成 Class 对象
                  return compiler.compile(code, classLoader);
                  }
                  - -

                  为了更直观地了解代码生成的效果及其实现的功能,以 Protocol 为例,生成的完整代码(已经经过格式化)展示如下

                  -
                  package org.apache.dubbo.rpc;

                  import org.apache.dubbo.common.extension.ExtensionLoader;

                  public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
                  public void destroy() {
                  throw new UnsupportedOperationException(
                  "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                  }

                  public int getDefaultPort() {
                  throw new UnsupportedOperationException(
                  "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                  }

                  public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1)
                  throws org.apache.dubbo.rpc.RpcException {
                  if (arg1 == null)
                  throw new IllegalArgumentException("url == null");
                  org.apache.dubbo.common.URL url = arg1;
                  String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
                  if (extName == null)
                  throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
                  + url.toString() + ") use keys([protocol])");
                  org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
                  .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                  return extension.refer(arg0, arg1);
                  }

                  public java.util.List getServers() {
                  throw new UnsupportedOperationException(
                  "The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                  }

                  public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0)
                  throws org.apache.dubbo.rpc.RpcException {
                  if (arg0 == null)
                  throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
                  if (arg0.getUrl() == null)
                  throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
                  org.apache.dubbo.common.URL url = arg0.getUrl();
                  String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
                  if (extName == null)
                  throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
                  + url.toString() + ") use keys([protocol])");
                  org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
                  .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                  return extension.export(arg0);
                  }
                  }
                  - -

                  生成的代理类需完成以下功能:

                  -
                    -
                  1. adaptive 方法,直接抛出异常。
                  2. -
                  3. adaptive 方法:
                      -
                    • 准备工作:在参数判空校验之后,从中获取到 URL 对象,结合 URL 对象和默认拓展名得到最终的拓展名 extName
                    • -
                    • 核心功能:先获取拓展的 ExtensionLoader,再根据拓展名 extName 获取拓展,最后调用拓展的同名方法。
                    • -
                    -
                  4. -
                  -

                  以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo 会直接或间接地从参数列表中查找 URL 对象,所谓直接就是 URL 对象直接在参数列表中,所谓间接就是 URL 对象是其中一个参数的属性。在得到 URL 对象后,Dubbo 会尝试以 Adaptive 注解的 valuekey,从 URL 中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName。实际的实现更加复杂,需要耐心阅读和测试。

                  -

                  自适应拓展类代码生成器

                  新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 typedefaultExtName,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。

                  -
                  public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
                  this.type = type;
                  this.defaultExtName = defaultExtName;
                  }

                  public String generate() {
                  // 检测是否至少存在一个方法标注了 Adaptive 注解
                  if (!hasAdaptiveMethod()) {
                  throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
                  }

                  // 区别于旧版本:抽象为几个命名含义清晰的方法,提升了可读性
                  // 生成类:包名、导入、类声明
                  StringBuilder code = new StringBuilder();
                  code.append(generatePackageInfo());
                  code.append(generateImports());
                  code.append(generateClassDeclaration());

                  // 生成方法
                  Method[] methods = type.getMethods();
                  for (Method method : methods) {
                  code.append(generateMethod(method));
                  }
                  code.append("}");

                  if (logger.isDebugEnabled()) {
                  logger.debug(code.toString());
                  }
                  return code.toString();
                  }
                  - -

                  检测 Adaptive 注解

                  在生成代理类源码之前,generate 方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive 注解,若不满足,就会抛出异常。

                  -
                  -

                  流式编程使用得当的话很有可读性啊。

                  -
                  -
                  private boolean hasAdaptiveMethod() {
                  return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
                  }
                  - -

                  生成类

                  生成代理类源码的顺序和普通 Java 类文件中内容的顺序一致:

                  -
                    -
                  • package
                  • -
                  • import
                  • -
                  • 类声明
                  • -
                  -

                  先忽略“生成方法”的部分,以 DubboProtocol 拓展为例,生成的代码如下:

                  -
                  package org.apache.dubbo.rpc;
                  import org.apache.dubbo.common.extension.ExtensionLoader;
                  public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
                  // 省略方法代码
                  }
                  - -

                  生成方法

                  生成方法的过程同样被抽象为几个命名含义清晰的方法,包含以下五个部分:

                  -
                    -
                  • 返回值
                  • -
                  • 方法名
                  • -
                  • 方法内容
                  • -
                  • 方法参数
                  • -
                  • 方法抛出的异常
                  • -
                  -
                  private String generateMethod(Method method) {
                  String methodReturnType = method.getReturnType().getCanonicalName();
                  String methodName = method.getName();
                  String methodContent = generateMethodContent(method);
                  String methodArgs = generateMethodArguments(method);
                  String methodThrows = generateMethodThrows(method);
                  return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
                  }
                  - -

                  除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive 注解标注。

                  -
                  private String generateMethodContent(Method method) {
                  Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
                  StringBuilder code = new StringBuilder(512);
                  // 检测方法是否被 Adaptive 注解标注
                  if (adaptiveAnnotation == null) {
                  return generateUnsupported(method);
                  } else {
                  // ...
                  }
                  return code.toString();
                  }
                  - -
                  无 Adaptive 注解标注的方法

                  对于无 Adaptive 注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。

                  -
                  private String generateUnsupported(Method method) {
                  return String.format(CODE_UNSUPPORTED, method, type.getName());
                  }
                  - -

                  Protocol 接口的 destroy 方法为例,生成的内容如下

                  -
                  throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                  - -
                  有 Adaptive 注解标注的方法

                  对于有 Adaptive 注解标注的方法,

                  -
                  // 查找 URL 类型的参数
                  int urlTypeIndex = getUrlTypeIndex(method);
                  if (urlTypeIndex != -1) {
                  // 生成 URL 判空检查和赋值的代码
                  code.append(generateUrlNullCheck(urlTypeIndex));
                  } else {
                  // 如果参数中没有直接出现 URL 类型,生成间接情况下的 URL 判空和赋值代码
                  code.append(generateUrlAssignmentIndirectly(method));
                  }
                  // 获取方法的 Adaptive 注解的 value
                  String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
                  // 检测是否有 Invocation 类型的参数
                  boolean hasInvocation = hasInvocationArgument(method);
                  // 生成 Invocation 判空检查代码
                  code.append(generateInvocationArgumentNullCheck(method));
                  // 生成拓展名赋值代码
                  code.append(generateExtNameAssignment(value, hasInvocation));
                  // 生成拓展名判空检查代码
                  code.append(generateExtNameNullCheck(value));
                  // 生成获取拓展和赋值代码
                  code.append(generateExtensionAssignment());
                  // 生成调用和返回代码
                  code.append(generateReturnAndInvocation(method));
                  - -
                  查找 URL 类型的参数

                  直接从方法的参数类型列表中查找第一个 URL 类型的参数,返回其索引。

                  -
                  private int getUrlTypeIndex(Method method) {
                  int urlTypeIndex = -1;
                  // 遍历方法的参数类型列表
                  Class<?>[] pts = method.getParameterTypes();
                  for (int i = 0; i < pts.length; ++i) {
                  // 查找第一个 URL 类型的参数
                  if (pts[i].equals(URL.class)) {
                  urlTypeIndex = i;
                  break;
                  }
                  }
                  return urlTypeIndex;
                  }

                  // 生成 URL 参数判空检查和赋值代码
                  private String generateUrlNullCheck(int index) {
                  return String.format(CODE_URL_NULL_CHECK, index, URL.class.getName(), index);
                  }
                  - -

                  间接从方法的参数类型列表中,查找 URL 类型的参数,并生成判空检查和赋值代码。

                  -
                  private String generateUrlAssignmentIndirectly(Method method) {
                  Class<?>[] pts = method.getParameterTypes();

                  Map<String, Integer> getterReturnUrl = new HashMap<>();
                  // 遍历方法的参数类型列表
                  for (int i = 0; i < pts.length; ++i) {
                  // 遍历某一个参数类型的全部方法,查找可以返回 URL 类型的 “getter” 方法
                  for (Method m : pts[i].getMethods()) {
                  String name = m.getName();
                  // 1. 方法名以 get 开头,或者方法名大于 3 个字符
                  // 2. 方法的访问权限为 public
                  // 3. 非静态方法
                  // 4. 方法参数数量为 0
                  // 5. 方法返回值类型为 URL
                  if ((name.startsWith("get") || name.length() > 3)
                  && Modifier.isPublic(m.getModifiers())
                  && !Modifier.isStatic(m.getModifiers())
                  && m.getParameterTypes().length == 0
                  && m.getReturnType() == URL.class) {
                  // 保存方法名->索引的映射
                  getterReturnUrl.put(name, i);
                  }
                  }
                  }

                  if (getterReturnUrl.size() <= 0) {
                  // 如果没有找到 “getter” 方法,抛出异常
                  throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
                  + ": not found url parameter or url attribute in parameters of method " + method.getName());
                  }

                  // 优先选择方法名为 getUrl 的方法,如果没有则选第一个
                  Integer index = getterReturnUrl.get("getUrl");
                  if (index != null) {
                  return generateGetUrlNullCheck(index, pts[index], "getUrl");
                  } else {
                  Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
                  return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
                  }
                  }

                  // 生成 URL 参数判空检查和赋值代码
                  private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
                  StringBuilder code = new StringBuilder();
                  code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");\n",
                  index, type.getName()));
                  code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");\n",
                  index, method, type.getName(), method));

                  code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
                  return code.toString();
                  }
                  - -

                  Protocolreferexport 方法为例,生成的内容如下

                  -
                  // refer
                  if (arg1 == null) throw new IllegalArgumentException("url == null");
                  org.apache.dubbo.common.URL url = arg1;
                  // export
                  if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
                  if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
                  org.apache.dubbo.common.URL url = arg0.getUrl();
                  - -
                  获取 Adaptive 注解的 value
                  private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
                  String[] value = adaptiveAnnotation.value();
                  // 如果 value 为空,使用类名生成 value
                  // 效果:LoadBalance -> load.balance
                  if (value.length == 0) {
                  String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
                  value = new String[]{splitName};
                  }
                  return value;
                  }
                  - -
                  检测 Invocation 类型的参数

                  检测是否有 Invocation 类型的参数,并生成判空检查代码和赋值代码。从 Invocation 可以获得 methodName

                  -
                  private boolean hasInvocationArgument(Method method) {
                  Class<?>[] pts = method.getParameterTypes();
                  return Arrays.stream(pts).anyMatch(p -> CLASSNAME_INVOCATION.equals(p.getName()));
                  }

                  private String generateInvocationArgumentNullCheck(Method method) {
                  Class<?>[] pts = method.getParameterTypes();
                  return IntStream.range(0, pts.length).filter(i -> CLASSNAME_INVOCATION.equals(pts[i].getName()))
                  .mapToObj(i -> String.format(CODE_INVOCATION_ARGUMENT_NULL_CHECK, i, i))
                  .findFirst().orElse("");
                  }
                  - -

                  LoadBalanceselect 方法为例,生成的内容如下:

                  -
                  if (arg2 == null) throw new IllegalArgumentException("invocation == null");
                  String methodName = arg2.getMethodName();
                  - -
                  获取拓展名

                  本方法用于根据 SPIAdaptive 注解的 value 生成“获取拓展名”的代码,同时生成逻辑还受 Invocation 影响,因此相对复杂。总结的规则如下:

                  -
                    -
                  1. 正常情况下,使用 url.getParameter(value[i]) 获取
                  2. -
                  3. 如果默认拓展名非空,使用 url.getParameter(value[i], defaultExtName) 获取
                  4. -
                  5. 如果存在 Invocation,不论默认拓展名是否为空,总是使用 url.getMethodParameter(methodName, value[i], defaultExtName) 获取
                  6. -
                  7. 因为 protocol 是 url 的一部分,所以可以直接通过 getProtocol 获取。是否使用默认拓展名的方式就退化为原始的三元表达式。
                  8. -
                  -
                  private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
                  // TODO: refactor it
                  String getNameCode = null;
                  // 逆序遍历 value(Adaptive 的 value)
                  for (int i = value.length - 1; i >= 0; --i) {
                  // 当 i 为最后一个元素的索引(因为是逆序遍历,第一轮就进入本分支)
                  if (i == value.length - 1) {
                  // 默认拓展名非空
                  if (null != defaultExtName) {
                  // protocol 是 url 的一部分,可以通过 getProtocol 方法获取,其他的则必须从 URL 参数中获取
                  if (!"protocol".equals(value[i])) {
                  if (hasInvocation) {
                  // 如果有 Invocation,则使用 url.getMethodParameter 获取
                  // url.getMethodParameter(methodName, value[i], defaultExtName)
                  getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                  } else {
                  // url.getParameter(value[i], defaultExtName)
                  getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                  }
                  } else {
                  // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                  getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                  }
                  // 默认拓展名为空
                  } else {
                  if (!"protocol".equals(value[i])) {
                  if (hasInvocation) {
                  // url.getMethodParameter(methodName, value[i], defaultExtName)
                  getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                  } else {
                  // url.getParameter(value[i])
                  getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                  }
                  } else {
                  // url.getProtocol()
                  getNameCode = "url.getProtocol()";
                  }
                  }
                  } else {
                  if (!"protocol".equals(value[i])) {
                  if (hasInvocation) {
                  // url.getMethodParameter(methodName, value[i], defaultExtName)
                  getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                  } else {
                  // url.getParameter(value[i], getNameCode)
                  getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                  }
                  } else {
                  // url.getProtocol() == null ? "dubbo" : url.getProtocol()
                  getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                  }
                  }
                  }
                  return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
                  }
                  - -
                  加载拓展
                  private String generateExtensionAssignment() {
                  return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                  }
                  - -

                  Protocol 接口的 refer 方法为例,生成的内容如下:

                  -
                  org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                  - -
                  调用与返回

                  生成方法调用语句,如有必要,返回结果。

                  -
                  private String generateReturnAndInvocation(Method method) {
                  String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

                  String args = IntStream.range(0, method.getParameters().length)
                  .mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
                  .collect(Collectors.joining(", "));

                  return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
                  }
                  - -

                  Protocol 接口的 refer 方法为例,生成的内容如下:

                  -
                  return extension.refer(arg0, arg1);
                  - -
                  -

                  新版本通过提炼方法、使用流式编程和使用 String.format() 代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。

                  -
                  -

                  参考文章

                  -]]>
                  - - java - dubbo - spi - -
                  - - Spring @Configuration 注解的源码分析 - /2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/ - Configuration 注解是 Spring 中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean 注解将创建的 bean 交给 Spring 容器管理。神奇的是,被 Bean 注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug 时注意过配置类的实例,你会发现配置类的 Class 名称中携带 EnhancerBySpringCGLIB。本文将从源码角度,分析 Configuration 注解是如何工作的。

                  - - -

                  测试用例

                    -
                  • 配置类
                    @Configuration
                    public class BeanConfig {

                    @Bean
                    public Person lisi() {
                    return new Person("lisi", 20);
                    }

                    @Bean(value = "customName")
                    public Person person() {
                    // 测试 lisi() 在配置类拥有注解 @Configuration 时只会真正执行一次
                    lisi();
                    return new Person("wangwu", 30);
                    }
                    }
                  • -
                  • 测试类
                    @Test
                    public void annotationConfigTest() {
                    ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);
                    BeanConfig beanConfig = (BeanConfig) ac.getBean("beanConfig");
                    // 即使通过 beanConfig 调用,也不会执行第二次
                    Person lisi = beanConfig.lisi();
                    }
                  • -
                  -

                  解析配置类

                  什么是配置类?

                  通常情况下,我们称被 Configuration 注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。

                  -
                  ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);

                  public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
                  this();
                  // 注册类,几乎可以说无条件地注册 annotatedClasses 的 bean 定义
                  register(annotatedClasses);
                  refresh();
                  }
                  - -

                  本文不详细介绍配置类本身如何注册到 BeanFactory 中。当 BeanConfig 被传递给 AnnotationConfigApplicationContext,自身会先被解析为 BeanDefinition 注册到 beanFactory 中。有两点需要注意:

                  -
                    -
                  1. annotatedClasses 可以传入多个,意味着一开始静态指定的配置类可以有多个。
                  2. -
                  3. annotatedClasses 除了在命名上提示用户应传入被注解的类外,register(annotatedClasses) 实际上只是将它们视作普通的 Bean 注册到 beanFactory 中。它们是从外界传入的**首批 BeanDefinition**。
                  4. -
                  -

                  之后 Spring 进入 refresh 流程。使用 IDEA Debug 观察此时的 beanDefinitionMap,除了 beanConfig 外,AnnotationConfigApplicationContext 在创建时,已经自动注册了 6bean 定义,其中一个就是我们今天的主角 org.springframework.context.annotation.internalConfigurationAnnotationProcessor -> org.springframework.context.annotation.ConfigurationClassPostProcessor。显而易见,此时配置类还未被处理得到新的 bean 定义。

                  -
                  - -

                  配置类后处理器 ConfigurationClassPostProcessor

                  配置类后处理器 ConfigurationClassPostProcessor 实现了接口 BeanDefinitionRegistryPostProcessor,也因此同时实现了接口 BeanFactoryPostProcessor。在Spring 应用 context 刷新流程中,我们介绍过这两个接口,它们作为工厂后处理器,被用于 refresh 过程的调用工厂后处理器阶段invokeBeanFactoryPostProcessors(beanFactory))。工厂后处理器的作用,一言以蔽之,允许自定义修改应用上下文中的 bean 定义。

                  -

                  配置类后处理器 ConfigurationClassPostProcessor 的具体作用可以概括为两点:

                  -
                    -
                  1. 解析配置类中配置的 Bean,将它们的 bean 定义注册到 BeanFactory 中。
                  2. -
                  3. (如有必要)增强配置类
                  4. -
                  -

                  处理配置类的核心方法 processConfigBeanDefinitions

                  根据之前的介绍,进入 invokeBeanFactoryPostProcessors(beanFactory)ConfigurationClassPostProcessor 会先作为 BeanDefinitionRegistryPostProcessor 被调用。

                  -
                  -

                  个人的理解是,先将 BeanFactory 视作 BeanDefinitionRegistry 注册好 BeanDefinition,再视作 BeanFactory 进行处理,有点预备好原材料再统一处理的意思。

                  -
                  -
                  @Override
                  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
                  // ...
                  // 处理配置的 BeanDefinition
                  processConfigBeanDefinitions(registry);
                  }
                  - -
                  -

                  核心方法 processConfigBeanDefinitions(registry) 冗长,个人建议无需过度关注细节(但同时个人感受是反复阅读和 Debug 确实有益于加深理解,看个人时间和精力)。

                  -
                  -

                  基于配置类的 BeanDefinition Registry(也就是 BeanFactory),获取配置类,构建和校验配置模型:

                  -
                    -
                  1. BeanDefinition Registry(即 BeanFactory)中查找配置类。
                  2. -
                  3. 解析配置类得到配置模型,从模型中读取 BeanDefinitions 注册到 BeanDefinition Registry
                  4. -
                  5. 新的 BeanDefinitions 可能有新的配置类,回到 1 再来一遍。重复循环直到不再引入新的配置类。
                  6. -
                  -

                  以本文示例进行说明,静态添加的配置类只有 BeanConfig,假如 BeanConfig 不仅被 Configuration 注解标注,还被 ComponentScan 注解标注,并且刚好 Spring 通过扫描获得并添加了新的配置类,那么新的配置类就需要继续被解析。

                  -
                  -

                  应正视配置模型这个概念,它可以理解为配置类到 BeanDefinitions 的中间产物。最初我先入为主,带着解析得到 BeanDefinitions 这样“一阶段”完成的观念,非常不理解 processConfigBeanDefinitions 方法上 Build and validate a configuration model based on the registry of Configuration classes 这句注释。先行强调注意,处理配置类得到 bean 定义分为“两阶段”,解析配置类得到配置模型,从配置模型中读取 bean 定义

                  -
                  -
                  - -
                  public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
                  List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
                  // 刚开始,获取全部的 BeanDefinitions 作为候选
                  String[] candidateNames = registry.getBeanDefinitionNames();

                  for (String beanName : candidateNames) {
                  BeanDefinition beanDef = registry.getBeanDefinition(beanName);
                  if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                  ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
                  // 根据 beanDef 的 attributes 判断是 Full 还是 Lite 的配置类
                  // 已经处理过的配置类,会在 attributes 中添加标识
                  if (logger.isDebugEnabled()) {
                  logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
                  }
                  }
                  else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                  // 未处理过的候选者,检查是否是配置类。
                  // configCandidates 的命名让人有些困惑,个人认为这里指代的就是配置类
                  configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
                  }
                  }

                  // Return immediately if no @Configuration classes were found
                  // 和注释略有不符,配置类的判定没有这么单一,不仅限于 @Configuration 注解的 Full 配置类
                  if (configCandidates.isEmpty()) {
                  return;
                  }

                  // 根据 attributes 中的 order 排序(来源于 @Order 注解,可能不存在)
                  Collections.sort(configCandidates, new Comparator<BeanDefinitionHolder>() {
                  @Override
                  public int compare(BeanDefinitionHolder bd1, BeanDefinitionHolder bd2) {
                  int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
                  int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
                  return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
                  }
                  });

                  // 检测应用上下文中是否配置了自定义 bean 名称生成策略
                  SingletonBeanRegistry sbr = null;
                  if (registry instanceof SingletonBeanRegistry) {
                  sbr = (SingletonBeanRegistry) registry;
                  if (!this.localBeanNameGeneratorSet && sbr.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
                  // 如果 localBeanNameGenerator 未设置,且 SingletonBeanRegistry 中存在,就获取并使用
                  BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
                  this.componentScanBeanNameGenerator = generator;
                  this.importBeanNameGenerator = generator;
                  }
                  }

                  // 创建 ConfigurationClassParser,解析每一个配置类(不限于 Full 类型)
                  ConfigurationClassParser parser = new ConfigurationClassParser(
                  this.metadataReaderFactory, this.problemReporter, this.environment,
                  this.resourceLoader, this.componentScanBeanNameGenerator, registry);

                  // 配置类候选
                  Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
                  // 处理过的配置模型,已经被读取过 Bean 定义
                  // 个人感觉命名为 alreadyRead 更为准确
                  Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
                  do {
                  // 解析
                  parser.parse(candidates);
                  // 校验
                  parser.validate();

                  // 从 parser 中获取 ConfigurationClass,这就是配置模型(忍不住吐槽一下这个命名和配置类好容易搞混)
                  Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
                  // 排除已经读取过 Bean 定义的 ConfigurationClass
                  configClasses.removeAll(alreadyParsed);

                  // Read the model and create bean definitions based on its content
                  // 读取模型并根据它的内容创建 BeanDefinitions
                  if (this.reader == null) {
                  this.reader = new ConfigurationClassBeanDefinitionReader(
                  registry, this.sourceExtractor, this.resourceLoader, this.environment,
                  this.importBeanNameGenerator, parser.getImportRegistry());
                  }
                  // 加载(读取并注册) BeanDefinitions
                  this.reader.loadBeanDefinitions(configClasses);
                  // 添加到已经读取过的配置模型集合中(上面的 configClasses.removeAll(alreadyParsed) 造成的干扰好大)
                  alreadyParsed.addAll(configClasses);

                  // 清空这一轮处理的配置类集合
                  candidates.clear();
                  // 通过 registry 中的 BeanDefinitions 数量判断是否有新增的 BeanDefinitions
                  if (registry.getBeanDefinitionCount() > candidateNames.length) {
                  // 重新获取全部 BeanDefinitions 作为候选
                  String[] newCandidateNames = registry.getBeanDefinitionNames();
                  // 保留旧的候选集合用于快速筛选
                  Set<String> oldCandidateNames = new HashSet<String>(Arrays.asList(candidateNames));
                  // 为什么 alreadyParsedClasses 不定义在循环外,需要每次动态地从 alreadyParsed 获取?难道 configurationClass.getMetadata().getClassName() 的结果会变化吗?
                  // 如果不存在上述情况,直接在添加到 alreadyParsed 时,添加到 alreadyParsedClasses 就好了呀?
                  Set<String> alreadyParsedClasses = new HashSet<String>();
                  for (ConfigurationClass configurationClass : alreadyParsed) {
                  alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
                  }
                  for (String candidateName : newCandidateNames) {
                  // 快速地排除旧候选
                  if (!oldCandidateNames.contains(candidateName)) {
                  BeanDefinition bd = registry.getBeanDefinition(candidateName);
                  // 检查是否是配置类并且未被解析过(比如同一个配置类,重复注册,无需多次处理)
                  if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                  !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                  // 添加到新的配置类集合
                  candidates.add(new BeanDefinitionHolder(bd, candidateName));
                  }
                  }
                  }
                  candidateNames = newCandidateNames;
                  }
                  }
                  while (!candidates.isEmpty());

                  // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
                  if (sbr != null) {
                  if (!sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
                  sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
                  }
                  }

                  // 清理 metadataReaderFactory 缓存
                  if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
                  ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
                  }
                  }
                  +
                  public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
                  List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
                  // 刚开始,获取全部的 BeanDefinitions 作为候选
                  String[] candidateNames = registry.getBeanDefinitionNames();

                  for (String beanName : candidateNames) {
                  BeanDefinition beanDef = registry.getBeanDefinition(beanName);
                  if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                  ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
                  // 根据 beanDef 的 attributes 判断是 Full 还是 Lite 的配置类
                  // 已经处理过的配置类,会在 attributes 中添加标识
                  if (logger.isDebugEnabled()) {
                  logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
                  }
                  }
                  else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                  // 未处理过的候选者,检查是否是配置类。
                  // configCandidates 的命名让人有些困惑,个人认为这里指代的就是配置类
                  configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
                  }
                  }

                  // Return immediately if no @Configuration classes were found
                  // 和注释略有不符,配置类的判定没有这么单一,不仅限于 @Configuration 注解的 Full 配置类
                  if (configCandidates.isEmpty()) {
                  return;
                  }

                  // 根据 attributes 中的 order 排序(来源于 @Order 注解,可能不存在)
                  Collections.sort(configCandidates, new Comparator<BeanDefinitionHolder>() {
                  @Override
                  public int compare(BeanDefinitionHolder bd1, BeanDefinitionHolder bd2) {
                  int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
                  int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
                  return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
                  }
                  });

                  // 检测应用上下文中是否配置了自定义 bean 名称生成策略
                  SingletonBeanRegistry sbr = null;
                  if (registry instanceof SingletonBeanRegistry) {
                  sbr = (SingletonBeanRegistry) registry;
                  if (!this.localBeanNameGeneratorSet && sbr.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
                  // 如果 localBeanNameGenerator 未设置,且 SingletonBeanRegistry 中存在,就获取并使用
                  BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
                  this.componentScanBeanNameGenerator = generator;
                  this.importBeanNameGenerator = generator;
                  }
                  }

                  // 创建 ConfigurationClassParser,解析每一个配置类(不限于 Full 类型)
                  ConfigurationClassParser parser = new ConfigurationClassParser(
                  this.metadataReaderFactory, this.problemReporter, this.environment,
                  this.resourceLoader, this.componentScanBeanNameGenerator, registry);

                  // 配置类候选
                  Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
                  // 处理过的配置模型,已经被读取过 Bean 定义
                  // 个人感觉命名为 alreadyRead 更为准确
                  Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
                  do {
                  // 解析
                  parser.parse(candidates);
                  // 校验
                  parser.validate();

                  // 从 parser 中获取 ConfigurationClass,这就是配置模型(忍不住吐槽一下这个命名和配置类好容易搞混)
                  Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
                  // 排除已经读取过 Bean 定义的 ConfigurationClass
                  configClasses.removeAll(alreadyParsed);

                  // Read the model and create bean definitions based on its content
                  // 读取模型并根据它的内容创建 BeanDefinitions
                  if (this.reader == null) {
                  this.reader = new ConfigurationClassBeanDefinitionReader(
                  registry, this.sourceExtractor, this.resourceLoader, this.environment,
                  this.importBeanNameGenerator, parser.getImportRegistry());
                  }
                  // 加载(读取并注册) BeanDefinitions
                  this.reader.loadBeanDefinitions(configClasses);
                  // 添加到已经读取过的配置模型集合中(上面的 configClasses.removeAll(alreadyParsed) 造成的干扰好大)
                  alreadyParsed.addAll(configClasses);

                  // 清空这一轮处理的配置类集合
                  candidates.clear();
                  // 通过 registry 中的 BeanDefinitions 数量判断是否有新增的 BeanDefinitions
                  if (registry.getBeanDefinitionCount() > candidateNames.length) {
                  // 重新获取全部 BeanDefinitions 作为候选
                  String[] newCandidateNames = registry.getBeanDefinitionNames();
                  // 保留旧的候选集合用于快速筛选
                  Set<String> oldCandidateNames = new HashSet<String>(Arrays.asList(candidateNames));
                  // 为什么 alreadyParsedClasses 不定义在循环外,需要每次动态地从 alreadyParsed 获取?难道 configurationClass.getMetadata().getClassName() 的结果会变化吗?
                  // 如果不存在上述情况,直接在添加到 alreadyParsed 时,添加到 alreadyParsedClasses 就好了呀?
                  Set<String> alreadyParsedClasses = new HashSet<String>();
                  for (ConfigurationClass configurationClass : alreadyParsed) {
                  alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
                  }
                  for (String candidateName : newCandidateNames) {
                  // 快速地排除旧候选
                  if (!oldCandidateNames.contains(candidateName)) {
                  BeanDefinition bd = registry.getBeanDefinition(candidateName);
                  // 检查是否是配置类并且未被解析过(比如同一个配置类,重复注册,无需多次处理)
                  if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                  !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                  // 添加到新的配置类集合
                  candidates.add(new BeanDefinitionHolder(bd, candidateName));
                  }
                  }
                  }
                  candidateNames = newCandidateNames;
                  }
                  }
                  while (!candidates.isEmpty());

                  // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
                  if (sbr != null) {
                  if (!sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
                  sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
                  }
                  }

                  // 清理 metadataReaderFactory 缓存
                  if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
                  ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
                  }
                  }

                  判断是否是配置类

                  checkConfigurationClassCandidate 方法:

                    @@ -3787,41 +3627,158 @@ - Nginx 反向代理在家庭网络中的应用 - /2023/12/01/Nginx-reverse-proxy-for-home-networks/ - 原先在使用 Cloudflare Tunnel 访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare 已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare 统计中缺少的新,决定使用 Nginx 反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts 文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port 的形式去访问。

                    + Dubbo SPI 的工作原理 + /2023/11/28/how-does-Dubbo-SPI-works/ + SPI 作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI 被广泛应用于各类技术框架中,例如 JDBC 驱动、SpringDubbo 等等。Dubbo 并未使用原生的 Java SPI,而是重新实现了一套更加强大的 Dubbo SPI。本文将简单介绍 SPI 的设计理念,通过示例带你体会 SPI 的作用,通过 Dubbo 获取拓展的流程图源码分析带你理解 Dubbo SPI 的工作原理。深入了解 Dubbo SPI,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。

                    -
                    -

                    Cloudflare Tunnel 相较于 ZerotierOpenVPN,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。

                    -
                    -

                    网络结构示意图

                    客户端发起请求,请求经 Cloudflare 转发到局域网中的 Tunnel。原先,Tunnel 如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx,由 Nginx 反向代理,发向目标服务。

                    - +

                    SPI 简介

                    SPI 的全称是 Service Provider Interface,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI 机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI 具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 JavaJDBC 驱动为例,JDK 提供了 java.sql.Driver 接口,各个数据库厂商,例如 MySQLOracle 提供具体的实现。

                    + -

                    配置

                    docker-compose.yml

                    NginxTunnel 还有其他内部服务应处于同一个网络中。

                    -
                    version: "1.0"
                    services:
                    nginx:
                    image: nginx:1.25.3
                    container_name: nginx
                    ports:
                    - 80:80
                    volumes:
                    - $PWD/etc/nginx/conf.d:/etc/nginx/conf.d
                    - $PWD/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
                    - $PWD/log:/var/log/nginx
                    networks:
                    - my-network
                    restart: unless-stopped

                    networks:
                    my-network:
                    external: true
                    +

                    目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:

                    +
                      +
                    • Java SPI:META-INF/services/full.qualified.interface.name
                    • +
                    • Dubbo SPI:META-INF/dubbo/full.qualified.interface.name(还有其他目录可供选择)
                    • +
                    • Spring SPI: META-INF/spring.factories
                    • +
                    +

                    SPI 示例

                    Java SPI 示例

                    定义一个接口 Animal

                    +
                    public interface Animal {
                    void bark();
                    }
                    -

                    /etc/nginx/nginx.conf

                    在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel 的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx 服务。

                    -
                    user  nginx;
                    worker_processes auto;

                    error_log /var/log/nginx/error.log notice;
                    pid /var/run/nginx.pid;


                    events {
                    worker_connections 1024;
                    }


                    http {
                    include /etc/nginx/mime.types;
                    default_type application/octet-stream;

                    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

                    access_log /var/log/nginx/access.log main;

                    sendfile on;
                    #tcp_nopush on;

                    keepalive_timeout 65;

                    #gzip on;

                    include /etc/nginx/conf.d/*.conf;

                    #以上是默认的配置内容,新增拒绝未匹配成功的域名
                    server {
                    listen 80 default_server;
                    server_name _;

                    return 404;
                    }
                    }
                    +

                    定义两个实现类 DogCat

                    +
                    public class Dog implements Animal {
                    @Override
                    public void bark() {
                    System.out.println("Dog bark...");
                    }
                    }

                    public class Cat implements Animal {
                    @Override
                    public void bark() {
                    System.out.println("Cat bark...");
                    }
                    }
                    -

                    /etc/nginx/conf.d

                    本目录下,配置 server 块。

                    -
                    server {
                    listen 80;
                    listen [::]:80;
                    server_name your.domain.com;

                    location / {
                    #转发请求
                    proxy_pass http://your-service;
                    }
                    }
                    +

                    META-INF/services 文件夹下创建一个文件,名称为 Animal 的全限定名 com.moralok.dubbo.spi.test.Animal,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。

                    +
                    com.moralok.dubbo.spi.test.Dog
                    com.moralok.dubbo.spi.test.Cat
                    -

                    正向代理和反向代理

                    代理(正向代理)

                    代理(Proxy)也称为网络代理,是一种特殊的网络服务,允许一个终端通过这个服务与另一个终端进行非直接的连接。一般认为代理服务有利于保障网络终端的隐私或安全,在一定程度上能够阻止网络攻击。

                    - +

                    进行测试。

                    +
                    public class JavaSPITest {
                    @Test
                    void bark() {
                    System.out.println("Java SPI");
                    System.out.println("============");
                    ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
                    serviceLoader.forEach(Animal::bark);
                    }
                    }
                    -

                    功能

                      -
                    • 提高访问速度
                    • -
                    • 隐藏真实IP
                    • -
                    • 突破网站的区域限制
                    • -
                    • 突破网络审查
                    • -
                    • ……
                    • -
                    -

                    反向代理

                    反向代理(Reverse Proxy)在电脑网络中是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的 IP 地址,而不知道在代理服务器后面的服务器集群的存在。

                    - +

                    测试结果

                    +
                    Java SPI
                    ============
                    Dog bark...
                    Cat bark...
                    -

                    功能

                      -
                    • 对客户端隐藏服务器(集群)的 IP 地址
                    • +

                      Dubbo SPI 示例

                      Dubbo 并未使用原生的 Java SPI,而是重新实现了一套功能更加强大的 SPI 机制。Dubbo SPI 的配置文件放在 META-INF/dubbo 文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI 注解。

                      +
                      dog = com.moralok.dubbo.spi.test.Dog
                      cat = com.moralok.dubbo.spi.test.Cat
                      + +

                      进行测试。

                      +
                      public class DubboSPITest {

                      @Test
                      void bark() {
                      System.out.println("Dubbo SPI");
                      System.out.println("============");
                      ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
                      Animal dog = extensionLoader.getExtension("dog");
                      dog.bark();
                      Animal cat = extensionLoader.getExtension("cat");
                      cat.bark();
                      }
                      }
                      + +

                      测试结果

                      +
                      Dubbo SPI
                      ============
                      Dog bark...
                      Cat bark...
                      + +

                      Dubbo 获取扩展流程图

                      + +

                      Dubbo SPI 源码分析

                      获取 ExtensionLoader

                      private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);

                      public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
                      if (type == null) {
                      throw new IllegalArgumentException("Extension type == null");
                      }
                      if (!type.isInterface()) {
                      throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
                      }
                      if (!withExtensionAnnotation(type)) {
                      throw new IllegalArgumentException("Extension type (" + type +
                      ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
                      }

                      // 从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回
                      ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
                      if (loader == null) {
                      EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
                      loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
                      }
                      return loader;
                      }
                      + +

                      这个方法包含了如下步骤:

                      +
                        +
                      1. 参数校验。
                      2. +
                      3. 从缓存 EXTENSION_LOADERS 中获取与拓展类对应的 ExtensionLoader,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。
                      4. +
                      +
                      +

                      从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的 getOrCreate 的处理模式在 Dubbo 的源码中经常出现。

                      +
                      +

                      EXTENSION_LOADERSExtensionLoader 的静态变量,保存了“拓展类->ExtensionLoader”的映射关系。

                      +

                      根据 name 获取 Extension

                      public T getExtension(String name) {
                      return getExtension(name, true);
                      }

                      public T getExtension(String name, boolean wrap) {
                      if (StringUtils.isEmpty(name)) {
                      throw new IllegalArgumentException("Extension name == null");
                      }
                      if ("true".equals(name)) {
                      // 获取默认的拓展实现
                      return getDefaultExtension();
                      }
                      // Holder,用于持有目标对象
                      final Holder<Object> holder = getOrCreateHolder(name);
                      // 双重检查
                      Object instance = holder.get();
                      if (instance == null) {
                      synchronized (holder) {
                      instance = holder.get();
                      if (instance == null) {
                      // 创建拓展实例,设置到 holder 中。
                      instance = createExtension(name, wrap);
                      holder.set(instance);
                      }
                      }
                      }
                      return (T) instance;
                      }

                      private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

                      private Holder<Object> getOrCreateHolder(String name) {
                      Holder<Object> holder = cachedInstances.get(name);
                      if (holder == null) {
                      cachedInstances.putIfAbsent(name, new Holder<>());
                      holder = cachedInstances.get(name);
                      }
                      return holder;
                      }
                      + +

                      这个方法中获取 Holder 和获取拓展实例都是使用 getOrCreate 的模式。

                      +

                      Holder 用于持有拓展实例。cachedInstancesExtensionLoader 的成员变量,保存了“name->Holder(拓展实例)”的映射关系。

                      +

                      创建 Extension

                      private T createExtension(String name, boolean wrap) {
                      // 从配置文件中加载所有的拓展类,可得到“name->拓展实现类”的映射关系表
                      Class<?> clazz = getExtensionClasses().get(name);
                      if (clazz == null || unacceptableExceptions.contains(name)) {
                      throw findException(name);
                      }
                      try {
                      // 使用 getOrCreate 模式获取拓展实例
                      T instance = (T) EXTENSION_INSTANCES.get(clazz);
                      if (instance == null) {
                      EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
                      instance = (T) EXTENSION_INSTANCES.get(clazz);
                      }
                      // 向拓展实例中注入依赖
                      injectExtension(instance);

                      // 是否包装默认为 true
                      if (wrap) {

                      List<Class<?>> wrapperClassesList = new ArrayList<>();
                      if (cachedWrapperClasses != null) {
                      wrapperClassesList.addAll(cachedWrapperClasses);
                      wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                      Collections.reverse(wrapperClassesList);
                      }

                      if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                      // 遍历包装类
                      for (Class<?> wrapperClass : wrapperClassesList) {
                      Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                      // 区别于旧版本:支持使用 Wrapper 注解进行匹配
                      if (wrapper == null
                      || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                      // 如果有匹配的包装类,包装拓展实例
                      instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                      }
                      }
                      }
                      }

                      // 初始化拓展实例
                      initExtension(instance);
                      return instance;
                      } catch (Throwable t) {
                      throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                      type + ") couldn't be instantiated: " + t.getMessage(), t);
                      }
                      }
                      + +

                      这个方法包含如下步骤:

                      +
                        +
                      1. 通过 getExtensionClasses 获取所有拓展类
                      2. +
                      3. 通过反射创建拓展实例
                      4. +
                      5. 向拓展实例中注入依赖
                      6. +
                      7. 将拓展实例包装在适配的 Wrapper 对象中
                      8. +
                      9. 初始化拓展实例
                      10. +
                      +

                      第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOCAOP 的具体实现。

                      +

                      最后拓展实例的结构如下图。

                      + + +

                      加载 Extension Class

                      private Map<String, Class<?>> getExtensionClasses() {
                      // 使用 getOrCreate 模式获取所有拓展类
                      Map<String, Class<?>> classes = cachedClasses.get();
                      if (classes == null) {
                      synchronized (cachedClasses) {
                      classes = cachedClasses.get();
                      if (classes == null) {
                      classes = loadExtensionClasses();
                      cachedClasses.set(classes);
                      }
                      }
                      }
                      return classes;
                      }

                      private Map<String, Class<?>> loadExtensionClasses() {
                      // 1. 缓存默认拓展名
                      cacheDefaultExtensionName();

                      Map<String, Class<?>> extensionClasses = new HashMap<>();

                      // 遍历加载策略,加载各个策略的目录下的配置文件,获取拓展类
                      for (LoadingStrategy strategy : strategies) {
                      loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
                      strategy.overridden(), strategy.excludedPackages());
                      loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
                      strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
                      }

                      return extensionClasses;
                      }

                      // 如果存在默认拓展名,提取并缓存
                      private void cacheDefaultExtensionName() {
                      // 获取 SPI 注解
                      final SPI defaultAnnotation = type.getAnnotation(SPI.class);
                      if (defaultAnnotation == null) {
                      return;
                      }

                      String value = defaultAnnotation.value();
                      if ((value = value.trim()).length() > 0) {
                      // 对 SPI 的 value 内容进行切分
                      String[] names = NAME_SEPARATOR.split(value);
                      // 检测 name 是否合法
                      if (names.length > 1) {
                      throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                      + ": " + Arrays.toString(names));
                      }
                      if (names.length == 1) {
                      // 缓存默认拓展名,用于 getDefaultExtension
                      cachedDefaultName = names[0];
                      }
                      }
                      }
                      + +

                      依次处理特定目录

                      代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/ 三个目录下的配置文件,获取拓展类。

                      +
                      loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
                      loadDirectory(extensionClasses, DUBBO_DIRECTORY);
                      loadDirectory(extensionClasses, SERVICES_DIRECTORY);
                      + +

                      新版本使用原生的 Java SPI 加载 LoadingStrategy,允许用户自定义加载策略。

                      +
                        +
                      1. DubboInternalLoadingStrategy,目录 META-INF/dubbo/internal/,优先级最高
                      2. +
                      3. DubboLoadingStrategy,目录 META-INF/dubbo/,优先级普通
                      4. +
                      5. ServicesLoadingStrategy,目录 META-INF/services/,优先级最低
                      6. +
                      +
                      private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

                      private static LoadingStrategy[] loadLoadingStrategies() {
                      // 通过 Java SPI 加载 LoadingStrategy
                      return stream(load(LoadingStrategy.class).spliterator(), false)
                      .sorted()
                      .toArray(LoadingStrategy[]::new);
                      }
                      + +

                      LoadingStrategyJava SPI 配置文件

                      + + +

                      loadDirectory 方法

                      loadDirectory 方法先通过 classLoader 获取所有的资源链接,然后再通过 loadResource 方法加载资源。

                      +

                      新版本中 extensionLoaderClassLoaderFirst 可以设置是否优先使用 ExtensionLoader's ClassLoader 获取资源链接。

                      +
                      private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                      boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
                      // filename = 文件夹路径 + type 的全限定名
                      String fileName = dir + type;
                      try {
                      Enumeration<java.net.URL> urls = null;
                      ClassLoader classLoader = findClassLoader();

                      // 区别于旧版本:先从 ExtensionLoader's ClassLoader 获取资源链接,默认为 false
                      if (extensionLoaderClassLoaderFirst) {
                      ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                      if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                      urls = extensionLoaderClassLoader.getResources(fileName);
                      }
                      }

                      // 根据文件名加载所有同名文件
                      if (urls == null || !urls.hasMoreElements()) {
                      if (classLoader != null) {
                      urls = classLoader.getResources(fileName);
                      } else {
                      urls = ClassLoader.getSystemResources(fileName);
                      }
                      }

                      if (urls != null) {
                      while (urls.hasMoreElements()) {
                      java.net.URL resourceURL = urls.nextElement();
                      // 加载资源
                      loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
                      }
                      }
                      } catch (Throwable t) {
                      logger.error("Exception occurred when loading extension class (interface: " +
                      type + ", description file: " + fileName + ").", t);
                      }
                      }
                      + +

                      loadResource 方法

                      loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于操作缓存。

                      +

                      新版本中 excludedPackages 可以设置将指定包内的类都排除。

                      +
                      private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                      java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
                      try {
                      try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                      String line;
                      String clazz = null;
                      // 按行读取配置内容
                      while ((line = reader.readLine()) != null) {
                      // 定位 # 字符,截取 # 字符之前的内容,# 字符之后的内容为注释,需要忽略
                      final int ci = line.indexOf('#');
                      if (ci >= 0) {
                      line = line.substring(0, ci);
                      }
                      line = line.trim();
                      if (line.length() > 0) {
                      try {
                      String name = null;
                      // 定位 = 字符,以 = 字符为界,截取键值对
                      int i = line.indexOf('=');
                      if (i > 0) {
                      name = line.substring(0, i).trim();
                      clazz = line.substring(i + 1).trim();
                      } else {
                      clazz = line;
                      }
                      if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
                      // 加载类,并通过 loadClass 进行缓存
                      // 区别于旧版本:根据 excludedPackages 判断是否排除
                      loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
                      }
                      } catch (Throwable t) {
                      IllegalStateException e = new IllegalStateException(
                      "Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
                      ", cause: " + t.getMessage(), t);
                      exceptions.put(line, e);
                      }
                      }
                      }
                      }
                      } catch (Throwable t) {
                      logger.error("Exception occurred when loading extension class (interface: " +
                      type + ", class file: " + resourceURL + ") in " + resourceURL, t);
                      }
                      }
                      + +

                      loadClass 方法

                      loadClass 方法设置了多个缓存,比如 cachedAdaptiveClasscachedWrapperClassescachedNamescachedClasses

                      +

                      新版本中 overridden 可以设置是否覆盖 cachedAdaptiveClasscachedClassesname->clazz

                      +
                      private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                      boolean overridden) throws NoSuchMethodException {
                      // 检测 clazz 是否合法
                      if (!type.isAssignableFrom(clazz)) {
                      throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                      type + ", class line: " + clazz.getName() + "), class "
                      + clazz.getName() + " is not subtype of interface.");
                      }
                      if (clazz.isAnnotationPresent(Adaptive.class)) {
                      // 检测 clazz 是否有 Adaptive 注解,有则设置 cachedAdaptiveClass 缓存
                      // 区别于旧版本:根据 overriden 判断是否可覆盖
                      cacheAdaptiveClass(clazz, overridden);
                      } else if (isWrapperClass(clazz)) {
                      // 检测 clazz 是否是 Wrapper 类型,是则添加到 cachedWrapperClasses 缓存
                      cacheWrapperClass(clazz);
                      } else {
                      // 检测 clazz 是否有默认的构造器方法,如果没有,则抛出异常
                      clazz.getConstructor();
                      if (StringUtils.isEmpty(name)) {
                      // 如果 name 为空,则尝试从 Extension 注解中获取 name,或者使用小写的类名(可能截取 type 后缀)作为 name
                      name = findAnnotationName(clazz);
                      if (name.length() == 0) {
                      throw new IllegalStateException(
                      "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                      }
                      }

                      // 切分 name
                      String[] names = NAME_SEPARATOR.split(name);
                      if (ArrayUtils.isNotEmpty(names)) {
                      // 如果 clazz 有 Activate 注解,则缓存 names[0]->Activate 的映射关系
                      cacheActivateClass(clazz, names[0]);
                      for (String n : names) {
                      // 缓存 clazz->n 的映射关系
                      cacheName(clazz, n);
                      // 缓存 n->clazz 的映射关系,传递到最后,终于轮到 extensionClasses
                      // 区别于旧版本:根据 overriden 判断是否可覆盖
                      saveInExtensionClass(extensionClasses, clazz, n, overridden);
                      }
                      }
                      }
                      }

                      private void cacheAdaptiveClass(Class<?> clazz, boolean overridden) {
                      if (cachedAdaptiveClass == null || overridden) {
                      // 可以设置是否覆盖
                      cachedAdaptiveClass = clazz;
                      } else if (!cachedAdaptiveClass.equals(clazz)) {
                      throw new IllegalStateException("More than 1 adaptive class found: "
                      + cachedAdaptiveClass.getName()
                      + ", " + clazz.getName());
                      }
                      }

                      private void cacheWrapperClass(Class<?> clazz) {
                      if (cachedWrapperClasses == null) {
                      cachedWrapperClasses = new ConcurrentHashSet<>();
                      }
                      cachedWrapperClasses.add(clazz);
                      }

                      private void cacheActivateClass(Class<?> clazz, String name) {
                      Activate activate = clazz.getAnnotation(Activate.class);
                      if (activate != null) {
                      cachedActivates.put(name, activate);
                      } else {
                      // support com.alibaba.dubbo.common.extension.Activate
                      com.alibaba.dubbo.common.extension.Activate oldActivate =
                      clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                      if (oldActivate != null) {
                      cachedActivates.put(name, oldActivate);
                      }
                      }
                      }
                      + +

                      Dubbo IOC

                      Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter 方法特征并满足条件,若有,则通过 objectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。

                      +
                      +

                      Spring IOC 相比,Dubbo IOC 实现的依赖注入功能更加简单,代码也更加容易理解。

                      +
                      +
                      private T injectExtension(T instance) {
                      // 检测是否有 objectFactory
                      if (objectFactory == null) {
                      return instance;
                      }
                      try {
                      // 遍历目标类的所有方法
                      // 区别于旧版本:增加了对 DisbaleInject、Inject 注解的处理
                      for (Method method : instance.getClass().getMethods()) {
                      // 检测是否是 setter 方法
                      if (!isSetter(method)) {
                      continue;
                      }

                      // 检测是否标注 DisableInject 注解
                      if (method.getAnnotation(DisableInject.class) != null) {
                      continue;
                      }

                      // 检测参数类型是否是原始类型
                      Class<?> pt = method.getParameterTypes()[0];
                      if (ReflectUtils.isPrimitives(pt)) {
                      continue;
                      }

                      // 获取属性名
                      String property = getSetterProperty(method);
                      // 检测是否标注 Inject 注解
                      Inject inject = method.getAnnotation(Inject.class);
                      if (inject == null) {
                      injectValue(instance, method, pt, property);
                      } else {
                      // 检测 Inject 是否启动、是否按照类型注入
                      if (!inject.enable()) {
                      continue;
                      }

                      if (inject.type() == Inject.InjectType.ByType) {
                      injectValue(instance, method, pt, null);
                      } else {
                      injectValue(instance, method, pt, property);
                      }
                      }
                      }
                      } catch (Exception e) {
                      logger.error(e.getMessage(), e);
                      }
                      return instance;
                      }

                      // 获取属性名
                      private String getSetterProperty(Method method) {
                      return method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                      }

                      // public、set 开头、只有一个参数
                      private boolean isSetter(Method method) {
                      return method.getName().startsWith("set")
                      && method.getParameterTypes().length == 1
                      && Modifier.isPublic(method.getModifiers());
                      }

                      private void injectValue(T instance, Method method, Class<?> pt, String property) {
                      try {
                      // 从 ObjectFactory 中获取依赖对象
                      Object object = objectFactory.getExtension(pt, property);
                      if (object != null) {
                      // 通过反射调用 setter 方法设置依赖
                      method.invoke(instance, object);
                      }
                      } catch (Exception e) {
                      logger.error("Failed to inject via method " + method.getName()
                      + " of interface " + type.getName() + ": " + e.getMessage(), e);
                      }
                      }
                      + +

                      objectFactoryExtensionFactory 的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理

                      +
                      public <T> T getExtension(Class<T> type, String name) {
                      if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
                      ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
                      if (!loader.getSupportedExtensions().isEmpty()) {
                      return loader.getAdaptiveExtension();
                      }
                      }
                      return null;
                      }
                      + +

                      参考文章

                      +]]> + + java + dubbo + spi + + + + Nginx 反向代理在家庭网络中的应用 + /2023/12/01/Nginx-reverse-proxy-for-home-networks/ + 原先在使用 Cloudflare Tunnel 访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare 已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare 统计中缺少的新,决定使用 Nginx 反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts 文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port 的形式去访问。

                      + + +
                      +

                      Cloudflare Tunnel 相较于 ZerotierOpenVPN,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。

                      +
                      +

                      网络结构示意图

                      客户端发起请求,请求经 Cloudflare 转发到局域网中的 Tunnel。原先,Tunnel 如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx,由 Nginx 反向代理,发向目标服务。

                      + + +

                      配置

                      docker-compose.yml

                      NginxTunnel 还有其他内部服务应处于同一个网络中。

                      +
                      version: "1.0"
                      services:
                      nginx:
                      image: nginx:1.25.3
                      container_name: nginx
                      ports:
                      - 80:80
                      volumes:
                      - $PWD/etc/nginx/conf.d:/etc/nginx/conf.d
                      - $PWD/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
                      - $PWD/log:/var/log/nginx
                      networks:
                      - my-network
                      restart: unless-stopped

                      networks:
                      my-network:
                      external: true
                      + +

                      /etc/nginx/nginx.conf

                      在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel 的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx 服务。

                      +
                      user  nginx;
                      worker_processes auto;

                      error_log /var/log/nginx/error.log notice;
                      pid /var/run/nginx.pid;


                      events {
                      worker_connections 1024;
                      }


                      http {
                      include /etc/nginx/mime.types;
                      default_type application/octet-stream;

                      log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

                      access_log /var/log/nginx/access.log main;

                      sendfile on;
                      #tcp_nopush on;

                      keepalive_timeout 65;

                      #gzip on;

                      include /etc/nginx/conf.d/*.conf;

                      #以上是默认的配置内容,新增拒绝未匹配成功的域名
                      server {
                      listen 80 default_server;
                      server_name _;

                      return 404;
                      }
                      }
                      + +

                      /etc/nginx/conf.d

                      本目录下,配置 server 块。

                      +
                      server {
                      listen 80;
                      listen [::]:80;
                      server_name your.domain.com;

                      location / {
                      #转发请求
                      proxy_pass http://your-service;
                      }
                      }
                      + +

                      正向代理和反向代理

                      代理(正向代理)

                      代理(Proxy)也称为网络代理,是一种特殊的网络服务,允许一个终端通过这个服务与另一个终端进行非直接的连接。一般认为代理服务有利于保障网络终端的隐私或安全,在一定程度上能够阻止网络攻击。

                      + + +

                      功能

                        +
                      • 提高访问速度
                      • +
                      • 隐藏真实IP
                      • +
                      • 突破网站的区域限制
                      • +
                      • 突破网络审查
                      • +
                      • ……
                      • +
                      +

                      反向代理

                      反向代理(Reverse Proxy)在电脑网络中是代理服务器的一种。服务器根据客户端的请求,从其关联的一组或多组后端服务器上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的 IP 地址,而不知道在代理服务器后面的服务器集群的存在。

                      + + +

                      功能

                        +
                      • 对客户端隐藏服务器(集群)的 IP 地址
                      • 安全,可作为应用层防火墙
                      • 负载均衡
                      • 缓存服务,缓存静态内容和短时间内大量访问请求的动态内容
                      • @@ -3863,102 +3820,285 @@ - 使用 logrotate 滚动 Docker 容器内的 Nginx 的日志 - /2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/ - Nginx 没有提供开箱即用的日志滚动功能,而是将其交给使用者自己实现。你既可以按照官方文档的建议通过编写脚本实现,也可以使用 logrotate 管理日志。但是和在普通场景下不同,在使用 Docker 运行 Nginx 时,你可能需要额外考虑一点细节。本文记录了在为 Docker 中的 Nginx 的日志文件配置滚动功能过程中遇到的一些问题和思考。

                        + Dubbo SPI 自适应拓展的工作原理 + /2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/ + 直接展示一个具体的 Dubbo SPI 自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo 的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI 自适应拓展的相关源码和工作原理。

                        -

                        Nginx 滚动日志

                        官方文档

                        -

                        In order to rotate log files, they need to be renamed first. After that USR1 signal should be sent to the master process. The master process will then re-open all currently open log files and assign them an unprivileged user under which the worker processes are running, as an owner. After successful re-opening, the master process closes all open files and sends the message to worker process to ask them to re-open files. Worker processes also open new files and close old files right away. As a result, old files are almost immediately available for post processing, such as compression.

                        -
                        -

                        根据官方文档的解释,滚动日志文件的流程应如下,你可以自己编写 Shell 脚本配合 crontab 实现定时滚动功能。

                        -
                          -
                        1. 首先重命名日志文件
                        2. -
                        3. 之后发送 USR1 信号给 Nginx 主进程,Nginx 将重新打开日志文件
                        4. -
                        5. 对日志文件进行后处理,比如压缩(可选)
                        6. -
                        -
                        $ mv access.log access.log.0
                        $ kill -USR1 `cat master.nginx.pid`
                        $ sleep 1
                        $ gzip access.log.0 # do something with access.log.0
                        - -

                        说明

                          -
                        1. 在没有执行 kill 命令前,即便已经重命名了日志文件,Nginx 还是会向重命名后的文件写入日志。因为在 Linux 系统中,系统内核是根据文件描述符定位文件的。
                        2. -
                        3. USR1 是自定义信号,软件的作者自己确定收到该信号后做什么。在 Nginx 中,主进程收到信号后,会重新打开所有当前打开的日志文件并将它们分配给一个非特权用户作为所有者,工作进程就是在该所有者下运行的。成功重新打开后,主进程关闭所有打开的文件并向工作进程发送消息,要求它们重新打开文件。工作进程也打开新文件并立即关闭旧文件。
                        4. -
                        -

                        使用 logrotate

                        -

                        logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.

                        +
                        +

                        站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。

                        -

                        logrotate 旨在简化生成大量日志文件的系统的管理。它允许自动滚动、压缩、删除和邮寄日志文件。每个日志文件可以在每天、每周、每月或当它变得太大时处理。Linux 一般默认安装了 logrotate

                        -

                        默认配置文件

                        查看默认配置文件:cat /etc/logrotate.conf

                        -
                        # see "man logrotate" for details
                        # 每周滚动日志文件
                        weekly

                        # 默认使用 adm group,因为这是 /var/log/syslog 的所属组
                        su root adm

                        # 保留 4 周的备份(其实是保留 4 个备份,对应 weekly 的设置,就是保留 4 周)
                        rotate 4

                        # 在滚动旧日志文件后,创建新的空日志文件
                        create

                        # 使用日期作为滚动日志文件的后缀
                        #dateext

                        # 如果你希望压缩日志文件,请取消注释
                        #compress

                        # 软件包将日志滚动的配置信息放入此目录中
                        include /etc/logrotate.d

                        # system-specific logs may be also be configured here.
                        +

                        原理

                        Dubbo 中,很多拓展都是通过 SPI 机制动态加载的,比如 ProtocolClusterLoadBalance 等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。

                        +

                        示例

                        定义一个接口 Animal

                        +
                        public interface Animal {
                        void bark();
                        }
                        -

                        配置信息所在目录

                        查看日志滚动的配置信息所在的目录:ls /etc/logrotate.d/

                        -
                        alternatives  apport  apt  bootlog  btmp  dpkg  rsyslog  ubuntu-advantage-tools  ufw  unattended-upgrades  wtmp
                        +

                        定义两个实现类 DogCat

                        +
                        public class Dog implements Animal {
                        @Override
                        public void bark() {
                        System.out.println("Dog bark...");
                        }
                        }

                        public class Cat implements Animal {
                        @Override
                        public void bark() {
                        System.out.println("Cat bark...");
                        }
                        }
                        -

                        为 Nginx 新增配置

                        Nginx 新增日志滚动配置,vim /etc/logrotate.d/nginx

                        -
                        /path/to/your/nginx/logs/*.log {
                        # 切换用户
                        su moralok moralok
                        # 每天滚动日志文件
                        daily
                        # 使用日期作为滚动日志文件的后缀
                        dateext
                        # 如果日志丢失,不报错继续滚动下一个日志
                        missingok
                        # 保留 31 个备份
                        rotate 31
                        # 不压缩
                        nocompress
                        # 整个日志组运行一次的脚本
                        sharedscripts
                        # 滚动后的处理
                        postrotate
                        # 重新打开日志文件
                        docker exec nginx sh -c "[ ! -f /var/run/nginx.pid ] || (kill -USR1 `docker exec nginx cat /var/run/nginx.pid`; echo 'Successfully rotating nginx logs.')"
                        endscript
                        }
                        +

                        在运行时根据参数动态地加载拓展。

                        +
                        public void bark(String type) {
                        if (type == null) {
                        throw new IllegalArgumentException("type == null");
                        }
                        // 通过 SPI 动态地加载具体的 Animal
                        Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                        // 调用目标方法
                        animal.bark();
                        }
                        -

                        验证配置和测试

                        测试配置文件是否有错误,logrotate -d /etc/logrotate.d/nginx

                        -

                        强制滚动:logrotate -f /etc/logrotate.d/nginx

                        -

                        一些坑

                        /var/lib/logrotate/ 权限问题

                        当你使用校验过配置文件的正确性后,尝试强制滚动时,可能会遇到报错。

                        -
                        $ logrotate -f /etc/logrotate.d/nginx 
                        error: error creating output file /var/lib/logrotate/status.tmp: Permission denied
                        +

                        改进

                        是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:

                        +
                        Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                        animal.bark();

                        WeelMaker weelMaker = ExtensionLoader.getExtensionLoader(WeelMaker.class).getExtension(weelMakerName);
                        weelMaker.makeWeel();

                        LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invocation.getLoadBalanceType());
                        loadBalance.select();

                        // ...
                        -

                        这是因为 logrotate 会在 /var/lib/logrotate/ 目录下创建 status 文件。查看目录权限可知,需要以 root 身份运行 logrotate

                        -
                        $ ll /var/lib/logrotate
                        total 12
                        drwxr-xr-x 2 root root 4096 Dec 2 08:35 ./
                        drwxr-xr-x 44 root root 4096 Jun 26 09:02 ../
                        -rw-r--r-- 1 root root 1395 Dec 2 08:35 status
                        +

                        这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter) 这样重复的代码;引入了 ExtensionLoader 这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo 和 动物 Animal 举例。

                        +

                        在非动态加载情况下,我们可能会这样写:

                        +
                        public class Zoo {
                        private List<Animal> animals;

                        public void bark(String type) {
                        for (Animal animal : animals) {
                        if (type.equals(animal.name)) {
                        animal.bark();
                        }
                        }
                        }
                        }
                        -

                        事实上,logrotate -d /etc/logrotate.d/nginx 命令也会读取 /var/lib/logrotate/status,但是 other 对该目录也有 r 读取权限,所以没有报错。

                        -
                        $ logrotate -d /etc/logrotate.d/nginx 
                        WARNING: logrotate in debug mode does nothing except printing debug messages! Consider using verbose mode (-v) instead if this is not what you want.

                        reading config file /etc/logrotate.d/nginx
                        Reading state from file: /var/lib/logrotate/status
                        ...
                        +

                        在动态加载情况下,我们可能会这样写。在这种情况下,Zoo 没有合适的方式直接持有 Animal,而是通过 ExtensionLoader 间接地持有。

                        +
                        public class Zoo {
                        private ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);

                        public void bark(String type) {
                        Animal animal = extensionLoader.getExtension(type);
                        animal.bark();
                        }
                        }
                        -

                        日志文件夹的权限

                        即使你使用 root 身份运行 logrotate,你可能还会遇到以下报错

                        -
                        $ logrotate -f /etc/logrotate.d/nginx 
                        error: skipping "/path/to/your/nginx/logs/*.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.
                        +

                        我们更想要以下这种直接持有 Animal 的方式,在运行时 animal 可以是 Dog,也可以是 Cat,还可以是其他的动物。

                        +
                        public class Zoo {
                        private Animal animal;

                        public void bark(String type) {
                        animal.bark();
                        }
                        }
                        -

                        你需要在配置文件中,使用 su <user> <group> 指定日志所在文件夹所属的用户和组,logrotate 才能正确读写。

                        -

                        由宿主机还是容器主导

                        首先 Nginx 的日志文件夹通过挂载映射到宿主机,日志滚动既可以由宿主机主导,也可以由容器主导,不过不论如何我们都需要向 Docker 容器内的 Nginx 发送 USR1 信号。有人倾向于在容器内完成所有工作,和宿主机几乎完全隔离;我个人更青睐于由宿主机主导,因为容器内的环境并不总是拥有你想要使用的软件(除非你总是定制自己使用的镜像),甚至标准镜像往往非常精简。

                        -

                        logrotate 配置中的 postrotate 部分添加脚本,使用 docker exec 在容器内执行命令,完成向 Nginx 发送信号的工作。脚本的处理逻辑大概是“如果存在 /var/run/nginx.pid,就执行 kill -USR1 \`cat /var/run/nginx.pid\` 命令,并打印成功的消息”。但是我看到很多文章中分享的配置类似下面这样:

                        -
                        docker exec nginx sh -c "if [ -f /var/run/nginx.pid ]; then kill -USR1 $(docker exec nginx cat /var/run/nginx.pid); fi"

                        docker exec nginx sh -c "[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`;"
                        +

                        Dubbo 采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal 的自适应拓展,就像下面这样:

                        +
                        public class AdaptiveAnimal implements Animal {
                        public void bark(String type) {
                        if (type == null) {
                        throw new IllegalArgumentException("type == null");
                        }

                        Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
                        animal.bark();
                        }
                        }

                        Animal animal = new AdaptiveAnimal();
                        animal.bark(type);
                        -

                        经过测试都会有以下报错,我不清楚是否大多是抓取发布的文章,也不清楚他们是否测试过,对于 Shell 脚本写得不多的我来说,半夜测试反复排查错误真是头昏脑胀。在我原先的理解里,-c 后面的脚本是整体发到容器内部执行的,后来我才意识到,我对脚本内部的命令在宿主机还是容器里执行的理解是错误的。

                        -
                        cat: /var/run/nginx.pid: No such file or directory
                        +

                        当然,我们不希望需要手动地为每一个拓展编写 Adaptive 代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark 方法的参数列表中新增了 type 类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo 的另一个设计规避了这一缺点,Dubbo公共契约中提到:所有扩展点参数都包含 URL 参数,URL 作为上下文信息贯穿整个扩展点设计体系。因此围绕着 DubboURL 为中心的拓展体系,你很难设计出 Animal.bark(URL url) 这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo 并未完全抛弃手工编写自适应拓展的方式,而是予以保留。

                        +

                        手工编码的自适应拓展

                        在在 Dubbo 中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL 作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory 的自适应拓展,当你调用它的 getExtension 方法时,它就是将工作全权委托给 factory.getExtension(type, name) 完成的,而 factories 在创建 AdaptiveExtensionFactory 时就已经获取了。

                        +
                        @Adaptive
                        public class AdaptiveExtensionFactory implements ExtensionFactory {

                        private final List<ExtensionFactory> factories;

                        public AdaptiveExtensionFactory() {
                        // 获取 ExtensionFactory 的 ExtensionLoader
                        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
                        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
                        // 获取全部支持的(不包含自适应拓展)拓展名称,依次获取拓展加入 factories
                        for (String name : loader.getSupportedExtensions()) {
                        list.add(loader.getExtension(name));
                        }
                        factories = Collections.unmodifiableList(list);
                        }

                        @Override
                        public <T> T getExtension(Class<T> type, String name) {
                        for (ExtensionFactory factory : factories) {
                        // 委托给其他 ExtensionFactory 拓展获取,比如 SpiExtensionFactory
                        T extension = factory.getExtension(type, name);
                        if (extension != null) {
                        return extension;
                        }
                        }
                        return null;
                        }

                        }
                        -

                        继续写入旧日志文件

                        在使用 logrotate -f /etc/logrotate.d/nginx 测试通过后的第二天,发现虽然创建了新的日志文件,但是 Nginx 继续写入到旧的日志文件。这不同于网上很多文章提到的“没有发送 USR1 信号给 Nginx ”的情况。

                        -

                        尝试手动发送信号,观察效果。

                        -
                        docker exec nginx sh -c "kill -USR1 `docker exec nginx cat /var/run/nginx.pid`"
                        +

                        至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。

                        +

                        源码分析

                        Adaptive 注解

                        Adaptive 注解是一个与自适应拓展息息相关的注解,该定义如下:

                        +
                        @Documented
                        @Retention(RetentionPolicy.RUNTIME)
                        @Target({ElementType.TYPE, ElementType.METHOD})
                        public @interface Adaptive {
                        String[] value() default {};
                        }
                        -

                        发现虽然终止了继续写入到旧的文件,但是在宿主机读取日志时,提示没有权限。

                        -
                        $ cat access.log
                        cat: access.log: Permission denied
                        +

                        根据 Target 注解的 value 可知,Adaptive 注解可标注在类或者方法上。当 Adaptive 注解标注在类上时,Dubbo 不会为该类生成代理类。当 Adaptive 注解标注在接口方法上时,Dubbo 则会为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解标注,分别是 AdaptiveCompilerAdaptiveExtensionFactory。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive 注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。

                        +

                        获取自适应拓展

                        获取自适应拓展的入口方法是 getAdaptiveExtension,使用 getOrCreate 的模式获取。

                        +
                        public T getAdaptiveExtension() {
                        // 双重检查
                        // 从缓存中获取自适应拓展
                        Object instance = cachedAdaptiveInstance.get();
                        if (instance == null) {
                        // 创建自适应拓展失败的结果也会被缓存,避免重复尝试
                        if (createAdaptiveInstanceError != null) {
                        throw new IllegalStateException("Failed to create adaptive instance: " +
                        createAdaptiveInstanceError.toString(),
                        createAdaptiveInstanceError);
                        }

                        synchronized (cachedAdaptiveInstance) {
                        instance = cachedAdaptiveInstance.get();
                        if (instance == null) {
                        try {
                        // 创建自适应拓展
                        instance = createAdaptiveExtension();
                        // 将自适应拓展设置到缓存中
                        cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                        }
                        }
                        }
                        }

                        return (T) instance;
                        }
                        -

                        查看日志文件权限,发现不对劲。新建的 access.log 的权限为 600,所属用户从 moralok 变为 systemd-resolve,失去了只有所属用户才拥有的 rw 权限。

                        +

                        创建自适应拓展

                        当缓存为空时,就会通过 createAdaptiveExtension 方法创建。方法包含以下三个处理逻辑:

                        +
                          +
                        1. 调用 getAdaptiveExtensionClass 方法获取自适应拓展的 Class 对象。
                        2. +
                        3. 通过反射进行实例化。
                        4. +
                        5. 调用 injectExtension 方法对拓展实例进行依赖注入。
                        6. +
                        -

                        此时虽然注意到没有成功创建新的 error.log,但是只是以为对于空的日志文件不滚动。并且在后续重现问题时发现此时其实可以在容器里看到日志开始写入新的日志文件。

                        +

                        手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展

                        -
                        $ ll
                        total 136
                        drwxrwxr-x 2 moralok moralok 4096 Dec 3 00:00 ./
                        drwxrwxr-x 4 moralok moralok 4096 Nov 30 17:25 ../
                        -rw------- 1 moralok moralok 0 Dec 3 00:00 access.log
                        -rw-r--r-- 1 systemd-resolve root 109200 Dec 3 07:01 access.log-20231203
                        -rw-r--r-- 1 systemd-resolve root 0 Dec 2 19:07 error.log
                        +
                        private T createAdaptiveExtension() {
                        try {
                        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
                        } catch (Exception e) {
                        throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
                        }
                        }
                        -

                        这个时候我的想法是,既然成功创建了新的日志文件,肯定是 Nginx 接收到了 USR1 信号。怎么会出现“crontab 触发时发送信号有问题,手动发送却没问题”的情况呢?难道是两者触发的执行方式有所不同?还是说宿主机创建的文件会有问题?注意到新的日志文件所属的用户和组和原日志文件所属的用户和组不同,我开始怀疑创建文件的过程有问题。在反复测试尝试重现问题后,我把关注点放到了 create 配置上。其实在最开始,我就关注了它,我想既然在默认的配置文件中已经设置而且我也不修改,那么就不在 /etc/logrotate.d/nginx 添加了。我甚至花了很多时间浏览文档,确认它在缺省后面的权限属性时会使用原日志文件的权限属性。当时我还专门记录了一个疑问,“文档说在运行 postrotate 脚本前创建新文件,可是在测试验证时,新文件是 Nginx 接收 USR1 信号后重新打开文件时创建的,在脚本执行报错或者脚本中并不发送信号时,不会产生新文件”。现在想来,都是坑,坑里注定要灌满眼泪!

                        -

                        /etc/logrotate.d/nginx 添加 create 后,成功重现问题。

                        -
                        ...
                        renaming /path/to/your/nginx/logs/access.log to /path/to/your/nginx/logs/access.log-20231203
                        creating new /path/to/your/nginx/logs/access.log mode = 0644 uid = 101 gid = 0
                        error: error setting owner of /path/to/your/nginx/logs/access.log to uid 101 and gid 0: Operation not permitted
                        switching euid to 0 and egid to 0
                        +

                        获取自适应拓展类

                        获取自适应拓展类的 getAdaptiveExtensionClass 方法包含以下三个处理逻辑:

                        +
                          +
                        1. 通过 getExtensionClasses 方法获取所有拓展类。
                        2. +
                        3. 检查缓存 cachedAdaptiveClass,如果不为 null,则返回缓存。
                        4. +
                        5. 如果缓存为 null,则调用 createAdaptiveExtensionClass 创建自适应拓展类(代理类)。
                        6. +
                        +

                        Dubbo SPI 的工作原理中我们分析过 getExtensionClasses 方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive 注解标注了,那么该类就会被赋值给 cachedAdaptiveClass 变量。“原理”部分介绍的 AdaptiveExtensionFactory 就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive 注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。

                        +
                        private Class<?> getAdaptiveExtensionClass() {
                        getExtensionClasses();
                        if (cachedAdaptiveClass != null) {
                        return cachedAdaptiveClass;
                        }
                        return cachedAdaptiveClass = createAdaptiveExtensionClass();
                        }
                        -

                        可见 logrotate -f /etc/logrotate.d/nginx,并没有使用到默认配置 /etc/logrotate.conf,而 crontab 触发 logrotate 时使用到了。修改为 create 0644 moralok moralok,成功解决问题。可以确认 create 在缺省权限属性的时候,如果日志文件因为挂载到容器中而被修改了所属用户,logrotate 按照原文件的权限属性创建新文件时会报错,从而导致脚本不能正常执行,Nginx 不会收到信号,error.log 也不会继续滚动。按此原因推理,在配置文件中,加入 nocreate 也可以解决问题,并且更加符合官方文档建议的流程。

                        -

                        枯坐一下午,百思不得其解,想到抓狂。不得不说真的很讨厌这类问题,特定条件下奇怪的问题,食之无味,弃之还不行!如果照着网上的文章,一开始就添加配置真的不会遇到啊。可是不喜欢不明不白地修改配置来解决问题,也不喜欢一次性加很多设置却不知道各个配置的功能,特别是在我的理解里这个默认配置似乎没问题的情况下。明明想要克制住不知重点还不断深入探索细节的坏习惯,却还是被一个 Bug 带着花费了大量的时间和精力,解决了一个照着抄就不会遇到的问题。虽然真的有收获,真的解决了前一晚留下的疑问,可是不甘心啊,气气气!而且为什么这样会有问题,我还是不懂!

                        +

                        到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用 getOrCreate 的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。

                        -

                        logrotate 备忘

                        帮助文档

                        -

                        命令参数

                        -d, --debug : 打开调试模式,这意味着不会对日志进行任何更改,并且 logrotate 状态文件不会更新。仅打印调试消息。
                        -f, --force : 告诉 logrotate 强制滚动,即使它认为这没有必要。有时,在将新条目添加到 logrotate 配置文件后,或者如果已手动删除旧日志文件,这会很有用,因为将创建新文件,并且正确地继续记录日志。
                        -m, --mail <command> : 告诉 logrotate 在邮寄日志时使用哪个命令。此命令应接受两个参数:消息的主题,收件人。然后,该命令必须读取标准输入上的消息并将其邮寄给收件人。默认邮件命令是 /bin/mail -s。
                        -s, --state <statefile> : 告诉 logrotate 使用备用状态文件。如果 logrotate 以不同用户身份运行不同的日志文件集,这会非常有用。默认状态文件是 /var/lib/logrotate/status。
                        --usage : 打印简短的用法信息。
                        --?, --help : 打印帮助信息。
                        -v, --verbose : 打开详细模式,例如在滚动期间显示消息。
                        +

                        生成自适应拓展类

                        生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。

                        +
                          +
                        1. 通过拼接字符串得到代理类的源码。
                        2. +
                        3. 使用编译器编译得到 Class 对象。
                        4. +
                        +
                        +

                        在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。

                        +
                        +
                        private Class<?> createAdaptiveExtensionClass() {
                        // 区别于旧版本:新版本抽象出一个 AdaptiveClassCodeGenerator 用于生成代码
                        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
                        ClassLoader classLoader = findClassLoader();
                        // 获取编译器拓展
                        org.apache.dubbo.common.compiler.Compiler compiler =
                        ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
                        // 编译代码,生成 Class 对象
                        return compiler.compile(code, classLoader);
                        }
                        -

                        常用配置文件参数

                        - - - - - - - - - - - - - - - - +

                        为了更直观地了解代码生成的效果及其实现的功能,以 Protocol 为例,生成的完整代码(已经经过格式化)展示如下

                        +
                        参数说明
                        daily周期:每天
                        weekly周期:每周
                        monthly
                        package org.apache.dubbo.rpc;

                        import org.apache.dubbo.common.extension.ExtensionLoader;

                        public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
                        public void destroy() {
                        throw new UnsupportedOperationException(
                        "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                        }

                        public int getDefaultPort() {
                        throw new UnsupportedOperationException(
                        "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                        }

                        public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1)
                        throws org.apache.dubbo.rpc.RpcException {
                        if (arg1 == null)
                        throw new IllegalArgumentException("url == null");
                        org.apache.dubbo.common.URL url = arg1;
                        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
                        if (extName == null)
                        throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
                        + url.toString() + ") use keys([protocol])");
                        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
                        .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                        return extension.refer(arg0, arg1);
                        }

                        public java.util.List getServers() {
                        throw new UnsupportedOperationException(
                        "The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                        }

                        public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0)
                        throws org.apache.dubbo.rpc.RpcException {
                        if (arg0 == null)
                        throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
                        if (arg0.getUrl() == null)
                        throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
                        org.apache.dubbo.common.URL url = arg0.getUrl();
                        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
                        if (extName == null)
                        throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
                        + url.toString() + ") use keys([protocol])");
                        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
                        .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                        return extension.export(arg0);
                        }
                        }
                        + +

                        生成的代理类需完成以下功能:

                        +
                          +
                        1. adaptive 方法,直接抛出异常。
                        2. +
                        3. adaptive 方法:
                            +
                          • 准备工作:在参数判空校验之后,从中获取到 URL 对象,结合 URL 对象和默认拓展名得到最终的拓展名 extName
                          • +
                          • 核心功能:先获取拓展的 ExtensionLoader,再根据拓展名 extName 获取拓展,最后调用拓展的同名方法。
                          • +
                          +
                        4. +
                        +

                        以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo 会直接或间接地从参数列表中查找 URL 对象,所谓直接就是 URL 对象直接在参数列表中,所谓间接就是 URL 对象是其中一个参数的属性。在得到 URL 对象后,Dubbo 会尝试以 Adaptive 注解的 valuekey,从 URL 中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName。实际的实现更加复杂,需要耐心阅读和测试。

                        +

                        自适应拓展类代码生成器

                        新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 typedefaultExtName,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。

                        +
                        public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
                        this.type = type;
                        this.defaultExtName = defaultExtName;
                        }

                        public String generate() {
                        // 检测是否至少存在一个方法标注了 Adaptive 注解
                        if (!hasAdaptiveMethod()) {
                        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
                        }

                        // 区别于旧版本:抽象为几个命名含义清晰的方法,提升了可读性
                        // 生成类:包名、导入、类声明
                        StringBuilder code = new StringBuilder();
                        code.append(generatePackageInfo());
                        code.append(generateImports());
                        code.append(generateClassDeclaration());

                        // 生成方法
                        Method[] methods = type.getMethods();
                        for (Method method : methods) {
                        code.append(generateMethod(method));
                        }
                        code.append("}");

                        if (logger.isDebugEnabled()) {
                        logger.debug(code.toString());
                        }
                        return code.toString();
                        }
                        + +

                        检测 Adaptive 注解

                        在生成代理类源码之前,generate 方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive 注解,若不满足,就会抛出异常。

                        +
                        +

                        流式编程使用得当的话很有可读性啊。

                        +
                        +
                        private boolean hasAdaptiveMethod() {
                        return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
                        }
                        + +

                        生成类

                        生成代理类源码的顺序和普通 Java 类文件中内容的顺序一致:

                        +
                          +
                        • package
                        • +
                        • import
                        • +
                        • 类声明
                        • +
                        +

                        先忽略“生成方法”的部分,以 DubboProtocol 拓展为例,生成的代码如下:

                        +
                        package org.apache.dubbo.rpc;
                        import org.apache.dubbo.common.extension.ExtensionLoader;
                        public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
                        // 省略方法代码
                        }
                        + +

                        生成方法

                        生成方法的过程同样被抽象为几个命名含义清晰的方法,包含以下五个部分:

                        +
                          +
                        • 返回值
                        • +
                        • 方法名
                        • +
                        • 方法内容
                        • +
                        • 方法参数
                        • +
                        • 方法抛出的异常
                        • +
                        +
                        private String generateMethod(Method method) {
                        String methodReturnType = method.getReturnType().getCanonicalName();
                        String methodName = method.getName();
                        String methodContent = generateMethodContent(method);
                        String methodArgs = generateMethodArguments(method);
                        String methodThrows = generateMethodThrows(method);
                        return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
                        }
                        + +

                        除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive 注解标注。

                        +
                        private String generateMethodContent(Method method) {
                        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
                        StringBuilder code = new StringBuilder(512);
                        // 检测方法是否被 Adaptive 注解标注
                        if (adaptiveAnnotation == null) {
                        return generateUnsupported(method);
                        } else {
                        // ...
                        }
                        return code.toString();
                        }
                        + +
                        无 Adaptive 注解标注的方法

                        对于无 Adaptive 注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。

                        +
                        private String generateUnsupported(Method method) {
                        return String.format(CODE_UNSUPPORTED, method, type.getName());
                        }
                        + +

                        Protocol 接口的 destroy 方法为例,生成的内容如下

                        +
                        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
                        + +
                        有 Adaptive 注解标注的方法

                        对于有 Adaptive 注解标注的方法,

                        +
                        // 查找 URL 类型的参数
                        int urlTypeIndex = getUrlTypeIndex(method);
                        if (urlTypeIndex != -1) {
                        // 生成 URL 判空检查和赋值的代码
                        code.append(generateUrlNullCheck(urlTypeIndex));
                        } else {
                        // 如果参数中没有直接出现 URL 类型,生成间接情况下的 URL 判空和赋值代码
                        code.append(generateUrlAssignmentIndirectly(method));
                        }
                        // 获取方法的 Adaptive 注解的 value
                        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
                        // 检测是否有 Invocation 类型的参数
                        boolean hasInvocation = hasInvocationArgument(method);
                        // 生成 Invocation 判空检查代码
                        code.append(generateInvocationArgumentNullCheck(method));
                        // 生成拓展名赋值代码
                        code.append(generateExtNameAssignment(value, hasInvocation));
                        // 生成拓展名判空检查代码
                        code.append(generateExtNameNullCheck(value));
                        // 生成获取拓展和赋值代码
                        code.append(generateExtensionAssignment());
                        // 生成调用和返回代码
                        code.append(generateReturnAndInvocation(method));
                        + +
                        查找 URL 类型的参数

                        直接从方法的参数类型列表中查找第一个 URL 类型的参数,返回其索引。

                        +
                        private int getUrlTypeIndex(Method method) {
                        int urlTypeIndex = -1;
                        // 遍历方法的参数类型列表
                        Class<?>[] pts = method.getParameterTypes();
                        for (int i = 0; i < pts.length; ++i) {
                        // 查找第一个 URL 类型的参数
                        if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                        }
                        }
                        return urlTypeIndex;
                        }

                        // 生成 URL 参数判空检查和赋值代码
                        private String generateUrlNullCheck(int index) {
                        return String.format(CODE_URL_NULL_CHECK, index, URL.class.getName(), index);
                        }
                        + +

                        间接从方法的参数类型列表中,查找 URL 类型的参数,并生成判空检查和赋值代码。

                        +
                        private String generateUrlAssignmentIndirectly(Method method) {
                        Class<?>[] pts = method.getParameterTypes();

                        Map<String, Integer> getterReturnUrl = new HashMap<>();
                        // 遍历方法的参数类型列表
                        for (int i = 0; i < pts.length; ++i) {
                        // 遍历某一个参数类型的全部方法,查找可以返回 URL 类型的 “getter” 方法
                        for (Method m : pts[i].getMethods()) {
                        String name = m.getName();
                        // 1. 方法名以 get 开头,或者方法名大于 3 个字符
                        // 2. 方法的访问权限为 public
                        // 3. 非静态方法
                        // 4. 方法参数数量为 0
                        // 5. 方法返回值类型为 URL
                        if ((name.startsWith("get") || name.length() > 3)
                        && Modifier.isPublic(m.getModifiers())
                        && !Modifier.isStatic(m.getModifiers())
                        && m.getParameterTypes().length == 0
                        && m.getReturnType() == URL.class) {
                        // 保存方法名->索引的映射
                        getterReturnUrl.put(name, i);
                        }
                        }
                        }

                        if (getterReturnUrl.size() <= 0) {
                        // 如果没有找到 “getter” 方法,抛出异常
                        throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
                        + ": not found url parameter or url attribute in parameters of method " + method.getName());
                        }

                        // 优先选择方法名为 getUrl 的方法,如果没有则选第一个
                        Integer index = getterReturnUrl.get("getUrl");
                        if (index != null) {
                        return generateGetUrlNullCheck(index, pts[index], "getUrl");
                        } else {
                        Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
                        return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
                        }
                        }

                        // 生成 URL 参数判空检查和赋值代码
                        private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
                        StringBuilder code = new StringBuilder();
                        code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");\n",
                        index, type.getName()));
                        code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");\n",
                        index, method, type.getName(), method));

                        code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
                        return code.toString();
                        }
                        + +

                        Protocolreferexport 方法为例,生成的内容如下

                        +
                        // refer
                        if (arg1 == null) throw new IllegalArgumentException("url == null");
                        org.apache.dubbo.common.URL url = arg1;
                        // export
                        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
                        if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
                        org.apache.dubbo.common.URL url = arg0.getUrl();
                        + +
                        获取 Adaptive 注解的 value
                        private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
                        String[] value = adaptiveAnnotation.value();
                        // 如果 value 为空,使用类名生成 value
                        // 效果:LoadBalance -> load.balance
                        if (value.length == 0) {
                        String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
                        value = new String[]{splitName};
                        }
                        return value;
                        }
                        + +
                        检测 Invocation 类型的参数

                        检测是否有 Invocation 类型的参数,并生成判空检查代码和赋值代码。从 Invocation 可以获得 methodName

                        +
                        private boolean hasInvocationArgument(Method method) {
                        Class<?>[] pts = method.getParameterTypes();
                        return Arrays.stream(pts).anyMatch(p -> CLASSNAME_INVOCATION.equals(p.getName()));
                        }

                        private String generateInvocationArgumentNullCheck(Method method) {
                        Class<?>[] pts = method.getParameterTypes();
                        return IntStream.range(0, pts.length).filter(i -> CLASSNAME_INVOCATION.equals(pts[i].getName()))
                        .mapToObj(i -> String.format(CODE_INVOCATION_ARGUMENT_NULL_CHECK, i, i))
                        .findFirst().orElse("");
                        }
                        + +

                        LoadBalanceselect 方法为例,生成的内容如下:

                        +
                        if (arg2 == null) throw new IllegalArgumentException("invocation == null");
                        String methodName = arg2.getMethodName();
                        + +
                        获取拓展名

                        本方法用于根据 SPIAdaptive 注解的 value 生成“获取拓展名”的代码,同时生成逻辑还受 Invocation 影响,因此相对复杂。总结的规则如下:

                        +
                          +
                        1. 正常情况下,使用 url.getParameter(value[i]) 获取
                        2. +
                        3. 如果默认拓展名非空,使用 url.getParameter(value[i], defaultExtName) 获取
                        4. +
                        5. 如果存在 Invocation,不论默认拓展名是否为空,总是使用 url.getMethodParameter(methodName, value[i], defaultExtName) 获取
                        6. +
                        7. 因为 protocol 是 url 的一部分,所以可以直接通过 getProtocol 获取。是否使用默认拓展名的方式就退化为原始的三元表达式。
                        8. +
                        +
                        private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
                        // TODO: refactor it
                        String getNameCode = null;
                        // 逆序遍历 value(Adaptive 的 value)
                        for (int i = value.length - 1; i >= 0; --i) {
                        // 当 i 为最后一个元素的索引(因为是逆序遍历,第一轮就进入本分支)
                        if (i == value.length - 1) {
                        // 默认拓展名非空
                        if (null != defaultExtName) {
                        // protocol 是 url 的一部分,可以通过 getProtocol 方法获取,其他的则必须从 URL 参数中获取
                        if (!"protocol".equals(value[i])) {
                        if (hasInvocation) {
                        // 如果有 Invocation,则使用 url.getMethodParameter 获取
                        // url.getMethodParameter(methodName, value[i], defaultExtName)
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        } else {
                        // url.getParameter(value[i], defaultExtName)
                        getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                        }
                        } else {
                        // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                        getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                        }
                        // 默认拓展名为空
                        } else {
                        if (!"protocol".equals(value[i])) {
                        if (hasInvocation) {
                        // url.getMethodParameter(methodName, value[i], defaultExtName)
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        } else {
                        // url.getParameter(value[i])
                        getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                        }
                        } else {
                        // url.getProtocol()
                        getNameCode = "url.getProtocol()";
                        }
                        }
                        } else {
                        if (!"protocol".equals(value[i])) {
                        if (hasInvocation) {
                        // url.getMethodParameter(methodName, value[i], defaultExtName)
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        } else {
                        // url.getParameter(value[i], getNameCode)
                        getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                        }
                        } else {
                        // url.getProtocol() == null ? "dubbo" : url.getProtocol()
                        getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                        }
                        }
                        }
                        return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
                        }
                        + +
                        加载拓展
                        private String generateExtensionAssignment() {
                        return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                        }
                        + +

                        Protocol 接口的 refer 方法为例,生成的内容如下:

                        +
                        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
                        + +
                        调用与返回

                        生成方法调用语句,如有必要,返回结果。

                        +
                        private String generateReturnAndInvocation(Method method) {
                        String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

                        String args = IntStream.range(0, method.getParameters().length)
                        .mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
                        .collect(Collectors.joining(", "));

                        return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
                        }
                        + +

                        Protocol 接口的 refer 方法为例,生成的内容如下:

                        +
                        return extension.refer(arg0, arg1);
                        + +
                        +

                        新版本通过提炼方法、使用流式编程和使用 String.format() 代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。

                        +
                        +

                        参考文章

                        +]]> + + java + dubbo + spi + + + + 使用 logrotate 滚动 Docker 容器内的 Nginx 的日志 + /2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/ + Nginx 没有提供开箱即用的日志滚动功能,而是将其交给使用者自己实现。你既可以按照官方文档的建议通过编写脚本实现,也可以使用 logrotate 管理日志。但是和在普通场景下不同,在使用 Docker 运行 Nginx 时,你可能需要额外考虑一点细节。本文记录了在为 Docker 中的 Nginx 的日志文件配置滚动功能过程中遇到的一些问题和思考。

                        + + +

                        Nginx 滚动日志

                        官方文档

                        +

                        In order to rotate log files, they need to be renamed first. After that USR1 signal should be sent to the master process. The master process will then re-open all currently open log files and assign them an unprivileged user under which the worker processes are running, as an owner. After successful re-opening, the master process closes all open files and sends the message to worker process to ask them to re-open files. Worker processes also open new files and close old files right away. As a result, old files are almost immediately available for post processing, such as compression.

                        +
                        +

                        根据官方文档的解释,滚动日志文件的流程应如下,你可以自己编写 Shell 脚本配合 crontab 实现定时滚动功能。

                        +
                          +
                        1. 首先重命名日志文件
                        2. +
                        3. 之后发送 USR1 信号给 Nginx 主进程,Nginx 将重新打开日志文件
                        4. +
                        5. 对日志文件进行后处理,比如压缩(可选)
                        6. +
                        +
                        $ mv access.log access.log.0
                        $ kill -USR1 `cat master.nginx.pid`
                        $ sleep 1
                        $ gzip access.log.0 # do something with access.log.0
                        + +

                        说明

                          +
                        1. 在没有执行 kill 命令前,即便已经重命名了日志文件,Nginx 还是会向重命名后的文件写入日志。因为在 Linux 系统中,系统内核是根据文件描述符定位文件的。
                        2. +
                        3. USR1 是自定义信号,软件的作者自己确定收到该信号后做什么。在 Nginx 中,主进程收到信号后,会重新打开所有当前打开的日志文件并将它们分配给一个非特权用户作为所有者,工作进程就是在该所有者下运行的。成功重新打开后,主进程关闭所有打开的文件并向工作进程发送消息,要求它们重新打开文件。工作进程也打开新文件并立即关闭旧文件。
                        4. +
                        +

                        使用 logrotate

                        +

                        logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.

                        +
                        +

                        logrotate 旨在简化生成大量日志文件的系统的管理。它允许自动滚动、压缩、删除和邮寄日志文件。每个日志文件可以在每天、每周、每月或当它变得太大时处理。Linux 一般默认安装了 logrotate

                        +

                        默认配置文件

                        查看默认配置文件:cat /etc/logrotate.conf

                        +
                        # see "man logrotate" for details
                        # 每周滚动日志文件
                        weekly

                        # 默认使用 adm group,因为这是 /var/log/syslog 的所属组
                        su root adm

                        # 保留 4 周的备份(其实是保留 4 个备份,对应 weekly 的设置,就是保留 4 周)
                        rotate 4

                        # 在滚动旧日志文件后,创建新的空日志文件
                        create

                        # 使用日期作为滚动日志文件的后缀
                        #dateext

                        # 如果你希望压缩日志文件,请取消注释
                        #compress

                        # 软件包将日志滚动的配置信息放入此目录中
                        include /etc/logrotate.d

                        # system-specific logs may be also be configured here.
                        + +

                        配置信息所在目录

                        查看日志滚动的配置信息所在的目录:ls /etc/logrotate.d/

                        +
                        alternatives  apport  apt  bootlog  btmp  dpkg  rsyslog  ubuntu-advantage-tools  ufw  unattended-upgrades  wtmp
                        + +

                        为 Nginx 新增配置

                        Nginx 新增日志滚动配置,vim /etc/logrotate.d/nginx

                        +
                        /path/to/your/nginx/logs/*.log {
                        # 切换用户
                        su moralok moralok
                        # 每天滚动日志文件
                        daily
                        # 使用日期作为滚动日志文件的后缀
                        dateext
                        # 如果日志丢失,不报错继续滚动下一个日志
                        missingok
                        # 保留 31 个备份
                        rotate 31
                        # 不压缩
                        nocompress
                        # 整个日志组运行一次的脚本
                        sharedscripts
                        # 滚动后的处理
                        postrotate
                        # 重新打开日志文件
                        docker exec nginx sh -c "[ ! -f /var/run/nginx.pid ] || (kill -USR1 `docker exec nginx cat /var/run/nginx.pid`; echo 'Successfully rotating nginx logs.')"
                        endscript
                        }
                        + +

                        验证配置和测试

                        测试配置文件是否有错误,logrotate -d /etc/logrotate.d/nginx

                        +

                        强制滚动:logrotate -f /etc/logrotate.d/nginx

                        +

                        一些坑

                        /var/lib/logrotate/ 权限问题

                        当你使用校验过配置文件的正确性后,尝试强制滚动时,可能会遇到报错。

                        +
                        $ logrotate -f /etc/logrotate.d/nginx 
                        error: error creating output file /var/lib/logrotate/status.tmp: Permission denied
                        + +

                        这是因为 logrotate 会在 /var/lib/logrotate/ 目录下创建 status 文件。查看目录权限可知,需要以 root 身份运行 logrotate

                        +
                        $ ll /var/lib/logrotate
                        total 12
                        drwxr-xr-x 2 root root 4096 Dec 2 08:35 ./
                        drwxr-xr-x 44 root root 4096 Jun 26 09:02 ../
                        -rw-r--r-- 1 root root 1395 Dec 2 08:35 status
                        + +

                        事实上,logrotate -d /etc/logrotate.d/nginx 命令也会读取 /var/lib/logrotate/status,但是 other 对该目录也有 r 读取权限,所以没有报错。

                        +
                        $ logrotate -d /etc/logrotate.d/nginx 
                        WARNING: logrotate in debug mode does nothing except printing debug messages! Consider using verbose mode (-v) instead if this is not what you want.

                        reading config file /etc/logrotate.d/nginx
                        Reading state from file: /var/lib/logrotate/status
                        ...
                        + +

                        日志文件夹的权限

                        即使你使用 root 身份运行 logrotate,你可能还会遇到以下报错

                        +
                        $ logrotate -f /etc/logrotate.d/nginx 
                        error: skipping "/path/to/your/nginx/logs/*.log" because parent directory has insecure permissions (It's world writable or writable by group which is not "root") Set "su" directive in config file to tell logrotate which user/group should be used for rotation.
                        + +

                        你需要在配置文件中,使用 su <user> <group> 指定日志所在文件夹所属的用户和组,logrotate 才能正确读写。

                        +

                        由宿主机还是容器主导

                        首先 Nginx 的日志文件夹通过挂载映射到宿主机,日志滚动既可以由宿主机主导,也可以由容器主导,不过不论如何我们都需要向 Docker 容器内的 Nginx 发送 USR1 信号。有人倾向于在容器内完成所有工作,和宿主机几乎完全隔离;我个人更青睐于由宿主机主导,因为容器内的环境并不总是拥有你想要使用的软件(除非你总是定制自己使用的镜像),甚至标准镜像往往非常精简。

                        +

                        logrotate 配置中的 postrotate 部分添加脚本,使用 docker exec 在容器内执行命令,完成向 Nginx 发送信号的工作。脚本的处理逻辑大概是“如果存在 /var/run/nginx.pid,就执行 kill -USR1 \`cat /var/run/nginx.pid\` 命令,并打印成功的消息”。但是我看到很多文章中分享的配置类似下面这样:

                        +
                        docker exec nginx sh -c "if [ -f /var/run/nginx.pid ]; then kill -USR1 $(docker exec nginx cat /var/run/nginx.pid); fi"

                        docker exec nginx sh -c "[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`;"
                        + +

                        经过测试都会有以下报错,我不清楚是否大多是抓取发布的文章,也不清楚他们是否测试过,对于 Shell 脚本写得不多的我来说,半夜测试反复排查错误真是头昏脑胀。在我原先的理解里,-c 后面的脚本是整体发到容器内部执行的,后来我才意识到,我对脚本内部的命令在宿主机还是容器里执行的理解是错误的。

                        +
                        cat: /var/run/nginx.pid: No such file or directory
                        + +

                        继续写入旧日志文件

                        在使用 logrotate -f /etc/logrotate.d/nginx 测试通过后的第二天,发现虽然创建了新的日志文件,但是 Nginx 继续写入到旧的日志文件。这不同于网上很多文章提到的“没有发送 USR1 信号给 Nginx ”的情况。

                        +

                        尝试手动发送信号,观察效果。

                        +
                        docker exec nginx sh -c "kill -USR1 `docker exec nginx cat /var/run/nginx.pid`"
                        + +

                        发现虽然终止了继续写入到旧的文件,但是在宿主机读取日志时,提示没有权限。

                        +
                        $ cat access.log
                        cat: access.log: Permission denied
                        + +

                        查看日志文件权限,发现不对劲。新建的 access.log 的权限为 600,所属用户从 moralok 变为 systemd-resolve,失去了只有所属用户才拥有的 rw 权限。

                        +
                        +

                        此时虽然注意到没有成功创建新的 error.log,但是只是以为对于空的日志文件不滚动。并且在后续重现问题时发现此时其实可以在容器里看到日志开始写入新的日志文件。

                        +
                        +
                        $ ll
                        total 136
                        drwxrwxr-x 2 moralok moralok 4096 Dec 3 00:00 ./
                        drwxrwxr-x 4 moralok moralok 4096 Nov 30 17:25 ../
                        -rw------- 1 moralok moralok 0 Dec 3 00:00 access.log
                        -rw-r--r-- 1 systemd-resolve root 109200 Dec 3 07:01 access.log-20231203
                        -rw-r--r-- 1 systemd-resolve root 0 Dec 2 19:07 error.log
                        + +

                        这个时候我的想法是,既然成功创建了新的日志文件,肯定是 Nginx 接收到了 USR1 信号。怎么会出现“crontab 触发时发送信号有问题,手动发送却没问题”的情况呢?难道是两者触发的执行方式有所不同?还是说宿主机创建的文件会有问题?注意到新的日志文件所属的用户和组和原日志文件所属的用户和组不同,我开始怀疑创建文件的过程有问题。在反复测试尝试重现问题后,我把关注点放到了 create 配置上。其实在最开始,我就关注了它,我想既然在默认的配置文件中已经设置而且我也不修改,那么就不在 /etc/logrotate.d/nginx 添加了。我甚至花了很多时间浏览文档,确认它在缺省后面的权限属性时会使用原日志文件的权限属性。当时我还专门记录了一个疑问,“文档说在运行 postrotate 脚本前创建新文件,可是在测试验证时,新文件是 Nginx 接收 USR1 信号后重新打开文件时创建的,在脚本执行报错或者脚本中并不发送信号时,不会产生新文件”。现在想来,都是坑,坑里注定要灌满眼泪!

                        +

                        /etc/logrotate.d/nginx 添加 create 后,成功重现问题。

                        +
                        ...
                        renaming /path/to/your/nginx/logs/access.log to /path/to/your/nginx/logs/access.log-20231203
                        creating new /path/to/your/nginx/logs/access.log mode = 0644 uid = 101 gid = 0
                        error: error setting owner of /path/to/your/nginx/logs/access.log to uid 101 and gid 0: Operation not permitted
                        switching euid to 0 and egid to 0
                        + +

                        可见 logrotate -f /etc/logrotate.d/nginx,并没有使用到默认配置 /etc/logrotate.conf,而 crontab 触发 logrotate 时使用到了。修改为 create 0644 moralok moralok,成功解决问题。可以确认 create 在缺省权限属性的时候,如果日志文件因为挂载到容器中而被修改了所属用户,logrotate 按照原文件的权限属性创建新文件时会报错,从而导致脚本不能正常执行,Nginx 不会收到信号,error.log 也不会继续滚动。按此原因推理,在配置文件中,加入 nocreate 也可以解决问题,并且更加符合官方文档建议的流程。

                        +
                        +

                        枯坐一下午,百思不得其解,想到抓狂。不得不说真的很讨厌这类问题,特定条件下奇怪的问题,食之无味,弃之还不行!如果照着网上的文章,一开始就添加配置真的不会遇到啊。可是不喜欢不明不白地修改配置来解决问题,也不喜欢一次性加很多设置却不知道各个配置的功能,特别是在我的理解里这个默认配置似乎没问题的情况下。明明想要克制住不知重点还不断深入探索细节的坏习惯,却还是被一个 Bug 带着花费了大量的时间和精力,解决了一个照着抄就不会遇到的问题。虽然真的有收获,真的解决了前一晚留下的疑问,可是不甘心啊,气气气!而且为什么这样会有问题,我还是不懂!

                        +
                        +

                        logrotate 备忘

                        帮助文档

                        +

                        命令参数

                        -d, --debug : 打开调试模式,这意味着不会对日志进行任何更改,并且 logrotate 状态文件不会更新。仅打印调试消息。
                        -f, --force : 告诉 logrotate 强制滚动,即使它认为这没有必要。有时,在将新条目添加到 logrotate 配置文件后,或者如果已手动删除旧日志文件,这会很有用,因为将创建新文件,并且正确地继续记录日志。
                        -m, --mail <command> : 告诉 logrotate 在邮寄日志时使用哪个命令。此命令应接受两个参数:消息的主题,收件人。然后,该命令必须读取标准输入上的消息并将其邮寄给收件人。默认邮件命令是 /bin/mail -s。
                        -s, --state <statefile> : 告诉 logrotate 使用备用状态文件。如果 logrotate 以不同用户身份运行不同的日志文件集,这会非常有用。默认状态文件是 /var/lib/logrotate/status。
                        --usage : 打印简短的用法信息。
                        --?, --help : 打印帮助信息。
                        -v, --verbose : 打开详细模式,例如在滚动期间显示消息。
                        + +

                        常用配置文件参数

                        + + + + + + + + + + + + + + + + @@ -4340,153 +4480,36 @@ - Dubbo SPI 的工作原理 - /2023/11/28/how-does-Dubbo-SPI-works/ - SPI 作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI 被广泛应用于各类技术框架中,例如 JDBC 驱动、SpringDubbo 等等。Dubbo 并未使用原生的 Java SPI,而是重新实现了一套更加强大的 Dubbo SPI。本文将简单介绍 SPI 的设计理念,通过示例带你体会 SPI 的作用,通过 Dubbo 获取拓展的流程图源码分析带你理解 Dubbo SPI 的工作原理。深入了解 Dubbo SPI,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。

                        + Spring 中 @PropertySource 注解的使用和源码分析 + /2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/ + @PropertySource 注解提供了一种方便的声明性机制,用于将 PropertySource 添加到 Spring 容器的 Environment 环境中。该注解通常搭配 @Configuration 注解一起使用。本文将介绍如何使用 @PropertySource 注解,并通过分析源码解释外部配置文件是如何被解析进入 SpringEnvironment 中。

                        -

                        SPI 简介

                        SPI 的全称是 Service Provider Interface,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI 机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI 具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 JavaJDBC 驱动为例,JDK 提供了 java.sql.Driver 接口,各个数据库厂商,例如 MySQLOracle 提供具体的实现。

                        - - -

                        目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:

                        -
                          -
                        • Java SPI:META-INF/services/full.qualified.interface.name
                        • -
                        • Dubbo SPI:META-INF/dubbo/full.qualified.interface.name(还有其他目录可供选择)
                        • -
                        • Spring SPI: META-INF/spring.factories
                        • -
                        -

                        SPI 示例

                        Java SPI 示例

                        定义一个接口 Animal

                        -
                        参数说明
                        daily周期:每天
                        weekly周期:每周
                        monthly 周期:每月
                        public interface Animal {
                        void bark();
                        }
                        - -

                        定义两个实现类 DogCat

                        -
                        public class Dog implements Animal {
                        @Override
                        public void bark() {
                        System.out.println("Dog bark...");
                        }
                        }

                        public class Cat implements Animal {
                        @Override
                        public void bark() {
                        System.out.println("Cat bark...");
                        }
                        }
                        - -

                        META-INF/services 文件夹下创建一个文件,名称为 Animal 的全限定名 com.moralok.dubbo.spi.test.Animal,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。

                        -
                        com.moralok.dubbo.spi.test.Dog
                        com.moralok.dubbo.spi.test.Cat
                        - -

                        进行测试。

                        -
                        public class JavaSPITest {
                        @Test
                        void bark() {
                        System.out.println("Java SPI");
                        System.out.println("============");
                        ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
                        serviceLoader.forEach(Animal::bark);
                        }
                        }
                        - -

                        测试结果

                        -
                        Java SPI
                        ============
                        Dog bark...
                        Cat bark...
                        +

                        使用方式

                        @Configuration 注解表示这是一个配置类,Spring 在处理配置类时,会解析并处理配置类上的 @PropertySource 注解,将对应的配置文件解析为 PropertySource,添加到 Spring 容器的 Environment 环境中。这样就可以在其他的 Bean 中,使用 @Value 注解使用这些配置

                        +
                        @Configuration
                        @PropertySource(value = "classpath:/player.properties", encoding = "UTF-8")
                        public class PropertySourceConfig {

                        @Bean
                        public Player player() {
                        return new Player();
                        }
                        }

                        public class Player {
                        private String name;
                        private Integer age;
                        @Value("${player.nickname}")
                        private String nickname;
                        // 省略 setter 和 getter 方法
                        }
                        -

                        Dubbo SPI 示例

                        Dubbo 并未使用原生的 Java SPI,而是重新实现了一套功能更加强大的 SPI 机制。Dubbo SPI 的配置文件放在 META-INF/dubbo 文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI 注解。

                        -
                        dog = com.moralok.dubbo.spi.test.Dog
                        cat = com.moralok.dubbo.spi.test.Cat
                        +

                        配置文件

                        +
                        player.nickname=Tom
                        -

                        进行测试。

                        -
                        public class DubboSPITest {

                        @Test
                        void bark() {
                        System.out.println("Dubbo SPI");
                        System.out.println("============");
                        ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
                        Animal dog = extensionLoader.getExtension("dog");
                        dog.bark();
                        Animal cat = extensionLoader.getExtension("cat");
                        cat.bark();
                        }
                        }
                        +

                        测试类

                        +
                        public class PropertySourceTest {

                        @Test
                        public void test() {
                        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PropertySourceConfig.class);

                        Player player = (Player) ac.getBean("player");
                        System.out.println(player);

                        ConfigurableEnvironment environment = ac.getEnvironment();
                        String property = environment.getProperty("player.nickname");
                        System.out.println(property);

                        ac.close();
                        }
                        }

                        测试结果

                        -
                        Dubbo SPI
                        ============
                        Dog bark...
                        Cat bark...
                        +
                        Player{name='null', age=null, nickname='Tom'}
                        Tom
                        -

                        Dubbo 获取扩展流程图

                        +

                        源码分析

                        关于 Spring 是如何处理配置类的请参见之前的文章:

                        + +

                        获取 @PropertySource 注解属性

                        Spring 在解析配置类构建配置模型时,会对配置类上的 @PropertySource 注解进行处理。Spring 将获取所有的 @PropertySource 注解属性,并遍历进行处理。

                        +
                          +
                        • @PropertySource 注解是可重复的,一个类上可以标注多个
                        • +
                        • @PropertySources 注解包含 @PropertySource 注解
                        • +
                        +
                        protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                        throws IOException {
                        // ...
                        // 处理 @PropertySource 注解
                        // 获取所有 @PropertySource 注解的属性并遍历。注意该注解为可重复的。
                        for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
                        sourceClass.getMetadata(), PropertySources.class,
                        org.springframework.context.annotation.PropertySource.class)) {
                        // 如果 environment 是 ConfigurableEnvironment 的一个实例,目前恒为 true
                        if (this.environment instanceof ConfigurableEnvironment) {
                        // 处理单个 @PropertySource 注解的属性
                        processPropertySource(propertySource);
                        }
                        else {
                        logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                        "]. Reason: Environment must implement ConfigurableEnvironment");
                        }
                        }
                        // ...
                        }
                        -

                        Dubbo SPI 源码分析

                        获取 ExtensionLoader

                        private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);

                        public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
                        if (type == null) {
                        throw new IllegalArgumentException("Extension type == null");
                        }
                        if (!type.isInterface()) {
                        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
                        }
                        if (!withExtensionAnnotation(type)) {
                        throw new IllegalArgumentException("Extension type (" + type +
                        ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
                        }

                        // 从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回
                        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
                        if (loader == null) {
                        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
                        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
                        }
                        return loader;
                        }
                        - -

                        这个方法包含了如下步骤:

                        -
                          -
                        1. 参数校验。
                        2. -
                        3. 从缓存 EXTENSION_LOADERS 中获取与拓展类对应的 ExtensionLoader,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。
                        4. -
                        -
                        -

                        从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的 getOrCreate 的处理模式在 Dubbo 的源码中经常出现。

                        -
                        -

                        EXTENSION_LOADERSExtensionLoader 的静态变量,保存了“拓展类->ExtensionLoader”的映射关系。

                        -

                        根据 name 获取 Extension

                        public T getExtension(String name) {
                        return getExtension(name, true);
                        }

                        public T getExtension(String name, boolean wrap) {
                        if (StringUtils.isEmpty(name)) {
                        throw new IllegalArgumentException("Extension name == null");
                        }
                        if ("true".equals(name)) {
                        // 获取默认的拓展实现
                        return getDefaultExtension();
                        }
                        // Holder,用于持有目标对象
                        final Holder<Object> holder = getOrCreateHolder(name);
                        // 双重检查
                        Object instance = holder.get();
                        if (instance == null) {
                        synchronized (holder) {
                        instance = holder.get();
                        if (instance == null) {
                        // 创建拓展实例,设置到 holder 中。
                        instance = createExtension(name, wrap);
                        holder.set(instance);
                        }
                        }
                        }
                        return (T) instance;
                        }

                        private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

                        private Holder<Object> getOrCreateHolder(String name) {
                        Holder<Object> holder = cachedInstances.get(name);
                        if (holder == null) {
                        cachedInstances.putIfAbsent(name, new Holder<>());
                        holder = cachedInstances.get(name);
                        }
                        return holder;
                        }
                        - -

                        这个方法中获取 Holder 和获取拓展实例都是使用 getOrCreate 的模式。

                        -

                        Holder 用于持有拓展实例。cachedInstancesExtensionLoader 的成员变量,保存了“name->Holder(拓展实例)”的映射关系。

                        -

                        创建 Extension

                        private T createExtension(String name, boolean wrap) {
                        // 从配置文件中加载所有的拓展类,可得到“name->拓展实现类”的映射关系表
                        Class<?> clazz = getExtensionClasses().get(name);
                        if (clazz == null || unacceptableExceptions.contains(name)) {
                        throw findException(name);
                        }
                        try {
                        // 使用 getOrCreate 模式获取拓展实例
                        T instance = (T) EXTENSION_INSTANCES.get(clazz);
                        if (instance == null) {
                        EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
                        instance = (T) EXTENSION_INSTANCES.get(clazz);
                        }
                        // 向拓展实例中注入依赖
                        injectExtension(instance);

                        // 是否包装默认为 true
                        if (wrap) {

                        List<Class<?>> wrapperClassesList = new ArrayList<>();
                        if (cachedWrapperClasses != null) {
                        wrapperClassesList.addAll(cachedWrapperClasses);
                        wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                        Collections.reverse(wrapperClassesList);
                        }

                        if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                        // 遍历包装类
                        for (Class<?> wrapperClass : wrapperClassesList) {
                        Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                        // 区别于旧版本:支持使用 Wrapper 注解进行匹配
                        if (wrapper == null
                        || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        // 如果有匹配的包装类,包装拓展实例
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                        }
                        }
                        }
                        }

                        // 初始化拓展实例
                        initExtension(instance);
                        return instance;
                        } catch (Throwable t) {
                        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                        type + ") couldn't be instantiated: " + t.getMessage(), t);
                        }
                        }
                        - -

                        这个方法包含如下步骤:

                        -
                          -
                        1. 通过 getExtensionClasses 获取所有拓展类
                        2. -
                        3. 通过反射创建拓展实例
                        4. -
                        5. 向拓展实例中注入依赖
                        6. -
                        7. 将拓展实例包装在适配的 Wrapper 对象中
                        8. -
                        9. 初始化拓展实例
                        10. -
                        -

                        第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOCAOP 的具体实现。

                        -

                        最后拓展实例的结构如下图。

                        - - -

                        加载 Extension Class

                        private Map<String, Class<?>> getExtensionClasses() {
                        // 使用 getOrCreate 模式获取所有拓展类
                        Map<String, Class<?>> classes = cachedClasses.get();
                        if (classes == null) {
                        synchronized (cachedClasses) {
                        classes = cachedClasses.get();
                        if (classes == null) {
                        classes = loadExtensionClasses();
                        cachedClasses.set(classes);
                        }
                        }
                        }
                        return classes;
                        }

                        private Map<String, Class<?>> loadExtensionClasses() {
                        // 1. 缓存默认拓展名
                        cacheDefaultExtensionName();

                        Map<String, Class<?>> extensionClasses = new HashMap<>();

                        // 遍历加载策略,加载各个策略的目录下的配置文件,获取拓展类
                        for (LoadingStrategy strategy : strategies) {
                        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
                        strategy.overridden(), strategy.excludedPackages());
                        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
                        strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
                        }

                        return extensionClasses;
                        }

                        // 如果存在默认拓展名,提取并缓存
                        private void cacheDefaultExtensionName() {
                        // 获取 SPI 注解
                        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
                        if (defaultAnnotation == null) {
                        return;
                        }

                        String value = defaultAnnotation.value();
                        if ((value = value.trim()).length() > 0) {
                        // 对 SPI 的 value 内容进行切分
                        String[] names = NAME_SEPARATOR.split(value);
                        // 检测 name 是否合法
                        if (names.length > 1) {
                        throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
                        }
                        if (names.length == 1) {
                        // 缓存默认拓展名,用于 getDefaultExtension
                        cachedDefaultName = names[0];
                        }
                        }
                        }
                        - -

                        依次处理特定目录

                        代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/ 三个目录下的配置文件,获取拓展类。

                        -
                        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
                        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
                        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
                        - -

                        新版本使用原生的 Java SPI 加载 LoadingStrategy,允许用户自定义加载策略。

                        -
                          -
                        1. DubboInternalLoadingStrategy,目录 META-INF/dubbo/internal/,优先级最高
                        2. -
                        3. DubboLoadingStrategy,目录 META-INF/dubbo/,优先级普通
                        4. -
                        5. ServicesLoadingStrategy,目录 META-INF/services/,优先级最低
                        6. -
                        -
                        private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

                        private static LoadingStrategy[] loadLoadingStrategies() {
                        // 通过 Java SPI 加载 LoadingStrategy
                        return stream(load(LoadingStrategy.class).spliterator(), false)
                        .sorted()
                        .toArray(LoadingStrategy[]::new);
                        }
                        - -

                        LoadingStrategyJava SPI 配置文件

                        - - -

                        loadDirectory 方法

                        loadDirectory 方法先通过 classLoader 获取所有的资源链接,然后再通过 loadResource 方法加载资源。

                        -

                        新版本中 extensionLoaderClassLoaderFirst 可以设置是否优先使用 ExtensionLoader's ClassLoader 获取资源链接。

                        -
                        private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                        boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
                        // filename = 文件夹路径 + type 的全限定名
                        String fileName = dir + type;
                        try {
                        Enumeration<java.net.URL> urls = null;
                        ClassLoader classLoader = findClassLoader();

                        // 区别于旧版本:先从 ExtensionLoader's ClassLoader 获取资源链接,默认为 false
                        if (extensionLoaderClassLoaderFirst) {
                        ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                        if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                        urls = extensionLoaderClassLoader.getResources(fileName);
                        }
                        }

                        // 根据文件名加载所有同名文件
                        if (urls == null || !urls.hasMoreElements()) {
                        if (classLoader != null) {
                        urls = classLoader.getResources(fileName);
                        } else {
                        urls = ClassLoader.getSystemResources(fileName);
                        }
                        }

                        if (urls != null) {
                        while (urls.hasMoreElements()) {
                        java.net.URL resourceURL = urls.nextElement();
                        // 加载资源
                        loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
                        }
                        }
                        } catch (Throwable t) {
                        logger.error("Exception occurred when loading extension class (interface: " +
                        type + ", description file: " + fileName + ").", t);
                        }
                        }
                        - -

                        loadResource 方法

                        loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于操作缓存。

                        -

                        新版本中 excludedPackages 可以设置将指定包内的类都排除。

                        -
                        private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                        java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
                        try {
                        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                        String line;
                        String clazz = null;
                        // 按行读取配置内容
                        while ((line = reader.readLine()) != null) {
                        // 定位 # 字符,截取 # 字符之前的内容,# 字符之后的内容为注释,需要忽略
                        final int ci = line.indexOf('#');
                        if (ci >= 0) {
                        line = line.substring(0, ci);
                        }
                        line = line.trim();
                        if (line.length() > 0) {
                        try {
                        String name = null;
                        // 定位 = 字符,以 = 字符为界,截取键值对
                        int i = line.indexOf('=');
                        if (i > 0) {
                        name = line.substring(0, i).trim();
                        clazz = line.substring(i + 1).trim();
                        } else {
                        clazz = line;
                        }
                        if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
                        // 加载类,并通过 loadClass 进行缓存
                        // 区别于旧版本:根据 excludedPackages 判断是否排除
                        loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
                        }
                        } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException(
                        "Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
                        ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                        }
                        }
                        }
                        }
                        } catch (Throwable t) {
                        logger.error("Exception occurred when loading extension class (interface: " +
                        type + ", class file: " + resourceURL + ") in " + resourceURL, t);
                        }
                        }
                        - -

                        loadClass 方法

                        loadClass 方法设置了多个缓存,比如 cachedAdaptiveClasscachedWrapperClassescachedNamescachedClasses

                        -

                        新版本中 overridden 可以设置是否覆盖 cachedAdaptiveClasscachedClassesname->clazz

                        -
                        private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                        boolean overridden) throws NoSuchMethodException {
                        // 检测 clazz 是否合法
                        if (!type.isAssignableFrom(clazz)) {
                        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                        type + ", class line: " + clazz.getName() + "), class "
                        + clazz.getName() + " is not subtype of interface.");
                        }
                        if (clazz.isAnnotationPresent(Adaptive.class)) {
                        // 检测 clazz 是否有 Adaptive 注解,有则设置 cachedAdaptiveClass 缓存
                        // 区别于旧版本:根据 overriden 判断是否可覆盖
                        cacheAdaptiveClass(clazz, overridden);
                        } else if (isWrapperClass(clazz)) {
                        // 检测 clazz 是否是 Wrapper 类型,是则添加到 cachedWrapperClasses 缓存
                        cacheWrapperClass(clazz);
                        } else {
                        // 检测 clazz 是否有默认的构造器方法,如果没有,则抛出异常
                        clazz.getConstructor();
                        if (StringUtils.isEmpty(name)) {
                        // 如果 name 为空,则尝试从 Extension 注解中获取 name,或者使用小写的类名(可能截取 type 后缀)作为 name
                        name = findAnnotationName(clazz);
                        if (name.length() == 0) {
                        throw new IllegalStateException(
                        "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                        }
                        }

                        // 切分 name
                        String[] names = NAME_SEPARATOR.split(name);
                        if (ArrayUtils.isNotEmpty(names)) {
                        // 如果 clazz 有 Activate 注解,则缓存 names[0]->Activate 的映射关系
                        cacheActivateClass(clazz, names[0]);
                        for (String n : names) {
                        // 缓存 clazz->n 的映射关系
                        cacheName(clazz, n);
                        // 缓存 n->clazz 的映射关系,传递到最后,终于轮到 extensionClasses
                        // 区别于旧版本:根据 overriden 判断是否可覆盖
                        saveInExtensionClass(extensionClasses, clazz, n, overridden);
                        }
                        }
                        }
                        }

                        private void cacheAdaptiveClass(Class<?> clazz, boolean overridden) {
                        if (cachedAdaptiveClass == null || overridden) {
                        // 可以设置是否覆盖
                        cachedAdaptiveClass = clazz;
                        } else if (!cachedAdaptiveClass.equals(clazz)) {
                        throw new IllegalStateException("More than 1 adaptive class found: "
                        + cachedAdaptiveClass.getName()
                        + ", " + clazz.getName());
                        }
                        }

                        private void cacheWrapperClass(Class<?> clazz) {
                        if (cachedWrapperClasses == null) {
                        cachedWrapperClasses = new ConcurrentHashSet<>();
                        }
                        cachedWrapperClasses.add(clazz);
                        }

                        private void cacheActivateClass(Class<?> clazz, String name) {
                        Activate activate = clazz.getAnnotation(Activate.class);
                        if (activate != null) {
                        cachedActivates.put(name, activate);
                        } else {
                        // support com.alibaba.dubbo.common.extension.Activate
                        com.alibaba.dubbo.common.extension.Activate oldActivate =
                        clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                        if (oldActivate != null) {
                        cachedActivates.put(name, oldActivate);
                        }
                        }
                        }
                        - -

                        Dubbo IOC

                        Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter 方法特征并满足条件,若有,则通过 objectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。

                        -
                        -

                        Spring IOC 相比,Dubbo IOC 实现的依赖注入功能更加简单,代码也更加容易理解。

                        -
                        -
                        private T injectExtension(T instance) {
                        // 检测是否有 objectFactory
                        if (objectFactory == null) {
                        return instance;
                        }
                        try {
                        // 遍历目标类的所有方法
                        // 区别于旧版本:增加了对 DisbaleInject、Inject 注解的处理
                        for (Method method : instance.getClass().getMethods()) {
                        // 检测是否是 setter 方法
                        if (!isSetter(method)) {
                        continue;
                        }

                        // 检测是否标注 DisableInject 注解
                        if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                        }

                        // 检测参数类型是否是原始类型
                        Class<?> pt = method.getParameterTypes()[0];
                        if (ReflectUtils.isPrimitives(pt)) {
                        continue;
                        }

                        // 获取属性名
                        String property = getSetterProperty(method);
                        // 检测是否标注 Inject 注解
                        Inject inject = method.getAnnotation(Inject.class);
                        if (inject == null) {
                        injectValue(instance, method, pt, property);
                        } else {
                        // 检测 Inject 是否启动、是否按照类型注入
                        if (!inject.enable()) {
                        continue;
                        }

                        if (inject.type() == Inject.InjectType.ByType) {
                        injectValue(instance, method, pt, null);
                        } else {
                        injectValue(instance, method, pt, property);
                        }
                        }
                        }
                        } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                        }
                        return instance;
                        }

                        // 获取属性名
                        private String getSetterProperty(Method method) {
                        return method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        }

                        // public、set 开头、只有一个参数
                        private boolean isSetter(Method method) {
                        return method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers());
                        }

                        private void injectValue(T instance, Method method, Class<?> pt, String property) {
                        try {
                        // 从 ObjectFactory 中获取依赖对象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                        // 通过反射调用 setter 方法设置依赖
                        method.invoke(instance, object);
                        }
                        } catch (Exception e) {
                        logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                        }
                        - -

                        objectFactoryExtensionFactory 的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理

                        -
                        public <T> T getExtension(Class<T> type, String name) {
                        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
                        ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
                        if (!loader.getSupportedExtensions().isEmpty()) {
                        return loader.getAdaptiveExtension();
                        }
                        }
                        return null;
                        }
                        - -

                        参考文章

                        -]]>
                        - - java - dubbo - spi - -
                        - - Spring 中 @PropertySource 注解的使用和源码分析 - /2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/ - @PropertySource 注解提供了一种方便的声明性机制,用于将 PropertySource 添加到 Spring 容器的 Environment 环境中。该注解通常搭配 @Configuration 注解一起使用。本文将介绍如何使用 @PropertySource 注解,并通过分析源码解释外部配置文件是如何被解析进入 SpringEnvironment 中。

                        - - -

                        使用方式

                        @Configuration 注解表示这是一个配置类,Spring 在处理配置类时,会解析并处理配置类上的 @PropertySource 注解,将对应的配置文件解析为 PropertySource,添加到 Spring 容器的 Environment 环境中。这样就可以在其他的 Bean 中,使用 @Value 注解使用这些配置

                        -
                        @Configuration
                        @PropertySource(value = "classpath:/player.properties", encoding = "UTF-8")
                        public class PropertySourceConfig {

                        @Bean
                        public Player player() {
                        return new Player();
                        }
                        }

                        public class Player {
                        private String name;
                        private Integer age;
                        @Value("${player.nickname}")
                        private String nickname;
                        // 省略 setter 和 getter 方法
                        }
                        - -

                        配置文件

                        -
                        player.nickname=Tom
                        - -

                        测试类

                        -
                        public class PropertySourceTest {

                        @Test
                        public void test() {
                        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PropertySourceConfig.class);

                        Player player = (Player) ac.getBean("player");
                        System.out.println(player);

                        ConfigurableEnvironment environment = ac.getEnvironment();
                        String property = environment.getProperty("player.nickname");
                        System.out.println(property);

                        ac.close();
                        }
                        }
                        - -

                        测试结果

                        -
                        Player{name='null', age=null, nickname='Tom'}
                        Tom
                        - -

                        源码分析

                        关于 Spring 是如何处理配置类的请参见之前的文章:

                        - -

                        获取 @PropertySource 注解属性

                        Spring 在解析配置类构建配置模型时,会对配置类上的 @PropertySource 注解进行处理。Spring 将获取所有的 @PropertySource 注解属性,并遍历进行处理。

                        -
                          -
                        • @PropertySource 注解是可重复的,一个类上可以标注多个
                        • -
                        • @PropertySources 注解包含 @PropertySource 注解
                        • -
                        -
                        protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                        throws IOException {
                        // ...
                        // 处理 @PropertySource 注解
                        // 获取所有 @PropertySource 注解的属性并遍历。注意该注解为可重复的。
                        for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
                        sourceClass.getMetadata(), PropertySources.class,
                        org.springframework.context.annotation.PropertySource.class)) {
                        // 如果 environment 是 ConfigurableEnvironment 的一个实例,目前恒为 true
                        if (this.environment instanceof ConfigurableEnvironment) {
                        // 处理单个 @PropertySource 注解的属性
                        processPropertySource(propertySource);
                        }
                        else {
                        logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                        "]. Reason: Environment must implement ConfigurableEnvironment");
                        }
                        }
                        // ...
                        }
                        - -

                        使用 IDEA 查看 AnnotationAttributes

                        -
                        +

                        使用 IDEA 查看 AnnotationAttributes

                        +

                        处理 @PropertySource 注解属性

                        • 读取 @PropertySource 注解属性的信息,如名称、编码和位置等等
                        • @@ -4525,6 +4548,65 @@ spring + + ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗 + /2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/ + @ConfigurationProperties@EnableConfigurationPropertiesSpring Boot 中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot 的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机工作原理

                          + + + +

                          注解

                          ConfigurationProperties 是用于外部化配置的注解。如果你想绑定验证某些外部属性(例如来自 .properties 文件),就将其添加到类定义或 @Configuration 类中的 @Bean 方法。请注意,和 @Value 相反,SpEL 表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties 注解的源码可知,该注解主要起到标记和存储一些信息的作用。

                          +
                          @Target({ ElementType.TYPE, ElementType.METHOD })
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          public @interface ConfigurationProperties {

                          // 可有效绑定到此对象的属性的名称前缀
                          @AliasFor("prefix")
                          String value() default "";

                          // 可有效绑定到此对象的属性的名称前缀
                          @AliasFor("value")
                          String prefix() default "";

                          // 绑定到此对象时是否忽略无效字段
                          boolean ignoreInvalidFields() default false;

                          // 绑定到此对象时是否忽略未知字段
                          boolean ignoreUnknownFields() default true;

                          }
                          + +

                          查看 EnableConfigurationProperties 的源码,我们注意到它通过 @Import 导入了 EnableConfigurationPropertiesImportSelector

                          +
                          @Target(ElementType.TYPE)
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          @Import(EnableConfigurationPropertiesImportSelector.class)
                          public @interface EnableConfigurationProperties {

                          // 使用 Spring 快速注册标注了 @ConfigurationProperties 的 bean。无论 value 如何,标准的 Spring Bean 也将被扫描。
                          Class<?>[] value() default {};

                          }
                          + +

                          注解的作用

                          查看 EnableConfigurationPropertiesImportSelector 的源码,关注 selectImports 方法。该方法返回了 ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar 的全限定类名,Spring 将注册它们。

                          +
                          class EnableConfigurationPropertiesImportSelector implements ImportSelector {

                          private static final String[] IMPORTS = {
                          ConfigurationPropertiesBeanRegistrar.class.getName(),
                          ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

                          @Override
                          public String[] selectImports(AnnotationMetadata metadata) {
                          return IMPORTS;
                          }
                          }
                          + +

                          注册目标类

                          ConfigurationPropertiesBeanRegistrar 是一个内部类,查看 ConfigurationPropertiesBeanRegistrar 的源码,关注 registerBeanDefinitions 方法。注册的目标来自于:

                          +
                            +
                          • @EnableConfigurationPropertiesvalue 所指定的类中
                          • +
                          • 且标注了 @ConfigurationProperties 的类
                          • +
                          +
                          public static class ConfigurationPropertiesBeanRegistrar
                          implements ImportBeanDefinitionRegistrar {

                          @Override
                          public void registerBeanDefinitions(AnnotationMetadata metadata,
                          BeanDefinitionRegistry registry) {
                          // 1. 获取要注册的 Class
                          // 2. 遍历获取的结果进行注册
                          getTypes(metadata).forEach((type) -> register(registry,
                          (ConfigurableListableBeanFactory) registry, type));
                          }


                          private List<Class<?>> getTypes(AnnotationMetadata metadata) {
                          // 获取所有 @EnableConfigurationProperties 的属性
                          MultiValueMap<String, Object> attributes = metadata
                          .getAllAnnotationAttributes(
                          EnableConfigurationProperties.class.getName(), false);
                          // 获取属性 value 的值,处理后返回
                          return collectClasses(attributes == null ? Collections.emptyList()
                          : attributes.get("value"));
                          }

                          // 收集 Class
                          private List<Class<?>> collectClasses(List<?> values) {
                          return values.stream().flatMap((value) -> Arrays.stream((Object[]) value))
                          .map((o) -> (Class<?>) o).filter((type) -> void.class != type)
                          .collect(Collectors.toList());
                          }

                          // 注册 Bean 定义
                          private void register(BeanDefinitionRegistry registry,
                          ConfigurableListableBeanFactory beanFactory, Class<?> type) {
                          String name = getName(type);
                          // 检测是否包含 Bean 定义
                          if (!containsBeanDefinition(beanFactory, name)) {
                          // 如果没找到,则注册
                          registerBeanDefinition(registry, name, type);
                          }
                          }

                          // beanName = prefix + 全限定类名 or 全限定类名
                          private String getName(Class<?> type) {
                          ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
                          ConfigurationProperties.class);
                          String prefix = (annotation != null ? annotation.prefix() : "");
                          return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
                          : type.getName());
                          }

                          // 检测是否包含 Bean 定义
                          private boolean containsBeanDefinition(
                          ConfigurableListableBeanFactory beanFactory, String name) {
                          // 先检测当前工厂中是否包含
                          if (beanFactory.containsBeanDefinition(name)) {
                          return true;
                          }
                          // 如果没找到则检测父工厂是否包含
                          BeanFactory parent = beanFactory.getParentBeanFactory();
                          if (parent instanceof ConfigurableListableBeanFactory) {
                          return containsBeanDefinition((ConfigurableListableBeanFactory) parent,
                          name);
                          }
                          return false;
                          }

                          // 注册 Bean 定义
                          private void registerBeanDefinition(BeanDefinitionRegistry registry, String name,
                          Class<?> type) {
                          // 断言目标类标注了 @ConfigurationProperties
                          assertHasAnnotation(type);
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(type);
                          // registry 注册 Bean 定义
                          registry.registerBeanDefinition(name, definition);
                          }

                          // 断言目标类标注了 @ConfigurationProperties
                          private void assertHasAnnotation(Class<?> type) {
                          Assert.notNull(
                          AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
                          "No " + ConfigurationProperties.class.getSimpleName()
                          + " annotation found on '" + type.getName() + "'.");
                          }

                          }
                          + +

                          注册后处理器

                          查看 ConfigurationPropertiesBindingPostProcessorRegistrar 的源码,关注 registerBeanDefinitions 方法。该方法注册了 ConfigurationPropertiesBindingPostProcessorConfigurationBeanFactoryMetadata

                          +
                            +
                          • 前者顾名思义,用于处理 ConfigurationProperties 的绑定
                          • +
                          • 后者是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类
                          • +
                          +
                          public class ConfigurationPropertiesBindingPostProcessorRegistrar
                          implements ImportBeanDefinitionRegistrar {

                          @Override
                          public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                          BeanDefinitionRegistry registry) {
                          if (!registry.containsBeanDefinition(
                          ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
                          registerConfigurationPropertiesBindingPostProcessor(registry);
                          registerConfigurationBeanFactoryMetadata(registry);
                          }
                          }

                          private void registerConfigurationPropertiesBindingPostProcessor(
                          BeanDefinitionRegistry registry) {
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
                          // Bean 定义的角色为基础设施
                          definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                          registry.registerBeanDefinition(
                          ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);

                          }

                          private void registerConfigurationBeanFactoryMetadata(
                          BeanDefinitionRegistry registry) {
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
                          definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                          registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                          definition);
                          }

                          }
                          + +

                          绑定

                          ConfigurationPropertiesBindingPostProcessor 是用于 ConfigurationProperties 绑定的后处理器,关注 afterPropertiesSet 方法还有核心方法 postProcessBeforeInitialization

                          +
                            +
                          • afterPropertiesSet 方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
                          • +
                          • postProcessBeforeInitialization 方法中,先获取 @ConfigurationProperties,再进行绑定。
                          • +
                          +
                          public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
                          PriorityOrdered, ApplicationContextAware, InitializingBean {

                          public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class
                          .getName();

                          public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

                          private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

                          private ApplicationContext applicationContext;

                          private ConfigurationPropertiesBinder configurationPropertiesBinder;

                          @Override
                          public void setApplicationContext(ApplicationContext applicationContext)
                          throws BeansException {
                          this.applicationContext = applicationContext;
                          }

                          @Override
                          public void afterPropertiesSet() throws Exception {
                          // We can't use constructor injection of the application context because
                          // it causes eager factory bean initialization
                          // 注入了 ConfigurationBeanFactoryMetadata(不理解注解描述的情况?)
                          this.beanFactoryMetadata = this.applicationContext.getBean(
                          ConfigurationBeanFactoryMetadata.BEAN_NAME,
                          ConfigurationBeanFactoryMetadata.class);
                          this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
                          this.applicationContext, VALIDATOR_BEAN_NAME);
                          }

                          @Override
                          public int getOrder() {
                          return Ordered.HIGHEST_PRECEDENCE + 1;
                          }

                          @Override
                          public Object postProcessBeforeInitialization(Object bean, String beanName)
                          throws BeansException {
                          // 获取 @ConfigurationProperties
                          ConfigurationProperties annotation = getAnnotation(bean, beanName,
                          ConfigurationProperties.class);
                          if (annotation != null) {
                          // 绑定
                          bind(bean, beanName, annotation);
                          }
                          return bean;
                          }

                          private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
                          // 获取 Bean 的类型
                          ResolvableType type = getBeanType(bean, beanName);
                          // 获取 @Validated 用于校验
                          Validated validated = getAnnotation(bean, beanName, Validated.class);
                          Annotation[] annotations = (validated == null ? new Annotation[] { annotation }
                          : new Annotation[] { annotation, validated });
                          // 创建 Bindable 对象
                          Bindable<?> target = Bindable.of(type).withExistingValue(bean)
                          .withAnnotations(annotations);
                          try {
                          // 绑定
                          this.configurationPropertiesBinder.bind(target);
                          }
                          catch (Exception ex) {
                          throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
                          ex);
                          }
                          }

                          private ResolvableType getBeanType(Object bean, String beanName) {
                          // 先查找工厂方法
                          Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
                          if (factoryMethod != null) {
                          // 如果存在,返回工厂方法的返回值类型
                          return ResolvableType.forMethodReturnType(factoryMethod);
                          }
                          // 否则返回 bean 的类型(难道 bean 的类型不是工厂方法的返回值类型吗?)
                          return ResolvableType.forClass(bean.getClass());
                          }

                          private <A extends Annotation> A getAnnotation(Object bean, String beanName,
                          Class<A> type) {
                          // 先到 @Bean 方法中获取
                          A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
                          if (annotation == null) {
                          // 如果没有,再到类上获取
                          annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
                          }
                          return annotation;
                          }

                          }
                          + +

                          ConfigurationBeanFactoryMetadata 是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties 不仅可以添加到类定义,还可以用于标注 @Bean 方法,ConfigurationBeanFactoryMetadata 正是应用于在后者这类情况下获取 @ConfigurationProperties

                          +
                          public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {

                          private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

                          // 后处理 BeanFactory
                          @Override
                          public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                          throws BeansException {
                          this.beanFactory = beanFactory;
                          // 遍历 Bean 定义
                          for (String name : beanFactory.getBeanDefinitionNames()) {
                          BeanDefinition definition = beanFactory.getBeanDefinition(name);
                          String method = definition.getFactoryMethodName();
                          String bean = definition.getFactoryBeanName();、
                          // 检测是否是一个 @Bean 方法对应的 Bean 定义
                          if (method != null && bean != null) {
                          this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
                          }
                          }
                          }

                          // 获取工厂方法上的注解
                          public <A extends Annotation> A findFactoryAnnotation(String beanName,
                          Class<A> type) {
                          Method method = findFactoryMethod(beanName);
                          return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
                          }

                          // 获取工厂方法
                          public Method findFactoryMethod(String beanName) {
                          if (!this.beansFactoryMetadata.containsKey(beanName)) {
                          return null;
                          }
                          AtomicReference<Method> found = new AtomicReference<>(null);
                          FactoryMetadata metadata = this.beansFactoryMetadata.get(beanName);
                          Class<?> factoryType = this.beanFactory.getType(metadata.getBean());
                          String factoryMethod = metadata.getMethod();
                          // 如果是代理类,获取其父类
                          if (ClassUtils.isCglibProxyClass(factoryType)) {
                          factoryType = factoryType.getSuperclass();
                          }
                          // 遍历声明的方法进行匹配(名字相同即可没问题吗?)
                          ReflectionUtils.doWithMethods(factoryType, (method) -> {
                          if (method.getName().equals(factoryMethod)) {
                          found.compareAndSet(null, method);
                          }
                          });
                          return found.get();
                          }
                          }
                          + +

                          总结

                          @EnableConfigurationProperties 的目的有两个:

                          +
                            +
                          • 注册目标
                          • +
                          • 注册后处理器用于在目标进行 Bean 初始化工作时,介入进行绑定
                          • +
                          +

                          尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties 类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties 类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties 注册的,因此你需要保证至少有一个 @EnableConfigurationProperties 标注的类被注册(并被处理了 @Import)。
                          Spring Boot 中,@SpringBootApplication 通过 @EnableAutoConfiguration 启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfigurationConfigurationPropertiesAutoConfiguration 标注了 @EnableConfigurationProperties。因此,对于 Spring Boot 而言,扫描范围内的所有 ConfigurationProperties 类,其实都不需要 @EnableAutoConfiguration。事实上,由于默认生成的 beanName 不同,多余的配置还会重复注册两个 Bean 定义。

                          +
                          @Configuration
                          @EnableConfigurationProperties
                          public class ConfigurationPropertiesAutoConfiguration {

                          }
                          + +]]>
                          + + java + spring + spring boot + +
                          Spring AutowiredAnnotationBeanPostProcessor 的源码分析 /2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/ @@ -4640,65 +4722,6 @@ spring - - ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗 - /2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/ - @ConfigurationProperties@EnableConfigurationPropertiesSpring Boot 中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot 的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机工作原理

                          - - - -

                          注解

                          ConfigurationProperties 是用于外部化配置的注解。如果你想绑定验证某些外部属性(例如来自 .properties 文件),就将其添加到类定义或 @Configuration 类中的 @Bean 方法。请注意,和 @Value 相反,SpEL 表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties 注解的源码可知,该注解主要起到标记和存储一些信息的作用。

                          -
                          @Target({ ElementType.TYPE, ElementType.METHOD })
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          public @interface ConfigurationProperties {

                          // 可有效绑定到此对象的属性的名称前缀
                          @AliasFor("prefix")
                          String value() default "";

                          // 可有效绑定到此对象的属性的名称前缀
                          @AliasFor("value")
                          String prefix() default "";

                          // 绑定到此对象时是否忽略无效字段
                          boolean ignoreInvalidFields() default false;

                          // 绑定到此对象时是否忽略未知字段
                          boolean ignoreUnknownFields() default true;

                          }
                          - -

                          查看 EnableConfigurationProperties 的源码,我们注意到它通过 @Import 导入了 EnableConfigurationPropertiesImportSelector

                          -
                          @Target(ElementType.TYPE)
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          @Import(EnableConfigurationPropertiesImportSelector.class)
                          public @interface EnableConfigurationProperties {

                          // 使用 Spring 快速注册标注了 @ConfigurationProperties 的 bean。无论 value 如何,标准的 Spring Bean 也将被扫描。
                          Class<?>[] value() default {};

                          }
                          - -

                          注解的作用

                          查看 EnableConfigurationPropertiesImportSelector 的源码,关注 selectImports 方法。该方法返回了 ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar 的全限定类名,Spring 将注册它们。

                          -
                          class EnableConfigurationPropertiesImportSelector implements ImportSelector {

                          private static final String[] IMPORTS = {
                          ConfigurationPropertiesBeanRegistrar.class.getName(),
                          ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

                          @Override
                          public String[] selectImports(AnnotationMetadata metadata) {
                          return IMPORTS;
                          }
                          }
                          - -

                          注册目标类

                          ConfigurationPropertiesBeanRegistrar 是一个内部类,查看 ConfigurationPropertiesBeanRegistrar 的源码,关注 registerBeanDefinitions 方法。注册的目标来自于:

                          -
                            -
                          • @EnableConfigurationPropertiesvalue 所指定的类中
                          • -
                          • 且标注了 @ConfigurationProperties 的类
                          • -
                          -
                          public static class ConfigurationPropertiesBeanRegistrar
                          implements ImportBeanDefinitionRegistrar {

                          @Override
                          public void registerBeanDefinitions(AnnotationMetadata metadata,
                          BeanDefinitionRegistry registry) {
                          // 1. 获取要注册的 Class
                          // 2. 遍历获取的结果进行注册
                          getTypes(metadata).forEach((type) -> register(registry,
                          (ConfigurableListableBeanFactory) registry, type));
                          }


                          private List<Class<?>> getTypes(AnnotationMetadata metadata) {
                          // 获取所有 @EnableConfigurationProperties 的属性
                          MultiValueMap<String, Object> attributes = metadata
                          .getAllAnnotationAttributes(
                          EnableConfigurationProperties.class.getName(), false);
                          // 获取属性 value 的值,处理后返回
                          return collectClasses(attributes == null ? Collections.emptyList()
                          : attributes.get("value"));
                          }

                          // 收集 Class
                          private List<Class<?>> collectClasses(List<?> values) {
                          return values.stream().flatMap((value) -> Arrays.stream((Object[]) value))
                          .map((o) -> (Class<?>) o).filter((type) -> void.class != type)
                          .collect(Collectors.toList());
                          }

                          // 注册 Bean 定义
                          private void register(BeanDefinitionRegistry registry,
                          ConfigurableListableBeanFactory beanFactory, Class<?> type) {
                          String name = getName(type);
                          // 检测是否包含 Bean 定义
                          if (!containsBeanDefinition(beanFactory, name)) {
                          // 如果没找到,则注册
                          registerBeanDefinition(registry, name, type);
                          }
                          }

                          // beanName = prefix + 全限定类名 or 全限定类名
                          private String getName(Class<?> type) {
                          ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
                          ConfigurationProperties.class);
                          String prefix = (annotation != null ? annotation.prefix() : "");
                          return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
                          : type.getName());
                          }

                          // 检测是否包含 Bean 定义
                          private boolean containsBeanDefinition(
                          ConfigurableListableBeanFactory beanFactory, String name) {
                          // 先检测当前工厂中是否包含
                          if (beanFactory.containsBeanDefinition(name)) {
                          return true;
                          }
                          // 如果没找到则检测父工厂是否包含
                          BeanFactory parent = beanFactory.getParentBeanFactory();
                          if (parent instanceof ConfigurableListableBeanFactory) {
                          return containsBeanDefinition((ConfigurableListableBeanFactory) parent,
                          name);
                          }
                          return false;
                          }

                          // 注册 Bean 定义
                          private void registerBeanDefinition(BeanDefinitionRegistry registry, String name,
                          Class<?> type) {
                          // 断言目标类标注了 @ConfigurationProperties
                          assertHasAnnotation(type);
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(type);
                          // registry 注册 Bean 定义
                          registry.registerBeanDefinition(name, definition);
                          }

                          // 断言目标类标注了 @ConfigurationProperties
                          private void assertHasAnnotation(Class<?> type) {
                          Assert.notNull(
                          AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
                          "No " + ConfigurationProperties.class.getSimpleName()
                          + " annotation found on '" + type.getName() + "'.");
                          }

                          }
                          - -

                          注册后处理器

                          查看 ConfigurationPropertiesBindingPostProcessorRegistrar 的源码,关注 registerBeanDefinitions 方法。该方法注册了 ConfigurationPropertiesBindingPostProcessorConfigurationBeanFactoryMetadata

                          -
                            -
                          • 前者顾名思义,用于处理 ConfigurationProperties 的绑定
                          • -
                          • 后者是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类
                          • -
                          -
                          public class ConfigurationPropertiesBindingPostProcessorRegistrar
                          implements ImportBeanDefinitionRegistrar {

                          @Override
                          public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                          BeanDefinitionRegistry registry) {
                          if (!registry.containsBeanDefinition(
                          ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
                          registerConfigurationPropertiesBindingPostProcessor(registry);
                          registerConfigurationBeanFactoryMetadata(registry);
                          }
                          }

                          private void registerConfigurationPropertiesBindingPostProcessor(
                          BeanDefinitionRegistry registry) {
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
                          // Bean 定义的角色为基础设施
                          definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                          registry.registerBeanDefinition(
                          ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);

                          }

                          private void registerConfigurationBeanFactoryMetadata(
                          BeanDefinitionRegistry registry) {
                          GenericBeanDefinition definition = new GenericBeanDefinition();
                          definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
                          definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                          registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                          definition);
                          }

                          }
                          - -

                          绑定

                          ConfigurationPropertiesBindingPostProcessor 是用于 ConfigurationProperties 绑定的后处理器,关注 afterPropertiesSet 方法还有核心方法 postProcessBeforeInitialization

                          -
                            -
                          • afterPropertiesSet 方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
                          • -
                          • postProcessBeforeInitialization 方法中,先获取 @ConfigurationProperties,再进行绑定。
                          • -
                          -
                          public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
                          PriorityOrdered, ApplicationContextAware, InitializingBean {

                          public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class
                          .getName();

                          public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

                          private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

                          private ApplicationContext applicationContext;

                          private ConfigurationPropertiesBinder configurationPropertiesBinder;

                          @Override
                          public void setApplicationContext(ApplicationContext applicationContext)
                          throws BeansException {
                          this.applicationContext = applicationContext;
                          }

                          @Override
                          public void afterPropertiesSet() throws Exception {
                          // We can't use constructor injection of the application context because
                          // it causes eager factory bean initialization
                          // 注入了 ConfigurationBeanFactoryMetadata(不理解注解描述的情况?)
                          this.beanFactoryMetadata = this.applicationContext.getBean(
                          ConfigurationBeanFactoryMetadata.BEAN_NAME,
                          ConfigurationBeanFactoryMetadata.class);
                          this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
                          this.applicationContext, VALIDATOR_BEAN_NAME);
                          }

                          @Override
                          public int getOrder() {
                          return Ordered.HIGHEST_PRECEDENCE + 1;
                          }

                          @Override
                          public Object postProcessBeforeInitialization(Object bean, String beanName)
                          throws BeansException {
                          // 获取 @ConfigurationProperties
                          ConfigurationProperties annotation = getAnnotation(bean, beanName,
                          ConfigurationProperties.class);
                          if (annotation != null) {
                          // 绑定
                          bind(bean, beanName, annotation);
                          }
                          return bean;
                          }

                          private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
                          // 获取 Bean 的类型
                          ResolvableType type = getBeanType(bean, beanName);
                          // 获取 @Validated 用于校验
                          Validated validated = getAnnotation(bean, beanName, Validated.class);
                          Annotation[] annotations = (validated == null ? new Annotation[] { annotation }
                          : new Annotation[] { annotation, validated });
                          // 创建 Bindable 对象
                          Bindable<?> target = Bindable.of(type).withExistingValue(bean)
                          .withAnnotations(annotations);
                          try {
                          // 绑定
                          this.configurationPropertiesBinder.bind(target);
                          }
                          catch (Exception ex) {
                          throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
                          ex);
                          }
                          }

                          private ResolvableType getBeanType(Object bean, String beanName) {
                          // 先查找工厂方法
                          Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
                          if (factoryMethod != null) {
                          // 如果存在,返回工厂方法的返回值类型
                          return ResolvableType.forMethodReturnType(factoryMethod);
                          }
                          // 否则返回 bean 的类型(难道 bean 的类型不是工厂方法的返回值类型吗?)
                          return ResolvableType.forClass(bean.getClass());
                          }

                          private <A extends Annotation> A getAnnotation(Object bean, String beanName,
                          Class<A> type) {
                          // 先到 @Bean 方法中获取
                          A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
                          if (annotation == null) {
                          // 如果没有,再到类上获取
                          annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
                          }
                          return annotation;
                          }

                          }
                          - -

                          ConfigurationBeanFactoryMetadata 是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties 不仅可以添加到类定义,还可以用于标注 @Bean 方法,ConfigurationBeanFactoryMetadata 正是应用于在后者这类情况下获取 @ConfigurationProperties

                          -
                          public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {

                          private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

                          // 后处理 BeanFactory
                          @Override
                          public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                          throws BeansException {
                          this.beanFactory = beanFactory;
                          // 遍历 Bean 定义
                          for (String name : beanFactory.getBeanDefinitionNames()) {
                          BeanDefinition definition = beanFactory.getBeanDefinition(name);
                          String method = definition.getFactoryMethodName();
                          String bean = definition.getFactoryBeanName();、
                          // 检测是否是一个 @Bean 方法对应的 Bean 定义
                          if (method != null && bean != null) {
                          this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
                          }
                          }
                          }

                          // 获取工厂方法上的注解
                          public <A extends Annotation> A findFactoryAnnotation(String beanName,
                          Class<A> type) {
                          Method method = findFactoryMethod(beanName);
                          return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
                          }

                          // 获取工厂方法
                          public Method findFactoryMethod(String beanName) {
                          if (!this.beansFactoryMetadata.containsKey(beanName)) {
                          return null;
                          }
                          AtomicReference<Method> found = new AtomicReference<>(null);
                          FactoryMetadata metadata = this.beansFactoryMetadata.get(beanName);
                          Class<?> factoryType = this.beanFactory.getType(metadata.getBean());
                          String factoryMethod = metadata.getMethod();
                          // 如果是代理类,获取其父类
                          if (ClassUtils.isCglibProxyClass(factoryType)) {
                          factoryType = factoryType.getSuperclass();
                          }
                          // 遍历声明的方法进行匹配(名字相同即可没问题吗?)
                          ReflectionUtils.doWithMethods(factoryType, (method) -> {
                          if (method.getName().equals(factoryMethod)) {
                          found.compareAndSet(null, method);
                          }
                          });
                          return found.get();
                          }
                          }
                          - -

                          总结

                          @EnableConfigurationProperties 的目的有两个:

                          -
                            -
                          • 注册目标
                          • -
                          • 注册后处理器用于在目标进行 Bean 初始化工作时,介入进行绑定
                          • -
                          -

                          尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties 类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties 类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties 注册的,因此你需要保证至少有一个 @EnableConfigurationProperties 标注的类被注册(并被处理了 @Import)。
                          Spring Boot 中,@SpringBootApplication 通过 @EnableAutoConfiguration 启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfigurationConfigurationPropertiesAutoConfiguration 标注了 @EnableConfigurationProperties。因此,对于 Spring Boot 而言,扫描范围内的所有 ConfigurationProperties 类,其实都不需要 @EnableAutoConfiguration。事实上,由于默认生成的 beanName 不同,多余的配置还会重复注册两个 Bean 定义。

                          -
                          @Configuration
                          @EnableConfigurationProperties
                          public class ConfigurationPropertiesAutoConfiguration {

                          }
                          - -]]>
                          - - java - spring - spring boot - -
                          当 MySQL 以 skip-name-resolve 模式启动时如何使用 grant 命令 /2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/ @@ -4871,1787 +4894,1764 @@ - Unsafe,一个“反 Java”的 class - /2023/12/25/Unsafe-an-anti-Java-class/ - Unsafe 类位于 sun.misc 包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe 类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CASCompare and Swap)操作等。本文将介绍这个“反 Java”的类及其方法的典型使用场景。

                          + synchronized 锁机制的分析和验证 + /2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/ + 本文详细介绍了 Javasynchronized 锁的机制、存储结构、优化措施以及升级过程,并通过 jol-core 演示 Mark Word 的变化来验证锁升级的多个 case

                          -

                          由于 Unsafe 类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过 Java 语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用 Unsafe 类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用 Unsafe 类来进行一些性能优化。

                          -
                          -
                          -

                          尽管在生产中需要谨慎使用 Unsafe,但是可以在测试中使用它来更真实地接触 Java 对象在内存中的存储结构,验证自己的理论知识。

                          -
                          -

                          获取 Unsafe 实例

                          -

                          Java 9 及之后的版本中,Unsafe 类中的 getUnsafe() 方法被标记为不安全(Unsafe),不再允许普通的 Java 应用程序代码通过此方法获取 Unsafe 实例。这是为了提高 Java 的安全性,防止滥用 Unsafe 类的功能。

                          +

                          待完善

                          -

                          在正常的 Java 应用程序中,获取 Unsafe 实例是不被推荐的,因为它违反了 Java 语言的安全性和封装原则。Unsafe 类的设计本意是为了 Java 库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe 对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe 对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。

                          -

                          获取 Unsafe 实例的静态方法如下:

                          -
                          @CallerSensitive
                          public static Unsafe getUnsafe() {
                          Class<?> caller = Reflection.getCallerClass();
                          // 检查调用方法的类是被引导类加载器所加载
                          if (!VM.isSystemDomainLoader(caller.getClassLoader()))
                          throw new SecurityException("Unsafe");
                          return theUnsafe;
                          }
                          - -

                          Unsafe 使用单例模式,可以通过静态方法 getUnsafe 获取 Unsafe 实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe 实例有以下两种可行方案:

                          -
                            -
                          1. 通过 -Xbootclasspath/a:${path} 把调用方法的类所在的 jar 包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径
                          2. -
                          3. 通过反射获取 Unsafe 类中的 Unsafe 实例
                            private static Unsafe getUnsafe() {
                            try {
                            Field f = Unsafe.class.getDeclaredField("theUnsafe");
                            f.setAccessible(true);
                            return (Unsafe) f.get(null);
                            } catch (NoSuchFieldException | IllegalAccessException e) {
                            throw new RuntimeException(e);
                            }
                            }
                          4. -
                          -

                          内存操作

                          Unsafe 类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java 语言的内置安全性和类型检查。以下是一些常见的 Unsafe 类中关于内存操作的方法:

                          +

                          利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

                            -
                          • allocateMemory: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。
                            public native long allocateMemory(long bytes);
                          • -
                          • reallocateMemory: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。
                            public native long reallocateMemory(long address, long bytes);
                          • -
                          • freeMemory: 释放之前由 allocateMemoryreallocateMemory 分配的内存。
                            public native void freeMemory(long address);
                          • -
                          • setMemory: 将给定内存块中的所有字节设置为固定值(通常为零)。
                            public native void setMemory(Object o, long offset, long bytes, byte value);
                            public void setMemory(long address, long bytes, byte value) {
                            setMemory(null, address, bytes, value);
                            }
                          • -
                          • copyMemory: 复制指定长度的内存块
                            public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
                          • -
                          • putXxx: 将指定偏移量处的内存设置为指定的值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                            public native void putObject(Object o, long offset, Object x);
                          • -
                          • getXxx: 从指定偏移量处的内存读取值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                            public native Object getObject(Object o, long offset);
                          • -
                          • putXxxgetXxx 也提供了按绝对基地址操作内存的方法。
                            public native byte getByte(long address);
                            public native void putByte(long address, byte x);
                          • -
                          -

                          从内存读取值时,除非满足以下情况之一,否则结果不确定:

                          -
                            -
                          1. 偏移量是通过 objectFieldOffset 从字段的 Field 对象获取的,o 指向的对象的类与字段所属的类兼容。
                          2. -
                          3. 偏移量和 o 指向的对象(无论是否为 null)分别是通过 staticFieldOffsetstaticFieldBaseField 对象获得的。
                          4. -
                          5. o 指向的是一个数组,偏移量是一个形式为 B+N*S 的整数,其中 N 是数组的有效索引,BS 分别是通过 arrayBaseOffsetarrayIndexScale 获得的值。
                          6. -
                          -
                          -

                          做一些“不确定”的测试,比如使用 byte 相关的方法操作 int 所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。

                          -
                          -

                          分配堆外内存

                          Java NIONew I/O)中,分配堆外内存使用了 Unsafe 类的 allocateMemory 方法。堆外内存是一种在 Java 虚拟机之外分配的内存,它不受 Java 堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O 操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java 堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting)的机制来处理。

                          -
                          DirectByteBuffer(int cap) {
                          super(-1, 0, cap, cap);
                          boolean pa = VM.isDirectMemoryPageAligned();
                          int ps = Bits.pageSize();
                          long size = Math.max(1L, (long)cap + (pa ? ps : 0));
                          Bits.reserveMemory(size, cap);

                          long base = 0;
                          try {
                          // 分配本地内存
                          base = unsafe.allocateMemory(size);
                          } catch (OutOfMemoryError x) {
                          Bits.unreserveMemory(size, cap);
                          throw x;
                          }
                          // 初始化本地内存
                          unsafe.setMemory(base, size, (byte) 0);
                          if (pa && (base % ps != 0)) {
                          // Round up to page boundary
                          address = base + ps - (base & (ps - 1));
                          } else {
                          address = base;
                          }
                          // 使用虚引用 Cleaner 对象跟踪 DirectByteBuffer 对象的垃圾回收
                          cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
                          att = null;
                          }
                          - -

                          DirectByteBuffer 对象仅被 Cleaner 对象(虚引用)引用时,它可以在任意一次 GC 中被垃圾回收。在 DirectByteBuffer 对象被垃圾回收后,Cleaner 对象会被加入到引用队列,ReferenceHandler 线程将调用 Deallocator 对象的 run 方法,从而实现本地内存的自动释放。

                          -
                          private static class Deallocator implements Runnable {

                          private static Unsafe unsafe = Unsafe.getUnsafe();

                          private long address;
                          private long size;
                          private int capacity;

                          private Deallocator(long address, long size, int capacity) {
                          assert (address != 0);
                          this.address = address;
                          this.size = size;
                          this.capacity = capacity;
                          }

                          public void run() {
                          if (address == 0) {
                          // Paranoia
                          return;
                          }
                          // 释放本地内存
                          unsafe.freeMemory(address);
                          address = 0;
                          Bits.unreserveMemory(size, capacity);
                          }

                          }
                          - -

                          CAS 相关

                          Unsafe 提供了 3CAS 相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU 会自动将该位置更新为新值,否则,CPU 不做任何操作。这些方法的底层实现对应着 CPU 指令 cmpxchg

                          -
                          // 如果 Java 变量当前符合预期,则自动将其更新为 x。
                          public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
                          public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
                          public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
                          - -

                          AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 UnsafeobjectFieldOffset 方法获取。

                          -
                          public class AtomicInteger extends Number implements java.io.Serializable {
                          private static final long serialVersionUID = 6214790243416807050L;

                          // setup to use Unsafe.compareAndSwapInt for updates
                          private static final Unsafe unsafe = Unsafe.getUnsafe();
                          private static final long valueOffset;

                          static {
                          try {
                          valueOffset = unsafe.objectFieldOffset
                          (AtomicInteger.class.getDeclaredField("value"));
                          } catch (Exception ex) { throw new Error(ex); }
                          }

                          private volatile int value;
                          }
                          - -

                          CAS 更新变量的值的内存变化如下:

                          -
                          - -

                          配合 ClassLayout 打印 AtomicInteger 的内部结构更直观地感受 offset 的含义:

                          -
                          java.util.concurrent.atomic.AtomicInteger object internals:
                          OFF SZ TYPE DESCRIPTION VALUE
                          0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                          8 4 (object header: class) 0xf8003dbc
                          12 4 int AtomicInteger.value 1
                          Instance size: 16 bytes
                          Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                          - - - -

                          参考文章

                          -]]>
                          - - java - -
                          - - ComponentScan 扫描路径覆盖的真相 - /2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/ - @ComponentScan 注解是 Spring 中很常用的注解,用于扫描并加载指定类路径下的 Bean,而 Spring Boot 为了便捷使用 @SpringBootApplication 组合注解集成了 @ComponentScan 的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication 可以间接设置 @ComponentScan 属性的原因?本文从源码角度分析 @ComponentScan 的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。

                          - - - -

                          入口

                          对于标注了 @ComponentScan 注解的配置类,处理过程如下:

                          -
                            -
                          • 获取 @ComponentScan 的注解属性
                          • -
                          • 遍历注解属性集合,依次根据其中的信息进行扫描,获取 Bean 定义
                          • -
                          • 如果获取到的 Bean 定义中有任何其他配置类,将递归解析(处理配置类)
                          • -
                          -
                          -

                          这里和处理 @Import 的过程很像,都出现了递归解析新获得的配置类。

                          -
                          -
                          protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                          throws IOException {
                          // ...
                          // 处理任何 @ComponentScan 注解
                          // 获取 @ComponentScan 的注解属性,该注解是可重复的
                          Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                          sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
                          if (!componentScans.isEmpty() &&
                          !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
                          // 遍历
                          for (AnnotationAttributes componentScan : componentScans) {
                          // 如果配置类被标注了 @ComponentScan -> 立即扫描
                          Set<BeanDefinitionHolder> scannedBeanDefinitions =
                          this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                          // 检测被扫描到的 Bean 定义中是否有任何其他配置类,如有需要递归解析
                          for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                          if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                          holder.getBeanDefinition(), this.metadataReaderFactory)) {
                          // 递归解析配置类
                          parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                          }
                          }
                          }
                          }
                          // ...
                          }
                          - -

                          扫描获取 Bean 定义

                          我们先跳过“获取 @ComponentScan 的注解属性”的过程,来看“扫描获取 Bean 定义”的过程。扫描是通过 ComponentScanAnnotationParserparse 方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner 设置一些来自 @ComponentScan 的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner 顾名思义是基于类路径的 Bean 定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages 的设置:

                          -
                            -
                          • 使用 Set 存储合并结果,用于去重
                          • -
                          • 获取设置的 basePackages 值并添加
                          • -
                          • 获取设置的 basePackageClasses 值,转换为它们所在的包名并添加
                          • -
                          • 如果结果集现在还是空的,获取被标注的配置类所在的包名并添加
                          • -
                          -
                          -

                          最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。

                          -
                          -
                          public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
                          Assert.state(this.environment != null, "Environment must not be null");
                          Assert.state(this.resourceLoader != null, "ResourceLoader must not be null");
                          // 创建 ClassPathBeanDefinitionScanner
                          ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                          componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
                          // Bean 名称生成器
                          Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
                          boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
                          scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                          BeanUtils.instantiateClass(generatorClass));

                          ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
                          if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
                          scanner.setScopedProxyMode(scopedProxyMode);
                          }
                          else {
                          Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
                          scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
                          }

                          scanner.setResourcePattern(componentScan.getString("resourcePattern"));
                          // 设置 Filter
                          for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
                          for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                          scanner.addIncludeFilter(typeFilter);
                          }
                          }
                          for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
                          for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                          scanner.addExcludeFilter(typeFilter);
                          }
                          }
                          // 是否懒加载
                          boolean lazyInit = componentScan.getBoolean("lazyInit");
                          if (lazyInit) {
                          scanner.getBeanDefinitionDefaults().setLazyInit(true);
                          }
                          // 设置 basePackages(使用 Set 去重)
                          Set<String> basePackages = new LinkedHashSet<String>();
                          // 获取设置的 basePackages 值
                          String[] basePackagesArray = componentScan.getStringArray("basePackages");
                          for (String pkg : basePackagesArray) {
                          // 允许占位符
                          String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
                          basePackages.addAll(Arrays.asList(tokenized));
                          }
                          // 获取 basePackageClasses,本质上是为了获取它们所在的包名
                          for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
                          basePackages.add(ClassUtils.getPackageName(clazz));
                          }
                          // 如果为空,获取被标注的配置类所在的包名
                          if (basePackages.isEmpty()) {
                          basePackages.add(ClassUtils.getPackageName(declaringClass));
                          }
                          // 排除被标注的配置类本身
                          scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
                          @Override
                          protected boolean matchClassName(String className) {
                          return declaringClass.equals(className);
                          }
                          });
                          // 执行扫描
                          return scanner.doScan(StringUtils.toStringArray(basePackages));
                          }
                          - -

                          parse 方法与其说是解析,不如说是封装了一些设置并最终调用 ClassPathBeanDefinitionScanner,而设置的属性值来源于 @ComponentScan 的注解属性。关于获取 @ComponentScan 的注解属性的方法 AnnotationConfigUtils.attributesForRepeatable 在分析 @PropertySource 时也曾经遇到过,顾名思义我们知道它应该是用于获取可重复的注解的属性。可是它和直接获取注解对象有什么区别呢?

                          -
                          -

                          我们知道 @SpringBootApplication 拥有和 @ComponentScan 具备相似的功能,并且可以使用 scanBasePackagesscanBasePackageClasses 这两个属性设置扫描的包。也许你还知道 @SpringBootApplication 之所以如此是因为它被标注了 @ComponentScanscanBasePackagesscanBasePackageClasses 分别是它的元注解 @ComponentScanbasePackagesbasePackageClasses 的别名。你甚至可能知道如果在配置类上使用 @ComponentScan 设置包扫描后会导致 @SpringBootApplication 设置的包扫描失效
                          可是为什么呢?Spring 中我们会看到从指定类上直接获取目标注解的代码,我们还会看到递归地从元注解上获取目标注解的代码,我们使用 @ComponentScan 的经验告诉我们可重复注解不是覆盖彼此而是共同生效,那么为什么 @SpringBootApplication 上的 @ComponentScan 就被覆盖了呢?想当然的认为 @SpringBootApplication 上标注了 @ComponentScan 是一切的原因是不够的

                          -
                          -
                          @Target(ElementType.TYPE)
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          @Inherited
                          @SpringBootConfiguration
                          @EnableAutoConfiguration
                          @ComponentScan(excludeFilters = {
                          @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                          @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
                          public @interface SpringBootApplication {

                          @AliasFor(annotation = EnableAutoConfiguration.class)
                          Class<?>[] exclude() default {};

                          @AliasFor(annotation = EnableAutoConfiguration.class)
                          String[] excludeName() default {};

                          @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
                          String[] scanBasePackages() default {};

                          @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
                          Class<?>[] scanBasePackageClasses() default {};

                          }
                          - -

                          获取注解属性

                          attributesForRepeatable 方法有两个重载方法,最终调用的版本如下。先后处理了 @ComponentScan@ComponentScans

                          -
                          static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
                          String containerClassName, String annotationClassName) {
                          // Set 用于存储结果
                          Set<AnnotationAttributes> result = new LinkedHashSet<AnnotationAttributes>();
                          // 处理 @ComponentScan
                          addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false));
                          // 处理 @ComponentScans
                          Map<String, Object> container = metadata.getAnnotationAttributes(containerClassName, false);
                          if (container != null && container.containsKey("value")) {
                          for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container.get("value")) {
                          addAttributesIfNotNull(result, containedAttributes);
                          }
                          }
                          return Collections.unmodifiableSet(result);
                          }
                          - -

                          检索注解的规则

                          根据注释,getAnnotationAttributes 方法检索给定类型的注解的属性,检索的目标可以是直接注解也可以是元注解,同时考虑组合注解上的属性覆盖

                          -
                            -
                          • 元注解指的是标注在其他注解上的注解,用于对被标注的注解进行说明,比如 @SpringBootApplication 上的 @ComponentScan 就被称为元注解,此时 @SpringBootApplication 被称为组合注解
                          • -
                          • 组合注解中存在属性覆盖现象
                          • -
                          -
                          -

                          其实这两点分别对应了我们想要探究的两个问题:@ComponentScan 究竟是如何被检索的?注解属性比如 basePackages 又是如何被覆盖的?

                          -
                          -
                          public Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString) {
                          // 获取合并的注解属性
                          return (this.annotations.length > 0 ? AnnotatedElementUtils.getMergedAnnotationAttributes(
                          getIntrospectedClass(), annotationName, classValuesAsString, this.nestedAnnotationsAsMap) : null);
                          }
                          - -

                          根据注释,getMergedAnnotationAttributes 方法获取所提供元素上方的注解层次结构中指定的 annotationName 的第一个注解,并将该注解的属性与注解层次结构较低级别中的注解中的匹配属性合并。注解层次结构中较低级别的属性会覆盖较高级别中的同名属性,并且完全支持单个注解中或是注解层次结构中的 @AliasFor 语义。与 getAllAnnotationAttributes 方法相反,一旦找到指定 annotationName 的第一个注解,此方法使用的搜索算法将停止搜索注解层次结构。因此,指定的 annotationName 的附加注解将被忽略。

                          -
                          -

                          这注释有点太抽象了,理解代码后再来回味吧。

                          -
                          -
                          public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element,
                          String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
                          // 以 get 语义进行搜索(是指找到即终止搜索?)
                          AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName,
                          new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap));
                          // 后处理注解属性
                          AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap);
                          return attributes;
                          }
                          - -

                          searchWithGetSemantics 方法有多个重载方法,最终调用的版本如下:

                          -
                            -
                          • 先获取 element 上的所有注解(包括重复的,不包括继承的),这意味着可重复注解 @ComponentScan 标注了多个就会有多个实例
                          • -
                          • 在注解中搜索
                          • -
                          • 如果没找到,就从继承的注解中继续搜索
                          • -
                          -
                          -

                          本方法是一个会被递归调用的方法,在第一次调用时 element 是配置类,之后就是注解。

                          -
                          -
                          private static <T> T searchWithGetSemantics(AnnotatedElement element,
                          @Nullable Class<? extends Annotation> annotationType, @Nullable String annotationName,
                          @Nullable Class<? extends Annotation> containerType, Processor<T> processor,
                          Set<AnnotatedElement> visited, int metaDepth) {
                          // 防止无限递归
                          if (visited.add(element)) {
                          try {
                          // 获取 element 上的所有注解(包括重复,不包括继承的)
                          List<Annotation> declaredAnnotations = Arrays.asList(element.getDeclaredAnnotations());
                          // 在获得的注解中搜索
                          T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
                          annotationType, annotationName, containerType, processor, visited, metaDepth);
                          if (result != null) {
                          return result;
                          }
                          // 表明在直接声明的注解中没有找到
                          // 如果 element 是一个类
                          if (element instanceof Class) {
                          // 获取所有的注解(包括重复的和继承的)
                          List<Annotation> inheritedAnnotations = new ArrayList<>();
                          for (Annotation annotation : element.getAnnotations()) {
                          // 排除已经搜索过的,只留下继承的注解
                          if (!declaredAnnotations.contains(annotation)) {
                          inheritedAnnotations.add(annotation);
                          }
                          }
                          // 继续搜索
                          result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations,
                          annotationType, annotationName, containerType, processor, visited, metaDepth);
                          if (result != null) {
                          return result;
                          }
                          }
                          }
                          catch (Throwable ex) {
                          AnnotationUtils.handleIntrospectionFailure(element, ex);
                          }
                          }

                          return null;
                          }
                          - -

                          遍历注解进行搜索。

                          -
                            -
                          • 先在注解中搜索,这意味着如果配置类标注了 @ComponentScan,直接就找到了
                          • -
                          • 如果没找到再在元注解中搜索,如果配置类只标注了 @SpringBootApplication,就是在这部分找到元注解 @ComponentScan
                          • +
                          • 对于普通同步方法,锁是当前实例对象。
                          • +
                          • 对于静态同步方法,锁是当前类的 Class 对象。
                          • +
                          • 对于同步方法块,锁是 synchronized 括号里配置的对象。
                          -
                          -

                          严格意义上说,并不是直接标注的 @ComponentScan 会覆盖 @SpringBootApplication 上间接标注的 @ComponentScan,而是搜索在找到第一个注解后终止没有继续查找。这解答了我们的第一个疑问。

                          -
                          -
                          private static <T> T searchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,
                          List<Annotation> annotations, @Nullable Class<? extends Annotation> annotationType,
                          @Nullable String annotationName, @Nullable Class<? extends Annotation> containerType,
                          Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {

                          // 遍历注解进行查找,如果同时标注 @SpringBootApplication 和 @ComponentScan,在这部分就会找到 @ComponentScan 就返回了
                          for (Annotation annotation : annotations) {
                          // 获取注解的 Class
                          Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                          // 检测是否属于 Java 语言注解包中(以 java.lang.annotation 开头)的注解,例如 @Documented,是的话跳过
                          if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
                          // 检测是否满足条件:等于 annotationType(传入 null),或者和目标的名字(@ComponentScan 全限定类名)相同,或者属于总是处理(默认 false)
                          if (currentAnnotationType == annotationType ||
                          currentAnnotationType.getName().equals(annotationName) ||
                          processor.alwaysProcesses()) {
                          // 处理注解获得注解属性
                          T result = processor.process(element, annotation, metaDepth);
                          if (result != null) {
                          // processor.aggregates() 默认返回 false
                          if (processor.aggregates() && metaDepth == 0) {
                          processor.getAggregatedResults().add(result);
                          }
                          else {
                          // 注意:难道标注多个 @ComponentScan 也只找到一个就返回了?
                          return result;
                          }
                          }
                          }
                          // 容器里的可重复注解,因为 containerType 为 null,跳过
                          else if (currentAnnotationType == containerType) {
                          for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
                          T result = processor.process(element, contained, metaDepth);
                          if (result != null) {
                          // No need to post-process since repeatable annotations within a
                          // container cannot be composed annotations.
                          processor.getAggregatedResults().add(result);
                          }
                          }
                          }
                          }
                          }

                          // 在元注解中递归的搜索,@SpringBootApplication 中的 @ComponentScan 就是在这找到的
                          for (Annotation annotation : annotations) {
                          // 获取注解的 Class
                          Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                          // 检测是否属于 Java 语言注解包中
                          if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
                          // 递归到元注解中搜索,深度加 1
                          T result = searchWithGetSemantics(currentAnnotationType, annotationType,
                          annotationName, containerType, processor, visited, metaDepth + 1);
                          if (result != null) {
                          // 进行后处理,注解层次结构中较低级别的属性会覆盖较高级别中的同名属性就是在这发生的
                          processor.postProcess(element, annotation, result);
                          if (processor.aggregates() && metaDepth == 0) {
                          processor.getAggregatedResults().add(result);
                          }
                          else {
                          return result;
                          }
                          }
                          }
                          }

                          return null;
                          }
                          - -

                          处理 @ComponentScan 获得 AnnotationAttributes

                          -
                          public AnnotationAttributes process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
                          return AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,
                          this.classValuesAsString, this.nestedAnnotationsAsMap);
                          }
                          - -

                          AnnotationAttributes 映射的形式检索给定注解的属性。

                          -
                          static AnnotationAttributes retrieveAnnotationAttributes(@Nullable Object annotatedElement, Annotation annotation,
                          boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

                          Class<? extends Annotation> annotationType = annotation.annotationType();
                          AnnotationAttributes attributes = new AnnotationAttributes(annotationType);
                          // 遍历属性方法
                          for (Method method : getAttributeMethods(annotationType)) {
                          try {
                          // 获取属性值
                          Object attributeValue = method.invoke(annotation);
                          // 获取默认值
                          Object defaultValue = method.getDefaultValue();
                          // 如果默认值不为 null 且和属性值相同
                          if (defaultValue != null && ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
                          attributeValue = new DefaultValueHolder(defaultValue);
                          }
                          // 属性名 -> 属性值
                          attributes.put(method.getName(),
                          adaptValue(annotatedElement, attributeValue, classValuesAsString, nestedAnnotationsAsMap));
                          }
                          catch (Throwable ex) {
                          if (ex instanceof InvocationTargetException) {
                          Throwable targetException = ((InvocationTargetException) ex).getTargetException();
                          rethrowAnnotationConfigurationException(targetException);
                          }
                          throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
                          }
                          }

                          return attributes;
                          }

                          // 获取在所提供的 annotationType 中声明的与 Java 对注释属性的要求相匹配的所有方法
                          static List<Method> getAttributeMethods(Class<? extends Annotation> annotationType) {
                          // 先从缓存中获取
                          List<Method> methods = attributeMethodsCache.get(annotationType);
                          if (methods != null) {
                          return methods;
                          }
                          // 遍历方法筛选
                          methods = new ArrayList<>();
                          for (Method method : annotationType.getDeclaredMethods()) {
                          if (isAttributeMethod(method)) {
                          ReflectionUtils.makeAccessible(method);
                          methods.add(method);
                          }
                          }
                          // 存入缓存
                          attributeMethodsCache.put(annotationType, methods);
                          return methods;
                          }

                          // 确定提供的方法是否是注解的属性方法。
                          static boolean isAttributeMethod(@Nullable Method method) {
                          // 无参数 && 返回值非 void
                          return (method != null && method.getParameterCount() == 0 && method.getReturnType() != void.class);
                          }
                          - -

                          组合注解的属性覆盖

                          在获得注解属性后还要进行后处理,使用注解层次结构中较低级别的属性覆盖较高级别中的同名(包括 @AliasFor 指定的)属性。比如使用 @SpringBootApplication 中的 scanBasePackages 的值覆盖 @ComponentScan 中的 basePackages 的值。

                          -
                          public void postProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes) {
                          annotation = AnnotationUtils.synthesizeAnnotation(annotation, element);
                          // 获取 AnnotationAttributes 的注解类型(@ComponentScan)
                          Class<? extends Annotation> targetAnnotationType = attributes.annotationType();

                          // Track which attribute values have already been replaced so that we can short
                          // circuit the search algorithms.
                          Set<String> valuesAlreadyReplaced = new HashSet<>();
                          // 获取注解的属性方法(SpringBootApplication)
                          for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
                          String attributeName = attributeMethod.getName();
                          // 获取被覆盖的别名
                          String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType);

                          // Explicit annotation attribute override declared via @AliasFor
                          if (attributeOverrideName != null) {
                          // 被覆盖的属性的值是否已经被替换
                          if (valuesAlreadyReplaced.contains(attributeOverrideName)) {
                          continue;
                          }

                          List<String> targetAttributeNames = new ArrayList<>();
                          targetAttributeNames.add(attributeOverrideName);
                          valuesAlreadyReplaced.add(attributeOverrideName);

                          // 确保覆盖目标注解中的所有别名属性。 (SPR-14069)
                          List<String> aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName);
                          if (aliases != null) {
                          for (String alias : aliases) {
                          if (!valuesAlreadyReplaced.contains(alias)) {
                          targetAttributeNames.add(alias);
                          valuesAlreadyReplaced.add(alias);
                          }
                          }
                          }

                          overrideAttributes(element, annotation, attributes, attributeName, targetAttributeNames);
                          }
                          // Implicit annotation attribute override based on convention
                          else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
                          overrideAttribute(element, annotation, attributes, attributeName, attributeName);
                          }
                          }
                          }

                          // 根据提供的注解属性方法的 @AliasFor,获取被覆盖的属性的名称
                          static String getAttributeOverrideName(Method attribute, @Nullable Class<? extends Annotation> metaAnnotationType) {
                          // 获取别名描述符
                          AliasDescriptor descriptor = AliasDescriptor.from(attribute);
                          // 从元注解中被覆盖的属性名
                          return (descriptor != null && metaAnnotationType != null ?
                          descriptor.getAttributeOverrideName(metaAnnotationType) : null);
                          }

                          // 获取在提供的注解类型中通过 @AliasFor 声明的所有属性别名的映射。该映射由属性名称作为键,每个值代表别名属性的名称列表。空返回值意味着注解没有声明任何属性别名。
                          static Map<String, List<String>> getAttributeAliasMap(@Nullable Class<? extends Annotation> annotationType) {
                          if (annotationType == null) {
                          return Collections.emptyMap();
                          }
                          // 从缓存中获取
                          Map<String, List<String>> map = attributeAliasesCache.get(annotationType);
                          if (map != null) {
                          return map;
                          }

                          map = new LinkedHashMap<>();
                          // 遍历属性方法
                          for (Method attribute : getAttributeMethods(annotationType)) {
                          // 获取别名列表
                          List<String> aliasNames = getAttributeAliasNames(attribute);
                          if (!aliasNames.isEmpty()) {
                          map.put(attribute.getName(), aliasNames);
                          }
                          }
                          // 存入缓存
                          attributeAliasesCache.put(annotationType, map);
                          return map;
                          }

                          // 获取通过提供的注解属性的 @AliasFor 配置的别名属性的名称列表
                          static List<String> getAttributeAliasNames(Method attribute) {
                          AliasDescriptor descriptor = AliasDescriptor.from(attribute);
                          return (descriptor != null ? descriptor.getAttributeAliasNames() : Collections.<String> emptyList());
                          }

                          // 覆盖属性
                          private void overrideAttributes(@Nullable AnnotatedElement element, Annotation annotation,
                          AnnotationAttributes attributes, String sourceAttributeName, List<String> targetAttributeNames) {

                          Object adaptedValue = getAdaptedValue(element, annotation, sourceAttributeName);
                          // 遍历目标属性中的所有应被覆盖的属性(本尊+别名)
                          for (String targetAttributeName : targetAttributeNames) {
                          attributes.put(targetAttributeName, adaptedValue);
                          }
                          }
                          - -

                          在代码的注释中我们留下过一个疑问,如果找到了第一个注解就立即返回,那么标注了多个 @ComponentScan 呢?当你 Debug 时,你会发现并没有走出现直接标注了 @ComponentScan 的处理,其实看到反编译后的代码你就知道了,多个 @ComponentScan 被合成了一个 @ComponentScans,甚至此时设置的三个 basePackages 都是生效的。在 JDK 8 引入的重复注解机制,并非一个语言层面上的改动,而是编译器层面的改动。在编译后,多个可重复注解 @ComponentScan 会被合并到一个容器注解 @ComponentScans 中。

                          -
                          -

                          因此,“@ComponentScan 的配置会覆盖 @SpringBootApplication 关于包扫描的配置”这句话既对又不对,它在一个常见的个例上表现出的现象是对的,在更普遍的情况中以及本质上是错误的。你也许可以再根据一些情况罗列出类似的“@ComponentScan 使用规则”,但是如果你不明白背后的本质,那么这些只是一些死记硬背的陈述,甚至会带给你错误的认知。

                          -
                          -
                          // 标注了两个 `@ComponentScan`,对编译后的字节码进行反编译
                          @SpringBootApplication(
                          scanBasePackages = {"com.example"}
                          )
                          @ComponentScans({@ComponentScan(
                          basePackages = {"com.example.demo"}
                          ), @ComponentScan({"com"})})
                          public class DemoApplication {
                          public DemoApplication() {
                          }

                          public static void main(String[] args) {
                          SpringApplication.run(DemoApplication.class, args);
                          }
                          }
                          - -

                          注解内的别名属性

                          postProcess 方法完成了组合注解的属性覆盖,可是对于 @ComponentScan 注解而言,它没有被 postProcess 方法处理,它又是如何做到设置 basePackages 等于设置 value 呢?其实这发生在后处理注解属性方法中,该方法会对注解中标注了 @AliasFor 的属性强制执行别名语义。通俗地讲,就是统一校验互为别名的属性值,要么只设置了其中一个属性的值,其他别名属性会被赋值为相同的值,要么设置为相同的值,否则会报错。

                          -
                          public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element,
                          String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
                          // 以 get 语义进行搜索(是指找到即终止搜索?)
                          AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName,
                          new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap));
                          // 后处理注解属性
                          AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap);
                          return attributes;
                          }

                          static void postProcessAnnotationAttributes(@Nullable Object annotatedElement,
                          @Nullable AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

                          if (attributes == null) {
                          return;
                          }
                          // 获取 AnnotationAttributes 的注解类型(@ComponentScan)
                          Class<? extends Annotation> annotationType = attributes.annotationType();

                          // Track which attribute values have already been replaced so that we can short
                          // circuit the search algorithms.
                          Set<String> valuesAlreadyReplaced = new HashSet<>();

                          if (!attributes.validated) {
                          // 校验 @AliasFor 配置
                          // 获取别名映射
                          Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
                          // 遍历
                          for (String attributeName : aliasMap.keySet()) {
                          // 跳过已处理的
                          if (valuesAlreadyReplaced.contains(attributeName)) {
                          continue;
                          }
                          Object value = attributes.get(attributeName);
                          // 属性是否已有值
                          boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder));
                          // 遍历属性的别名列表
                          for (String aliasedAttributeName : aliasMap.get(attributeName)) {
                          // 跳过已处理的
                          if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
                          continue;
                          }
                          // 获取别名属性的值
                          Object aliasedValue = attributes.get(aliasedAttributeName);
                          // 别名属性是否已有值
                          boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder));

                          // Something to validate or replace with an alias?
                          if (valuePresent || aliasPresent) {
                          // 如果属性已有值且别名属性也有值,校验是否相等
                          if (valuePresent && aliasPresent) {
                          // Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
                          if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
                          String elementAsString =
                          (annotatedElement != null ? annotatedElement.toString() : "unknown element");
                          throw new AnnotationConfigurationException(String.format(
                          "In AnnotationAttributes for annotation [%s] declared on %s, " +
                          "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " +
                          "but only one is permitted.", attributes.displayName, elementAsString,
                          attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value),
                          ObjectUtils.nullSafeToString(aliasedValue)));
                          }
                          }
                          else if (aliasPresent) {
                          // 复制别名属性的值给属性
                          attributes.put(attributeName,
                          adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
                          valuesAlreadyReplaced.add(attributeName);
                          }
                          else {
                          // 复制属性的值给别名属性
                          attributes.put(aliasedAttributeName,
                          adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
                          valuesAlreadyReplaced.add(aliasedAttributeName);
                          }
                          }
                          }
                          }
                          // 校验完毕
                          attributes.validated = true;
                          }

                          // 将 `value` 从 `DefaultValueHolder` 替换为原始的 `value`
                          for (String attributeName : attributes.keySet()) {
                          if (valuesAlreadyReplaced.contains(attributeName)) {
                          continue;
                          }
                          Object value = attributes.get(attributeName);
                          if (value instanceof DefaultValueHolder) {
                          value = ((DefaultValueHolder) value).defaultValue;
                          attributes.put(attributeName,
                          adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
                          }
                          }
                          }
                          - -

                          总结

                          -

                          又是一篇在写之前自认心里有数,以为可以很快总结完,却不知不觉写了很久,也收获了很多的文章。在刚开始,我只是想接续分析 @Configuration 的思路补充关于 @ComponentScan 的内容,但是渐渐地我又想要回应心里的疑问,@ComponentScan@SpringBootApplication 一起使用的问题的本质原因是什么?Spring 框架真的很好用,好用到你不用太关心背后的原理,好用到你有时候用一个本质上不太正确的结论“走遍天下却几乎不会遇到问题”。说实话,研究完也有点索然无味,尤其是花了这么多时间看自己很讨厌的关于解析的代码,只能说解开了一个卡点也算疏通了一口气,但是时间成本好大啊,得多看点能“面试”的技术啊!!!

                          -
                          -

                          综上分析,@SpringBootApplication 的包扫描功能本质上还是 @ComponentScan 提供的,但是和常见的嵌套注解不同,检索 @ComponentScan 有一套独特的算法,导致 @SpringBootApplication@ComponentScan 并非简单的叠加效果。

                          +

                          当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

                            -
                          • Spring 会先获取 @ComponentScan 的注解属性再获取 @ComponentScans 的注解属性
                          • -
                          • @ComponentScan 为例,只获取给定配置类上的注解层次结构中的第一个 @ComponentScan
                          • -
                          • 先从直接标注的注解开始,再递归地搜索元注解,这一点决定了 @ComponentScan 优先级高于 @SpringBootApplication
                          • -
                          • 使用注解层次结构中较低级别的属性覆盖较高级别的同名(支持 @AliasFor)属性,这一点决定了 @SpringBootApplication 可以设置扫描路径
                          • -
                          • 多个 @ComponentScan 在编译后隐式生成 @ComponentScans,这一点决定多个 @ComponentScan 彼此之间以及和 @SpringBootApplication 互不冲突
                          • +
                          • JVM 层面,synchronized 锁是基于进入和退出 Monitor 来实现的,每一个对象都有一个 Monitor 与之相关联。
                          • +
                          • 在字节码层面,同步方法块是使用 monitorentermonitorexit 指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。
                          -]]>
                          - - java - spring - spring boot - -
                          - - Ubuntu server 20.04 安装后没有分配全部磁盘空间 - /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ - 使用 VMware 安装 Ubuntu server 20.04,注意到实际文件系统的总空间大小仅占设置的虚拟磁盘空间大小的一半左右。本文介绍了如何解决该问题。

                          - - -
                          -

                          最近在本地测试 KubesphereMinikube,使用 Ubuntu server 20.04 搭建了多个虚拟机,磁盘空间紧张。注意到在安装后,实际文件系统的总空间大小仅占设置的虚拟磁盘空间大小的一半左右。如果 Ubuntu server 20.04 安装时使用默认的 LVM 选项,就会出现这种情况。

                          +

                          存储结构

                          +

                          锁存在哪里呢?锁里面又会存储什么信息呢?

                          -

                          解决步骤

                            -
                          1. 使用 df -h 命令显示文件系统的总空间和可用空间信息。分配了 40G 磁盘空间,可用仅 19G
                            $ df -h
                            Filesystem Size Used Avail Use% Mounted on
                            udev 3.9G 0 3.9G 0% /dev
                            tmpfs 792M 7.5M 785M 1% /run
                            /dev/mapper/ubuntu--vg-ubuntu--lv 19G 17G 995M 95% /
                            tmpfs 3.9G 0 3.9G 0% /dev/shm
                            tmpfs 5.0M 0 5.0M 0% /run/lock
                            tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
                            /dev/sda2 2.0G 108M 1.7G 6% /boot
                            /dev/loop0 64M 64M 0 100% /snap/core20/1828
                            /dev/loop2 50M 50M 0 100% /snap/snapd/18357
                            /dev/loop1 92M 92M 0 100% /snap/lxd/24061
                            tmpfs 792M 0 792M 0% /run/user/1000
                            /dev/loop3 54M 54M 0 100% /snap/snapd/19457
                          2. -
                          3. 使用 sudo vgdisplay 命令查看发现 Free PE / Size 还有 19G
                            $ sudo vgdisplay
                            --- Volume group ---
                            VG Name ubuntu-vg
                            System ID
                            Format lvm2
                            Metadata Areas 1
                            Metadata Sequence No 2
                            VG Access read/write
                            VG Status resizable
                            MAX LV 0
                            Cur LV 1
                            Open LV 1
                            Max PV 0
                            Cur PV 1
                            Act PV 1
                            VG Size <38.00 GiB
                            PE Size 4.00 MiB
                            Total PE 9727
                            Alloc PE / Size 4863 / <19.00 GiB
                            Free PE / Size 4864 / 19.00 GiB
                            VG UUID NuEjzH-CKXm-W6lA-gqzj-4bds-IR1Y-dTZ8IP
                          4. -
                          5. 使用 sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 调整逻辑卷的大小。
                            $ sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
                            Size of logical volume ubuntu-vg/ubuntu-lv changed from <19.00 GiB (4863 extents) to <38.00 GiB (9727 extents).
                            Logical volume ubuntu-vg/ubuntu-lv successfully resized.
                          6. -
                          7. 使用 sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv 调整文件系统的大小。
                            $ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
                            resize2fs 1.45.5 (07-Jan-2020)
                            Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
                            old_desc_blocks = 3, new_desc_blocks = 5
                            The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 9960448 (4k) blocks long.
                          8. -
                          9. 使用 df -h 命令再次查看,确认文件系统的总空间大小调整为 38G
                            df -h
                            Filesystem Size Used Avail Use% Mounted on
                            udev 3.9G 0 3.9G 0% /dev
                            tmpfs 792M 7.5M 785M 1% /run
                            /dev/mapper/ubuntu--vg-ubuntu--lv 38G 17G 19G 47% /
                            tmpfs 3.9G 0 3.9G 0% /dev/shm
                            tmpfs 5.0M 0 5.0M 0% /run/lock
                            tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
                            /dev/sda2 2.0G 108M 1.7G 6% /boot
                            /dev/loop0 64M 64M 0 100% /snap/core20/1828
                            /dev/loop2 50M 50M 0 100% /snap/snapd/18357
                            /dev/loop1 92M 92M 0 100% /snap/lxd/24061
                            tmpfs 792M 0 792M 0% /run/user/1000
                            /dev/loop3 54M 54M 0 100% /snap/snapd/19457
                            /dev/loop4 64M 64M 0 100% /snap/core20/1950
                          10. -
                          -

                          参考链接

                          ubuntu20.04 server 安装后磁盘空间只有一半的处理
                          Ubuntu Server 20.04.1 LTS, not all disk space was allocated during installation?

                          -]]> - - linux - ubuntu - - - - 谈谈 MySQL 事务的隔离性 - /2024/01/06/talk-about-isolation-of-MySQL-transactions/ - 事务就是一组数据库操作,它具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称为 ACID。本文将介绍 MySQL 事务的隔离性以及对其的思考。
                          尽管这是一个老生常谈的话题,网上也有很多相关的资料,但是要理解它并不容易。即使林晓斌老师在 《MySQL 实战 45 讲》 中用了两个章节进行介绍,但是你在评论区中会发现有些分享或讨论的观点彼此矛盾。原因可能有很多,比如为了易于理解使用简化概念进行分析,有些具体细节各人各执一词同时它们又不好通过测试进行验证,用词不严谨等等。本文尽可能为自己梳理出一个完善并且前后一致的认知体系,再针对一些容易引起误解的地方作进一步的说明。

                          - - -

                          隔离级别

                          SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。当多个事务同时执行时,不同的隔离级别可能发生脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)等一个或多个现象。隔离级别越高,效率越低,因此很多时候,我们需要在二者之间寻找一个平衡点。

                          +

                          对象头

                          synchronized 用的锁是存在 Java 对象头(object header)里的。如果对象是数组类型,则虚拟机用 3 字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。在 64 位虚拟机中,1 字宽等于 8 字节,即 64bit

                          +

                          Java 对象头的组成结构如下:

                          - - - - + + + - - - - - - - - - - + + + - - - - + + + - - - - + + +
                          隔离级别脏读不可重复读幻读长度内容说明
                          读未提交YYY
                          读提交NYY32/64bitMark Word存储对象的 hashCode 或锁信息
                          可重复读NNY32/64bitClass Metadata Address存储指向对象类型数据的指针
                          串行化NNN32/64bitArray length数组的长度(如果当前对象是数组)
                          -
                          -

                          读未提交和串行化很少在实际应用中使用。

                          -
                          -

                          通过以下示例说明隔离级别的影响,V1V2V3 在不同隔离级别下的值有所不同。

                          +

                          Mark Word

                          Java 对象头里的 Mark Word 里默认存储对象的 HashCode,分代年龄和锁标记位。在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为另外 4 种数据。

                          +

                          32 位虚拟机为例:

                          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          事务 A事务 B读未提交读提交可重复读串行化
                          开启事务开启事务
                          查询得到值 1
                          查询得到值 1
                          锁状态25bit4bit1bit2bit
                          23bit2bit是否是偏向锁锁标志位
                          无锁状态对象的 hashCode对象分代年龄001
                          偏向锁线程 IDEpoch对象分代年龄101
                          轻量级锁指向栈中锁记录的指针00
                          重量级锁指向互斥量(重量级锁)的指针10
                          GC 标记11
                          + +

                          64 位虚拟机为例:

                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          锁状态56bit1bit4bit1bit2bit
                          25bit31bit--是否是偏向锁锁标志位
                          无锁状态unused对象的 hashCodecms_free对象分代年龄001
                          偏向锁线程 ID(54bit) | Epoch(2bit)cms_free对象分代年龄101
                          轻量级锁指向栈中锁记录的指针00
                          重量级锁指向互斥量(重量级锁)的指针10
                          GC 标记11
                          + +
                          +

                          在上述表述中,很容易让人产生困惑的地方是 hashCode 和分代年龄是对象的固有属性,当 Mark Word 中存储的数据发生变化时,这些重要的数据去哪了?

                          +
                          +

                          内部结构可视化

                          “百闻不如一见”,jol-core 提供了打印对象内部结构的能力。

                          +
                            +
                          1. 添加依赖,新版本比旧版本打印结果的可读性更好
                            <dependency>
                            <groupId>org.openjdk.jol</groupId>
                            <artifactId>jol-core</artifactId>
                            <version>${org.openjdk.jol.version}</version>
                            </dependency>
                          2. +
                          3. 使用 ClassLayout.parseInstance(objectExample).toPrintable() 打印
                            @Data
                            @Slf4j
                            public class ObjectInternalTest {

                            private byte aByte;
                            private int aInt;

                            public static void main(String[] args) {
                            ObjectInternalTest objectInternalTest = new ObjectInternalTest();
                            log.info(ClassLayout.parseInstance(objectInternalTest).toPrintable());
                            }
                            }
                          4. +
                          5. 打印结果:mark|class|fields|alignment。这样我们就能通过查看 Mark Word 的值更直观地确定当前锁的状态。
                            2023-12-23 20:21:02 - com.moralok.concurrency.ch2.ObjectExample object internals:
                            OFF SZ TYPE DESCRIPTION VALUE
                            0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                            8 4 (object header: class) 0x00060828
                            12 4 int ObjectExample.aInt 0
                            16 1 byte ObjectExample.aByte 0
                            17 7 (object alignment gap)
                            Instance size: 24 bytes
                            Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
                          6. +
                          +

                          指针压缩和 cms_free

                          注意到指向对象类型数据的指针仅 4 个字节,这是因为默认情况下 JVM 参数 UseCompressedOops 是启用的。

                          +

                          markOop.hpp

                          +
                          |--------------------------------------------------------------------------------------------------------------|--------------------|
                          | Object Header (96 bits) | State |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | Mark Word (64 bits) | Klass Word (32 bits) | |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | unused:25 | identity_hashcode:31 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | thread:54 | epoch:2 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | ptr_to_lock_record | lock:2 | OOP to metadata object | Lightweight Locked |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | ptr_to_heavyweight_monitor | lock:2 | OOP to metadata object | Heavyweight Locked |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | | lock:2 | OOP to metadata object | Marked for GC |
                          |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                          + +

                          使用 -XX:-UseCompressedOops 关闭指针压缩,指向对象类型数据的指针才会变回 8 个字节

                          +
                          |------------------------------------------------------------------------------------------------------------|--------------------|
                          | Object Header (128 bits) | State |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | Mark Word (64 bits) | Klass Word (64 bits) | |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | Lightweight Locked |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | Heavyweight Locked |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          | | lock:2 | OOP to metadata object | Marked for GC |
                          |------------------------------------------------------------------------------|-----------------------------|--------------------|
                          + +

                          你可能还会注意到开启和关闭指针压缩时,还有一个 bitcms_free 变成 unused。这个 cms_free 是做什么用的呢?在未开启指针压缩的情况下,指针的低位因为内存对齐的缘故往往是 0,我们可以给这些 bit 设置 1 用于标记特殊状态。CMSKlass 指针的最低位设置为 1 用于表示特定的内存块不是一个对象,而是空闲的内存。在开启指针压缩后,JVM 通过右移移除指针中没用到的低位,因此 CMS 需要一个地方存储这个表示是否为空闲内存的 bit,就是 cms_free

                          +
                          +

                          这在一定程度上解决了我心中的一个问题:JVM 是怎么判断一个空闲的内存块的?

                          +
                          +

                          concurrentMarkSweepGeneration.cpp

                          +
                          // A block of storage in the CMS generation is always in
                          // one of three states. A free block (FREE), an allocated
                          // object (OBJECT) whose size() method reports the correct size,
                          // and an intermediate state (TRANSIENT) in which its size cannot
                          // be accurately determined.
                          // STATE IDENTIFICATION: (32 bit and 64 bit w/o COOPS)
                          // -----------------------------------------------------
                          // FREE: klass_word & 1 == 1; mark_word holds block size
                          //
                          // OBJECT: klass_word installed; klass_word != 0 && klass_word & 1 == 0;
                          // obj->size() computes correct size
                          //
                          // TRANSIENT: klass_word == 0; size is indeterminate until we become an OBJECT
                          //
                          // STATE IDENTIFICATION: (64 bit+COOPS)
                          // ------------------------------------
                          // FREE: mark_word & CMS_FREE_BIT == 1; mark_word & ~CMS_FREE_BIT gives block_size
                          //
                          // OBJECT: klass_word installed; klass_word != 0;
                          // obj->size() computes correct size
                          //
                          // TRANSIENT: klass_word == 0; size is indeterminate until we become an OBJECT
                          + +

                          使用 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 开启 HotSpot Debugger,比对 ClassLayout 打印的 Klass 指针和 Class Browser 中的指针。

                          + + - - - - - - + + + + + + + + + - - - - - - + + + - - - - - - + + + - - - - - - + + + +
                          修改值为 2指针压缩关闭开启
                          ClassLayout0xf800c1050x00000245eb873d20
                          查询得到值 V12(读到B未提交的修改)111二进制表达1111100000000000110000010000010100100100010111101011100001110011110100100000
                          提交事务HotSpot Debugger0x00000007c00608280x00000245EB873D20
                          查询得到值 V222(读到B已提交的修改)11二进制表达01111100000000000110000010000010100000100100010111101011100001110011110100100000
                          +

                          对象分代年龄

                          通过以下示例可以测试和验证对象分代年龄的变化。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          log.info("测试 Mark Word 中的分代年龄");

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 无锁状态,age: 0");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          System.gc();
                          TimeUnit.SECONDS.sleep(1);
                          log.info("GC 后 =====> 无锁状态,age: 1");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +

                          重量级锁

                          锁优化

                          Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java 6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,锁的状态会随着竞争的激化逐渐升级。锁状态可以升级但不能降级,举例来说偏向锁状态升级成轻量级锁状态后不能降级成偏向锁状态。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

                          +
                          +

                          上述的表述并不容易理解,甚至容易让人产生误解。锁状态描述的是锁本身的状态,和是否处于加锁状态无关。以下列表格举例说明,一个偏向锁状态的对象,即使未加锁,也是偏向锁状态,而非无锁状态。

                          +
                          + + - - - - - - + + + + + + + + + - - - - - + + - - - - - - + + +
                          提交事务层次未加锁加锁
                          1匿名偏向锁状态 or 偏向锁状态偏向锁状态
                          查询得到值 V3 222(A在事务期间数据一致)1无锁状态轻量级锁状态
                          补充说明B的修改阻塞至A提交3重要级锁状态重要级锁状态
                          -

                          通过测试验证以上结论可以帮助你更直观地感受隔离级别的作用:

                          -
                            -
                          • 新建连接 mysql –h localhost –u root -P 3306 –p
                          • -
                          • 查看会话的事务隔离级别 show variables like 'transaction_isolation';
                          • -
                          • 设置会话的事务隔离级别 set session transaction isolation level read uncommitted|read committed|repeatable read|serializable;
                          • -
                          • 测试和验证
                          • -
                          -
                          mysql> show variables like 'transaction_isolation';
                          +-----------------------+-----------------+
                          | Variable_name | Value |
                          +-----------------------+-----------------+
                          | transaction_isolation | REPEATABLE-READ |
                          +-----------------------+-----------------+
                          -
                          -

                          5.7 引入了 transaction_isolation 作为 tx_isolation 的别名,8.0.3 废弃后者。

                          -
                          -

                          了解数据库的隔离级别及其影响对于理解自身正在使用的数据库的行为、根据业务场景设置隔离级别优化性能以及迁移数据都是有帮助的。Oracle 数据库的默认隔离级别是“读提交”,MySQL 的默认隔离级别是“可重复读”。

                          -

                          事务隔离的实现

                          -

                          MySQL 中,事务隔离是通过 lockundo logread view 共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。

                          +

                          在查阅的众多资料中,关于锁升级过程的介绍并不详尽和准确,虽然大体上大家的观点是比较一致的,但是在一些细节的描述上却有些模糊不清,有些观点自相矛盾,有些观点互相矛盾,有些观点和我的知识或者测试结果矛盾,甚至有些逻辑不通顺以至于不能相互联系形成和谐的整体。以下内容尽可能结合相对权威和详细的资料,补充个人的思考和猜想作为缝合剂,并通过一些测试用例验证部分猜想,试图建立更加连续平滑以及可信服的知识面。

                          -

                          MySQL 各个事务隔离级别的实现原理简述如下:

                          +

                          锁升级变化图

                          提前放出锁升级变化图,用于在后续分析和测试过程中对照查看。重点关注以下可能引起锁状态变化的事件:

                          +
                            +
                          1. 获取锁和释放锁
                          2. +
                          3. 竞争,其中弱竞争是指线程交替进入同步块,没有发生直接冲突;强竞争是指线程在同步块内的时候有其他线程想要进入同步块
                          4. +
                          5. 调用特殊方法,比如计算 hashCode(非自定义)或者 wait 方法
                          6. +
                          +
                          + +

                          偏向锁

                          HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

                          +

                          偏向锁在 Java 6 之后是默认开启的,可以通过 JVM 参数 -XX:-UseBiasedLocking 关闭偏向锁。尽管偏向锁是默认开启的,但是它在应用程序启动几秒钟之后才激活,延迟时间可以通过 JVM 参数 -XX:BiasedLockingStartupDelay 设置,默认情况下是 4000ms

                          +

                          测试偏向锁配置

                          延迟偏向

                          通过以下示例测试并验证延迟偏向。

                          +
                          public static void main(String[] args) throws IOException, InterruptedException {
                          log.info("测试:偏向锁是延迟激活的");

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 无锁状态(非可偏向的)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          // 默认情况下偏向延迟的设置为 -XX:BiasedLockingStartupDelay=4000
                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          log.info("偏向锁激活之后,新创建的对象的对象头的 Mark Word 是 =====> 匿名偏向锁");
                          Object biasedLock = new Object();
                          log.info(ClassLayout.parseInstance(biasedLock).toPrintable());

                          log.info("偏向锁激活之前创建的对象的对象头的 Mark Word 仍然是 =====> 无锁状态");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +

                          测试结果如下:

                          +
                          OFF  SZ   TYPE DESCRIPTION               VALUE
                          0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)

                          0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)

                          0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                          +
                            -
                          • 串行化:读加共享锁,写加排他锁,读写互斥
                          • -
                          • 读未提交:写加排他锁,读不加锁
                          • -
                          • 可重复读:第一次读操作时创建快照,基于该快照进行读取
                          • -
                          • 读提交:每次读操作时重置快照,基于该快照进行读取
                          • +
                          • JVM 启动后,偏向锁尚未激活前,创建的对象的 Mark Word 的末尾 3 位为 0|01non-biasable,表示无锁状态(非可偏向的)。
                          • +
                          • 4000 毫秒后,新创建的对象的 Mark Word 的末尾 3 位为 1|01biasable,表示匿名偏向锁(可偏向的)。
                          • +
                          • 偏向锁尚未激活前创建的对象的对象头的 Mark Word 的末尾 3仍然0|01
                          -

                          前两者通过锁(lock)实现比较容易理解;后两者通过多版本并发控制(MVCC)实现。MVCC 是一种实现非阻塞并发读的设计思路,在 InnoDB 引擎中主要通过 undo logread view 实现。

                          -

                          以下示意图表现了在 InnoDB 引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC),当你基于快照读取时可以获得旧版本的数据。

                          -
                            -
                          • 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
                          • -
                          • 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
                          • -
                          -
                          - -

                          在接下来我们将通过锁、事务 ID、回滚日志和一致性视图逐步介绍 InnoDB 事务隔离的实现原理。

                          -

                          锁(lock)

                          事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。MySQL 正是通过共享锁排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC 就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。

                          -

                          InnoDB 的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议

                          +

                          在虚拟机启动后,偏向锁激活前,创建的对象的锁标记位为 1|01,此时记录线程 IDbit 全是 0(代表指向 null),没有偏向任何一个线程,该状态称之为匿名偏向锁。

                          -

                          理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及 MVCC 的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。

                          -

                          事务 ID

                          InnoDB 引擎中,每个事务都有唯一的一个事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段 trx_id,记录了插入或更新该行数据的事务 ID

                          -
                          +
                          关闭偏向延迟

                          通过以下示例测试关闭偏向延迟:

                          +
                          // JVM 参数设置为 -XX:BiasedLockingStartupDelay=0
                          public static void main(String[] args) throws IOException, InterruptedException {
                          log.info("测试:关闭偏向锁的延迟偏向");

                          Object lock = new Object();
                          log.info("在虚拟机一启动,新创建的对象的对象头的 Mark Word 就是 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          -

                          创建事务的时机

                          事务启动方式如下:

                          -
                            -
                          1. 显式启动事务语句是 beginstart transaction,配套的提交语句是 commit,回滚语句是 rollback
                          2. -
                          3. 隐式启动事务语句是 set autocommit = 0,该设置将关闭自动提交。当你执行 select,将自动启动一个事务,直到你主动 commitrollback
                          4. -
                          -

                          但注意,实际上不论是显式启动事务情况下的 beginstart transaction,还是隐式启动事务情况下的 commitrollback 都不会立即创建一个新事务,而是直到第一次操作 InnoDB 表的语句执行时,才会真正创建一个新事务

                          -

                          可以通过以下语句查看当前“活跃”的事务进行验证:

                          -
                          select * from information_schema.innodb_trx;
                          +
                          关闭偏向锁

                          通过以下示例测试关闭偏向锁:

                          +
                          // JVM 参数设置为 -XX:-UseBiasedLocking
                          public static void main(String[] args) throws InterruptedException {
                          log.info("测试:关闭偏向锁");

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 无锁状态(非可偏向的)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          log.info("sleep 4000ms");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          log.info("即使过了偏向延迟时间,创建的对象的对象头的 Mark Word 仍然是 =====> 无锁状态(非可偏向的)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          -
                          -

                          只读事务的事务 ID 和更新事务不同。

                          -
                          -
                          -

                          可以使用 commit work and chain; 在提交的同时开启下一次事务,减少一次 begin; 指令的交互开销。

                          -
                          -

                          回滚日志(undo log)

                          InnoDB 引擎中,每条记录在更新的时候都会同时记录一条回滚操作。记录的最新值,通过回滚操作,可以得到之前版本的值。它的作用是:

                          +

                          《Java 并发编程的艺术》中写的是 -XX:-UseBiasedLocking=false,测试中报错:

                          +
                          Error: Could not create the Java Virtual Machine.
                          Error: A fatal exception has occurred. Program will exit.
                          Improperly specified VM option 'UseBiasedLocking=false'
                          + +

                          另外书中说“在关闭偏向锁后程序默认会进入轻量级锁状态”,个人认为可能会让人产生误解,默认在未获取锁时为无锁状态,获取锁将变为轻量级锁状态。

                          +

                          偏向锁加锁

                          当一个线程访问同步块时,先测试 Mark Word 里是否存储着当前线程 ID

                            -
                          • 数据回滚:当事务回滚或者数据库崩溃时,通过 undolog 进行数据回滚。
                          • -
                          • 多版本并发控制:当读取一行记录时,如果该行记录已经被其他事务修改,通过 undo log 读取之前版本的数据,以此实现非阻塞并发读。
                          • +
                          • 如果否,则再测试 Mark Word 中偏向锁的标识是否设置成 1
                              +
                            • 如果为 0,则说明不是偏向锁状态 =====> 获取偏向锁失败后续处理一
                            • +
                            • 如果为 1,则说明是偏向锁状态,通过 CAS 操作设置偏向锁
                                +
                              • 如果成功,说明获得偏向锁
                              • +
                              • 如果失败,说明发生竞争 =====> 获取偏向锁失败后续处理二
                              • +
                              +
                            • +
                            +
                          • +
                          • 如果是,则说明当前线程就是之前获得偏向锁的线程,此刻再次获得锁
                          -

                          实际上,每一行数据还有一个隐藏字段 roll_ptr。很多相关资料简单地描述“roll_ptr 用于指向该行数据的上一个版本”,但是该说法容易让人误解旧版本的数据是物理上真实存在的,好像有一张链表结构的历史记录表按顺序记录了每一个版本的数据。

                          -
                          - -

                          有些资料会特地强调旧版本的数据不是物理上真实存在的,undo log 是逻辑日志,记录了与实际操作语句相反的操作,旧版本的数据是通过 undo log 计算得到的。

                          -
                          - -
                          -

                          说实话,在不了解细节的前提下,通过计算得到旧版本的数据更加反直觉。总而言之,InnoDB 的数据总是存储最新版本,尽管该版本所属的事务可能尚未提交;任何事务其实都是从最新版本开始回溯,直到获得该事务认为可见的版本。

                          -
                          -

                          回滚日志的删除时机

                          回滚日志不会一直保留,在没有事务需要的时候,系统会自动判断和删除。基于该结论,我们应该避免使用长事务。长事务意味着系统里面可能会存在很老的 read view,这些事务可能访问数据库里的任何数据,所以在这个事务提交之前,数据库里它可能用到的回滚日志都必须保留,这就会导致大量存储空间被占用。在 MySQL 5.5 及之前的版本中,回滚日志是和数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,但只是代表那部分存储空间可复用,文件并不会变小,需要重建整个库才能解决问题。

                          -

                          一致性视图(read view)

                          一致性读视图(read view)又可以称之为快照(snapshot),它是基于整库的,但是它并不是真的拷贝了整个数据库的数据,否则随着数据量的增长,显然无法实现秒级创建快照。read view 可以理解为发出一个声明:“以我创建的时刻为准,如果一个数据版本所属的事务是在这之前提交的,就可见;如果是在这之后提交的,就不可见,需要回溯上一个版本判断,重复直到获得可见的版本;如果该数据版本属于当前事务自身,是可见的”。

                          -

                          以上声明类似于功能的需求描述,它比具体实现更简洁和易于理解。

                          +

                          通过 CAS 操作设置偏向锁中,Compare 操作是“测试 Mark Word 存储线程 IDbit 是否全部为 0,代表偏向的线程 IDnull”,Swap 操作是将当前线程 ID 设置到 Mark Word 的相应位置。

                          -

                          “快照”结合“多版本”等词,和 undo log 的情况类似很容易让人误解为有一个物理上真实存在的数据快照,但实际上 read view 只是在沿着数据版本链回溯时用于判断该版本对当前事务是否可见的依据。在具体实现上,InnoDB 为每一个事务构造了一个数组用于保存创建 read view 时,当前正在“活跃”的所有事务 ID ,其中“活跃”指的是启动了但尚未提交。数组中事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个数组和高水位就组成了当前事务的一致性视图(read view)。对于当前事务的 read view 而言,一个数据版本的 trx_id,有以下几种可能:

                          +

                          补充思考:

                            -
                          • 如果小于低水位,表示这个版本是已提交的事务生成的,可见
                          • -
                          • 如果大于等于高水位,表示这个版本是创建 read view 之后启动的事务,不可见
                          • -
                          • 如果大于等于低水位且小于高水位
                              -
                            • 如果这个版本的 trx_id 在数组中,表示这个版本是已启动但尚未提交的事务生成的,不可见
                            • -
                            • 如果这个版本的 trx_id 不在数组中,表示这个版本是已提交的事务生成的,可见
                            • -
                            -
                          • +
                          • “通过 CAS 操作将当前线程 ID 设置到 Mark Word”在偏向锁状态下是有且仅有一次的“偏向”动作。(此观点存疑,在《Java 并发编程的艺术》一书中有“重新偏向于其他线程”这样的描述,但是关于竞争偏向锁部分的原理难以理解。个人在测试中,不论是持有偏向锁的线程仍存活但已离开同步块,还是已死亡,后续线程都无法再获取到偏向锁,唯一一种不同线程获取到同一个偏向锁的情况是两个线程可以复用同一个局部变量表槽位,它们的 tid 相同,这代表着本质上 Mark Word 并无变化)
                          • +
                          • 当获得偏向锁的线程离开同步块时,没有“解锁操作”,Mark Word 维持不变。个人也不知道如何更准确地描述这个现象,从 synchronized 的语义来说,进出同步块代表着获取锁和释放锁;但是从偏向锁的实现来说,即便离开同步方法块,它仍然偏向原先获得锁的线程,甚至在讨论偏向锁发生竞争时,书中提到“检查持有偏向锁的线程是否存活”。个人更倾向于使用“撤销锁”一词描述偏向锁面临竞争时的处理,使用“释放锁”描述线程离开同步块时的处理。
                          • +
                          • 当获得偏向锁的线程再次访问同步块时,简单测试 Mark Word 里存储着当前线程 ID,如果成功即可进入同步块。
                          • +
                          • 计算过 hashCode 后偏向锁状态会变为其他状态,比如无锁状态,或者升级为轻量级锁甚至重量级锁,这符合 CAS 操作的判断条件。
                          -

                          InnoDB 利用“所有数据都有多个版本,每个版本都记录了所属事务 ID”这个特性,实现了“秒级创建快照”的能力。有了这个能力,系统里面随后发生的更新,就和当前事务可见的数据无关了,当前事务读取时也不必再加锁。

                          +

                          偏向锁撤销

                          偏向锁使用了一种等到竞争出现才撤销的机制,当获得偏向锁的线程离开同步块时,并没有“解锁操作”,Mark Word 将维持不变。当竞争出现时,从现象上说,如果持有偏向锁的线程已经离开同步块,则锁升级为轻量级锁;如果持有锁的线程尚未离开同步块,则锁直接升级为重量级锁。

                          -

                          以上“具体实现”相较于之前的“需求描述”显得有些啰嗦和复杂,然而这里的细节是值得推敲的。即便是林晓斌老师在《MySQL 实战 45 讲》中的详细讲解也让部分读者包括我本人感到困惑。

                          +

                          关于偏向锁的撤销,其原理晦涩难懂,个人仍有很多疑问:锁记录中存储偏向的线程 ID 的作用,检查持有偏向锁的线程是否存活的作用不符合测试结果,重新偏向于其他线程的复现条件。因为理解有限,不多赘述。

                          -

                          林晓斌老师的数据版本可见性示意图如下,容易让人产生误解的地方在于三段式的划分给人一种已提交的事务全都是小于低水位的错觉

                          -
                          +

                          测试偏向锁升级

                          匿名偏向锁->偏向锁
                          +

                          在一个匿名偏向锁状态的对象第一次被作为锁获取时,Mark Word 就会从匿名偏向锁变成偏向锁,并且再也不会变回到匿名偏向锁。

                          +
                          +

                          测试在匿名偏向锁状态下获取锁将变成偏向锁状态:

                          +
                          public static void main(String[] args) throws IOException, InterruptedException {
                          log.info("偏向锁基础测试:匿名偏向锁 -> 偏向锁");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          synchronized (lock) {
                          log.info("{} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          log.info("暂停,输入任意字符回车继续,可以使用 jstack 查看线程 tid 和 Mark Word 进行对比");
                          scanner.next();
                          }

                          log.info("偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 仍然不变");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          -

                          事实上,已提交事务的分布可能如下,大部分人的疑问其实只是“在大于等于低水位小于高水位的范围中,为什么会有已提交的事务”。

                          -
                          +

                          测试结果如下:

                          +
                          2023-12-21 00:34:39 - 偏向锁基础测试:匿名偏向锁 -> 偏向锁
                          2023-12-21 00:34:39 - sleep 4000ms,等待偏向锁激活
                          2023-12-21 00:34:43 - Mark Word 初始为 =====> 匿名偏向锁
                          2023-12-21 00:34:45 - java.lang.Object object internals:
                          OFF SZ TYPE DESCRIPTION VALUE
                          0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
                          8 4 (object header: class) 0xf80001e5
                          12 4 (object alignment gap)
                          Instance size: 16 bytes
                          Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

                          2023-12-21 00:34:45 - main 获取锁 =====> 偏向锁
                          2023-12-21 00:34:45 - java.lang.Object object internals:
                          OFF SZ TYPE DESCRIPTION VALUE
                          0 8 (object header: mark) 0x0000028761af3005 (biased: 0x00000000a1d86bcc; epoch: 0; age: 0)
                          8 4 (object header: class) 0xf80001e5
                          12 4 (object alignment gap)
                          Instance size: 16 bytes
                          Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

                          2023-12-21 00:34:45 - 暂停,输入任意字符回车继续,可以使用 jstack 查看线程 tid 和 Mark Word 进行对比
                          2023-12-21 00:34:55 - 偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 仍然不变
                          2023-12-21 00:34:55 - java.lang.Object object internals:
                          OFF SZ TYPE DESCRIPTION VALUE
                          0 8 (object header: mark) 0x0000028761af3005 (biased: 0x00000000a1d86bcc; epoch: 0; age: 0)
                          8 4 (object header: class) 0xf80001e5
                          12 4 (object alignment gap)
                          Instance size: 16 bytes
                          Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
                          -

                          要理解该问题需要理解另外一个问题——“创建 read view 的时机”。

                          -

                          创建 read view 的时机

                          很多资料介绍“可重复读”隔离级别下的 read view 创建时机为在事务启动时,但这并不严谨,还会导致理解 read view 数组困难。创建事务并不等于创建 read view

                          -
                          -

                          官方文档:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation.

                          -
                          -
                            -
                          • 对于“读提交”隔离级别,每次读操作都会重置快照。这意味着只要当前事务持续足够长的时间,它最后读取时完全可能熬到在它之前甚至之后创建的事务提交。
                          • -
                          • 对于“可重复读”隔离级别,在第一次执行快照读时创建快照。这意味着当前事务可以执行很多次以及很久的 update 语句后再执行读取,熬到在它之前甚至之后创建的事务提交。
                          • -
                          -

                          有些人可能想到了前者,但对于后者存疑或者不知道如何验证,其实测试并不复杂:

                          +

                          通过 jstack 获取线程 tid(以 Windows 为例):

                          +
                          jps | findstr "BiasedLockingBaseTest" | ForEach-Object { jstack $_.Split()[0]} | findstr "main"
                          "main" #1 prio=5 os_prio=0 tid=0x0000028761af3000 nid=0x8668 waiting on condition [0x000000ff7b8ff000]
                          at com.moralok.concurrency.ch2.BiasedLockingBaseTest.main(BiasedLockingBaseTest.java:27)
                          + +

                          关注 Mark Word 并转换为二进制表达:

                          - - + + - - - - - - - - - - + + - - + + - - + + - - + +
                          事务 A事务 B二进制表达
                          begin;begin;
                          update t set k = 2 where id = 2;(创建事务)
                          update t set k = 666 where id = 1;(创建事务)匿名偏向锁 Mark Word00000000000000000000000000000000000000000101
                          commit;偏向锁状态 Mark Word00101000011101100001101011110011000000000101
                          select * from t where id = 1;(创建 read view,k = 666)biased00000000000010100001110110000110101111001100
                          commit;main 线程 tid00101000011101100001101011110011000000000000
                          +
                            +
                          • 注意:存储的所谓“线程 ID”并非平时所说的线程 ID,该值左移可以得到 jstack 的返回结果中的 tidjol-core 打印了一个名为 biased 的值与之相同
                          • +
                          • 在离开同步方法块后,Mark Word 不变
                          • +
                          +
                          偏向锁->轻量级锁

                          测试当拥有偏向锁的线程已经离开同步块,其他线程尝试获取偏向锁(弱竞争),锁将升级为轻量级锁。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          Scanner scanner = new Scanner(System.in);
                          log.info("测试:当持有偏向锁的线程已经离开同步块,其他线程尝试获取偏向锁时,将获得轻量级锁");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.SECONDS.sleep(4);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          synchronized (lock) {
                          log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }

                          Thread thread = new Thread(() -> {
                          log.info("第二个线程 {} 尝试获取锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          synchronized (lock) {
                          log.info("第二个线程 {} 获取锁 =====> 轻量级锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread1");
                          thread.start();
                          thread.join();

                          log.info("离开同步块后轻量级锁释放 =====> 无锁状态(可偏向的)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +

                          有相关资料提到在拥有偏向锁的线程死亡后,锁可以偏向新的线程,但是验证失败。

                          +
                          public static void main(String[] args) throws IOException, InterruptedException {
                          log.info("测试:之前获得偏向锁的线程已死时,新线程获得的仍然是偏向锁");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          Thread thread1 = new Thread(() -> {
                          synchronized (lock) {
                          log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread1");
                          thread1.start();

                          Thread thread2 = new Thread(() -> {
                          try {
                          thread1.join();
                          } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                          }
                          boolean alive = thread1.isAlive();
                          log.info("第一个线程 {} 是否存活 {}", thread1.getName(), alive);
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          synchronized (lock) {
                          log.info("即使第一个线程已死亡,第二个线程 {} 获取锁 =====> 轻量级锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread2");
                          thread2.start();
                          thread2.join();

                          log.info("离开同步块后轻量级锁释放 =====> 无锁状态(可偏向的)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +
                          偏向锁->重量级锁

                          测试当拥有偏向锁的线程尚未离开同步块,其他线程尝试获取偏向锁(强竞争),锁将升级为重量级锁。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          Scanner scanner = new Scanner(System.in);
                          log.info("测试:当持有偏向锁的线程尚未离开同步块,其他线程尝试获取偏向锁时,将升级为重量级锁");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.SECONDS.sleep(4);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          Thread thread1 = new Thread(() -> {
                          synchronized (lock) {
                          log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          log.info("暂停,输入任意字符回车继续");
                          scanner.next();

                          log.info("第一个线程 {} 持有偏向锁,在同步块内发生竞争 =====> 升级为重量级锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          log.info("第一个线程 {} 结束", Thread.currentThread().getName());
                          }, "thread1");
                          thread1.start();

                          TimeUnit.SECONDS.sleep(1);

                          Thread thread2 = new Thread(() -> {
                          log.info("第二个线程 {} 尝试获取偏向锁失败", Thread.currentThread().getName());
                          synchronized (lock) {
                          log.info("第二个线程 {} 获取锁 =====> 重量级锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread2");
                          thread2.start();
                          thread2.join();

                          log.info("即使离开同步块后 =====> 重量级锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +
                          偏向锁->偏向锁(特例)

                          这是一个很奇怪的测试用例,它是在测试中唯一发生不同线程对同一个锁获得偏向锁的情况。但是排查过程中发现两个线程的 tid 相同,猜测是局部变量表槽位复用时有什么优化机制。

                          -

                          因此,严谨地说,创建事务的时机和创建一致性视图的时机是不同的。通过 start transaction with consistent snapshot; 可以在开启事务的同时立即创建 read view

                          +

                          卡了我好久,也没有探究到实质的新信息。

                          -

                          当前读和快照读

                          现在我们知道在 InnoDB 引擎中,一行数据存在多个版本。MVCC 使得在“可重复读”隔离级别下的事务好像与世无争。但是在以下示例中,事务 B 是在事务 A 的一致性视图之后创建和提交的,为什么事务 A 查询到的 k 为 3 呢?

                          - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                          事务 A事务 B
                          start transaction with consistent snapshot;(k = 1)
                          update t set k = k + 1 where id = 1;(自动提交事务)
                          update t set k = k + 1 where id = 1;当前读
                          select * from t where id = 1;(k = 3)
                          commit;
                          -

                          其实,更新数据是先读后写的,并且是“当前读”。

                          +
                          public static void main(String[] args) throws IOException, InterruptedException {
                          log.info("测试:之前获得偏向锁的线程已死时,新线程获得的仍然是偏向锁");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          Thread thread1 = new Thread(() -> {
                          synchronized (lock) {
                          log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread1");
                          thread1.start();
                          thread1.join();

                          Thread thread2 = new Thread(() -> {
                          synchronized (lock) {
                          log.info("第二个线程 {} 获取锁,=====> 偏向锁", Thread.currentThread().getName());
                          log.info("震惊!!!为什么两个 tid 相同啊,有什么复用机制吗");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          }, "thread2");
                          thread2.start();
                          thread2.join();

                          log.info("偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 不变");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +
                          匿名偏向锁状态计算 hashCode

                          在匿名偏向锁状态计算 hashCode,锁将变为无锁状态。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          log.info("测试:在匿名偏向锁状态计算 hashCode");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          int hashCode = lock.hashCode();
                          log.info("在计算 hashCode 后:Mark Word =====> 无锁状态(hash|age|0|01)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          synchronized (lock) {
                          log.info("获取锁 =====> 轻量级锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }

                          log.info("离开同步块后轻量级锁释放 =====> 无锁状态(hash|age|0|01)");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +
                          偏向锁状态无锁时计算 hashCode

                          在偏向锁状态无锁时计算 hashCode,锁将变为无锁状态。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          log.info("测试:在偏向锁状态无锁时计算 hashCode");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          synchronized (lock) {
                          log.info("获取锁 =====> 偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }

                          int hashCode = lock.hashCode();
                          log.info("离开同步块后再计算 hashCode:Mark Word =====> 无锁状态");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +
                          偏向锁状态加锁时计算 hashCode

                          在偏向锁状态加锁时计算 hashCode,锁将升级为重量级锁状态。

                          +
                          public static void main(String[] args) throws InterruptedException {
                          log.info("测试:在偏向锁状态计算 hashCode");

                          log.info("sleep 4000ms,等待偏向锁激活");
                          TimeUnit.MILLISECONDS.sleep(4000);

                          Object lock = new Object();
                          log.info("Mark Word 初始为 =====> 匿名偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          synchronized (lock) {
                          log.info("获取锁 =====> 偏向锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());

                          int hashCode = lock.hashCode();
                          log.info("在计算 hashCode 后:Mark Word =====> 重量级锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }

                          log.info("即使离开同步块后 =====> 重量级锁");
                          log.info(ClassLayout.parseInstance(lock).toPrintable());
                          }
                          + +

                          轻量级锁

                          轻量级锁加锁

                          获取偏向锁失败后续处理一(是否是偏向锁为 0):

                            -
                          • 当前读:读取一行数据的最新版本,并保证在读取时其他事务不能修改该行数据,因此需要在读取时加锁。以下操作属于当前读的情况:
                              -
                            • 共享锁:select lock in share mode
                            • -
                            • 排他锁:select for updateupdateinsertdelete
                            • +
                            • 检测锁标志位是否为 01 或者 00
                                +
                              • 如果否,则说明是重量级锁状态 =====> 获取轻量级锁失败后续处理一
                              • +
                              • 如果是,则说明是无锁状态或者轻量级锁状态,尝试通过 CAS 操作设置轻量级锁
                                  +
                                • 如果成功,说明获得轻量级锁
                                • +
                                • 如果失败,说明发生竞争 =====> 获取轻量级锁失败后续处理二
                              • -
                              • 快照读:在不加锁的情况下通过 select 读取一行数据,但和“读未提交”隔离级别下单纯地读取最新版本不同,它是基于一个“快照”进行读取。
                              -

                              因此在事务 A 中更新时,读取到的是事务 B 更新后的最新值,在事务 A 更新后,依据 read view 的可见性原则,它可以看到自身事务的更新后的最新值 3。

                              -

                              如果事务 B 尚未提交的情况下,事务 A 发起更新,会如何呢?这时候就轮到“两阶段锁协议”派上用场了:

                              +
                            • +
                            +
                            +

                            通过 CAS 操作设置轻量级锁中,Compare 操作是“测试 Mark Word 的锁标志位是否为 01,代表处于无锁状态”,Swap 操作是将 Mark Word 复制到栈中锁记录,并将指向栈中锁记录的指针设置到 Mark Word 的相应位置以及修改锁标志位。所谓“栈中锁记录”又称为 Displaced Mark WordJVM 会在当前线程的栈帧中创建用于存储锁记录的空间,用于在轻量级锁状态下临时存放 Mark Word

                            +
                            +
                            +

                            在轻量级锁状态下,明确提及了锁记录的作用,但偏向锁状态下,提及锁记录却并未加以解释。

                            +
                            +

                            获取偏向锁失败后续处理一:

                              -
                            • 事务 B 在更新时,对改行数据加排他锁,在事务 B 提交时才会释放
                            • -
                            • 当事务 A 发起更新,将阻塞直到事务 B 提交
                            • +
                            • 已经升级为重量级锁
                            - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                            事务 A事务 B
                            start transaction with consistent snapshot;(k = 1)
                            begin;
                            update t set k = k + 1 where id = 1;排他锁
                            update t set k = k + 1 where id = 1;阻塞至 B 提交
                            commit;
                            select * from t where id = 1;(k = 3)
                            commit;
                            -

                            至此,我们将锁和 MVCC 在事务隔离的实现原理中串联起来了。两者是互相独立又互相协作的两个机制,前者实现了“当前读”,后者实现了“快照读”。

                            -

                            总结

                            卡壳好几天,想到有不少好的文章却仍然会给读者留下困惑,想到自己在当初学习时对一些不严谨的表达抓耳挠腮想不通为什么,就有点不知道如何下笔。最终围绕着自己当初的一些困惑,一点一点修修补补完了。

                            -

                            参考文章

                              -
                            • 03 | 事务隔离:为什么你改了我还看不见
                            • -
                            • 08 | 事务到底是隔离的还是不隔离的?
                            • +

                              获取偏向锁失败后续处理二(通过 CAS 加偏向锁失败):

                              +
                                +
                              • 获取锁失败的线程将锁升级为重量级锁,修改 Mark Word指向互斥量(重量级锁)的指针|10(这个操作将影响到持有轻量级锁的线程的解锁)
                              • +
                              • 线程阻塞,等待唤醒
                              -]]> +

                              补充思考:

                              +
                                +
                              • 有相关资料提及偏向锁并非直接升级到重量级锁,无法验证是否总是有轻量级锁作为中间状态
                              • +
                              • 轻量级锁面临竞争时升级为重量级锁的过程相比于偏向锁面临竞争时的升级过程,更加容易理解,后者好多细节没有找到令人信服的答案。
                              • +
                              +

                              轻量级锁解锁

                              轻量级锁解锁时,会通过 CAS 操作解锁,Compare 操作是“测试 Mark Word 的锁标志位是否为 00,代表处于轻量级锁状态,Swap 操作是将栈中锁记录 Dispaced Mark Word 替换回对象头的 Mark Word 以及修改锁标志位。
                              如果 Compare 操作失败,则代表发生竞争,此时锁已经被其他线程升级为重量级锁以及 Mark Word 被修改为指向互斥量(重量级锁)的指针|10。持有轻量级锁的线程会释放锁(直接将 Dispaced Mark Word 替换回 Mark Word?)并唤醒等待的线程,开启新的一轮争抢。

                              +

                              测试轻量级锁升级

                              无锁->轻量级锁

                              测试在无锁状态下获取锁,锁将变成轻量级锁状态。

                              +
                              public static void main(String[] args) throws IOException, InterruptedException {
                              Scanner scanner = new Scanner(System.in);
                              log.info("轻量级锁基础测试:无锁状态 -> 轻量级锁");

                              Object lock = new Object();
                              log.info("在偏向锁激活之前创建的对象为 =====> 无锁状态(可偏向额)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("即使是单线程无竞争获取锁,=====> 轻量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              log.info("暂停,回车继续");
                              scanner.nextLine();
                              }

                              log.info("离开同步块后,-> 无锁状态(可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              + +
                              无锁状态计算 hashCode

                              在无锁状态计算 hashCode,仍然是无锁状态。

                              +
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:在无锁状态计算 hashCode");

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 无锁状态");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              int hashCode = lock.hashCode();
                              log.info("在计算 hashCode 后:Mark Word =====> 无锁状态(hash|age|0|01)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("获取锁 =====> 轻量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              log.info("离开同步块后轻量级锁释放 =====> 无锁状态(hash|age|0|01)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              + +
                              轻量级锁加锁时计算 hashCode

                              在轻量级锁状态加锁时计算 hashCode,锁将升级为重量级锁状态。

                              +
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:在轻量级锁状态计算 hashCode");

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 无锁状态");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("获取锁 =====> 轻量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              int hashCode = lock.hashCode();
                              log.info("在计算 hashCode 后:Mark Word =====> 重量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              log.info("即使离开同步块后 =====> 重量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              + +

                              参考文章

                              +]]> - mysql + java + lock + synchronized - 不使用 GParted 的情况下为 VMware 中的 Ubuntu Server 增大磁盘空间 - /2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/ - GParted 是一款适用于 Linux 的图形化磁盘分区管理工具,通过它可以便捷地为 VMware 中的 Ubuntu Desktop 增大磁盘空间。然而你可能正在使用 Ubuntu Server,并不想要安装或并不被允许安装图形化界面,本文介绍了如何在不使用 GParted 的情况下,通过命令行使用自带的工具为 VMware 中的 Ubuntu Server 增大磁盘空间。

                              + Unsafe,一个“反 Java”的 class + /2023/12/25/Unsafe-an-anti-Java-class/ + Unsafe 类位于 sun.misc 包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe 类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CASCompare and Swap)操作等。本文将介绍这个“反 Java”的类及其方法的典型使用场景。

                              -

                              请注意辨别磁盘空间是真的接近耗尽,而不是在系统安装时只真正使用了大约一半空间。参见 Ubuntu server 20.04 安装后没有分配全部磁盘空间

                              +

                              由于 Unsafe 类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过 Java 语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用 Unsafe 类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用 Unsafe 类来进行一些性能优化。

                              -

                              背景介绍

                              环境如下:

                              -
                                -
                              • VMware® Workstation 17 Pro 17.5.0 build-22583795
                              • -
                              • Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-169-generic x86_64)
                              • -
                              -

                              尽管最初按照心理预期为 Ubuntu Server 分配了 50G 的磁盘空间,主要用于运行一些 Docker 容器,但是不知不觉之间发现磁盘空间的占用率还是上升到了 90%。一时之间想不到可以清理什么,决定先增大一些磁盘空间。

                              -
                                -
                              • 使用 df -h 命令显示文件系统的总空间和可用空间信息。可知 /dev/mapper/ubuntu--vg-ubuntu--lv 已使用 95%
                                $ df -h
                                Filesystem Size Used Avail Use% Mounted on
                                udev 7.8G 0 7.8G 0% /dev
                                tmpfs 1.6G 3.0M 1.6G 1% /run
                                /dev/mapper/ubuntu--vg-ubuntu--lv 48G 43G 2.5G 95% /
                                tmpfs 7.8G 0 7.8G 0% /dev/shm
                                tmpfs 5.0M 0 5.0M 0% /run/lock
                                tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
                                vmhgfs-fuse 932G 859G 73G 93% /mnt/hgfs
                                /dev/sda2 2.0G 209M 1.6G 12% /boot
                                /dev/loop0 64M 64M 0 100% /snap/core20/2015
                                /dev/loop1 64M 64M 0 100% /snap/core20/2105
                                /dev/loop2 41M 41M 0 100% /snap/snapd/20290
                                /dev/loop3 92M 92M 0 100% /snap/lxd/24061
                                /dev/loop4 41M 41M 0 100% /snap/snapd/20671
                                tmpfs 1.6G 0 1.6G 0% /run/user/1000
                              • -
                              -

                              解决步骤

                              调整虚拟磁盘大小

                              -

                              不论如何,需要先修改 VMware 的相关设置。

                              +
                              +

                              尽管在生产中需要谨慎使用 Unsafe,但是可以在测试中使用它来更真实地接触 Java 对象在内存中的存储结构,验证自己的理论知识。

                              +
                              +

                              获取 Unsafe 实例

                              +

                              Java 9 及之后的版本中,Unsafe 类中的 getUnsafe() 方法被标记为不安全(Unsafe),不再允许普通的 Java 应用程序代码通过此方法获取 Unsafe 实例。这是为了提高 Java 的安全性,防止滥用 Unsafe 类的功能。

                              +

                              在正常的 Java 应用程序中,获取 Unsafe 实例是不被推荐的,因为它违反了 Java 语言的安全性和封装原则。Unsafe 类的设计本意是为了 Java 库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe 对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe 对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。

                              +

                              获取 Unsafe 实例的静态方法如下:

                              +
                              @CallerSensitive
                              public static Unsafe getUnsafe() {
                              Class<?> caller = Reflection.getCallerClass();
                              // 检查调用方法的类是被引导类加载器所加载
                              if (!VM.isSystemDomainLoader(caller.getClassLoader()))
                              throw new SecurityException("Unsafe");
                              return theUnsafe;
                              }
                              + +

                              Unsafe 使用单例模式,可以通过静态方法 getUnsafe 获取 Unsafe 实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe 实例有以下两种可行方案:

                                -
                              1. 先将客户机 Ubuntu server 关机
                              2. -
                              3. 然后通过“虚拟机设置 -> 硬盘 -> 扩展 -> 最大磁盘大小”将最大虚拟磁盘大小设置为目标值(50G -> 80G
                              4. -
                              5. 根据提示可知,在 VMware 中的扩展操作仅增大虚拟磁盘的大小,分区和文件系统的大小不受影响。你必须从客户机操作系统内部对磁盘重新进行分区和扩展文件系统。
                              6. -
                              -

                              调整分区大小

                              分区管理

                                -
                              1. 使用 sudo cfdisk 命令进入分区管理的交互式界面。可知可用空间为新增的 30G
                              2. -
                              3. 使用上下方向键选择准备调整大小的分区 /dev/sda3,使用左右方向键选择 Resize 操作。
                              4. -
                              5. 输入新的分区大小,默认为原大小加上可用空间大小等于 78G
                              6. -
                              7. 使用左右方向键选择 Write 操作,写入修改。然后输入 yes 确认。
                              8. -
                              9. 提示分区表已改变。然后使用左右方向键选择 Quit 操作,退出分区管理的交互式界面。
                              10. -
                              11. 退出时提示如下。
                                $ sudo cfdisk
                                GPT PMBR size mismatch (104857599 != 167772159) will be corrected by write.

                                Syncing disks.
                              12. -
                              -

                              调整物理卷大小

                                -
                              1. 使用 sudo pvresize /dev/sda3 命令调整 LVM 中物理卷的大小。
                                $ sudo pvresize /dev/sda3
                                Physical volume "/dev/sda3" changed
                                1 physical volume(s) resized or updated / 0 physical volume(s) not resized
                              2. -
                              -

                              调整逻辑卷大小

                                -
                              1. 使用 sudo fdisk -l 命令显示物理卷和逻辑卷的大小差异。在末尾可见 /dev/sda3 的大小为 78G/dev/mapper/ubuntu--vg-ubuntu--lv 的大小为 47.102G
                                $ sudo fdisk -l
                                Disk /dev/loop0: 63.48 MiB, 66547712 bytes, 129976 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop1: 63.93 MiB, 67014656 bytes, 130888 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop2: 40.88 MiB, 42840064 bytes, 83672 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop3: 91.85 MiB, 96292864 bytes, 188072 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop4: 40.44 MiB, 42393600 bytes, 82800 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/fd0: 1.42 MiB, 1474560 bytes, 2880 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                                Disklabel type: dos
                                Disk identifier: 0x90909090

                                Device Boot Start End Sectors Size Id Type
                                /dev/fd0p1 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p2 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p3 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p4 2425393296 4850786591 2425393296 1.1T 90 unknown


                                Disk /dev/sda: 80 GiB, 85899345920 bytes, 167772160 sectors
                                Disk model: VMware Virtual S
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                                Disklabel type: gpt
                                Disk identifier: 81C6F71E-C634-49E6-BC3D-9272C86326A4

                                Device Start End Sectors Size Type
                                /dev/sda1 2048 4095 2048 1M BIOS boot
                                /dev/sda2 4096 4198399 4194304 2G Linux filesystem
                                /dev/sda3 4198400 167772126 163573727 78G Linux filesystem


                                Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 47.102 GiB, 51535413248 bytes, 100655104 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                              2. -
                              3. 使用 sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 命令调整逻辑卷的大小。
                                $ sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
                                Size of logical volume ubuntu-vg/ubuntu-lv changed from <48.00 GiB (12287 extents) to <78.00 GiB (19967 extents).
                                Logical volume ubuntu-vg/ubuntu-lv successfully resized.
                              4. +
                              5. 通过 -Xbootclasspath/a:${path} 把调用方法的类所在的 jar 包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径
                              6. +
                              7. 通过反射获取 Unsafe 类中的 Unsafe 实例
                                private static Unsafe getUnsafe() {
                                try {
                                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                                f.setAccessible(true);
                                return (Unsafe) f.get(null);
                                } catch (NoSuchFieldException | IllegalAccessException e) {
                                throw new RuntimeException(e);
                                }
                                }
                              -

                              调整文件系统大小

                                -
                              1. 使用 sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv 命令调整文件系统的大小。
                                $ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
                                resize2fs 1.45.5 (07-Jan-2020)
                                Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
                                old_desc_blocks = 6, new_desc_blocks = 10
                                The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 20446208 (4k) blocks long.
                              2. -
                              3. 使用 df -h 命令显示文件系统的总空间和可用空间信息。确认 /dev/mapper/ubuntu--vg-ubuntu--lv 的大小已调整为 77G
                                $ df -h
                                Filesystem Size Used Avail Use% Mounted on
                                udev 7.8G 0 7.8G 0% /dev
                                tmpfs 1.6G 3.1M 1.6G 1% /run
                                /dev/mapper/ubuntu--vg-ubuntu--lv 77G 43G 31G 59% /
                                tmpfs 7.8G 0 7.8G 0% /dev/shm
                                tmpfs 5.0M 0 5.0M 0% /run/lock
                                tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
                                vmhgfs-fuse 932G 859G 73G 93% /mnt/hgfs
                                /dev/sda2 2.0G 209M 1.6G 12% /boot
                                /dev/loop0 64M 64M 0 100% /snap/core20/2015
                                /dev/loop1 64M 64M 0 100% /snap/core20/2105
                                /dev/loop2 41M 41M 0 100% /snap/snapd/20290
                                /dev/loop3 92M 92M 0 100% /snap/lxd/24061
                                /dev/loop4 41M 41M 0 100% /snap/snapd/20671
                                tmpfs 1.6G 0 1.6G 0% /run/user/1000
                              4. +

                                内存操作

                                Unsafe 类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java 语言的内置安全性和类型检查。以下是一些常见的 Unsafe 类中关于内存操作的方法:

                                +
                                  +
                                • allocateMemory: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。
                                  public native long allocateMemory(long bytes);
                                • +
                                • reallocateMemory: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。
                                  public native long reallocateMemory(long address, long bytes);
                                • +
                                • freeMemory: 释放之前由 allocateMemoryreallocateMemory 分配的内存。
                                  public native void freeMemory(long address);
                                • +
                                • setMemory: 将给定内存块中的所有字节设置为固定值(通常为零)。
                                  public native void setMemory(Object o, long offset, long bytes, byte value);
                                  public void setMemory(long address, long bytes, byte value) {
                                  setMemory(null, address, bytes, value);
                                  }
                                • +
                                • copyMemory: 复制指定长度的内存块
                                  public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
                                • +
                                • putXxx: 将指定偏移量处的内存设置为指定的值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                                  public native void putObject(Object o, long offset, Object x);
                                • +
                                • getXxx: 从指定偏移量处的内存读取值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                                  public native Object getObject(Object o, long offset);
                                • +
                                • putXxxgetXxx 也提供了按绝对基地址操作内存的方法。
                                  public native byte getByte(long address);
                                  public native void putByte(long address, byte x);
                                • +
                                +

                                从内存读取值时,除非满足以下情况之一,否则结果不确定:

                                +
                                  +
                                1. 偏移量是通过 objectFieldOffset 从字段的 Field 对象获取的,o 指向的对象的类与字段所属的类兼容。
                                2. +
                                3. 偏移量和 o 指向的对象(无论是否为 null)分别是通过 staticFieldOffsetstaticFieldBaseField 对象获得的。
                                4. +
                                5. o 指向的是一个数组,偏移量是一个形式为 B+N*S 的整数,其中 N 是数组的有效索引,BS 分别是通过 arrayBaseOffsetarrayIndexScale 获得的值。
                                +
                                +

                                做一些“不确定”的测试,比如使用 byte 相关的方法操作 int 所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。

                                +
                                +

                                分配堆外内存

                                Java NIONew I/O)中,分配堆外内存使用了 Unsafe 类的 allocateMemory 方法。堆外内存是一种在 Java 虚拟机之外分配的内存,它不受 Java 堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O 操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java 堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting)的机制来处理。

                                +
                                DirectByteBuffer(int cap) {
                                super(-1, 0, cap, cap);
                                boolean pa = VM.isDirectMemoryPageAligned();
                                int ps = Bits.pageSize();
                                long size = Math.max(1L, (long)cap + (pa ? ps : 0));
                                Bits.reserveMemory(size, cap);

                                long base = 0;
                                try {
                                // 分配本地内存
                                base = unsafe.allocateMemory(size);
                                } catch (OutOfMemoryError x) {
                                Bits.unreserveMemory(size, cap);
                                throw x;
                                }
                                // 初始化本地内存
                                unsafe.setMemory(base, size, (byte) 0);
                                if (pa && (base % ps != 0)) {
                                // Round up to page boundary
                                address = base + ps - (base & (ps - 1));
                                } else {
                                address = base;
                                }
                                // 使用虚引用 Cleaner 对象跟踪 DirectByteBuffer 对象的垃圾回收
                                cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
                                att = null;
                                }
                                + +

                                DirectByteBuffer 对象仅被 Cleaner 对象(虚引用)引用时,它可以在任意一次 GC 中被垃圾回收。在 DirectByteBuffer 对象被垃圾回收后,Cleaner 对象会被加入到引用队列,ReferenceHandler 线程将调用 Deallocator 对象的 run 方法,从而实现本地内存的自动释放。

                                +
                                private static class Deallocator implements Runnable {

                                private static Unsafe unsafe = Unsafe.getUnsafe();

                                private long address;
                                private long size;
                                private int capacity;

                                private Deallocator(long address, long size, int capacity) {
                                assert (address != 0);
                                this.address = address;
                                this.size = size;
                                this.capacity = capacity;
                                }

                                public void run() {
                                if (address == 0) {
                                // Paranoia
                                return;
                                }
                                // 释放本地内存
                                unsafe.freeMemory(address);
                                address = 0;
                                Bits.unreserveMemory(size, capacity);
                                }

                                }
                                + +

                                CAS 相关

                                Unsafe 提供了 3CAS 相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU 会自动将该位置更新为新值,否则,CPU 不做任何操作。这些方法的底层实现对应着 CPU 指令 cmpxchg

                                +
                                // 如果 Java 变量当前符合预期,则自动将其更新为 x。
                                public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
                                public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
                                public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
                                + +

                                AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 UnsafeobjectFieldOffset 方法获取。

                                +
                                public class AtomicInteger extends Number implements java.io.Serializable {
                                private static final long serialVersionUID = 6214790243416807050L;

                                // setup to use Unsafe.compareAndSwapInt for updates
                                private static final Unsafe unsafe = Unsafe.getUnsafe();
                                private static final long valueOffset;

                                static {
                                try {
                                valueOffset = unsafe.objectFieldOffset
                                (AtomicInteger.class.getDeclaredField("value"));
                                } catch (Exception ex) { throw new Error(ex); }
                                }

                                private volatile int value;
                                }
                                + +

                                CAS 更新变量的值的内存变化如下:

                                +
                                + +

                                配合 ClassLayout 打印 AtomicInteger 的内部结构更直观地感受 offset 的含义:

                                +
                                java.util.concurrent.atomic.AtomicInteger object internals:
                                OFF SZ TYPE DESCRIPTION VALUE
                                0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                                8 4 (object header: class) 0xf8003dbc
                                12 4 int AtomicInteger.value 1
                                Instance size: 16 bytes
                                Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                                + + +

                                参考文章

                                ]]> - linux - ubuntu + java - 使用 Vim - /2024/01/18/use-vim/ - 本文记录了 Vim 常用的快捷键作为备忘清单。

                                + Java 类 Reference 的源码分析 + /2023/12/27/source-code-analysis-of-Java-class-Reference/ + 我们知道 Java 扩充了“引用”的概念,引入了软引用、弱引用和虚引用,它们都属于 Reference 类型,也都可以配合 ReferenceQueue 使用。你是否好奇常常被一笔带过的“引用对象的处理过程”?你是否在探究 NIO 堆外内存的自动释放时看到了 Cleaner 的关键代码但不太能梳理整个过程?你是否好奇在研究 JVM 时偶尔看到的 Reference Handler 线程?本文将分析 ReferenceReferenceQueue 的源码带你理解引用对象的工作机制。

                                -

                                常用快捷键

                                移动光标

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                h, 光标向左移动一个字符
                                j, 光标向下移动一个字符
                                k, 光标向上移动一个字符
                                l, 光标向右移动一个字符
                                Ctrl + f, Page Down屏幕向下移动一页
                                Ctrl + b, Page Up屏幕向上移动一页
                                0光标移动至本行开头
                                $光标移动至本行末尾
                                G光标移动至文件最后一行
                                nG光标移动至文件第n行
                                gg光标移动至文件第一行
                                n<Enter>光标向下移动n行
                                n<space>光标向右移动n个字符
                                ^光标移动至本行第一个非空字符处
                                w光标移动到下一个词 (上一个字母和数字组成的词之后)
                                W光标移动到下一个词 (以空格分隔的词)
                                b光标移动到上一个词 (下一个字母和数字组成的词之前)
                                B光标移动到上一个词 (以空格分隔的词)
                                -

                                查找和替换

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                /word向光标之后搜索word
                                ?word向光标之前搜索word
                                n重复前一个查找操作
                                N反向进行前一个查找操作
                                :n1,n2s/original/replacement/g在第n1行到第n2行之间查找original并替换为replacement
                                :1,$s/original/replacement/g在第1行到最后一行之间查找original并替换为replacement
                                :1,$s/original/replacement/gc在第1行到最后一行之间查找original并替换为replacement,替换前需确认
                                :%s/original/replacement在所有行中查找行中第一个出现的original并替换为replacement
                                -

                                替换格式如下 :[range]s/<pattern>/[string]/[flags] [count]

                                +

                                事实上,个人感觉在无相关前置知识的情况下,单纯看 JDKJava 代码是没办法很好地理解引用对象是如何被添加到引用队列中的。因为 Referencepending 字段的含义和赋值操作是隐藏在 JVMC++ 代码中,本文搁置了其中的细节,仅分析 JDK 中相关的 Java 代码。

                                -

                                删除/复制/粘贴

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                x向后删除一个字符,相当于 Del
                                X向前删除一个字符,相当于 Backspace
                                nx向前删除n个字符
                                dd删除(剪切)光标所在的行
                                ndd删除(剪切)光标所在开始的n行
                                d1G删除(剪切)光标所在到第1行的所有行
                                dG删除(剪切)光标所在到最后一行的所有行
                                d$删除(剪切)光标所在到该行的最后一个字符
                                d0删除(剪切)光标所在到该行的第一个字符
                                yy复制光标所在的行
                                nyy复制光标所在开始的n行
                                y1G复制光标所在到第1行的所有行
                                yG复制光标所在到最后一行的所有行
                                y$复制光标所在到该行的最后一个字符
                                y0复制光标所在到该行的第一个字符
                                p将复制的内容粘贴到光标所在的下一行
                                P将复制的内容粘贴到光标所在的上一行
                                u恢复前一个操作
                                Ctrl+r重做上一个操作
                                .重复前一个操作
                                -

                                进入编辑模式

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                i进入插入模式,从光标所在处开始插入
                                I进入插入模式,从光标所在行的第一个非空格开始插入
                                a进入插入模式,从光标所在的下一个字符处开始插入
                                A进入插入模式,从光标所在行的最后一个字符处开始插入
                                o进入插入模式,在光标所在行的下一行插入新的一行
                                O进入插入模式,在光标所在行的上一行插入新的一行
                                r进入替换模式,只会替换光标所在的字符一次
                                R进入替换模式,替换光标所在的字符,直到通过Esc退出
                                Esc退出编辑模式,回到一般命令模式
                                -

                                保存和退出

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                :w保存文件
                                :w!若文件为只读,强制保存
                                :q退出 Vim,如果文件已修改,将退出失败
                                :q!强制退出 Vim,不保存文件修改
                                :wq保存文件并退出 Vim
                                :w filename另存为新文件
                                ZZ退出 Vim,若文件无修改,则不保存退出;如果文件已修改,保存并退出
                                :r filename读入另一个文件的数据并添加到光标所在行之后
                                -

                                额外功能

                                可视模式

                                - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                v字符选择,将光标经过的地方反白选择
                                V行选择,将光标经过的行反白选择
                                Ctrl + v区块选择,用矩形的方式反白选择
                                y复制反白选择的地方
                                d删除反白选择的地方
                                ~对反白选择的地方切换大小写
                                -

                                多文件编辑

                                - - - - - - - - - - - - - - - - - - -
                                快捷键功能
                                :n编辑下一个文件
                                :N编辑上一个文件
                                :files列出当前 Vim 打开的所有文件
                                -

                                多窗口功能

                                +

                                Reference

                                Reference引用对象的抽象基类。此类定义了所有引用对象通用的操作。由于引用对象是与垃圾收集器密切合作实现的,因此该类可能无法直接子类化。

                                +
                                + +

                                构造函数

                                  +
                                • referent: 引用对象关联的对象
                                • +
                                • queue: 引用对象准备注册到的引用队列
                                • +
                                +

                                Reference 提供了两个构造函数,一个需要传入引用队列ReferenceQueue),一个不需要。如果一个引用对象Reference)注册到一个引用队列,在检测到关联对象有适当的可达性变化后,垃圾收集器将把该引用对象添加到该引用队列。

                                +
                                +

                                “关联对象有适当的可达性变化”并不容易理解,在很多表述中它很容易被简化为“可以被回收”,但是同时我们又拥有另一条规则,即“一个对象是否可回收的判断依据是是否从 Root 对象可达”。在面对 Reference 的子类时,我们有种割裂感,好像一条和谐的规则出现了特殊条例。探索 Java 类 Cleaner 和 Finalizer

                                +
                                +
                                Reference(T referent) {
                                this(referent, null);
                                }

                                Reference(T referent, ReferenceQueue<? super T> queue) {
                                this.referent = referent;
                                // ReferenceQueue.NULL 表示没有注册到引用队列
                                this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
                                }
                                + +

                                属性

                                成员变量

                                  +
                                • referent: 引用对象关联的对象,该对象将被垃圾收集器特殊对待。我们很难直观地感受何谓“被垃圾收集器特殊对待”,它对应着“在检测到关联对象有适当的可达性变化后,垃圾收集器将把引用对象添加到该引用队列”。
                                • +
                                • queue: 引用对象注册到的引用队列
                                • +
                                • next: 用于指向下一个引用对象,当引用对象已经添加到引用队列中,next 指向引用队列中的下一个引用对象
                                • +
                                • discovered: 用于指向下一个引用对象,用于在全局的 pending 链表中,指向下一个待添加到引用队列引用对象
                                • +
                                +
                                + +

                                静态变量

                                +

                                注意:lockpending 是全局共享的。

                                +
                                +
                                  +
                                • lock: 用于与垃圾收集器同步的对象,垃圾收集器必须在每个收集周期开始时获取此锁。因此至关重要的是持有此锁的任何代码必须尽快运行完,不分配新对象并避免调用用户代码。
                                • +
                                • pending: 等待加入引用队列引用对象链表。垃圾收集器将引用对象添加到 pending 链表中,而 Reference-Handler 线程将删除它们,并做清理或入队操作。pending 链表受上述 lock 对象的保护,并使用 discovered 字段来链接下一个元素。
                                • +
                                +
                                public abstract class Reference<T> {
                                private T referent; /* Treated specially by GC */

                                volatile ReferenceQueue<? super T> queue;
                                @SuppressWarnings("rawtypes")
                                volatile Reference next;

                                transient private Reference<T> discovered; /* used by VM */

                                static private class Lock { }
                                private static Lock lock = new Lock();

                                private static Reference<Object> pending = null;
                                }
                                + +
                                +

                                Reference 其实可以理解为单链表中的一个节点,除了核心的 referentqueuenextdiscovered 都用于指向下一个引用对象,只是分别用于两条不同的单链表上。

                                +
                                +

                                pending 链表:

                                +
                                + +

                                ReferenceQueue

                                +
                                + +

                                ReferenceHandler 线程

                                启动任意一个非常简单的 Java 程序,通过 JVM 相关的工具,比如 JConsole,你都能看到一个名为 Reference Handler 的线程。

                                +
                                + +

                                ReferenceHandler 类本身的代码并不复杂。

                                +
                                private static class ReferenceHandler extends Thread {
                                // 确保类已经初始化
                                private static void ensureClassInitialized(Class<?> clazz) {
                                try {
                                Class.forName(clazz.getName(), true, clazz.getClassLoader());
                                } catch (ClassNotFoundException e) {
                                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
                                }
                                }

                                static {
                                // 预加载和初始化 InterruptedException 和 Cleaner,以避免在 run 方法中懒加载发生内存不足时陷入麻烦(咱也不知道具体啥麻烦)
                                ensureClassInitialized(InterruptedException.class);
                                ensureClassInitialized(Cleaner.class);
                                }

                                ReferenceHandler(ThreadGroup g, String name) {
                                super(g, name);
                                }

                                public void run() {
                                // run 方法循环调用 tryHandlePending
                                while (true) {
                                tryHandlePending(true);
                                }
                                }
                                }
                                + +

                                创建线程并启动

                                Reference-Handler 线程是通过静态代码块创建并启动的。

                                +
                                static {
                                // 不断获取父线程组,直到最高的系统线程组
                                ThreadGroup tg = Thread.currentThread().getThreadGroup();
                                for (ThreadGroup tgn = tg;
                                tgn != null;
                                tg = tgn, tgn = tg.getParent());
                                Thread handler = new ReferenceHandler(tg, "Reference Handler");
                                // 设置为最高优先级
                                handler.setPriority(Thread.MAX_PRIORITY);
                                // 设置为守护线程
                                handler.setDaemon(true);
                                handler.start();

                                // provide access in SharedSecrets
                                // 不懂,看到一个说法覆盖 JVM 的默认处理方式
                                SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
                                @Override
                                public boolean tryHandlePendingReference() {
                                return tryHandlePending(false);
                                }
                                });
                                }
                                + +

                                run 处理逻辑

                                run 方法的核心处理逻辑。本质上,ReferenceHandler 线程将 pending 链表上的引用对象分发到各自注册的引用队列中。如果理解了 Reference 作为单链表节点的一面,这部分代码不难理解,反而是其中应对 OOME 的处理很值得关注,但更多的可能是看了个寂寞,不好重现问题并验证。

                                +
                                static boolean tryHandlePending(boolean waitForNotify) {
                                Reference<Object> r;
                                Cleaner c;
                                try {
                                // 加锁(和垃圾回收共用一个锁)
                                synchronized (lock) {
                                // 如果不为 null
                                if (pending != null) {
                                // 获取头节点
                                r = pending;
                                // instanceof 可能抛出 OutOfMemoryError,因此在把 r 从 pending 链表中移除前进行
                                // 如果是 Cleaner 类型,进行类型转换,后续有特殊处理
                                c = r instanceof Cleaner ? (Cleaner) r : null;
                                // 从 pending 链表移除 r
                                pending = r.discovered;
                                r.discovered = null;
                                } else {
                                // 等待锁可能抛出 OutOfMemoryError,因为可能需要分配 exception 对象
                                if (waitForNotify) {
                                lock.wait();
                                }
                                // retry if waited
                                return waitForNotify;
                                }
                                }
                                } catch (OutOfMemoryError x) {
                                // 给其他线程 CPU 时间,以便它们能够丢弃一些存活的引用,然后通过 GC 回收一些空间
                                // 还可以防止 CPU 密集运行以至于上面的“r instanceof Cleaner”在一段时间内持续抛出 OOME
                                Thread.yield();
                                // retry
                                return true;
                                } catch (InterruptedException x) {
                                // retry
                                return true;
                                }

                                // 如果是 Cleaner 类型,快速清理并返回
                                if (c != null) {
                                c.clean();
                                return true;
                                }

                                // 如果 Reference 对象关联了引用队列,则添加到队列
                                ReferenceQueue<? super Object> q = r.queue;
                                if (q != ReferenceQueue.NULL) q.enqueue(r);
                                return true;
                                }
                                + +

                                关联对象和队列相关方法

                                /* -- Referent accessor and setters -- */

                                // 获取关联对象
                                public T get() {
                                return this.referent;
                                }

                                // 清理关联对象,该操作不会导致引用对象入队
                                public void clear() {
                                this.referent = null;
                                }

                                /* -- Queue operations -- */

                                // 判断引用对象是否已入队,如果未关联引用队列,则返回 false
                                public boolean isEnqueued() {
                                return (this.queue == ReferenceQueue.ENQUEUED);
                                }

                                // 将引用对象添加到其注册的引用队列中,该方法仅 Java 代码调用,JVM 不需要调用本方法可以直接进行入队操作(什么情况下?)
                                public boolean enqueue() {
                                return this.queue.enqueue(this);
                                }
                                + +

                                ReferenceQueue

                                引用队列,在检测到适当的可达性更改后,垃圾收集器将已注册的引用对象添加到该队列。

                                +

                                属性

                                public class ReferenceQueue<T> {

                                // 构造函数
                                public ReferenceQueue() { }

                                // 一个不可入队的队列
                                private static class Null<S> extends ReferenceQueue<S> {
                                boolean enqueue(Reference<? extends S> r) {
                                return false;
                                }
                                }
                                // 用于表示一个引用对象没有注册到引用队列
                                static ReferenceQueue<Object> NULL = new Null<>();
                                // 用于表示一个引用对象已经添加到引用队列
                                static ReferenceQueue<Object> ENQUEUED = new Null<>();

                                // 锁对象
                                static private class Lock { };
                                private Lock lock = new Lock();
                                // 头节点
                                private volatile Reference<? extends T> head = null;
                                // 队列长度
                                private long queueLength = 0;
                                }
                                + +

                                入队

                                enqueue 只能由 Reference 类调用。

                                +

                                引用对象queue 字段可以表达引用对象的状态:

                                +
                                  +
                                • NULL:表示没有注册到引用队列或者已经从引用队列中移除
                                • +
                                • ENQUEUED:表示已经添加到引用队列
                                • +
                                +
                                boolean enqueue(Reference<? extends T> r) {
                                synchronized (lock) {
                                // 检查引用对象的状态是否可以入队
                                ReferenceQueue<?> queue = r.queue;
                                if ((queue == NULL) || (queue == ENQUEUED)) {
                                return false;
                                }
                                // 检查注册的 queue 和调用的 queue 是否相同
                                assert queue == this;
                                // 标记为已入队
                                r.queue = ENQUEUED;
                                // 头插法,最后一个节点的 next 指向自身(为什么?)
                                r.next = (head == null) ? r : head;
                                head = r;
                                // 队列长度加一
                                queueLength++;
                                if (r instanceof FinalReference) {
                                sun.misc.VM.addFinalRefCount(1);
                                }
                                // 通知等待的线程
                                lock.notifyAll();
                                return true;
                                }
                                }
                                + +

                                出队

                                轮询队列以查看是否有引用对象可用,如果存在可用的引用对象则将其从队列中删除并返回,否则该方法立即返回 null

                                +
                                public Reference<? extends T> poll() {
                                // 缩小锁的范围
                                if (head == null)
                                return null;
                                synchronized (lock) {
                                return reallyPoll();
                                }
                                }

                                private Reference<? extends T> reallyPoll() {
                                Reference<? extends T> r = head;
                                if (r != null) {
                                @SuppressWarnings("unchecked")
                                Reference<? extends T> rn = r.next;
                                // 因为尾节点的 next 指向自身
                                head = (rn == r) ? null : rn;
                                // 标记为 NULL,避免再次入队
                                r.queue = NULL;
                                // next 指向自己
                                r.next = r;
                                // 队列长度减一
                                queueLength--;
                                if (r instanceof FinalReference) {
                                sun.misc.VM.addFinalRefCount(-1);
                                }
                                return r;
                                }
                                return null;
                                }
                                + +

                                出队操作提供了等待的选项。

                                +
                                // 从队列中移除下一个元素,阻塞直到有元素可用。
                                public Reference<? extends T> remove() throws InterruptedException {
                                return remove(0);
                                }

                                // 从队列中移除下一个元素,阻塞直到超时或有元素可用,timeout 以毫秒为单位。
                                public Reference<? extends T> remove(long timeout)
                                throws IllegalArgumentException, InterruptedException
                                {
                                if (timeout < 0) {
                                throw new IllegalArgumentException("Negative timeout value");
                                }
                                synchronized (lock) {
                                Reference<? extends T> r = reallyPoll();
                                if (r != null) return r;
                                long start = (timeout == 0) ? 0 : System.nanoTime();
                                for (;;) {
                                lock.wait(timeout);
                                r = reallyPoll();
                                if (r != null) return r;
                                // 如果 timeout 大于 0
                                if (timeout != 0) {
                                long end = System.nanoTime();
                                // 计算下一轮等待时间
                                timeout -= (end - start) / 1000_000;
                                // 到时间直接返回 null
                                if (timeout <= 0) return null;
                                // 更新开始时间
                                start = end;
                                }
                                }
                                }
                                }
                                + +

                                状态变化

                                Reference 实例(引用对象)可能处于四种内部状态之一:

                                +
                                  +
                                • Active: 新创建的实例处于 Active 状态,受到垃圾收集器的特殊处理。收集器在检测到关联对象的可达性变为适当状态后的一段时间,会将实例的状态更改为 PendingInactive,具体取决于实例在创建时是否注册到引用队列中。在前一种情况下,它还会将实例添加到待 pending-Reference 列表中。
                                • +
                                • Pending: 实例处在 pending-Reference 列表中,等待 Reference-Handler 线程将其加入引用队列。未注册到引用队列的实例永远不会处于这种状态。
                                • +
                                • Enqueued: 处在创建实例时注册到的引用队列中。当实例从引用队列中删除时,该实例将变为 Inactive 状态。未注册到引用队列的实例永远不会处于这种状态。
                                • +
                                • Inactive: 没有进一步的操作。一旦实例变为 Inactive 状态,其状态将永远不会再改变。
                                • +
                                +

                                Reference 实例(引用对象)的状态由 queuenext 字段共同表达:

                                +
                                  +
                                • Active: (queue == ReferenceQueue || queue == ReferenceQueue.NULL) && next == null
                                • +
                                • Pending: queue == ReferenceQueue && next == this
                                • +
                                • Enqueued: queue == ReferenceQueue.ENQUEUED && (next == Following || this)(在队列末尾时,next 指向自身,目前没有体现出这么设计的必要性啊?)
                                • +
                                • Inactive: queue == ReferenceQueue.NULL && next == this
                                • +
                                +
                                + +

                                Reference 的子类

                                参考文章

                                +]]>
                                + + java + +
                                + + 探索 Java 类 Cleaner 和 Finalizer + /2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/ + JavaCleanerFinalizer 都实现了一种 finalization 机制,前者更轻量和强大,你可能在了解 NIO 的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize 方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是 Reference 的具体子类。
                                Reference 作为和 GC 紧密联系的类,你可能从很多文字描述中了解过 SoftReferenceWeakReference 还有 PhantomReference 但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从 Root 对象可达”这条规则再面对 Reference 的子类时是否产生过割裂感;你是否好奇过 Finalizer 如何和重写 finalize 方法的类产生联系,本文将从 CleanerFinalizer 的源码揭示一些你可能已知的结论背后的朴素原理。

                                + + +
                                +

                                本文的写作动机继承自 Java 类 Reference 的源码分析,有时候也会自我怀疑研究一个涉及大家极力劝阻使用的 finalize 是否浪费精力,只能说确实如此!要不是半途而废会膈应难受肯定就停了!只能说这个过程确实帮助自己对 Java 引用和 GC 对其的处理有更加深刻的理解。

                                +
                                +

                                虚引用之 Cleaner

                                虚引用介绍

                                PhantomReference 对象在垃圾收集器确定其关联对象可以被回收时或可以被回收后一段时间,将被入队。“可以被回收”更明确的描述是“虚引用的关联对象变成 phantom reachable ,即只有虚引用引用了它”。但是和软引用和弱引用不同,当虚引用入队时并不会被垃圾收集器自动清理(其关联对象)。一个 phantom reachable 的对象会一直维持原样直到所有虚引用被清理或者它们自身变得不可达。

                                +

                                PhantomReference 的代码非常简单:

                                +
                                  +
                                1. PhantomReference 仅提供了一个 public 构造函数,必须提供 ReferenceQueue 参数。它不像 SoftReferenceWeakReference 可以离开 ReferenceQueue 单独使用,尽管 queue 可以为 null,但是这样做并没有意义。
                                2. +
                                3. get() 返回 null,这意味着不能通过 PhantomReference 获取其关联的对象 referent
                                4. +
                                +
                                +

                                get() 返回 null 并不是可以随意忽略的事情,它保证了 phantom reachable 对象不会被重新触达和修改(这是为清理工作留出时间吗)。

                                +
                                +
                                public class PhantomReference<T> extends Reference<T> {
                                public T get() {
                                return null;
                                }
                                public PhantomReference(T referent, ReferenceQueue<? super T> q) {
                                super(referent, q);
                                }
                                }
                                + +

                                通过以下示例验证 GC 不会自动清理虚引用的关联对象:

                                +
                                public static void main(String[] args) throws InterruptedException {
                                Scanner scanner = new Scanner(System.in);

                                byte[] bytes = new byte[100 * 1024 * 1024];
                                ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
                                PhantomReference<byte[]> phantomReference = new PhantomReference<>(bytes, queue);

                                Thread thread = new Thread(() -> {
                                for (; ; ) {
                                try {
                                Reference<? extends byte[]> remove = queue.remove(0);
                                System.out.println(remove + " enqueued");
                                // 需要调用 clear 主动清理关联对象,可以验证 gc 后总堆内存占用下降
                                // remove.clear();
                                // System.gc();
                                } catch (InterruptedException e) {
                                System.out.println(Thread.currentThread().getName() + " interrupt");
                                break;
                                }
                                }
                                });
                                thread.start();

                                System.out.println("暂停查看堆内存占用");
                                scanner.next();

                                bytes = null;
                                System.gc();
                                System.out.println("gc 后 sleep 3s,查看总堆内存占用未下降");
                                TimeUnit.SECONDS.sleep(3);

                                scanner.next();
                                thread.interrupt();
                                }
                                + +

                                Cleaner 介绍

                                虚引用最常用于以比 finalization 更灵活的方式安排清理工作,比如其子类 Cleaner 就是一种基于虚引用的清理器,它比 finalization 更轻量但更强大。Cleaner 追踪其关联对象并封装任意的清理代码,在 GC 检测到其关联对象变成 phantom reachable 后一段时间,Reference-Handler 线程将运行清理代码。同时 Cleaner 可以被直接调用,它是线程安全的并且可以保证清理代码最多运行一次。但是 Cleaner 不是 finalization 的替代品,为了避免阻塞 Reference-Handler 线程,清理代码应极其简单和直接。

                                +

                                构造函数

                                Cleaner 的构造函数为 private,仅可通过 create 方法创建实例。

                                +
                                  +
                                • referent: 关联对象
                                • +
                                • dummyQueue: 假队列,需要它仅仅是因为 PhantomReference 的构造函数需要一个 queue 参数,但是这个 queue 完全没用,在 ReferenceReference-Handler 线程会显式调用 cleaners 而不会执行入队操作。
                                • +
                                +
                                public class Cleaner extends PhantomReference<Object> {
                                private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
                                private final Runnable thunk;

                                private Cleaner(Object referent, Runnable thunk) {
                                super(referent, dummyQueue);
                                this.thunk = thunk;
                                }

                                public static Cleaner create(Object ob, Runnable thunk) {
                                if (thunk == null)
                                return null;
                                // 添加到 Cleaner 自身维护的双链表
                                return add(new Cleaner(ob, thunk));
                                }
                                }
                                + +

                                添加 cleaner

                                  +
                                • 使用 synchronized 同步
                                • +
                                • Cleaner 自身维护一个双向链表存储 cleaners,通过静态变量 first 存储头节点,以防止 cleaners 比其关联对象更早被 GC
                                • +
                                +
                                + +
                                // 头节点
                                static private Cleaner first = null;
                                // 双向指针
                                private Cleaner next = null, prev = null;
                                private static synchronized Cleaner add(Cleaner cl) {
                                // 头插法
                                if (first != null) {
                                cl.next = first;
                                first.prev = cl;
                                }
                                first = cl;
                                return cl;
                                }
                                + +

                                clean 方法

                                ReferenceReference-Handler 线程对于 Cleaner 类型的对象,会显式地调用其 clean 方法并返回,而不会将其入队。

                                +
                                  +
                                1. 使用 synchronized 同步,从双链表上移除自身
                                2. +
                                3. 调用 thunkrun 方法
                                4. +
                                +
                                public void clean() {
                                if (!remove(this))
                                return;
                                try {
                                thunk.run();
                                } catch (final Throwable x) {
                                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                                public Void run() {
                                if (System.err != null)
                                new Error("Cleaner terminated abnormally", x).printStackTrace();
                                System.exit(1);
                                return null;
                                }
                                });
                                }
                                }

                                private static synchronized boolean remove(Cleaner cl) {
                                // next 指针指向自身代表已经移除,可以避免重复移除和执行
                                if (cl.next == cl)
                                return false;
                                // 更新双链表
                                if (first == cl) {
                                if (cl.next != null)
                                first = cl.next;
                                else
                                first = cl.prev;
                                }
                                if (cl.next != null)
                                cl.next.prev = cl.prev;
                                if (cl.prev != null)
                                cl.prev.next = cl.next;
                                // 通过将 next 指针指向自身表示已经被移除
                                cl.next = cl;
                                cl.prev = cl;
                                return true;
                                }
                                + +

                                Cleaner 处理流程

                                  +
                                • 创建的 Cleaner 对象被 Cleaner 类的双链表直接或间接引用(强引用),因此不会被垃圾回收
                                • +
                                • 一切的起点仍然是 GC 特殊地对待虚引用的关联对象,当关联对象从 reachable 变成 phantom reachableGCCleaner 对象将加入 pending-list
                                • +
                                • Reference-Handler 线程又将其移除并调用 clean 方法
                                • +
                                • 在调用完毕后,Cleaner 对象变成 unreachable 并最终被垃圾回收,其关联对象也被垃圾回收
                                • +
                                +
                                +

                                注意,Cleaner 对象本身在被调用完毕之前始终是被静态变量引用,是 reachable 的,我们讨论的被判定为可回收的、变成 phantom reachable 状态的是关联对象。

                                +
                                +
                                + +
                                +

                                事实上,个人猜测“虚引用的关联对象不像软引用和弱引用会被自动清理”描述的仅仅是一个表象,判断是否要被垃圾回收的根本法则仍然是“对象是否从 Root 对象可达”,软引用和弱引用的关联对象之所以会被垃圾回收是因为它们在加入 pending-list 时被从引用对象断开,否则当引用对象被添加到引用队列时,引用队列如果从 Root 对象可达,将导致关联对象也从 Root 对象可达。在 Referenceclear() 的注释中提及该方法只被 Java 代码调用,GC 不需要调用该方法就可以直接清理,肯定是 GC 有直接清理关联对象的场景。同时 Reference 类有一句注释“GC 在检测到关联对象有特定的可达性变化后,将把引用对象添加到引用队列”,它并未将特定的可达性变化直接描述为关联对象变为不可达。目前尚未从 JVM 源代码验证该猜测。

                                +
                                +

                                终结引用之 Finalizer

                                FinalReference 用于实现 finalization,其代码很简单。

                                +
                                class FinalReference<T> extends Reference<T> {
                                public FinalReference(T referent, ReferenceQueue<? super T> q) {
                                super(referent, q);
                                }
                                }
                                + +

                                其子类 Finalizer 继承自 FinalReferenceCleaner 在代码设计上和它非常相似。

                                +

                                构造函数

                                Finalizer 的构造函数为 private,仅可通过 register 方法创建实例。

                                +
                                  +
                                • finalizee: 关联对象,即重写了 finalize 方法的类的实例
                                • +
                                • queue: 引用队列
                                • +
                                +
                                +

                                根据注释 registerVM 调用,我们可以合理猜测,这里就是重写了 finalize 方法的类的实例和 Finalizer 对象关联的起点。

                                +
                                +
                                final class Finalizer extends FinalReference<Object> {
                                private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

                                private Finalizer(Object finalizee) {
                                super(finalizee, queue);
                                add();
                                }
                                // 由 VM 调用
                                static void register(Object finalizee) {
                                new Finalizer(finalizee);
                                }
                                }
                                + +

                                添加 Finalizer

                                  +
                                • 使用 synchronized 同步
                                • +
                                • Finalizer 自身维护一个双向链表存储 finalizers,通过静态变量 unfinalized 存储头节点
                                • +
                                +
                                + +
                                private static Finalizer unfinalized = null;
                                private static final Object lock = new Object();
                                private Finalizer next = null, prev = null;

                                private void add() {
                                synchronized (lock) {
                                if (unfinalized != null) {
                                this.next = unfinalized;
                                unfinalized.prev = this;
                                }
                                unfinalized = this;
                                }
                                }
                                + +

                                Finalizer 线程

                                finalizers 的清理通常是由一条名为 Finalizer 的线程处理。启动任意一个非常简单的 Java 程序,通过 JVM 相关的工具,比如 JConsole,你都能看到一个名为 Finalizer 的线程。

                                +
                                + +

                                run 方法

                                private static class FinalizerThread extends Thread {
                                private volatile boolean running;
                                FinalizerThread(ThreadGroup g) {
                                super(g, "Finalizer");
                                }
                                public void run() {
                                // 防止递归调用 run(什么场景?)
                                if (running)
                                return;
                                // Finalizer thread 在 System.initializeSystemClass 被调用前启动,等待 JavaLangAccess 可用
                                while (!VM.isBooted()) {
                                // 推迟直到 VM 初始化完成
                                try {
                                VM.awaitBooted();
                                } catch (InterruptedException x) {
                                // 忽略并继续
                                }
                                }
                                final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                                // 标记为运行中
                                running = true;
                                for (;;) {
                                try {
                                // 从队列中移除
                                Finalizer f = (Finalizer)queue.remove();
                                // 调用 runFinalizer
                                f.runFinalizer(jla);
                                } catch (InterruptedException x) {
                                // 忽略并继续
                                }
                                }
                                }
                                }
                                + +

                                创建和启动

                                Finalizer 线程是通过静态代码块创建和启动的。

                                +
                                static {
                                // 向上获取父线程组,直到系统线程组
                                ThreadGroup tg = Thread.currentThread().getThreadGroup();
                                for (ThreadGroup tgn = tg;
                                tgn != null;
                                tg = tgn, tgn = tg.getParent());
                                // 创建 FinalizerThread 并启动
                                Thread finalizer = new FinalizerThread(tg);
                                // 设置优先级为最高减 2
                                finalizer.setPriority(Thread.MAX_PRIORITY - 2);
                                finalizer.setDaemon(true);
                                finalizer.start();
                                }
                                + +

                                获取 Finalizer 并调用

                                private void runFinalizer(JavaLangAccess jla) {
                                synchronized (this) {
                                // 判断是否已经终结过
                                if (hasBeenFinalized()) return;
                                // 从双链表上移除
                                remove();
                                }
                                try {
                                // 获取关联的 finalizee
                                Object finalizee = this.get();
                                // 如果不为 null 且不是 Enum 类型
                                if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                                // 调用 invokeFinalize
                                jla.invokeFinalize(finalizee);
                                // 清理栈槽以降低保守 GC 时误保留的可能性
                                finalizee = null;
                                }
                                } catch (Throwable x) { }
                                // 清理关联对象
                                super.clear();
                                }

                                // 和 Cleaner 类似,使用 next 指向自身表示已被移除
                                private boolean hasBeenFinalized() {
                                return (next == this);
                                }

                                // 和 Cleaner 类似的处理
                                private void remove() {
                                synchronized (lock) {
                                if (unfinalized == this) {
                                if (this.next != null) {
                                unfinalized = this.next;
                                } else {
                                unfinalized = this.prev;
                                }
                                }
                                if (this.next != null) {
                                this.next.prev = this.prev;
                                }
                                if (this.prev != null) {
                                this.prev.next = this.next;
                                }
                                this.next = this;
                                this.prev = this;
                                }
                                }
                                + +

                                finalize 的调用原理

                                关于如何调用 finalize 方法涉及不少平时接触不到的代码。

                                +
                                // 获取 JavaLangAccess
                                final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                                // 通过 JavaLangAccess 调用 finalizee 的 finalize 方法
                                jla.invokeFinalize(finalizee);
                                public static void setJavaLangAccess(JavaLangAccess jla) {
                                javaLangAccess = jla;
                                }
                                + +

                                SharedSecretsjavaLangAccess 通过 setJavaLangAccess 设置

                                +
                                public static void setJavaLangAccess(JavaLangAccess jla) {
                                javaLangAccess = jla;
                                }
                                public static JavaLangAccess getJavaLangAccess() {
                                return javaLangAccess;
                                }
                                + +

                                setJavaLangAccess 方法在 System 中被调用,javaLangAccess 被设置为一个匿名类实例,其中 invokeFinalize 方法间接调用了传入对象的 finalize 方法。

                                +
                                private static void setJavaLangAccess() {
                                // Allow privileged classes outside of java.lang
                                sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
                                // ...
                                public void invokeFinalize(Object o) throws Throwable {
                                o.finalize();
                                }
                                });
                                }
                                + +

                                SystemsetJavaLangAccess 方法在 initializeSystemClass 方法中被调用。这里正对应着 FinalizerThreadrun 方法中等待 VM 初始化完成的处理。

                                +
                                // 初始化 System class,在线程初始化之后调用
                                private static void initializeSystemClass() {
                                // ...
                                // register shared secrets
                                setJavaLangAccess();
                                // 通知 wait 的线程
                                sun.misc.VM.booted();
                                }
                                + +

                                Finalizer 的注册时机

                                你是否好奇过 JVM 是如何保证 finalize 方法最多被调用一次的?如果曾经猜测过 JVM 可能在对象中留有标记,那么在我们研究过对象的内部结构之后可以确认其中并没有用于记录对象是否已经 finalized 的地方。同时我们注意到 hasBeenFinalized 方法通过 next 指针是否指向自己表示是否已经 finalized。我们可以合理猜测 register 的调用时机是在对象创建时,因此最多仅有一次被注册。

                                +

                                通过以下示例可以测试:

                                +
                                  +
                                • 在创建重写了 finalize 方法的类创建对象期间会调用 register 创建并注册 Finalizer
                                • +
                                • 在未重写 finalize 方法的类创建对象期间不会调用register
                                • +
                                • Finalizer 不仅可以保证 finalize 只会被调用一次,甚至不会第二次被添加到 pending-list,因为 runFinalizer 最后调用了 super.clear()JVM 不会特殊对待复活的对象
                                • +
                                +
                                public class FinalReferenceTest_1 {

                                private static FinalizeObj save = null;

                                public static void main(String[] args) throws InterruptedException {
                                System.out.println("创建 finalize obj,使用 Debug 强制运行到 Finalizer.register");
                                FinalizeObj finalizeObj = new FinalizeObj();

                                System.out.println("gc");
                                finalizeObj = null;
                                System.gc();
                                System.out.println("sleep 1s");
                                TimeUnit.SECONDS.sleep(1);
                                save.echo();

                                save = null;
                                System.gc();
                                System.out.println("sleep 1s");
                                TimeUnit.SECONDS.sleep(1);
                                System.out.println(save == null);
                                }

                                static class FinalizeObj {
                                FinalizeObj() {
                                System.out.println("SaveSelf created");
                                }
                                @Override
                                protected void finalize() throws Throwable {
                                System.out.println("finalized");
                                save = this;
                                }
                                public void echo() {
                                System.out.println("I am alive.");
                                }
                                }
                                }
                                + + + + +

                                参考文章

                                +]]>
                                + + java + +
                                + + ComponentScan 扫描路径覆盖的真相 + /2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/ + @ComponentScan 注解是 Spring 中很常用的注解,用于扫描并加载指定类路径下的 Bean,而 Spring Boot 为了便捷使用 @SpringBootApplication 组合注解集成了 @ComponentScan 的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication 可以间接设置 @ComponentScan 属性的原因?本文从源码角度分析 @ComponentScan 的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。

                                + + + +

                                入口

                                对于标注了 @ComponentScan 注解的配置类,处理过程如下:

                                +
                                  +
                                • 获取 @ComponentScan 的注解属性
                                • +
                                • 遍历注解属性集合,依次根据其中的信息进行扫描,获取 Bean 定义
                                • +
                                • 如果获取到的 Bean 定义中有任何其他配置类,将递归解析(处理配置类)
                                • +
                                +
                                +

                                这里和处理 @Import 的过程很像,都出现了递归解析新获得的配置类。

                                +
                                +
                                protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                                throws IOException {
                                // ...
                                // 处理任何 @ComponentScan 注解
                                // 获取 @ComponentScan 的注解属性,该注解是可重复的
                                Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
                                if (!componentScans.isEmpty() &&
                                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
                                // 遍历
                                for (AnnotationAttributes componentScan : componentScans) {
                                // 如果配置类被标注了 @ComponentScan -> 立即扫描
                                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                                this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                                // 检测被扫描到的 Bean 定义中是否有任何其他配置类,如有需要递归解析
                                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                                if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                                holder.getBeanDefinition(), this.metadataReaderFactory)) {
                                // 递归解析配置类
                                parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                                }
                                }
                                }
                                }
                                // ...
                                }
                                + +

                                扫描获取 Bean 定义

                                我们先跳过“获取 @ComponentScan 的注解属性”的过程,来看“扫描获取 Bean 定义”的过程。扫描是通过 ComponentScanAnnotationParserparse 方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner 设置一些来自 @ComponentScan 的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner 顾名思义是基于类路径的 Bean 定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages 的设置:

                                +
                                  +
                                • 使用 Set 存储合并结果,用于去重
                                • +
                                • 获取设置的 basePackages 值并添加
                                • +
                                • 获取设置的 basePackageClasses 值,转换为它们所在的包名并添加
                                • +
                                • 如果结果集现在还是空的,获取被标注的配置类所在的包名并添加
                                • +
                                +
                                +

                                最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。

                                +
                                +
                                public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
                                Assert.state(this.environment != null, "Environment must not be null");
                                Assert.state(this.resourceLoader != null, "ResourceLoader must not be null");
                                // 创建 ClassPathBeanDefinitionScanner
                                ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
                                // Bean 名称生成器
                                Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
                                boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
                                scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                                BeanUtils.instantiateClass(generatorClass));

                                ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
                                if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
                                scanner.setScopedProxyMode(scopedProxyMode);
                                }
                                else {
                                Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
                                scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
                                }

                                scanner.setResourcePattern(componentScan.getString("resourcePattern"));
                                // 设置 Filter
                                for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
                                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                                scanner.addIncludeFilter(typeFilter);
                                }
                                }
                                for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
                                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                                scanner.addExcludeFilter(typeFilter);
                                }
                                }
                                // 是否懒加载
                                boolean lazyInit = componentScan.getBoolean("lazyInit");
                                if (lazyInit) {
                                scanner.getBeanDefinitionDefaults().setLazyInit(true);
                                }
                                // 设置 basePackages(使用 Set 去重)
                                Set<String> basePackages = new LinkedHashSet<String>();
                                // 获取设置的 basePackages 值
                                String[] basePackagesArray = componentScan.getStringArray("basePackages");
                                for (String pkg : basePackagesArray) {
                                // 允许占位符
                                String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
                                basePackages.addAll(Arrays.asList(tokenized));
                                }
                                // 获取 basePackageClasses,本质上是为了获取它们所在的包名
                                for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
                                basePackages.add(ClassUtils.getPackageName(clazz));
                                }
                                // 如果为空,获取被标注的配置类所在的包名
                                if (basePackages.isEmpty()) {
                                basePackages.add(ClassUtils.getPackageName(declaringClass));
                                }
                                // 排除被标注的配置类本身
                                scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
                                @Override
                                protected boolean matchClassName(String className) {
                                return declaringClass.equals(className);
                                }
                                });
                                // 执行扫描
                                return scanner.doScan(StringUtils.toStringArray(basePackages));
                                }
                                + +

                                parse 方法与其说是解析,不如说是封装了一些设置并最终调用 ClassPathBeanDefinitionScanner,而设置的属性值来源于 @ComponentScan 的注解属性。关于获取 @ComponentScan 的注解属性的方法 AnnotationConfigUtils.attributesForRepeatable 在分析 @PropertySource 时也曾经遇到过,顾名思义我们知道它应该是用于获取可重复的注解的属性。可是它和直接获取注解对象有什么区别呢?

                                +
                                +

                                我们知道 @SpringBootApplication 拥有和 @ComponentScan 具备相似的功能,并且可以使用 scanBasePackagesscanBasePackageClasses 这两个属性设置扫描的包。也许你还知道 @SpringBootApplication 之所以如此是因为它被标注了 @ComponentScanscanBasePackagesscanBasePackageClasses 分别是它的元注解 @ComponentScanbasePackagesbasePackageClasses 的别名。你甚至可能知道如果在配置类上使用 @ComponentScan 设置包扫描后会导致 @SpringBootApplication 设置的包扫描失效
                                可是为什么呢?Spring 中我们会看到从指定类上直接获取目标注解的代码,我们还会看到递归地从元注解上获取目标注解的代码,我们使用 @ComponentScan 的经验告诉我们可重复注解不是覆盖彼此而是共同生效,那么为什么 @SpringBootApplication 上的 @ComponentScan 就被覆盖了呢?想当然的认为 @SpringBootApplication 上标注了 @ComponentScan 是一切的原因是不够的

                                +
                                +
                                @Target(ElementType.TYPE)
                                @Retention(RetentionPolicy.RUNTIME)
                                @Documented
                                @Inherited
                                @SpringBootConfiguration
                                @EnableAutoConfiguration
                                @ComponentScan(excludeFilters = {
                                @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                                @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
                                public @interface SpringBootApplication {

                                @AliasFor(annotation = EnableAutoConfiguration.class)
                                Class<?>[] exclude() default {};

                                @AliasFor(annotation = EnableAutoConfiguration.class)
                                String[] excludeName() default {};

                                @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
                                String[] scanBasePackages() default {};

                                @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
                                Class<?>[] scanBasePackageClasses() default {};

                                }
                                + +

                                获取注解属性

                                attributesForRepeatable 方法有两个重载方法,最终调用的版本如下。先后处理了 @ComponentScan@ComponentScans

                                +
                                static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
                                String containerClassName, String annotationClassName) {
                                // Set 用于存储结果
                                Set<AnnotationAttributes> result = new LinkedHashSet<AnnotationAttributes>();
                                // 处理 @ComponentScan
                                addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationClassName, false));
                                // 处理 @ComponentScans
                                Map<String, Object> container = metadata.getAnnotationAttributes(containerClassName, false);
                                if (container != null && container.containsKey("value")) {
                                for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container.get("value")) {
                                addAttributesIfNotNull(result, containedAttributes);
                                }
                                }
                                return Collections.unmodifiableSet(result);
                                }
                                + +

                                检索注解的规则

                                根据注释,getAnnotationAttributes 方法检索给定类型的注解的属性,检索的目标可以是直接注解也可以是元注解,同时考虑组合注解上的属性覆盖

                                +
                                  +
                                • 元注解指的是标注在其他注解上的注解,用于对被标注的注解进行说明,比如 @SpringBootApplication 上的 @ComponentScan 就被称为元注解,此时 @SpringBootApplication 被称为组合注解
                                • +
                                • 组合注解中存在属性覆盖现象
                                • +
                                +
                                +

                                其实这两点分别对应了我们想要探究的两个问题:@ComponentScan 究竟是如何被检索的?注解属性比如 basePackages 又是如何被覆盖的?

                                +
                                +
                                public Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString) {
                                // 获取合并的注解属性
                                return (this.annotations.length > 0 ? AnnotatedElementUtils.getMergedAnnotationAttributes(
                                getIntrospectedClass(), annotationName, classValuesAsString, this.nestedAnnotationsAsMap) : null);
                                }
                                + +

                                根据注释,getMergedAnnotationAttributes 方法获取所提供元素上方的注解层次结构中指定的 annotationName 的第一个注解,并将该注解的属性与注解层次结构较低级别中的注解中的匹配属性合并。注解层次结构中较低级别的属性会覆盖较高级别中的同名属性,并且完全支持单个注解中或是注解层次结构中的 @AliasFor 语义。与 getAllAnnotationAttributes 方法相反,一旦找到指定 annotationName 的第一个注解,此方法使用的搜索算法将停止搜索注解层次结构。因此,指定的 annotationName 的附加注解将被忽略。

                                +
                                +

                                这注释有点太抽象了,理解代码后再来回味吧。

                                +
                                +
                                public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element,
                                String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
                                // 以 get 语义进行搜索(是指找到即终止搜索?)
                                AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName,
                                new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap));
                                // 后处理注解属性
                                AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap);
                                return attributes;
                                }
                                + +

                                searchWithGetSemantics 方法有多个重载方法,最终调用的版本如下:

                                +
                                  +
                                • 先获取 element 上的所有注解(包括重复的,不包括继承的),这意味着可重复注解 @ComponentScan 标注了多个就会有多个实例
                                • +
                                • 在注解中搜索
                                • +
                                • 如果没找到,就从继承的注解中继续搜索
                                • +
                                +
                                +

                                本方法是一个会被递归调用的方法,在第一次调用时 element 是配置类,之后就是注解。

                                +
                                +
                                private static <T> T searchWithGetSemantics(AnnotatedElement element,
                                @Nullable Class<? extends Annotation> annotationType, @Nullable String annotationName,
                                @Nullable Class<? extends Annotation> containerType, Processor<T> processor,
                                Set<AnnotatedElement> visited, int metaDepth) {
                                // 防止无限递归
                                if (visited.add(element)) {
                                try {
                                // 获取 element 上的所有注解(包括重复,不包括继承的)
                                List<Annotation> declaredAnnotations = Arrays.asList(element.getDeclaredAnnotations());
                                // 在获得的注解中搜索
                                T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
                                annotationType, annotationName, containerType, processor, visited, metaDepth);
                                if (result != null) {
                                return result;
                                }
                                // 表明在直接声明的注解中没有找到
                                // 如果 element 是一个类
                                if (element instanceof Class) {
                                // 获取所有的注解(包括重复的和继承的)
                                List<Annotation> inheritedAnnotations = new ArrayList<>();
                                for (Annotation annotation : element.getAnnotations()) {
                                // 排除已经搜索过的,只留下继承的注解
                                if (!declaredAnnotations.contains(annotation)) {
                                inheritedAnnotations.add(annotation);
                                }
                                }
                                // 继续搜索
                                result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations,
                                annotationType, annotationName, containerType, processor, visited, metaDepth);
                                if (result != null) {
                                return result;
                                }
                                }
                                }
                                catch (Throwable ex) {
                                AnnotationUtils.handleIntrospectionFailure(element, ex);
                                }
                                }

                                return null;
                                }
                                + +

                                遍历注解进行搜索。

                                +
                                  +
                                • 先在注解中搜索,这意味着如果配置类标注了 @ComponentScan,直接就找到了
                                • +
                                • 如果没找到再在元注解中搜索,如果配置类只标注了 @SpringBootApplication,就是在这部分找到元注解 @ComponentScan
                                • +
                                +
                                +

                                严格意义上说,并不是直接标注的 @ComponentScan 会覆盖 @SpringBootApplication 上间接标注的 @ComponentScan,而是搜索在找到第一个注解后终止没有继续查找。这解答了我们的第一个疑问。

                                +
                                +
                                private static <T> T searchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,
                                List<Annotation> annotations, @Nullable Class<? extends Annotation> annotationType,
                                @Nullable String annotationName, @Nullable Class<? extends Annotation> containerType,
                                Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {

                                // 遍历注解进行查找,如果同时标注 @SpringBootApplication 和 @ComponentScan,在这部分就会找到 @ComponentScan 就返回了
                                for (Annotation annotation : annotations) {
                                // 获取注解的 Class
                                Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                                // 检测是否属于 Java 语言注解包中(以 java.lang.annotation 开头)的注解,例如 @Documented,是的话跳过
                                if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
                                // 检测是否满足条件:等于 annotationType(传入 null),或者和目标的名字(@ComponentScan 全限定类名)相同,或者属于总是处理(默认 false)
                                if (currentAnnotationType == annotationType ||
                                currentAnnotationType.getName().equals(annotationName) ||
                                processor.alwaysProcesses()) {
                                // 处理注解获得注解属性
                                T result = processor.process(element, annotation, metaDepth);
                                if (result != null) {
                                // processor.aggregates() 默认返回 false
                                if (processor.aggregates() && metaDepth == 0) {
                                processor.getAggregatedResults().add(result);
                                }
                                else {
                                // 注意:难道标注多个 @ComponentScan 也只找到一个就返回了?
                                return result;
                                }
                                }
                                }
                                // 容器里的可重复注解,因为 containerType 为 null,跳过
                                else if (currentAnnotationType == containerType) {
                                for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
                                T result = processor.process(element, contained, metaDepth);
                                if (result != null) {
                                // No need to post-process since repeatable annotations within a
                                // container cannot be composed annotations.
                                processor.getAggregatedResults().add(result);
                                }
                                }
                                }
                                }
                                }

                                // 在元注解中递归的搜索,@SpringBootApplication 中的 @ComponentScan 就是在这找到的
                                for (Annotation annotation : annotations) {
                                // 获取注解的 Class
                                Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
                                // 检测是否属于 Java 语言注解包中
                                if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
                                // 递归到元注解中搜索,深度加 1
                                T result = searchWithGetSemantics(currentAnnotationType, annotationType,
                                annotationName, containerType, processor, visited, metaDepth + 1);
                                if (result != null) {
                                // 进行后处理,注解层次结构中较低级别的属性会覆盖较高级别中的同名属性就是在这发生的
                                processor.postProcess(element, annotation, result);
                                if (processor.aggregates() && metaDepth == 0) {
                                processor.getAggregatedResults().add(result);
                                }
                                else {
                                return result;
                                }
                                }
                                }
                                }

                                return null;
                                }
                                + +

                                处理 @ComponentScan 获得 AnnotationAttributes

                                +
                                public AnnotationAttributes process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
                                return AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,
                                this.classValuesAsString, this.nestedAnnotationsAsMap);
                                }
                                + +

                                AnnotationAttributes 映射的形式检索给定注解的属性。

                                +
                                static AnnotationAttributes retrieveAnnotationAttributes(@Nullable Object annotatedElement, Annotation annotation,
                                boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

                                Class<? extends Annotation> annotationType = annotation.annotationType();
                                AnnotationAttributes attributes = new AnnotationAttributes(annotationType);
                                // 遍历属性方法
                                for (Method method : getAttributeMethods(annotationType)) {
                                try {
                                // 获取属性值
                                Object attributeValue = method.invoke(annotation);
                                // 获取默认值
                                Object defaultValue = method.getDefaultValue();
                                // 如果默认值不为 null 且和属性值相同
                                if (defaultValue != null && ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
                                attributeValue = new DefaultValueHolder(defaultValue);
                                }
                                // 属性名 -> 属性值
                                attributes.put(method.getName(),
                                adaptValue(annotatedElement, attributeValue, classValuesAsString, nestedAnnotationsAsMap));
                                }
                                catch (Throwable ex) {
                                if (ex instanceof InvocationTargetException) {
                                Throwable targetException = ((InvocationTargetException) ex).getTargetException();
                                rethrowAnnotationConfigurationException(targetException);
                                }
                                throw new IllegalStateException("Could not obtain annotation attribute value for " + method, ex);
                                }
                                }

                                return attributes;
                                }

                                // 获取在所提供的 annotationType 中声明的与 Java 对注释属性的要求相匹配的所有方法
                                static List<Method> getAttributeMethods(Class<? extends Annotation> annotationType) {
                                // 先从缓存中获取
                                List<Method> methods = attributeMethodsCache.get(annotationType);
                                if (methods != null) {
                                return methods;
                                }
                                // 遍历方法筛选
                                methods = new ArrayList<>();
                                for (Method method : annotationType.getDeclaredMethods()) {
                                if (isAttributeMethod(method)) {
                                ReflectionUtils.makeAccessible(method);
                                methods.add(method);
                                }
                                }
                                // 存入缓存
                                attributeMethodsCache.put(annotationType, methods);
                                return methods;
                                }

                                // 确定提供的方法是否是注解的属性方法。
                                static boolean isAttributeMethod(@Nullable Method method) {
                                // 无参数 && 返回值非 void
                                return (method != null && method.getParameterCount() == 0 && method.getReturnType() != void.class);
                                }
                                + +

                                组合注解的属性覆盖

                                在获得注解属性后还要进行后处理,使用注解层次结构中较低级别的属性覆盖较高级别中的同名(包括 @AliasFor 指定的)属性。比如使用 @SpringBootApplication 中的 scanBasePackages 的值覆盖 @ComponentScan 中的 basePackages 的值。

                                +
                                public void postProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes) {
                                annotation = AnnotationUtils.synthesizeAnnotation(annotation, element);
                                // 获取 AnnotationAttributes 的注解类型(@ComponentScan)
                                Class<? extends Annotation> targetAnnotationType = attributes.annotationType();

                                // Track which attribute values have already been replaced so that we can short
                                // circuit the search algorithms.
                                Set<String> valuesAlreadyReplaced = new HashSet<>();
                                // 获取注解的属性方法(SpringBootApplication)
                                for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
                                String attributeName = attributeMethod.getName();
                                // 获取被覆盖的别名
                                String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType);

                                // Explicit annotation attribute override declared via @AliasFor
                                if (attributeOverrideName != null) {
                                // 被覆盖的属性的值是否已经被替换
                                if (valuesAlreadyReplaced.contains(attributeOverrideName)) {
                                continue;
                                }

                                List<String> targetAttributeNames = new ArrayList<>();
                                targetAttributeNames.add(attributeOverrideName);
                                valuesAlreadyReplaced.add(attributeOverrideName);

                                // 确保覆盖目标注解中的所有别名属性。 (SPR-14069)
                                List<String> aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName);
                                if (aliases != null) {
                                for (String alias : aliases) {
                                if (!valuesAlreadyReplaced.contains(alias)) {
                                targetAttributeNames.add(alias);
                                valuesAlreadyReplaced.add(alias);
                                }
                                }
                                }

                                overrideAttributes(element, annotation, attributes, attributeName, targetAttributeNames);
                                }
                                // Implicit annotation attribute override based on convention
                                else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
                                overrideAttribute(element, annotation, attributes, attributeName, attributeName);
                                }
                                }
                                }

                                // 根据提供的注解属性方法的 @AliasFor,获取被覆盖的属性的名称
                                static String getAttributeOverrideName(Method attribute, @Nullable Class<? extends Annotation> metaAnnotationType) {
                                // 获取别名描述符
                                AliasDescriptor descriptor = AliasDescriptor.from(attribute);
                                // 从元注解中被覆盖的属性名
                                return (descriptor != null && metaAnnotationType != null ?
                                descriptor.getAttributeOverrideName(metaAnnotationType) : null);
                                }

                                // 获取在提供的注解类型中通过 @AliasFor 声明的所有属性别名的映射。该映射由属性名称作为键,每个值代表别名属性的名称列表。空返回值意味着注解没有声明任何属性别名。
                                static Map<String, List<String>> getAttributeAliasMap(@Nullable Class<? extends Annotation> annotationType) {
                                if (annotationType == null) {
                                return Collections.emptyMap();
                                }
                                // 从缓存中获取
                                Map<String, List<String>> map = attributeAliasesCache.get(annotationType);
                                if (map != null) {
                                return map;
                                }

                                map = new LinkedHashMap<>();
                                // 遍历属性方法
                                for (Method attribute : getAttributeMethods(annotationType)) {
                                // 获取别名列表
                                List<String> aliasNames = getAttributeAliasNames(attribute);
                                if (!aliasNames.isEmpty()) {
                                map.put(attribute.getName(), aliasNames);
                                }
                                }
                                // 存入缓存
                                attributeAliasesCache.put(annotationType, map);
                                return map;
                                }

                                // 获取通过提供的注解属性的 @AliasFor 配置的别名属性的名称列表
                                static List<String> getAttributeAliasNames(Method attribute) {
                                AliasDescriptor descriptor = AliasDescriptor.from(attribute);
                                return (descriptor != null ? descriptor.getAttributeAliasNames() : Collections.<String> emptyList());
                                }

                                // 覆盖属性
                                private void overrideAttributes(@Nullable AnnotatedElement element, Annotation annotation,
                                AnnotationAttributes attributes, String sourceAttributeName, List<String> targetAttributeNames) {

                                Object adaptedValue = getAdaptedValue(element, annotation, sourceAttributeName);
                                // 遍历目标属性中的所有应被覆盖的属性(本尊+别名)
                                for (String targetAttributeName : targetAttributeNames) {
                                attributes.put(targetAttributeName, adaptedValue);
                                }
                                }
                                + +

                                在代码的注释中我们留下过一个疑问,如果找到了第一个注解就立即返回,那么标注了多个 @ComponentScan 呢?当你 Debug 时,你会发现并没有走出现直接标注了 @ComponentScan 的处理,其实看到反编译后的代码你就知道了,多个 @ComponentScan 被合成了一个 @ComponentScans,甚至此时设置的三个 basePackages 都是生效的。在 JDK 8 引入的重复注解机制,并非一个语言层面上的改动,而是编译器层面的改动。在编译后,多个可重复注解 @ComponentScan 会被合并到一个容器注解 @ComponentScans 中。

                                +
                                +

                                因此,“@ComponentScan 的配置会覆盖 @SpringBootApplication 关于包扫描的配置”这句话既对又不对,它在一个常见的个例上表现出的现象是对的,在更普遍的情况中以及本质上是错误的。你也许可以再根据一些情况罗列出类似的“@ComponentScan 使用规则”,但是如果你不明白背后的本质,那么这些只是一些死记硬背的陈述,甚至会带给你错误的认知。

                                +
                                +
                                // 标注了两个 `@ComponentScan`,对编译后的字节码进行反编译
                                @SpringBootApplication(
                                scanBasePackages = {"com.example"}
                                )
                                @ComponentScans({@ComponentScan(
                                basePackages = {"com.example.demo"}
                                ), @ComponentScan({"com"})})
                                public class DemoApplication {
                                public DemoApplication() {
                                }

                                public static void main(String[] args) {
                                SpringApplication.run(DemoApplication.class, args);
                                }
                                }
                                + +

                                注解内的别名属性

                                postProcess 方法完成了组合注解的属性覆盖,可是对于 @ComponentScan 注解而言,它没有被 postProcess 方法处理,它又是如何做到设置 basePackages 等于设置 value 呢?其实这发生在后处理注解属性方法中,该方法会对注解中标注了 @AliasFor 的属性强制执行别名语义。通俗地讲,就是统一校验互为别名的属性值,要么只设置了其中一个属性的值,其他别名属性会被赋值为相同的值,要么设置为相同的值,否则会报错。

                                +
                                public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element,
                                String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
                                // 以 get 语义进行搜索(是指找到即终止搜索?)
                                AnnotationAttributes attributes = searchWithGetSemantics(element, null, annotationName,
                                new MergedAnnotationAttributesProcessor(classValuesAsString, nestedAnnotationsAsMap));
                                // 后处理注解属性
                                AnnotationUtils.postProcessAnnotationAttributes(element, attributes, classValuesAsString, nestedAnnotationsAsMap);
                                return attributes;
                                }

                                static void postProcessAnnotationAttributes(@Nullable Object annotatedElement,
                                @Nullable AnnotationAttributes attributes, boolean classValuesAsString, boolean nestedAnnotationsAsMap) {

                                if (attributes == null) {
                                return;
                                }
                                // 获取 AnnotationAttributes 的注解类型(@ComponentScan)
                                Class<? extends Annotation> annotationType = attributes.annotationType();

                                // Track which attribute values have already been replaced so that we can short
                                // circuit the search algorithms.
                                Set<String> valuesAlreadyReplaced = new HashSet<>();

                                if (!attributes.validated) {
                                // 校验 @AliasFor 配置
                                // 获取别名映射
                                Map<String, List<String>> aliasMap = getAttributeAliasMap(annotationType);
                                // 遍历
                                for (String attributeName : aliasMap.keySet()) {
                                // 跳过已处理的
                                if (valuesAlreadyReplaced.contains(attributeName)) {
                                continue;
                                }
                                Object value = attributes.get(attributeName);
                                // 属性是否已有值
                                boolean valuePresent = (value != null && !(value instanceof DefaultValueHolder));
                                // 遍历属性的别名列表
                                for (String aliasedAttributeName : aliasMap.get(attributeName)) {
                                // 跳过已处理的
                                if (valuesAlreadyReplaced.contains(aliasedAttributeName)) {
                                continue;
                                }
                                // 获取别名属性的值
                                Object aliasedValue = attributes.get(aliasedAttributeName);
                                // 别名属性是否已有值
                                boolean aliasPresent = (aliasedValue != null && !(aliasedValue instanceof DefaultValueHolder));

                                // Something to validate or replace with an alias?
                                if (valuePresent || aliasPresent) {
                                // 如果属性已有值且别名属性也有值,校验是否相等
                                if (valuePresent && aliasPresent) {
                                // Since annotation attributes can be arrays, we must use ObjectUtils.nullSafeEquals().
                                if (!ObjectUtils.nullSafeEquals(value, aliasedValue)) {
                                String elementAsString =
                                (annotatedElement != null ? annotatedElement.toString() : "unknown element");
                                throw new AnnotationConfigurationException(String.format(
                                "In AnnotationAttributes for annotation [%s] declared on %s, " +
                                "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s], " +
                                "but only one is permitted.", attributes.displayName, elementAsString,
                                attributeName, aliasedAttributeName, ObjectUtils.nullSafeToString(value),
                                ObjectUtils.nullSafeToString(aliasedValue)));
                                }
                                }
                                else if (aliasPresent) {
                                // 复制别名属性的值给属性
                                attributes.put(attributeName,
                                adaptValue(annotatedElement, aliasedValue, classValuesAsString, nestedAnnotationsAsMap));
                                valuesAlreadyReplaced.add(attributeName);
                                }
                                else {
                                // 复制属性的值给别名属性
                                attributes.put(aliasedAttributeName,
                                adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
                                valuesAlreadyReplaced.add(aliasedAttributeName);
                                }
                                }
                                }
                                }
                                // 校验完毕
                                attributes.validated = true;
                                }

                                // 将 `value` 从 `DefaultValueHolder` 替换为原始的 `value`
                                for (String attributeName : attributes.keySet()) {
                                if (valuesAlreadyReplaced.contains(attributeName)) {
                                continue;
                                }
                                Object value = attributes.get(attributeName);
                                if (value instanceof DefaultValueHolder) {
                                value = ((DefaultValueHolder) value).defaultValue;
                                attributes.put(attributeName,
                                adaptValue(annotatedElement, value, classValuesAsString, nestedAnnotationsAsMap));
                                }
                                }
                                }
                                + +

                                总结

                                +

                                又是一篇在写之前自认心里有数,以为可以很快总结完,却不知不觉写了很久,也收获了很多的文章。在刚开始,我只是想接续分析 @Configuration 的思路补充关于 @ComponentScan 的内容,但是渐渐地我又想要回应心里的疑问,@ComponentScan@SpringBootApplication 一起使用的问题的本质原因是什么?Spring 框架真的很好用,好用到你不用太关心背后的原理,好用到你有时候用一个本质上不太正确的结论“走遍天下却几乎不会遇到问题”。说实话,研究完也有点索然无味,尤其是花了这么多时间看自己很讨厌的关于解析的代码,只能说解开了一个卡点也算疏通了一口气,但是时间成本好大啊,得多看点能“面试”的技术啊!!!

                                +
                                +

                                综上分析,@SpringBootApplication 的包扫描功能本质上还是 @ComponentScan 提供的,但是和常见的嵌套注解不同,检索 @ComponentScan 有一套独特的算法,导致 @SpringBootApplication@ComponentScan 并非简单的叠加效果。

                                +
                                  +
                                • Spring 会先获取 @ComponentScan 的注解属性再获取 @ComponentScans 的注解属性
                                • +
                                • @ComponentScan 为例,只获取给定配置类上的注解层次结构中的第一个 @ComponentScan
                                • +
                                • 先从直接标注的注解开始,再递归地搜索元注解,这一点决定了 @ComponentScan 优先级高于 @SpringBootApplication
                                • +
                                • 使用注解层次结构中较低级别的属性覆盖较高级别的同名(支持 @AliasFor)属性,这一点决定了 @SpringBootApplication 可以设置扫描路径
                                • +
                                • 多个 @ComponentScan 在编译后隐式生成 @ComponentScans,这一点决定多个 @ComponentScan 彼此之间以及和 @SpringBootApplication 互不冲突
                                • +
                                +]]>
                                + + java + spring + spring boot + +
                                + + 谈谈 MySQL 事务的隔离性 + /2024/01/06/talk-about-isolation-of-MySQL-transactions/ + 事务就是一组数据库操作,它具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称为 ACID。本文将介绍 MySQL 事务的隔离性以及对其的思考。
                                尽管这是一个老生常谈的话题,网上也有很多相关的资料,但是要理解它并不容易。即使林晓斌老师在 《MySQL 实战 45 讲》 中用了两个章节进行介绍,但是你在评论区中会发现有些分享或讨论的观点彼此矛盾。原因可能有很多,比如为了易于理解使用简化概念进行分析,有些具体细节各人各执一词同时它们又不好通过测试进行验证,用词不严谨等等。本文尽可能为自己梳理出一个完善并且前后一致的认知体系,再针对一些容易引起误解的地方作进一步的说明。

                                + + +

                                隔离级别

                                SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。当多个事务同时执行时,不同的隔离级别可能发生脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)等一个或多个现象。隔离级别越高,效率越低,因此很多时候,我们需要在二者之间寻找一个平衡点。

                                + - - + + + + - - - - - - - - - - - - - - + + + + -
                                快捷键功能隔离级别脏读不可重复读幻读
                                :sp [filename]打开一个新窗口
                                Ctrl + w + j
                                Ctrl + w +
                                光标移动到下方的窗口
                                Ctrl + w + k
                                Ctrl + w +
                                光标移动到上方的窗口
                                Ctrl + w + q
                                :q
                                :close
                                关闭窗口读未提交YYY
                                -

                                关键词自动补全

                                - - - - - - - - + + + + - - + + + + - - + + + +
                                快捷键功能
                                Ctrl + x + Ctrl + n使用当前文件的内容文字作为关键词,予以补齐读提交NYY
                                Ctrl + x + Ctrl + f使用当前目录的文件名作为关键词,予以补齐可重复读NNY
                                Ctrl + x + Ctrl + o使用扩展名作为语法补充,以 Vim 内置的关键词,予以补齐串行化NNN
                                -

                                环境配置

                                +
                                +

                                读未提交和串行化很少在实际应用中使用。

                                +
                                +

                                通过以下示例说明隔离级别的影响,V1V2V3 在不同隔离级别下的值有所不同。

                                +
                                - - + + + + + + - - + + + + + + - - - -
                                设置参数功能事务 A事务 B读未提交读提交可重复读串行化
                                :set nu
                                :set nonu
                                设置和取消行号开启事务开启事务
                                :syntax on
                                :syntax off
                                是否依据程序相关语法显示不同颜色
                                -
                                -

                                可以通过 vim ~/.vimrc 修改配置文件。

                                -
                                -

                                参考文章

                                -]]>
                                - - linux - vim - -
                                - - k3s 的安装和使用 - /2024/01/30/installation-and-use-of-k3s/ - 本文记录了 k3s 的安装和使用,相较于 minikube,前者是一个完全兼容的 Kubernetes 发行版,安装和使用的体验更佳。

                                - - -

                                安装

                                -

                                参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!

                                -
                                -

                                步骤

                                  -
                                1. 获取并运行 k3s 安装脚本。官方为中国用户提供了镜像加速支持。
                                  $ curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -                                
                                  [sudo] password for moralok:
                                  [INFO] Finding release for channel stable
                                  [INFO] Using v1.28.5+k3s1 as release
                                  [INFO] Downloading hash rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/sha256sum-amd64.txt
                                  [INFO] Downloading binary rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/k3s
                                  [INFO] Verifying binary download
                                  [INFO] Installing k3s to /usr/local/bin/k3s
                                  [INFO] Skipping installation of SELinux RPM
                                  [INFO] Creating /usr/local/bin/kubectl symlink to k3s
                                  [INFO] Creating /usr/local/bin/crictl symlink to k3s
                                  [INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
                                  [INFO] Creating killall script /usr/local/bin/k3s-killall.sh
                                  [INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
                                  [INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
                                  [INFO] systemd: Creating service file /etc/systemd/system/k3s.service
                                  sh: 1014: restorecon: not found
                                  sh: 1015: restorecon: not found
                                  [INFO] systemd: Enabling k3s unit
                                  Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
                                  [INFO] systemd: Starting k3s
                                2. -
                                3. 可以通过使用 kubectl 确认安装成功。刚安装的时候使用 kubectl 需要 root 权限。
                                  $ sudo kubectl get node
                                  NAME STATUS ROLES AGE VERSION
                                  ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1
                                4. -
                                5. 实际上安装的就是一个 k3s 可执行文件,kubectlcrictl 只是软链接,指向 k3s
                                  $ ls /usr/local/bin/
                                  crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl
                                6. -
                                -
                                -

                                安装的信息中显示了 k3sservice fileenvironment file 的路径,后续修改启动参数和环境变量需要用到。

                                -
                                -

                                配置文件权限问题

                                在刚安装完 k3s 的时候,使用 kubectl 需要 root 权限。根据报错信息可知,是因为非 root 用户无法读取配置文件 /etc/rancher/k3s/k3s.yaml

                                -
                                $ kubectl get node                                                                                                                                            
                                WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode to modify kube config permissions
                                error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
                                - -

                                查看配置文件的信息可知其权限配置为 600,只有 root 用户具有读写权限。

                                -
                                $ ll /etc/rancher/k3s/k3s.yaml                                                                                               
                                -rw------- 1 root root 2961 Jan 30 18:58 /etc/rancher/k3s/k3s.yaml
                                - -

                                一般来说,我们希望能够通过非 root 用户使用 kubectl,避免通过 root 用户或者通过 sudo 加输入密码的形式来使用 kubectl。那么如何解决这个问题呢?本质上这是一个 Linux 的文件权限问题,似乎修改文件的权限配置就可以解决。但是提示信息给出的解决方案并不是那么直接,它告诉我们通过修改 k3s server 的启动参数来达到修改配置文件权限的目的。这是因为 k3s 服务在每次重启时会根据启动参数和环境变量重置配置文件 /etc/rancher/k3s/k3s.yaml,手动修改文件的权限配置并不能优雅地解决这个问题,一旦服务重启,修改就会丢失。

                                -
                                -

                                k3s 的 Github Discussions 中讨论了这个问题,并链接了文档 管理 Kubeconfig 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。

                                -
                                -

                                修改启动参数

                                第一种方式是修改启动参数。

                                -
                                  -
                                1. sudo vim /etc/systemd/system/k3s.service 添加 k3s 启动参数 --write-kubeconfig-mode 644
                                  ExecStart=/usr/local/bin/k3s \
                                  server --write-kubeconfig-mode 644 \
                                2. -
                                3. systemctl daemon-reload 重新加载 systemd 配置
                                4. -
                                5. systemctl restart k3s.service 重启服务
                                6. -
                                7. 验证修改生效
                                  $ ll /etc/rancher/k3s/k3s.yaml
                                  -rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml
                                8. -
                                -

                                修改环境变量

                                第二种方式是修改环境变量。

                                -
                                  -
                                1. sudo vim /etc/systemd/system/k3s.service.env 添加环境变量 K3S_KUBECONFIG_MODE=644
                                  K3S_KUBECONFIG_MODE=644
                                2. -
                                3. systemctl restart k3s.service 重启服务
                                4. -
                                -

                                修改配置文件路径

                                第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。

                                -
                                  -
                                1. 设置环境变量 export KUBECONFIG=~/.kube/config
                                2. -
                                3. 创建文件夹 mkdir ~/.kube 2> /dev/null
                                4. -
                                5. 复制配置信息 sudo k3s kubectl config view --raw > "$KUBECONFIG"
                                6. -
                                7. 修改配置文件的权限 chmod 600 "$KUBECONFIG"
                                8. -
                                -

                                配置代理

                                涉及 k8s,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及 k3s 安装脚本会自动使用当前 shell 中的 HTTP_PROXYHTTPS_PROXYNO_PROXY,以及 CONTAINERD_HTTP_PROXYCONTAINERD_HTTPS_PROXYCONTAINERD_NO_PROXY 变量(如果存在),并将它们写入 systemd 服务的环境文件。比如我设置过 shell 变量 HTTP_PROXYHTTPS_PROXYNO_PROXY/etc/systemd/system/k3s.service.env 如下,你也可以自行编辑修改。

                                -
                                http_proxy='http://127.0.0.1:7890'
                                https_proxy='http://127.0.0.1:7890'
                                no_proxy='localhost,127.0.0.1'
                                K3S_KUBECONFIG_MODE=644
                                - -

                                使用

                                -

                                k8s 基础教程可参考官方文档 Kubernetes 基础

                                -
                                -

                                创建 Deployment

                                  -
                                1. 使用 kubectl create 命令创建管理 PodDeployment。该 Pod 根据提供的 Docker 镜像运行容器。
                                  kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
                                2. -
                                3. 查看 Deployment
                                  $ kubectl get pods
                                  NAME READY STATUS RESTARTS AGE
                                  hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h
                                4. -
                                5. 查看 Pod 中容器的应用程序日志。
                                  $ kubectl logs hello-node-ccf4b9788-d8k9b
                                  I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
                                  I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
                                6. -
                                -

                                创建 Service

                                  -
                                1. 使用 kubectl expose 命令将 Pod 暴露给公网:
                                  kubectl expose deployment hello-node --type=LoadBalancer --port=8080
                                2. -
                                3. 查看你创建的 Service
                                  $ kubectl get services
                                  NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
                                  kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 15h
                                  hello-node LoadBalancer 10.43.37.170 192.168.46.128 8080:32117/TCP 15h
                                4. -
                                5. 使用 curl 发起请求:
                                  $ curl http://localhost:8080
                                  NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511
                                6. -
                                7. 再次查看 Pod 中容器的应用程序日志。
                                  $ kubectl logs hello-node-ccf4b9788-d8k9b
                                  I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
                                  I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
                                  I0130 19:32:21.074992 1 log.go:195] GET /
                                8. -
                                -

                                清理

                                  -
                                1. 删除 Service
                                  kubectl delete service hello-node
                                2. -
                                3. 删除 Deployment
                                  kubectl delete deployment hello-node
                                4. -
                                -

                                参考文章

                                  -
                                • 快速入门指南
                                • -
                                • 管理 Kubeconfig 选项
                                • -
                                • 配置 HTTP 代理
                                • -
                                • 你好,Minikube
                                • -
                                • Permission denied on non-existing /etc/rancher/k3s/config.yaml after fresh install
                                • -
                                • error: error loading config file “/etc/rancher/k3s/k3s.yaml”: open /etc/rancher/k3s/k3s.yaml: permission denied
                                • -
                                • /etc/rancher/k3s/k3s.yaml is world readable #389
                                • +查询得到值 1 + + + + + + + + +查询得到值 1 + + + + + + + +修改值为 2 + + + + + + +查询得到值 V1 + +2(读到B未提交的修改) +1 +1 +1 + + + +提交事务 + + + + + + +查询得到值 V2 + +2 +2(读到B已提交的修改) +1 +1 + + +提交事务 + + + + + + + +查询得到值 V3 + +2 +2 +2(A在事务期间数据一致) +1 + + +补充说明 + + + + +B的修改阻塞至A提交 + + +

                                  通过测试验证以上结论可以帮助你更直观地感受隔离级别的作用:

                                  +
                                    +
                                  • 新建连接 mysql –h localhost –u root -P 3306 –p
                                  • +
                                  • 查看会话的事务隔离级别 show variables like 'transaction_isolation';
                                  • +
                                  • 设置会话的事务隔离级别 set session transaction isolation level read uncommitted|read committed|repeatable read|serializable;
                                  • +
                                  • 测试和验证
                                  -]]> - - k8s - k3s - - - - 探索 Java 类 Cleaner 和 Finalizer - /2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/ - JavaCleanerFinalizer 都实现了一种 finalization 机制,前者更轻量和强大,你可能在了解 NIO 的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize 方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是 Reference 的具体子类。
                                  Reference 作为和 GC 紧密联系的类,你可能从很多文字描述中了解过 SoftReferenceWeakReference 还有 PhantomReference 但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从 Root 对象可达”这条规则再面对 Reference 的子类时是否产生过割裂感;你是否好奇过 Finalizer 如何和重写 finalize 方法的类产生联系,本文将从 CleanerFinalizer 的源码揭示一些你可能已知的结论背后的朴素原理。

                                  - +
                                  mysql> show variables like 'transaction_isolation';
                                  +-----------------------+-----------------+
                                  | Variable_name | Value |
                                  +-----------------------+-----------------+
                                  | transaction_isolation | REPEATABLE-READ |
                                  +-----------------------+-----------------+
                                  -

                                  本文的写作动机继承自 Java 类 Reference 的源码分析,有时候也会自我怀疑研究一个涉及大家极力劝阻使用的 finalize 是否浪费精力,只能说确实如此!要不是半途而废会膈应难受肯定就停了!只能说这个过程确实帮助自己对 Java 引用和 GC 对其的处理有更加深刻的理解。

                                  +

                                  5.7 引入了 transaction_isolation 作为 tx_isolation 的别名,8.0.3 废弃后者。

                                  -

                                  虚引用之 Cleaner

                                  虚引用介绍

                                  PhantomReference 对象在垃圾收集器确定其关联对象可以被回收时或可以被回收后一段时间,将被入队。“可以被回收”更明确的描述是“虚引用的关联对象变成 phantom reachable ,即只有虚引用引用了它”。但是和软引用和弱引用不同,当虚引用入队时并不会被垃圾收集器自动清理(其关联对象)。一个 phantom reachable 的对象会一直维持原样直到所有虚引用被清理或者它们自身变得不可达。

                                  -

                                  PhantomReference 的代码非常简单:

                                  -
                                    -
                                  1. PhantomReference 仅提供了一个 public 构造函数,必须提供 ReferenceQueue 参数。它不像 SoftReferenceWeakReference 可以离开 ReferenceQueue 单独使用,尽管 queue 可以为 null,但是这样做并没有意义。
                                  2. -
                                  3. get() 返回 null,这意味着不能通过 PhantomReference 获取其关联的对象 referent
                                  4. -
                                  -
                                  -

                                  get() 返回 null 并不是可以随意忽略的事情,它保证了 phantom reachable 对象不会被重新触达和修改(这是为清理工作留出时间吗)。

                                  +

                                  了解数据库的隔离级别及其影响对于理解自身正在使用的数据库的行为、根据业务场景设置隔离级别优化性能以及迁移数据都是有帮助的。Oracle 数据库的默认隔离级别是“读提交”,MySQL 的默认隔离级别是“可重复读”。

                                  +

                                  事务隔离的实现

                                  +

                                  MySQL 中,事务隔离是通过 lockundo logread view 共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。

                                  -
                                  public class PhantomReference<T> extends Reference<T> {
                                  public T get() {
                                  return null;
                                  }
                                  public PhantomReference(T referent, ReferenceQueue<? super T> q) {
                                  super(referent, q);
                                  }
                                  }
                                  - -

                                  通过以下示例验证 GC 不会自动清理虚引用的关联对象:

                                  -
                                  public static void main(String[] args) throws InterruptedException {
                                  Scanner scanner = new Scanner(System.in);

                                  byte[] bytes = new byte[100 * 1024 * 1024];
                                  ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
                                  PhantomReference<byte[]> phantomReference = new PhantomReference<>(bytes, queue);

                                  Thread thread = new Thread(() -> {
                                  for (; ; ) {
                                  try {
                                  Reference<? extends byte[]> remove = queue.remove(0);
                                  System.out.println(remove + " enqueued");
                                  // 需要调用 clear 主动清理关联对象,可以验证 gc 后总堆内存占用下降
                                  // remove.clear();
                                  // System.gc();
                                  } catch (InterruptedException e) {
                                  System.out.println(Thread.currentThread().getName() + " interrupt");
                                  break;
                                  }
                                  }
                                  });
                                  thread.start();

                                  System.out.println("暂停查看堆内存占用");
                                  scanner.next();

                                  bytes = null;
                                  System.gc();
                                  System.out.println("gc 后 sleep 3s,查看总堆内存占用未下降");
                                  TimeUnit.SECONDS.sleep(3);

                                  scanner.next();
                                  thread.interrupt();
                                  }
                                  - -

                                  Cleaner 介绍

                                  虚引用最常用于以比 finalization 更灵活的方式安排清理工作,比如其子类 Cleaner 就是一种基于虚引用的清理器,它比 finalization 更轻量但更强大。Cleaner 追踪其关联对象并封装任意的清理代码,在 GC 检测到其关联对象变成 phantom reachable 后一段时间,Reference-Handler 线程将运行清理代码。同时 Cleaner 可以被直接调用,它是线程安全的并且可以保证清理代码最多运行一次。但是 Cleaner 不是 finalization 的替代品,为了避免阻塞 Reference-Handler 线程,清理代码应极其简单和直接。

                                  -

                                  构造函数

                                  Cleaner 的构造函数为 private,仅可通过 create 方法创建实例。

                                  +

                                  MySQL 各个事务隔离级别的实现原理简述如下:

                                    -
                                  • referent: 关联对象
                                  • -
                                  • dummyQueue: 假队列,需要它仅仅是因为 PhantomReference 的构造函数需要一个 queue 参数,但是这个 queue 完全没用,在 ReferenceReference-Handler 线程会显式调用 cleaners 而不会执行入队操作。
                                  • +
                                  • 串行化:读加共享锁,写加排他锁,读写互斥
                                  • +
                                  • 读未提交:写加排他锁,读不加锁
                                  • +
                                  • 可重复读:第一次读操作时创建快照,基于该快照进行读取
                                  • +
                                  • 读提交:每次读操作时重置快照,基于该快照进行读取
                                  -
                                  public class Cleaner extends PhantomReference<Object> {
                                  private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
                                  private final Runnable thunk;

                                  private Cleaner(Object referent, Runnable thunk) {
                                  super(referent, dummyQueue);
                                  this.thunk = thunk;
                                  }

                                  public static Cleaner create(Object ob, Runnable thunk) {
                                  if (thunk == null)
                                  return null;
                                  // 添加到 Cleaner 自身维护的双链表
                                  return add(new Cleaner(ob, thunk));
                                  }
                                  }
                                  - -

                                  添加 cleaner

                                    -
                                  • 使用 synchronized 同步
                                  • -
                                  • Cleaner 自身维护一个双向链表存储 cleaners,通过静态变量 first 存储头节点,以防止 cleaners 比其关联对象更早被 GC
                                  • +

                                    前两者通过锁(lock)实现比较容易理解;后两者通过多版本并发控制(MVCC)实现。MVCC 是一种实现非阻塞并发读的设计思路,在 InnoDB 引擎中主要通过 undo logread view 实现。

                                    +

                                    以下示意图表现了在 InnoDB 引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC),当你基于快照读取时可以获得旧版本的数据。

                                    +
                                      +
                                    • 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
                                    • +
                                    • 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
                                    -
                                    +
                                    -
                                    // 头节点
                                    static private Cleaner first = null;
                                    // 双向指针
                                    private Cleaner next = null, prev = null;
                                    private static synchronized Cleaner add(Cleaner cl) {
                                    // 头插法
                                    if (first != null) {
                                    cl.next = first;
                                    first.prev = cl;
                                    }
                                    first = cl;
                                    return cl;
                                    }
                                    +

                                    在接下来我们将通过锁、事务 ID、回滚日志和一致性视图逐步介绍 InnoDB 事务隔离的实现原理。

                                    +

                                    锁(lock)

                                    事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。MySQL 正是通过共享锁排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC 就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。

                                    +
                                    +

                                    InnoDB 的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议

                                    +
                                    +

                                    理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及 MVCC 的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。

                                    +

                                    事务 ID

                                    InnoDB 引擎中,每个事务都有唯一的一个事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段 trx_id,记录了插入或更新该行数据的事务 ID

                                    +
                                    -

                                    clean 方法

                                    ReferenceReference-Handler 线程对于 Cleaner 类型的对象,会显式地调用其 clean 方法并返回,而不会将其入队。

                                    +

                                    创建事务的时机

                                    事务启动方式如下:

                                      -
                                    1. 使用 synchronized 同步,从双链表上移除自身
                                    2. -
                                    3. 调用 thunkrun 方法
                                    4. +
                                    5. 显式启动事务语句是 beginstart transaction,配套的提交语句是 commit,回滚语句是 rollback
                                    6. +
                                    7. 隐式启动事务语句是 set autocommit = 0,该设置将关闭自动提交。当你执行 select,将自动启动一个事务,直到你主动 commitrollback
                                    -
                                    public void clean() {
                                    if (!remove(this))
                                    return;
                                    try {
                                    thunk.run();
                                    } catch (final Throwable x) {
                                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                                    public Void run() {
                                    if (System.err != null)
                                    new Error("Cleaner terminated abnormally", x).printStackTrace();
                                    System.exit(1);
                                    return null;
                                    }
                                    });
                                    }
                                    }

                                    private static synchronized boolean remove(Cleaner cl) {
                                    // next 指针指向自身代表已经移除,可以避免重复移除和执行
                                    if (cl.next == cl)
                                    return false;
                                    // 更新双链表
                                    if (first == cl) {
                                    if (cl.next != null)
                                    first = cl.next;
                                    else
                                    first = cl.prev;
                                    }
                                    if (cl.next != null)
                                    cl.next.prev = cl.prev;
                                    if (cl.prev != null)
                                    cl.prev.next = cl.next;
                                    // 通过将 next 指针指向自身表示已经被移除
                                    cl.next = cl;
                                    cl.prev = cl;
                                    return true;
                                    }
                                    - -

                                    Cleaner 处理流程

                                      -
                                    • 创建的 Cleaner 对象被 Cleaner 类的双链表直接或间接引用(强引用),因此不会被垃圾回收
                                    • -
                                    • 一切的起点仍然是 GC 特殊地对待虚引用的关联对象,当关联对象从 reachable 变成 phantom reachableGCCleaner 对象将加入 pending-list
                                    • -
                                    • Reference-Handler 线程又将其移除并调用 clean 方法
                                    • -
                                    • 在调用完毕后,Cleaner 对象变成 unreachable 并最终被垃圾回收,其关联对象也被垃圾回收
                                    • -
                                    -
                                    -

                                    注意,Cleaner 对象本身在被调用完毕之前始终是被静态变量引用,是 reachable 的,我们讨论的被判定为可回收的、变成 phantom reachable 状态的是关联对象。

                                    -
                                    -
                                    +

                                    但注意,实际上不论是显式启动事务情况下的 beginstart transaction,还是隐式启动事务情况下的 commitrollback 都不会立即创建一个新事务,而是直到第一次操作 InnoDB 表的语句执行时,才会真正创建一个新事务

                                    +

                                    可以通过以下语句查看当前“活跃”的事务进行验证:

                                    +
                                    select * from information_schema.innodb_trx;
                                    -

                                    事实上,个人猜测“虚引用的关联对象不像软引用和弱引用会被自动清理”描述的仅仅是一个表象,判断是否要被垃圾回收的根本法则仍然是“对象是否从 Root 对象可达”,软引用和弱引用的关联对象之所以会被垃圾回收是因为它们在加入 pending-list 时被从引用对象断开,否则当引用对象被添加到引用队列时,引用队列如果从 Root 对象可达,将导致关联对象也从 Root 对象可达。在 Referenceclear() 的注释中提及该方法只被 Java 代码调用,GC 不需要调用该方法就可以直接清理,肯定是 GC 有直接清理关联对象的场景。同时 Reference 类有一句注释“GC 在检测到关联对象有特定的可达性变化后,将把引用对象添加到引用队列”,它并未将特定的可达性变化直接描述为关联对象变为不可达。目前尚未从 JVM 源代码验证该猜测。

                                    +

                                    只读事务的事务 ID 和更新事务不同。

                                    -

                                    终结引用之 Finalizer

                                    FinalReference 用于实现 finalization,其代码很简单。

                                    -
                                    class FinalReference<T> extends Reference<T> {
                                    public FinalReference(T referent, ReferenceQueue<? super T> q) {
                                    super(referent, q);
                                    }
                                    }
                                    - -

                                    其子类 Finalizer 继承自 FinalReferenceCleaner 在代码设计上和它非常相似。

                                    -

                                    构造函数

                                    Finalizer 的构造函数为 private,仅可通过 register 方法创建实例。

                                    -
                                      -
                                    • finalizee: 关联对象,即重写了 finalize 方法的类的实例
                                    • -
                                    • queue: 引用队列
                                    • -
                                    -

                                    根据注释 registerVM 调用,我们可以合理猜测,这里就是重写了 finalize 方法的类的实例和 Finalizer 对象关联的起点。

                                    +

                                    可以使用 commit work and chain; 在提交的同时开启下一次事务,减少一次 begin; 指令的交互开销。

                                    -
                                    final class Finalizer extends FinalReference<Object> {
                                    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

                                    private Finalizer(Object finalizee) {
                                    super(finalizee, queue);
                                    add();
                                    }
                                    // 由 VM 调用
                                    static void register(Object finalizee) {
                                    new Finalizer(finalizee);
                                    }
                                    }
                                    - -

                                    添加 Finalizer

                                      -
                                    • 使用 synchronized 同步
                                    • -
                                    • Finalizer 自身维护一个双向链表存储 finalizers,通过静态变量 unfinalized 存储头节点
                                    • -
                                    -
                                    - -
                                    private static Finalizer unfinalized = null;
                                    private static final Object lock = new Object();
                                    private Finalizer next = null, prev = null;

                                    private void add() {
                                    synchronized (lock) {
                                    if (unfinalized != null) {
                                    this.next = unfinalized;
                                    unfinalized.prev = this;
                                    }
                                    unfinalized = this;
                                    }
                                    }
                                    - -

                                    Finalizer 线程

                                    finalizers 的清理通常是由一条名为 Finalizer 的线程处理。启动任意一个非常简单的 Java 程序,通过 JVM 相关的工具,比如 JConsole,你都能看到一个名为 Finalizer 的线程。

                                    -
                                    - -

                                    run 方法

                                    private static class FinalizerThread extends Thread {
                                    private volatile boolean running;
                                    FinalizerThread(ThreadGroup g) {
                                    super(g, "Finalizer");
                                    }
                                    public void run() {
                                    // 防止递归调用 run(什么场景?)
                                    if (running)
                                    return;
                                    // Finalizer thread 在 System.initializeSystemClass 被调用前启动,等待 JavaLangAccess 可用
                                    while (!VM.isBooted()) {
                                    // 推迟直到 VM 初始化完成
                                    try {
                                    VM.awaitBooted();
                                    } catch (InterruptedException x) {
                                    // 忽略并继续
                                    }
                                    }
                                    final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                                    // 标记为运行中
                                    running = true;
                                    for (;;) {
                                    try {
                                    // 从队列中移除
                                    Finalizer f = (Finalizer)queue.remove();
                                    // 调用 runFinalizer
                                    f.runFinalizer(jla);
                                    } catch (InterruptedException x) {
                                    // 忽略并继续
                                    }
                                    }
                                    }
                                    }
                                    - -

                                    创建和启动

                                    Finalizer 线程是通过静态代码块创建和启动的。

                                    -
                                    static {
                                    // 向上获取父线程组,直到系统线程组
                                    ThreadGroup tg = Thread.currentThread().getThreadGroup();
                                    for (ThreadGroup tgn = tg;
                                    tgn != null;
                                    tg = tgn, tgn = tg.getParent());
                                    // 创建 FinalizerThread 并启动
                                    Thread finalizer = new FinalizerThread(tg);
                                    // 设置优先级为最高减 2
                                    finalizer.setPriority(Thread.MAX_PRIORITY - 2);
                                    finalizer.setDaemon(true);
                                    finalizer.start();
                                    }
                                    - -

                                    获取 Finalizer 并调用

                                    private void runFinalizer(JavaLangAccess jla) {
                                    synchronized (this) {
                                    // 判断是否已经终结过
                                    if (hasBeenFinalized()) return;
                                    // 从双链表上移除
                                    remove();
                                    }
                                    try {
                                    // 获取关联的 finalizee
                                    Object finalizee = this.get();
                                    // 如果不为 null 且不是 Enum 类型
                                    if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                                    // 调用 invokeFinalize
                                    jla.invokeFinalize(finalizee);
                                    // 清理栈槽以降低保守 GC 时误保留的可能性
                                    finalizee = null;
                                    }
                                    } catch (Throwable x) { }
                                    // 清理关联对象
                                    super.clear();
                                    }

                                    // 和 Cleaner 类似,使用 next 指向自身表示已被移除
                                    private boolean hasBeenFinalized() {
                                    return (next == this);
                                    }

                                    // 和 Cleaner 类似的处理
                                    private void remove() {
                                    synchronized (lock) {
                                    if (unfinalized == this) {
                                    if (this.next != null) {
                                    unfinalized = this.next;
                                    } else {
                                    unfinalized = this.prev;
                                    }
                                    }
                                    if (this.next != null) {
                                    this.next.prev = this.prev;
                                    }
                                    if (this.prev != null) {
                                    this.prev.next = this.next;
                                    }
                                    this.next = this;
                                    this.prev = this;
                                    }
                                    }
                                    - -

                                    finalize 的调用原理

                                    关于如何调用 finalize 方法涉及不少平时接触不到的代码。

                                    -
                                    // 获取 JavaLangAccess
                                    final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
                                    // 通过 JavaLangAccess 调用 finalizee 的 finalize 方法
                                    jla.invokeFinalize(finalizee);
                                    public static void setJavaLangAccess(JavaLangAccess jla) {
                                    javaLangAccess = jla;
                                    }
                                    - -

                                    SharedSecretsjavaLangAccess 通过 setJavaLangAccess 设置

                                    -
                                    public static void setJavaLangAccess(JavaLangAccess jla) {
                                    javaLangAccess = jla;
                                    }
                                    public static JavaLangAccess getJavaLangAccess() {
                                    return javaLangAccess;
                                    }
                                    - -

                                    setJavaLangAccess 方法在 System 中被调用,javaLangAccess 被设置为一个匿名类实例,其中 invokeFinalize 方法间接调用了传入对象的 finalize 方法。

                                    -
                                    private static void setJavaLangAccess() {
                                    // Allow privileged classes outside of java.lang
                                    sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
                                    // ...
                                    public void invokeFinalize(Object o) throws Throwable {
                                    o.finalize();
                                    }
                                    });
                                    }
                                    - -

                                    SystemsetJavaLangAccess 方法在 initializeSystemClass 方法中被调用。这里正对应着 FinalizerThreadrun 方法中等待 VM 初始化完成的处理。

                                    -
                                    // 初始化 System class,在线程初始化之后调用
                                    private static void initializeSystemClass() {
                                    // ...
                                    // register shared secrets
                                    setJavaLangAccess();
                                    // 通知 wait 的线程
                                    sun.misc.VM.booted();
                                    }
                                    - -

                                    Finalizer 的注册时机

                                    你是否好奇过 JVM 是如何保证 finalize 方法最多被调用一次的?如果曾经猜测过 JVM 可能在对象中留有标记,那么在我们研究过对象的内部结构之后可以确认其中并没有用于记录对象是否已经 finalized 的地方。同时我们注意到 hasBeenFinalized 方法通过 next 指针是否指向自己表示是否已经 finalized。我们可以合理猜测 register 的调用时机是在对象创建时,因此最多仅有一次被注册。

                                    -

                                    通过以下示例可以测试:

                                    +

                                    回滚日志(undo log)

                                    InnoDB 引擎中,每条记录在更新的时候都会同时记录一条回滚操作。记录的最新值,通过回滚操作,可以得到之前版本的值。它的作用是:

                                      -
                                    • 在创建重写了 finalize 方法的类创建对象期间会调用 register 创建并注册 Finalizer
                                    • -
                                    • 在未重写 finalize 方法的类创建对象期间不会调用register
                                    • -
                                    • Finalizer 不仅可以保证 finalize 只会被调用一次,甚至不会第二次被添加到 pending-list,因为 runFinalizer 最后调用了 super.clear()JVM 不会特殊对待复活的对象
                                    • +
                                    • 数据回滚:当事务回滚或者数据库崩溃时,通过 undolog 进行数据回滚。
                                    • +
                                    • 多版本并发控制:当读取一行记录时,如果该行记录已经被其他事务修改,通过 undo log 读取之前版本的数据,以此实现非阻塞并发读。
                                    -
                                    public class FinalReferenceTest_1 {

                                    private static FinalizeObj save = null;

                                    public static void main(String[] args) throws InterruptedException {
                                    System.out.println("创建 finalize obj,使用 Debug 强制运行到 Finalizer.register");
                                    FinalizeObj finalizeObj = new FinalizeObj();

                                    System.out.println("gc");
                                    finalizeObj = null;
                                    System.gc();
                                    System.out.println("sleep 1s");
                                    TimeUnit.SECONDS.sleep(1);
                                    save.echo();

                                    save = null;
                                    System.gc();
                                    System.out.println("sleep 1s");
                                    TimeUnit.SECONDS.sleep(1);
                                    System.out.println(save == null);
                                    }

                                    static class FinalizeObj {
                                    FinalizeObj() {
                                    System.out.println("SaveSelf created");
                                    }
                                    @Override
                                    protected void finalize() throws Throwable {
                                    System.out.println("finalized");
                                    save = this;
                                    }
                                    public void echo() {
                                    System.out.println("I am alive.");
                                    }
                                    }
                                    }
                                    - - - +

                                    实际上,每一行数据还有一个隐藏字段 roll_ptr。很多相关资料简单地描述“roll_ptr 用于指向该行数据的上一个版本”,但是该说法容易让人误解旧版本的数据是物理上真实存在的,好像有一张链表结构的历史记录表按顺序记录了每一个版本的数据。

                                    +
                                    -

                                    参考文章

                                    -]]> - - java - - - - Java 类 Reference 的源码分析 - /2023/12/27/source-code-analysis-of-Java-class-Reference/ - 我们知道 Java 扩充了“引用”的概念,引入了软引用、弱引用和虚引用,它们都属于 Reference 类型,也都可以配合 ReferenceQueue 使用。你是否好奇常常被一笔带过的“引用对象的处理过程”?你是否在探究 NIO 堆外内存的自动释放时看到了 Cleaner 的关键代码但不太能梳理整个过程?你是否好奇在研究 JVM 时偶尔看到的 Reference Handler 线程?本文将分析 ReferenceReferenceQueue 的源码带你理解引用对象的工作机制。

                                    - +

                                    有些资料会特地强调旧版本的数据不是物理上真实存在的,undo log 是逻辑日志,记录了与实际操作语句相反的操作,旧版本的数据是通过 undo log 计算得到的。

                                    +
                                    -

                                    事实上,个人感觉在无相关前置知识的情况下,单纯看 JDKJava 代码是没办法很好地理解引用对象是如何被添加到引用队列中的。因为 Referencepending 字段的含义和赋值操作是隐藏在 JVMC++ 代码中,本文搁置了其中的细节,仅分析 JDK 中相关的 Java 代码。

                                    +

                                    说实话,在不了解细节的前提下,通过计算得到旧版本的数据更加反直觉。总而言之,InnoDB 的数据总是存储最新版本,尽管该版本所属的事务可能尚未提交;任何事务其实都是从最新版本开始回溯,直到获得该事务认为可见的版本。

                                    -

                                    Reference

                                    Reference引用对象的抽象基类。此类定义了所有引用对象通用的操作。由于引用对象是与垃圾收集器密切合作实现的,因此该类可能无法直接子类化。

                                    -
                                    - -

                                    构造函数

                                      -
                                    • referent: 引用对象关联的对象
                                    • -
                                    • queue: 引用对象准备注册到的引用队列
                                    • +

                                      回滚日志的删除时机

                                      回滚日志不会一直保留,在没有事务需要的时候,系统会自动判断和删除。基于该结论,我们应该避免使用长事务。长事务意味着系统里面可能会存在很老的 read view,这些事务可能访问数据库里的任何数据,所以在这个事务提交之前,数据库里它可能用到的回滚日志都必须保留,这就会导致大量存储空间被占用。在 MySQL 5.5 及之前的版本中,回滚日志是和数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,但只是代表那部分存储空间可复用,文件并不会变小,需要重建整个库才能解决问题。

                                      +

                                      一致性视图(read view)

                                      一致性读视图(read view)又可以称之为快照(snapshot),它是基于整库的,但是它并不是真的拷贝了整个数据库的数据,否则随着数据量的增长,显然无法实现秒级创建快照。read view 可以理解为发出一个声明:“以我创建的时刻为准,如果一个数据版本所属的事务是在这之前提交的,就可见;如果是在这之后提交的,就不可见,需要回溯上一个版本判断,重复直到获得可见的版本;如果该数据版本属于当前事务自身,是可见的”。

                                      +
                                      +

                                      以上声明类似于功能的需求描述,它比具体实现更简洁和易于理解。

                                      +
                                      +

                                      “快照”结合“多版本”等词,和 undo log 的情况类似很容易让人误解为有一个物理上真实存在的数据快照,但实际上 read view 只是在沿着数据版本链回溯时用于判断该版本对当前事务是否可见的依据。在具体实现上,InnoDB 为每一个事务构造了一个数组用于保存创建 read view 时,当前正在“活跃”的所有事务 ID ,其中“活跃”指的是启动了但尚未提交。数组中事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个数组和高水位就组成了当前事务的一致性视图(read view)。对于当前事务的 read view 而言,一个数据版本的 trx_id,有以下几种可能:

                                      +
                                        +
                                      • 如果小于低水位,表示这个版本是已提交的事务生成的,可见
                                      • +
                                      • 如果大于等于高水位,表示这个版本是创建 read view 之后启动的事务,不可见
                                      • +
                                      • 如果大于等于低水位且小于高水位
                                          +
                                        • 如果这个版本的 trx_id 在数组中,表示这个版本是已启动但尚未提交的事务生成的,不可见
                                        • +
                                        • 如果这个版本的 trx_id 不在数组中,表示这个版本是已提交的事务生成的,可见
                                        -

                                        Reference 提供了两个构造函数,一个需要传入引用队列ReferenceQueue),一个不需要。如果一个引用对象Reference)注册到一个引用队列,在检测到关联对象有适当的可达性变化后,垃圾收集器将把该引用对象添加到该引用队列。

                                        +
                                      • +
                                      +

                                      InnoDB 利用“所有数据都有多个版本,每个版本都记录了所属事务 ID”这个特性,实现了“秒级创建快照”的能力。有了这个能力,系统里面随后发生的更新,就和当前事务可见的数据无关了,当前事务读取时也不必再加锁。

                                      -

                                      “关联对象有适当的可达性变化”并不容易理解,在很多表述中它很容易被简化为“可以被回收”,但是同时我们又拥有另一条规则,即“一个对象是否可回收的判断依据是是否从 Root 对象可达”。在面对 Reference 的子类时,我们有种割裂感,好像一条和谐的规则出现了特殊条例。探索 Java 类 Cleaner 和 Finalizer

                                      +

                                      以上“具体实现”相较于之前的“需求描述”显得有些啰嗦和复杂,然而这里的细节是值得推敲的。即便是林晓斌老师在《MySQL 实战 45 讲》中的详细讲解也让部分读者包括我本人感到困惑。

                                      -
                                      Reference(T referent) {
                                      this(referent, null);
                                      }

                                      Reference(T referent, ReferenceQueue<? super T> queue) {
                                      this.referent = referent;
                                      // ReferenceQueue.NULL 表示没有注册到引用队列
                                      this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
                                      }
                                      +

                                      林晓斌老师的数据版本可见性示意图如下,容易让人产生误解的地方在于三段式的划分给人一种已提交的事务全都是小于低水位的错觉

                                      +
                                      -

                                      属性

                                      成员变量

                                        -
                                      • referent: 引用对象关联的对象,该对象将被垃圾收集器特殊对待。我们很难直观地感受何谓“被垃圾收集器特殊对待”,它对应着“在检测到关联对象有适当的可达性变化后,垃圾收集器将把引用对象添加到该引用队列”。
                                      • -
                                      • queue: 引用对象注册到的引用队列
                                      • -
                                      • next: 用于指向下一个引用对象,当引用对象已经添加到引用队列中,next 指向引用队列中的下一个引用对象
                                      • -
                                      • discovered: 用于指向下一个引用对象,用于在全局的 pending 链表中,指向下一个待添加到引用队列引用对象
                                      • -
                                      -
                                      +

                                      事实上,已提交事务的分布可能如下,大部分人的疑问其实只是“在大于等于低水位小于高水位的范围中,为什么会有已提交的事务”。

                                      +
                                      -

                                      静态变量

                                      -

                                      注意:lockpending 是全局共享的。

                                      +

                                      要理解该问题需要理解另外一个问题——“创建 read view 的时机”。

                                      +

                                      创建 read view 的时机

                                      很多资料介绍“可重复读”隔离级别下的 read view 创建时机为在事务启动时,但这并不严谨,还会导致理解 read view 数组困难。创建事务并不等于创建 read view

                                      +
                                      +

                                      官方文档:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTED isolation level, the snapshot is reset to the time of each consistent read operation.

                                        -
                                      • lock: 用于与垃圾收集器同步的对象,垃圾收集器必须在每个收集周期开始时获取此锁。因此至关重要的是持有此锁的任何代码必须尽快运行完,不分配新对象并避免调用用户代码。
                                      • -
                                      • pending: 等待加入引用队列引用对象链表。垃圾收集器将引用对象添加到 pending 链表中,而 Reference-Handler 线程将删除它们,并做清理或入队操作。pending 链表受上述 lock 对象的保护,并使用 discovered 字段来链接下一个元素。
                                      • +
                                      • 对于“读提交”隔离级别,每次读操作都会重置快照。这意味着只要当前事务持续足够长的时间,它最后读取时完全可能熬到在它之前甚至之后创建的事务提交。
                                      • +
                                      • 对于“可重复读”隔离级别,在第一次执行快照读时创建快照。这意味着当前事务可以执行很多次以及很久的 update 语句后再执行读取,熬到在它之前甚至之后创建的事务提交。
                                      -
                                      public abstract class Reference<T> {
                                      private T referent; /* Treated specially by GC */

                                      volatile ReferenceQueue<? super T> queue;
                                      @SuppressWarnings("rawtypes")
                                      volatile Reference next;

                                      transient private Reference<T> discovered; /* used by VM */

                                      static private class Lock { }
                                      private static Lock lock = new Lock();

                                      private static Reference<Object> pending = null;
                                      }
                                      - +

                                      有些人可能想到了前者,但对于后者存疑或者不知道如何验证,其实测试并不复杂:

                                      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                      事务 A事务 B
                                      begin;begin;
                                      update t set k = 2 where id = 2;(创建事务)
                                      update t set k = 666 where id = 1;(创建事务)
                                      commit;
                                      select * from t where id = 1;(创建 read view,k = 666)
                                      commit;
                                      -

                                      Reference 其实可以理解为单链表中的一个节点,除了核心的 referentqueuenextdiscovered 都用于指向下一个引用对象,只是分别用于两条不同的单链表上。

                                      +

                                      因此,严谨地说,创建事务的时机和创建一致性视图的时机是不同的。通过 start transaction with consistent snapshot; 可以在开启事务的同时立即创建 read view

                                      -

                                      pending 链表:

                                      -
                                      - -

                                      ReferenceQueue

                                      -
                                      - -

                                      ReferenceHandler 线程

                                      启动任意一个非常简单的 Java 程序,通过 JVM 相关的工具,比如 JConsole,你都能看到一个名为 Reference Handler 的线程。

                                      -
                                      - -

                                      ReferenceHandler 类本身的代码并不复杂。

                                      -
                                      private static class ReferenceHandler extends Thread {
                                      // 确保类已经初始化
                                      private static void ensureClassInitialized(Class<?> clazz) {
                                      try {
                                      Class.forName(clazz.getName(), true, clazz.getClassLoader());
                                      } catch (ClassNotFoundException e) {
                                      throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
                                      }
                                      }

                                      static {
                                      // 预加载和初始化 InterruptedException 和 Cleaner,以避免在 run 方法中懒加载发生内存不足时陷入麻烦(咱也不知道具体啥麻烦)
                                      ensureClassInitialized(InterruptedException.class);
                                      ensureClassInitialized(Cleaner.class);
                                      }

                                      ReferenceHandler(ThreadGroup g, String name) {
                                      super(g, name);
                                      }

                                      public void run() {
                                      // run 方法循环调用 tryHandlePending
                                      while (true) {
                                      tryHandlePending(true);
                                      }
                                      }
                                      }
                                      - -

                                      创建线程并启动

                                      Reference-Handler 线程是通过静态代码块创建并启动的。

                                      -
                                      static {
                                      // 不断获取父线程组,直到最高的系统线程组
                                      ThreadGroup tg = Thread.currentThread().getThreadGroup();
                                      for (ThreadGroup tgn = tg;
                                      tgn != null;
                                      tg = tgn, tgn = tg.getParent());
                                      Thread handler = new ReferenceHandler(tg, "Reference Handler");
                                      // 设置为最高优先级
                                      handler.setPriority(Thread.MAX_PRIORITY);
                                      // 设置为守护线程
                                      handler.setDaemon(true);
                                      handler.start();

                                      // provide access in SharedSecrets
                                      // 不懂,看到一个说法覆盖 JVM 的默认处理方式
                                      SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
                                      @Override
                                      public boolean tryHandlePendingReference() {
                                      return tryHandlePending(false);
                                      }
                                      });
                                      }
                                      - -

                                      run 处理逻辑

                                      run 方法的核心处理逻辑。本质上,ReferenceHandler 线程将 pending 链表上的引用对象分发到各自注册的引用队列中。如果理解了 Reference 作为单链表节点的一面,这部分代码不难理解,反而是其中应对 OOME 的处理很值得关注,但更多的可能是看了个寂寞,不好重现问题并验证。

                                      -
                                      static boolean tryHandlePending(boolean waitForNotify) {
                                      Reference<Object> r;
                                      Cleaner c;
                                      try {
                                      // 加锁(和垃圾回收共用一个锁)
                                      synchronized (lock) {
                                      // 如果不为 null
                                      if (pending != null) {
                                      // 获取头节点
                                      r = pending;
                                      // instanceof 可能抛出 OutOfMemoryError,因此在把 r 从 pending 链表中移除前进行
                                      // 如果是 Cleaner 类型,进行类型转换,后续有特殊处理
                                      c = r instanceof Cleaner ? (Cleaner) r : null;
                                      // 从 pending 链表移除 r
                                      pending = r.discovered;
                                      r.discovered = null;
                                      } else {
                                      // 等待锁可能抛出 OutOfMemoryError,因为可能需要分配 exception 对象
                                      if (waitForNotify) {
                                      lock.wait();
                                      }
                                      // retry if waited
                                      return waitForNotify;
                                      }
                                      }
                                      } catch (OutOfMemoryError x) {
                                      // 给其他线程 CPU 时间,以便它们能够丢弃一些存活的引用,然后通过 GC 回收一些空间
                                      // 还可以防止 CPU 密集运行以至于上面的“r instanceof Cleaner”在一段时间内持续抛出 OOME
                                      Thread.yield();
                                      // retry
                                      return true;
                                      } catch (InterruptedException x) {
                                      // retry
                                      return true;
                                      }

                                      // 如果是 Cleaner 类型,快速清理并返回
                                      if (c != null) {
                                      c.clean();
                                      return true;
                                      }

                                      // 如果 Reference 对象关联了引用队列,则添加到队列
                                      ReferenceQueue<? super Object> q = r.queue;
                                      if (q != ReferenceQueue.NULL) q.enqueue(r);
                                      return true;
                                      }
                                      - -

                                      关联对象和队列相关方法

                                      /* -- Referent accessor and setters -- */

                                      // 获取关联对象
                                      public T get() {
                                      return this.referent;
                                      }

                                      // 清理关联对象,该操作不会导致引用对象入队
                                      public void clear() {
                                      this.referent = null;
                                      }

                                      /* -- Queue operations -- */

                                      // 判断引用对象是否已入队,如果未关联引用队列,则返回 false
                                      public boolean isEnqueued() {
                                      return (this.queue == ReferenceQueue.ENQUEUED);
                                      }

                                      // 将引用对象添加到其注册的引用队列中,该方法仅 Java 代码调用,JVM 不需要调用本方法可以直接进行入队操作(什么情况下?)
                                      public boolean enqueue() {
                                      return this.queue.enqueue(this);
                                      }
                                      - -

                                      ReferenceQueue

                                      引用队列,在检测到适当的可达性更改后,垃圾收集器将已注册的引用对象添加到该队列。

                                      -

                                      属性

                                      public class ReferenceQueue<T> {

                                      // 构造函数
                                      public ReferenceQueue() { }

                                      // 一个不可入队的队列
                                      private static class Null<S> extends ReferenceQueue<S> {
                                      boolean enqueue(Reference<? extends S> r) {
                                      return false;
                                      }
                                      }
                                      // 用于表示一个引用对象没有注册到引用队列
                                      static ReferenceQueue<Object> NULL = new Null<>();
                                      // 用于表示一个引用对象已经添加到引用队列
                                      static ReferenceQueue<Object> ENQUEUED = new Null<>();

                                      // 锁对象
                                      static private class Lock { };
                                      private Lock lock = new Lock();
                                      // 头节点
                                      private volatile Reference<? extends T> head = null;
                                      // 队列长度
                                      private long queueLength = 0;
                                      }
                                      - -

                                      入队

                                      enqueue 只能由 Reference 类调用。

                                      -

                                      引用对象queue 字段可以表达引用对象的状态:

                                      +

                                      当前读和快照读

                                      现在我们知道在 InnoDB 引擎中,一行数据存在多个版本。MVCC 使得在“可重复读”隔离级别下的事务好像与世无争。但是在以下示例中,事务 B 是在事务 A 的一致性视图之后创建和提交的,为什么事务 A 查询到的 k 为 3 呢?

                                      + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                      事务 A事务 B
                                      start transaction with consistent snapshot;(k = 1)
                                      update t set k = k + 1 where id = 1;(自动提交事务)
                                      update t set k = k + 1 where id = 1;当前读
                                      select * from t where id = 1;(k = 3)
                                      commit;
                                      +

                                      其实,更新数据是先读后写的,并且是“当前读”。

                                        -
                                      • NULL:表示没有注册到引用队列或者已经从引用队列中移除
                                      • -
                                      • ENQUEUED:表示已经添加到引用队列
                                      • +
                                      • 当前读:读取一行数据的最新版本,并保证在读取时其他事务不能修改该行数据,因此需要在读取时加锁。以下操作属于当前读的情况:
                                          +
                                        • 共享锁:select lock in share mode
                                        • +
                                        • 排他锁:select for updateupdateinsertdelete
                                        -
                                        boolean enqueue(Reference<? extends T> r) {
                                        synchronized (lock) {
                                        // 检查引用对象的状态是否可以入队
                                        ReferenceQueue<?> queue = r.queue;
                                        if ((queue == NULL) || (queue == ENQUEUED)) {
                                        return false;
                                        }
                                        // 检查注册的 queue 和调用的 queue 是否相同
                                        assert queue == this;
                                        // 标记为已入队
                                        r.queue = ENQUEUED;
                                        // 头插法,最后一个节点的 next 指向自身(为什么?)
                                        r.next = (head == null) ? r : head;
                                        head = r;
                                        // 队列长度加一
                                        queueLength++;
                                        if (r instanceof FinalReference) {
                                        sun.misc.VM.addFinalRefCount(1);
                                        }
                                        // 通知等待的线程
                                        lock.notifyAll();
                                        return true;
                                        }
                                        }
                                        - -

                                        出队

                                        轮询队列以查看是否有引用对象可用,如果存在可用的引用对象则将其从队列中删除并返回,否则该方法立即返回 null

                                        -
                                        public Reference<? extends T> poll() {
                                        // 缩小锁的范围
                                        if (head == null)
                                        return null;
                                        synchronized (lock) {
                                        return reallyPoll();
                                        }
                                        }

                                        private Reference<? extends T> reallyPoll() {
                                        Reference<? extends T> r = head;
                                        if (r != null) {
                                        @SuppressWarnings("unchecked")
                                        Reference<? extends T> rn = r.next;
                                        // 因为尾节点的 next 指向自身
                                        head = (rn == r) ? null : rn;
                                        // 标记为 NULL,避免再次入队
                                        r.queue = NULL;
                                        // next 指向自己
                                        r.next = r;
                                        // 队列长度减一
                                        queueLength--;
                                        if (r instanceof FinalReference) {
                                        sun.misc.VM.addFinalRefCount(-1);
                                        }
                                        return r;
                                        }
                                        return null;
                                        }
                                        - -

                                        出队操作提供了等待的选项。

                                        -
                                        // 从队列中移除下一个元素,阻塞直到有元素可用。
                                        public Reference<? extends T> remove() throws InterruptedException {
                                        return remove(0);
                                        }

                                        // 从队列中移除下一个元素,阻塞直到超时或有元素可用,timeout 以毫秒为单位。
                                        public Reference<? extends T> remove(long timeout)
                                        throws IllegalArgumentException, InterruptedException
                                        {
                                        if (timeout < 0) {
                                        throw new IllegalArgumentException("Negative timeout value");
                                        }
                                        synchronized (lock) {
                                        Reference<? extends T> r = reallyPoll();
                                        if (r != null) return r;
                                        long start = (timeout == 0) ? 0 : System.nanoTime();
                                        for (;;) {
                                        lock.wait(timeout);
                                        r = reallyPoll();
                                        if (r != null) return r;
                                        // 如果 timeout 大于 0
                                        if (timeout != 0) {
                                        long end = System.nanoTime();
                                        // 计算下一轮等待时间
                                        timeout -= (end - start) / 1000_000;
                                        // 到时间直接返回 null
                                        if (timeout <= 0) return null;
                                        // 更新开始时间
                                        start = end;
                                        }
                                        }
                                        }
                                        }
                                        - -

                                        状态变化

                                        Reference 实例(引用对象)可能处于四种内部状态之一:

                                        -
                                          -
                                        • Active: 新创建的实例处于 Active 状态,受到垃圾收集器的特殊处理。收集器在检测到关联对象的可达性变为适当状态后的一段时间,会将实例的状态更改为 PendingInactive,具体取决于实例在创建时是否注册到引用队列中。在前一种情况下,它还会将实例添加到待 pending-Reference 列表中。
                                        • -
                                        • Pending: 实例处在 pending-Reference 列表中,等待 Reference-Handler 线程将其加入引用队列。未注册到引用队列的实例永远不会处于这种状态。
                                        • -
                                        • Enqueued: 处在创建实例时注册到的引用队列中。当实例从引用队列中删除时,该实例将变为 Inactive 状态。未注册到引用队列的实例永远不会处于这种状态。
                                        • -
                                        • Inactive: 没有进一步的操作。一旦实例变为 Inactive 状态,其状态将永远不会再改变。
                                        • + +
                                        • 快照读:在不加锁的情况下通过 select 读取一行数据,但和“读未提交”隔离级别下单纯地读取最新版本不同,它是基于一个“快照”进行读取。
                                        -

                                        Reference 实例(引用对象)的状态由 queuenext 字段共同表达:

                                        +

                                        因此在事务 A 中更新时,读取到的是事务 B 更新后的最新值,在事务 A 更新后,依据 read view 的可见性原则,它可以看到自身事务的更新后的最新值 3。

                                        +

                                        如果事务 B 尚未提交的情况下,事务 A 发起更新,会如何呢?这时候就轮到“两阶段锁协议”派上用场了:

                                          -
                                        • Active: (queue == ReferenceQueue || queue == ReferenceQueue.NULL) && next == null
                                        • -
                                        • Pending: queue == ReferenceQueue && next == this
                                        • -
                                        • Enqueued: queue == ReferenceQueue.ENQUEUED && (next == Following || this)(在队列末尾时,next 指向自身,目前没有体现出这么设计的必要性啊?)
                                        • -
                                        • Inactive: queue == ReferenceQueue.NULL && next == this
                                        • +
                                        • 事务 B 在更新时,对改行数据加排他锁,在事务 B 提交时才会释放
                                        • +
                                        • 当事务 A 发起更新,将阻塞直到事务 B 提交
                                        -
                                        - -

                                        Reference 的子类

                                        参考文章

                                          -
                                        • 你不可不知的Java引用类型之——Reference源码解析
                                        • -
                                        • Java引用类型之:Reference源码解析
                                        • -
                                        • JVM之Reference源码分析
                                        • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                          事务 A事务 B
                                          start transaction with consistent snapshot;(k = 1)
                                          begin;
                                          update t set k = k + 1 where id = 1;排他锁
                                          update t set k = k + 1 where id = 1;阻塞至 B 提交
                                          commit;
                                          select * from t where id = 1;(k = 3)
                                          commit;
                                          +

                                          至此,我们将锁和 MVCC 在事务隔离的实现原理中串联起来了。两者是互相独立又互相协作的两个机制,前者实现了“当前读”,后者实现了“快照读”。

                                          +

                                          总结

                                          卡壳好几天,想到有不少好的文章却仍然会给读者留下困惑,想到自己在当初学习时对一些不严谨的表达抓耳挠腮想不通为什么,就有点不知道如何下笔。最终围绕着自己当初的一些困惑,一点一点修修补补完了。

                                          +

                                          参考文章

                                          ]]> - java + mysql - synchronized 锁机制的分析和验证 - /2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/ - 本文详细介绍了 Javasynchronized 锁的机制、存储结构、优化措施以及升级过程,并通过 jol-core 演示 Mark Word 的变化来验证锁升级的多个 case

                                          + 不使用 GParted 的情况下为 VMware 中的 Ubuntu Server 增大磁盘空间 + /2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/ + GParted 是一款适用于 Linux 的图形化磁盘分区管理工具,通过它可以便捷地为 VMware 中的 Ubuntu Desktop 增大磁盘空间。然而你可能正在使用 Ubuntu Server,并不想要安装或并不被允许安装图形化界面,本文介绍了如何在不使用 GParted 的情况下,通过命令行使用自带的工具为 VMware 中的 Ubuntu Server 增大磁盘空间。

                                          -

                                          待完善

                                          +

                                          请注意辨别磁盘空间是真的接近耗尽,而不是在系统安装时只真正使用了大约一半空间。参见 Ubuntu server 20.04 安装后没有分配全部磁盘空间

                                          -

                                          利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

                                          +

                                          背景介绍

                                          环境如下:

                                            -
                                          • 对于普通同步方法,锁是当前实例对象。
                                          • -
                                          • 对于静态同步方法,锁是当前类的 Class 对象。
                                          • -
                                          • 对于同步方法块,锁是 synchronized 括号里配置的对象。
                                          • +
                                          • VMware® Workstation 17 Pro 17.5.0 build-22583795
                                          • +
                                          • Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-169-generic x86_64)
                                          -

                                          当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

                                          +

                                          尽管最初按照心理预期为 Ubuntu Server 分配了 50G 的磁盘空间,主要用于运行一些 Docker 容器,但是不知不觉之间发现磁盘空间的占用率还是上升到了 90%。一时之间想不到可以清理什么,决定先增大一些磁盘空间。

                                            -
                                          • JVM 层面,synchronized 锁是基于进入和退出 Monitor 来实现的,每一个对象都有一个 Monitor 与之相关联。
                                          • -
                                          • 在字节码层面,同步方法块是使用 monitorentermonitorexit 指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。
                                          • +
                                          • 使用 df -h 命令显示文件系统的总空间和可用空间信息。可知 /dev/mapper/ubuntu--vg-ubuntu--lv 已使用 95%
                                            $ df -h
                                            Filesystem Size Used Avail Use% Mounted on
                                            udev 7.8G 0 7.8G 0% /dev
                                            tmpfs 1.6G 3.0M 1.6G 1% /run
                                            /dev/mapper/ubuntu--vg-ubuntu--lv 48G 43G 2.5G 95% /
                                            tmpfs 7.8G 0 7.8G 0% /dev/shm
                                            tmpfs 5.0M 0 5.0M 0% /run/lock
                                            tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
                                            vmhgfs-fuse 932G 859G 73G 93% /mnt/hgfs
                                            /dev/sda2 2.0G 209M 1.6G 12% /boot
                                            /dev/loop0 64M 64M 0 100% /snap/core20/2015
                                            /dev/loop1 64M 64M 0 100% /snap/core20/2105
                                            /dev/loop2 41M 41M 0 100% /snap/snapd/20290
                                            /dev/loop3 92M 92M 0 100% /snap/lxd/24061
                                            /dev/loop4 41M 41M 0 100% /snap/snapd/20671
                                            tmpfs 1.6G 0 1.6G 0% /run/user/1000
                                          -

                                          存储结构

                                          -

                                          锁存在哪里呢?锁里面又会存储什么信息呢?

                                          +

                                          解决步骤

                                          调整虚拟磁盘大小

                                          +

                                          不论如何,需要先修改 VMware 的相关设置。

                                          -

                                          对象头

                                          synchronized 用的锁是存在 Java 对象头(object header)里的。如果对象是数组类型,则虚拟机用 3 字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。在 64 位虚拟机中,1 字宽等于 8 字节,即 64bit

                                          -

                                          Java 对象头的组成结构如下:

                                          - +
                                            +
                                          1. 先将客户机 Ubuntu server 关机
                                          2. +
                                          3. 然后通过“虚拟机设置 -> 硬盘 -> 扩展 -> 最大磁盘大小”将最大虚拟磁盘大小设置为目标值(50G -> 80G
                                          4. +
                                          5. 根据提示可知,在 VMware 中的扩展操作仅增大虚拟磁盘的大小,分区和文件系统的大小不受影响。你必须从客户机操作系统内部对磁盘重新进行分区和扩展文件系统。
                                          6. +
                                          +

                                          调整分区大小

                                          分区管理

                                            +
                                          1. 使用 sudo cfdisk 命令进入分区管理的交互式界面。可知可用空间为新增的 30G
                                          2. +
                                          3. 使用上下方向键选择准备调整大小的分区 /dev/sda3,使用左右方向键选择 Resize 操作。
                                          4. +
                                          5. 输入新的分区大小,默认为原大小加上可用空间大小等于 78G
                                          6. +
                                          7. 使用左右方向键选择 Write 操作,写入修改。然后输入 yes 确认。
                                          8. +
                                          9. 提示分区表已改变。然后使用左右方向键选择 Quit 操作,退出分区管理的交互式界面。
                                          10. +
                                          11. 退出时提示如下。
                                          $ sudo cfdisk
                                          GPT PMBR size mismatch (104857599 != 167772159) will be corrected by write.

                                          Syncing disks.
                                          +
                              +

                              调整物理卷大小

                                +
                              1. 使用 sudo pvresize /dev/sda3 命令调整 LVM 中物理卷的大小。
                                $ sudo pvresize /dev/sda3
                                Physical volume "/dev/sda3" changed
                                1 physical volume(s) resized or updated / 0 physical volume(s) not resized
                              2. +
                              +

                              调整逻辑卷大小

                                +
                              1. 使用 sudo fdisk -l 命令显示物理卷和逻辑卷的大小差异。在末尾可见 /dev/sda3 的大小为 78G/dev/mapper/ubuntu--vg-ubuntu--lv 的大小为 47.102G
                                $ sudo fdisk -l
                                Disk /dev/loop0: 63.48 MiB, 66547712 bytes, 129976 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop1: 63.93 MiB, 67014656 bytes, 130888 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop2: 40.88 MiB, 42840064 bytes, 83672 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop3: 91.85 MiB, 96292864 bytes, 188072 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/loop4: 40.44 MiB, 42393600 bytes, 82800 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes


                                Disk /dev/fd0: 1.42 MiB, 1474560 bytes, 2880 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                                Disklabel type: dos
                                Disk identifier: 0x90909090

                                Device Boot Start End Sectors Size Id Type
                                /dev/fd0p1 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p2 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p3 2425393296 4850786591 2425393296 1.1T 90 unknown
                                /dev/fd0p4 2425393296 4850786591 2425393296 1.1T 90 unknown


                                Disk /dev/sda: 80 GiB, 85899345920 bytes, 167772160 sectors
                                Disk model: VMware Virtual S
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                                Disklabel type: gpt
                                Disk identifier: 81C6F71E-C634-49E6-BC3D-9272C86326A4

                                Device Start End Sectors Size Type
                                /dev/sda1 2048 4095 2048 1M BIOS boot
                                /dev/sda2 4096 4198399 4194304 2G Linux filesystem
                                /dev/sda3 4198400 167772126 163573727 78G Linux filesystem


                                Disk /dev/mapper/ubuntu--vg-ubuntu--lv: 47.102 GiB, 51535413248 bytes, 100655104 sectors
                                Units: sectors of 1 * 512 = 512 bytes
                                Sector size (logical/physical): 512 bytes / 512 bytes
                                I/O size (minimum/optimal): 512 bytes / 512 bytes
                              2. +
                              3. 使用 sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 命令调整逻辑卷的大小。
                                $ sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
                                Size of logical volume ubuntu-vg/ubuntu-lv changed from <48.00 GiB (12287 extents) to <78.00 GiB (19967 extents).
                                Logical volume ubuntu-vg/ubuntu-lv successfully resized.
                              4. +
                              +

                              调整文件系统大小

                                +
                              1. 使用 sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv 命令调整文件系统的大小。
                                $ sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
                                resize2fs 1.45.5 (07-Jan-2020)
                                Filesystem at /dev/mapper/ubuntu--vg-ubuntu--lv is mounted on /; on-line resizing required
                                old_desc_blocks = 6, new_desc_blocks = 10
                                The filesystem on /dev/mapper/ubuntu--vg-ubuntu--lv is now 20446208 (4k) blocks long.
                              2. +
                              3. 使用 df -h 命令显示文件系统的总空间和可用空间信息。确认 /dev/mapper/ubuntu--vg-ubuntu--lv 的大小已调整为 77G
                                $ df -h
                                Filesystem Size Used Avail Use% Mounted on
                                udev 7.8G 0 7.8G 0% /dev
                                tmpfs 1.6G 3.1M 1.6G 1% /run
                                /dev/mapper/ubuntu--vg-ubuntu--lv 77G 43G 31G 59% /
                                tmpfs 7.8G 0 7.8G 0% /dev/shm
                                tmpfs 5.0M 0 5.0M 0% /run/lock
                                tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
                                vmhgfs-fuse 932G 859G 73G 93% /mnt/hgfs
                                /dev/sda2 2.0G 209M 1.6G 12% /boot
                                /dev/loop0 64M 64M 0 100% /snap/core20/2015
                                /dev/loop1 64M 64M 0 100% /snap/core20/2105
                                /dev/loop2 41M 41M 0 100% /snap/snapd/20290
                                /dev/loop3 92M 92M 0 100% /snap/lxd/24061
                                /dev/loop4 41M 41M 0 100% /snap/snapd/20671
                                tmpfs 1.6G 0 1.6G 0% /run/user/1000
                              4. +
                              +

                              参考文章

                              +]]> + + linux + ubuntu + + + + 使用 Vim + /2024/01/18/use-vim/ + 本文记录了 Vim 常用的快捷键作为备忘清单。

                              + + +

                              常用快捷键

                              移动光标

                              - - - + + - - - + + - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                              长度内容说明快捷键功能
                              32/64bitMark Word存储对象的 hashCode 或锁信息h, 光标向左移动一个字符
                              32/64bitClass Metadata Address存储指向对象类型数据的指针j, 光标向下移动一个字符
                              32/64bitArray length数组的长度(如果当前对象是数组)k, 光标向上移动一个字符
                              l, 光标向右移动一个字符
                              Ctrl + f, Page Down屏幕向下移动一页
                              Ctrl + b, Page Up屏幕向上移动一页
                              0光标移动至本行开头
                              $光标移动至本行末尾
                              G光标移动至文件最后一行
                              nG光标移动至文件第n行
                              gg光标移动至文件第一行
                              n<Enter>光标向下移动n行
                              n<space>光标向右移动n个字符
                              ^光标移动至本行第一个非空字符处
                              w光标移动到下一个词 (上一个字母和数字组成的词之后)
                              W光标移动到下一个词 (以空格分隔的词)
                              b光标移动到上一个词 (下一个字母和数字组成的词之前)
                              B光标移动到上一个词 (以空格分隔的词)
                              +

                              查找和替换

                              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                              快捷键功能
                              /word向光标之后搜索word
                              ?word向光标之前搜索word
                              n重复前一个查找操作
                              N反向进行前一个查找操作
                              :n1,n2s/original/replacement/g在第n1行到第n2行之间查找original并替换为replacement
                              :1,$s/original/replacement/g在第1行到最后一行之间查找original并替换为replacement
                              :1,$s/original/replacement/gc在第1行到最后一行之间查找original并替换为replacement,替换前需确认
                              :%s/original/replacement在所有行中查找行中第一个出现的original并替换为replacement
                              -

                              Mark Word

                              Java 对象头里的 Mark Word 里默认存储对象的 HashCode,分代年龄和锁标记位。在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为另外 4 种数据。

                              -

                              32 位虚拟机为例:

                              - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                              锁状态25bit4bit1bit2bit
                              23bit2bit是否是偏向锁锁标志位
                              无锁状态对象的 hashCode对象分代年龄001
                              偏向锁线程 IDEpoch对象分代年龄101
                              轻量级锁指向栈中锁记录的指针00
                              重量级锁指向互斥量(重量级锁)的指针10
                              GC 标记11
                              - -

                              64 位虚拟机为例:

                              - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                              锁状态56bit1bit4bit1bit2bit
                              25bit31bit--是否是偏向锁锁标志位
                              无锁状态unused对象的 hashCodecms_free对象分代年龄001
                              偏向锁线程 ID(54bit) | Epoch(2bit)cms_free对象分代年龄101
                              轻量级锁指向栈中锁记录的指针00
                              重量级锁指向互斥量(重量级锁)的指针10
                              GC 标记11
                              - -
                              -

                              在上述表述中,很容易让人产生困惑的地方是 hashCode 和分代年龄是对象的固有属性,当 Mark Word 中存储的数据发生变化时,这些重要的数据去哪了?

                              -
                              -

                              内部结构可视化

                              “百闻不如一见”,jol-core 提供了打印对象内部结构的能力。

                              -
                                -
                              1. 添加依赖,新版本比旧版本打印结果的可读性更好
                                <dependency>
                                <groupId>org.openjdk.jol</groupId>
                                <artifactId>jol-core</artifactId>
                                <version>${org.openjdk.jol.version}</version>
                                </dependency>
                              2. -
                              3. 使用 ClassLayout.parseInstance(objectExample).toPrintable() 打印
                                @Data
                                @Slf4j
                                public class ObjectInternalTest {

                                private byte aByte;
                                private int aInt;

                                public static void main(String[] args) {
                                ObjectInternalTest objectInternalTest = new ObjectInternalTest();
                                log.info(ClassLayout.parseInstance(objectInternalTest).toPrintable());
                                }
                                }
                              4. -
                              5. 打印结果:mark|class|fields|alignment。这样我们就能通过查看 Mark Word 的值更直观地确定当前锁的状态。
                                2023-12-23 20:21:02 - com.moralok.concurrency.ch2.ObjectExample object internals:
                                OFF SZ TYPE DESCRIPTION VALUE
                                0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                                8 4 (object header: class) 0x00060828
                                12 4 int ObjectExample.aInt 0
                                16 1 byte ObjectExample.aByte 0
                                17 7 (object alignment gap)
                                Instance size: 24 bytes
                                Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
                              6. -
                              -

                              指针压缩和 cms_free

                              注意到指向对象类型数据的指针仅 4 个字节,这是因为默认情况下 JVM 参数 UseCompressedOops 是启用的。

                              -

                              markOop.hpp

                              -
                              |--------------------------------------------------------------------------------------------------------------|--------------------|
                              | Object Header (96 bits) | State |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | Mark Word (64 bits) | Klass Word (32 bits) | |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | unused:25 | identity_hashcode:31 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | thread:54 | epoch:2 | cms_free:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | ptr_to_lock_record | lock:2 | OOP to metadata object | Lightweight Locked |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | ptr_to_heavyweight_monitor | lock:2 | OOP to metadata object | Heavyweight Locked |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | | lock:2 | OOP to metadata object | Marked for GC |
                              |--------------------------------------------------------------------------------|-----------------------------|--------------------|
                              - -

                              使用 -XX:-UseCompressedOops 关闭指针压缩,指向对象类型数据的指针才会变回 8 个字节

                              -
                              |------------------------------------------------------------------------------------------------------------|--------------------|
                              | Object Header (128 bits) | State |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | Mark Word (64 bits) | Klass Word (64 bits) | |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Normal |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | Biased |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | Lightweight Locked |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | Heavyweight Locked |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              | | lock:2 | OOP to metadata object | Marked for GC |
                              |------------------------------------------------------------------------------|-----------------------------|--------------------|
                              - -

                              你可能还会注意到开启和关闭指针压缩时,还有一个 bitcms_free 变成 unused。这个 cms_free 是做什么用的呢?在未开启指针压缩的情况下,指针的低位因为内存对齐的缘故往往是 0,我们可以给这些 bit 设置 1 用于标记特殊状态。CMSKlass 指针的最低位设置为 1 用于表示特定的内存块不是一个对象,而是空闲的内存。在开启指针压缩后,JVM 通过右移移除指针中没用到的低位,因此 CMS 需要一个地方存储这个表示是否为空闲内存的 bit,就是 cms_free

                              -

                              这在一定程度上解决了我心中的一个问题:JVM 是怎么判断一个空闲的内存块的?

                              +

                              替换格式如下 :[range]s/<pattern>/[string]/[flags] [count]

                              -

                              concurrentMarkSweepGeneration.cpp

                              -
                              // A block of storage in the CMS generation is always in
                              // one of three states. A free block (FREE), an allocated
                              // object (OBJECT) whose size() method reports the correct size,
                              // and an intermediate state (TRANSIENT) in which its size cannot
                              // be accurately determined.
                              // STATE IDENTIFICATION: (32 bit and 64 bit w/o COOPS)
                              // -----------------------------------------------------
                              // FREE: klass_word & 1 == 1; mark_word holds block size
                              //
                              // OBJECT: klass_word installed; klass_word != 0 && klass_word & 1 == 0;
                              // obj->size() computes correct size
                              //
                              // TRANSIENT: klass_word == 0; size is indeterminate until we become an OBJECT
                              //
                              // STATE IDENTIFICATION: (64 bit+COOPS)
                              // ------------------------------------
                              // FREE: mark_word & CMS_FREE_BIT == 1; mark_word & ~CMS_FREE_BIT gives block_size
                              //
                              // OBJECT: klass_word installed; klass_word != 0;
                              // obj->size() computes correct size
                              //
                              // TRANSIENT: klass_word == 0; size is indeterminate until we become an OBJECT
                              - -

                              使用 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB 开启 HotSpot Debugger,比对 ClassLayout 打印的 Klass 指针和 Class Browser 中的指针。

                              - +

                              删除/复制/粘贴

                              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                              快捷键功能
                              x向后删除一个字符,相当于 Del
                              X向前删除一个字符,相当于 Backspace
                              nx向前删除n个字符
                              dd删除(剪切)光标所在的行
                              ndd删除(剪切)光标所在开始的n行
                              d1G删除(剪切)光标所在到第1行的所有行
                              dG删除(剪切)光标所在到最后一行的所有行
                              d$删除(剪切)光标所在到该行的最后一个字符
                              d0删除(剪切)光标所在到该行的第一个字符
                              yy复制光标所在的行
                              nyy复制光标所在开始的n行
                              y1G复制光标所在到第1行的所有行
                              yG复制光标所在到最后一行的所有行
                              y$复制光标所在到该行的最后一个字符
                              y0复制光标所在到该行的第一个字符
                              p将复制的内容粘贴到光标所在的下一行
                              P将复制的内容粘贴到光标所在的上一行
                              u恢复前一个操作
                              Ctrl+r重做上一个操作
                              .重复前一个操作
                              +

                              进入编辑模式

                              - - - + + - - - + + - - - + + - - - + + - - - + + + + + + + + + + + + + + + + + + + + + +
                              指针压缩关闭开启快捷键功能
                              ClassLayout0xf800c1050x00000245eb873d20i进入插入模式,从光标所在处开始插入
                              二进制表达1111100000000000110000010000010100100100010111101011100001110011110100100000I进入插入模式,从光标所在行的第一个非空格开始插入
                              HotSpot Debugger0x00000007c00608280x00000245EB873D20a进入插入模式,从光标所在的下一个字符处开始插入
                              二进制表达01111100000000000110000010000010100000100100010111101011100001110011110100100000A进入插入模式,从光标所在行的最后一个字符处开始插入
                              o进入插入模式,在光标所在行的下一行插入新的一行
                              O进入插入模式,在光标所在行的上一行插入新的一行
                              r进入替换模式,只会替换光标所在的字符一次
                              R进入替换模式,替换光标所在的字符,直到通过Esc退出
                              Esc退出编辑模式,回到一般命令模式
                              -

                              对象分代年龄

                              通过以下示例可以测试和验证对象分代年龄的变化。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试 Mark Word 中的分代年龄");

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 无锁状态,age: 0");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              System.gc();
                              TimeUnit.SECONDS.sleep(1);
                              log.info("GC 后 =====> 无锁状态,age: 1");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              重量级锁

                              锁优化

                              Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java 6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,锁的状态会随着竞争的激化逐渐升级。锁状态可以升级但不能降级,举例来说偏向锁状态升级成轻量级锁状态后不能降级成偏向锁状态。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

                              -
                              -

                              上述的表述并不容易理解,甚至容易让人产生误解。锁状态描述的是锁本身的状态,和是否处于加锁状态无关。以下列表格举例说明,一个偏向锁状态的对象,即使未加锁,也是偏向锁状态,而非无锁状态。

                              -
                              - +

                              保存和退出

                              - - - + + - - - + + - - - + + - - - + + + + + + + + + + + + + + + + + + + + + +
                              层次未加锁加锁快捷键功能
                              1匿名偏向锁状态 or 偏向锁状态偏向锁状态:w保存文件
                              2无锁状态轻量级锁状态:w!若文件为只读,强制保存
                              3重要级锁状态重要级锁状态:q退出 Vim,如果文件已修改,将退出失败
                              :q!强制退出 Vim,不保存文件修改
                              :wq保存文件并退出 Vim
                              :w filename另存为新文件
                              ZZ退出 Vim,若文件无修改,则不保存退出;如果文件已修改,保存并退出
                              :r filename读入另一个文件的数据并添加到光标所在行之后
                              -
                              -

                              在查阅的众多资料中,关于锁升级过程的介绍并不详尽和准确,虽然大体上大家的观点是比较一致的,但是在一些细节的描述上却有些模糊不清,有些观点自相矛盾,有些观点互相矛盾,有些观点和我的知识或者测试结果矛盾,甚至有些逻辑不通顺以至于不能相互联系形成和谐的整体。以下内容尽可能结合相对权威和详细的资料,补充个人的思考和猜想作为缝合剂,并通过一些测试用例验证部分猜想,试图建立更加连续平滑以及可信服的知识面。

                              -
                              -

                              锁升级变化图

                              提前放出锁升级变化图,用于在后续分析和测试过程中对照查看。重点关注以下可能引起锁状态变化的事件:

                              -
                                -
                              1. 获取锁和释放锁
                              2. -
                              3. 竞争,其中弱竞争是指线程交替进入同步块,没有发生直接冲突;强竞争是指线程在同步块内的时候有其他线程想要进入同步块
                              4. -
                              5. 调用特殊方法,比如计算 hashCode(非自定义)或者 wait 方法
                              6. -
                              -
                              - -

                              偏向锁

                              HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

                              -

                              偏向锁在 Java 6 之后是默认开启的,可以通过 JVM 参数 -XX:-UseBiasedLocking 关闭偏向锁。尽管偏向锁是默认开启的,但是它在应用程序启动几秒钟之后才激活,延迟时间可以通过 JVM 参数 -XX:BiasedLockingStartupDelay 设置,默认情况下是 4000ms

                              -

                              测试偏向锁配置

                              延迟偏向

                              通过以下示例测试并验证延迟偏向。

                              -
                              public static void main(String[] args) throws IOException, InterruptedException {
                              log.info("测试:偏向锁是延迟激活的");

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 无锁状态(非可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              // 默认情况下偏向延迟的设置为 -XX:BiasedLockingStartupDelay=4000
                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              log.info("偏向锁激活之后,新创建的对象的对象头的 Mark Word 是 =====> 匿名偏向锁");
                              Object biasedLock = new Object();
                              log.info(ClassLayout.parseInstance(biasedLock).toPrintable());

                              log.info("偏向锁激活之前创建的对象的对象头的 Mark Word 仍然是 =====> 无锁状态");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              测试结果如下:

                              -
                              OFF  SZ   TYPE DESCRIPTION               VALUE
                              0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)

                              0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)

                              0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                              - -
                                -
                              • JVM 启动后,偏向锁尚未激活前,创建的对象的 Mark Word 的末尾 3 位为 0|01non-biasable,表示无锁状态(非可偏向的)。
                              • -
                              • 4000 毫秒后,新创建的对象的 Mark Word 的末尾 3 位为 1|01biasable,表示匿名偏向锁(可偏向的)。
                              • -
                              • 偏向锁尚未激活前创建的对象的对象头的 Mark Word 的末尾 3仍然0|01
                              • -
                              -
                              -

                              在虚拟机启动后,偏向锁激活前,创建的对象的锁标记位为 1|01,此时记录线程 IDbit 全是 0(代表指向 null),没有偏向任何一个线程,该状态称之为匿名偏向锁。

                              -
                              -
                              关闭偏向延迟

                              通过以下示例测试关闭偏向延迟:

                              -
                              // JVM 参数设置为 -XX:BiasedLockingStartupDelay=0
                              public static void main(String[] args) throws IOException, InterruptedException {
                              log.info("测试:关闭偏向锁的延迟偏向");

                              Object lock = new Object();
                              log.info("在虚拟机一启动,新创建的对象的对象头的 Mark Word 就是 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              关闭偏向锁

                              通过以下示例测试关闭偏向锁:

                              -
                              // JVM 参数设置为 -XX:-UseBiasedLocking
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:关闭偏向锁");

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 无锁状态(非可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              log.info("sleep 4000ms");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              log.info("即使过了偏向延迟时间,创建的对象的对象头的 Mark Word 仍然是 =====> 无锁状态(非可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              《Java 并发编程的艺术》中写的是 -XX:-UseBiasedLocking=false,测试中报错:

                              -
                              Error: Could not create the Java Virtual Machine.
                              Error: A fatal exception has occurred. Program will exit.
                              Improperly specified VM option 'UseBiasedLocking=false'
                              - -

                              另外书中说“在关闭偏向锁后程序默认会进入轻量级锁状态”,个人认为可能会让人产生误解,默认在未获取锁时为无锁状态,获取锁将变为轻量级锁状态。

                              -

                              偏向锁加锁

                              当一个线程访问同步块时,先测试 Mark Word 里是否存储着当前线程 ID

                              -
                                -
                              • 如果否,则再测试 Mark Word 中偏向锁的标识是否设置成 1
                                  -
                                • 如果为 0,则说明不是偏向锁状态 =====> 获取偏向锁失败后续处理一
                                • -
                                • 如果为 1,则说明是偏向锁状态,通过 CAS 操作设置偏向锁
                                    -
                                  • 如果成功,说明获得偏向锁
                                  • -
                                  • 如果失败,说明发生竞争 =====> 获取偏向锁失败后续处理二
                                  • -
                                  -
                                • -
                                -
                              • -
                              • 如果是,则说明当前线程就是之前获得偏向锁的线程,此刻再次获得锁
                              • -
                              -
                              -

                              通过 CAS 操作设置偏向锁中,Compare 操作是“测试 Mark Word 存储线程 IDbit 是否全部为 0,代表偏向的线程 IDnull”,Swap 操作是将当前线程 ID 设置到 Mark Word 的相应位置。

                              -
                              -

                              补充思考:

                              -
                                -
                              • “通过 CAS 操作将当前线程 ID 设置到 Mark Word”在偏向锁状态下是有且仅有一次的“偏向”动作。(此观点存疑,在《Java 并发编程的艺术》一书中有“重新偏向于其他线程”这样的描述,但是关于竞争偏向锁部分的原理难以理解。个人在测试中,不论是持有偏向锁的线程仍存活但已离开同步块,还是已死亡,后续线程都无法再获取到偏向锁,唯一一种不同线程获取到同一个偏向锁的情况是两个线程可以复用同一个局部变量表槽位,它们的 tid 相同,这代表着本质上 Mark Word 并无变化)
                              • -
                              • 当获得偏向锁的线程离开同步块时,没有“解锁操作”,Mark Word 维持不变。个人也不知道如何更准确地描述这个现象,从 synchronized 的语义来说,进出同步块代表着获取锁和释放锁;但是从偏向锁的实现来说,即便离开同步方法块,它仍然偏向原先获得锁的线程,甚至在讨论偏向锁发生竞争时,书中提到“检查持有偏向锁的线程是否存活”。个人更倾向于使用“撤销锁”一词描述偏向锁面临竞争时的处理,使用“释放锁”描述线程离开同步块时的处理。
                              • -
                              • 当获得偏向锁的线程再次访问同步块时,简单测试 Mark Word 里存储着当前线程 ID,如果成功即可进入同步块。
                              • -
                              • 计算过 hashCode 后偏向锁状态会变为其他状态,比如无锁状态,或者升级为轻量级锁甚至重量级锁,这符合 CAS 操作的判断条件。
                              • -
                              -

                              偏向锁撤销

                              偏向锁使用了一种等到竞争出现才撤销的机制,当获得偏向锁的线程离开同步块时,并没有“解锁操作”,Mark Word 将维持不变。当竞争出现时,从现象上说,如果持有偏向锁的线程已经离开同步块,则锁升级为轻量级锁;如果持有锁的线程尚未离开同步块,则锁直接升级为重量级锁。

                              -
                              -

                              关于偏向锁的撤销,其原理晦涩难懂,个人仍有很多疑问:锁记录中存储偏向的线程 ID 的作用,检查持有偏向锁的线程是否存活的作用不符合测试结果,重新偏向于其他线程的复现条件。因为理解有限,不多赘述。

                              -
                              -

                              测试偏向锁升级

                              匿名偏向锁->偏向锁
                              -

                              在一个匿名偏向锁状态的对象第一次被作为锁获取时,Mark Word 就会从匿名偏向锁变成偏向锁,并且再也不会变回到匿名偏向锁。

                              -
                              -

                              测试在匿名偏向锁状态下获取锁将变成偏向锁状态:

                              -
                              public static void main(String[] args) throws IOException, InterruptedException {
                              log.info("偏向锁基础测试:匿名偏向锁 -> 偏向锁");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("{} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              log.info("暂停,输入任意字符回车继续,可以使用 jstack 查看线程 tid 和 Mark Word 进行对比");
                              scanner.next();
                              }

                              log.info("偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 仍然不变");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              测试结果如下:

                              -
                              2023-12-21 00:34:39 - 偏向锁基础测试:匿名偏向锁 -> 偏向锁
                              2023-12-21 00:34:39 - sleep 4000ms,等待偏向锁激活
                              2023-12-21 00:34:43 - Mark Word 初始为 =====> 匿名偏向锁
                              2023-12-21 00:34:45 - java.lang.Object object internals:
                              OFF SZ TYPE DESCRIPTION VALUE
                              0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
                              8 4 (object header: class) 0xf80001e5
                              12 4 (object alignment gap)
                              Instance size: 16 bytes
                              Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

                              2023-12-21 00:34:45 - main 获取锁 =====> 偏向锁
                              2023-12-21 00:34:45 - java.lang.Object object internals:
                              OFF SZ TYPE DESCRIPTION VALUE
                              0 8 (object header: mark) 0x0000028761af3005 (biased: 0x00000000a1d86bcc; epoch: 0; age: 0)
                              8 4 (object header: class) 0xf80001e5
                              12 4 (object alignment gap)
                              Instance size: 16 bytes
                              Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

                              2023-12-21 00:34:45 - 暂停,输入任意字符回车继续,可以使用 jstack 查看线程 tid 和 Mark Word 进行对比
                              2023-12-21 00:34:55 - 偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 仍然不变
                              2023-12-21 00:34:55 - java.lang.Object object internals:
                              OFF SZ TYPE DESCRIPTION VALUE
                              0 8 (object header: mark) 0x0000028761af3005 (biased: 0x00000000a1d86bcc; epoch: 0; age: 0)
                              8 4 (object header: class) 0xf80001e5
                              12 4 (object alignment gap)
                              Instance size: 16 bytes
                              Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
                              - -

                              通过 jstack 获取线程 tid(以 Windows 为例):

                              -
                              jps | findstr "BiasedLockingBaseTest" | ForEach-Object { jstack $_.Split()[0]} | findstr "main"
                              "main" #1 prio=5 os_prio=0 tid=0x0000028761af3000 nid=0x8668 waiting on condition [0x000000ff7b8ff000]
                              at com.moralok.concurrency.ch2.BiasedLockingBaseTest.main(BiasedLockingBaseTest.java:27)
                              - -

                              关注 Mark Word 并转换为二进制表达:

                              - +

                              额外功能

                              可视模式

                              - - + + - - + + - - + + - - + + - - + + + + + + + + + + + +
                              二进制表达快捷键功能
                              匿名偏向锁 Mark Word00000000000000000000000000000000000000000101v字符选择,将光标经过的地方反白选择
                              偏向锁状态 Mark Word00101000011101100001101011110011000000000101V行选择,将光标经过的行反白选择
                              biased00000000000010100001110110000110101111001100Ctrl + v区块选择,用矩形的方式反白选择
                              main 线程 tid00101000011101100001101011110011000000000000y复制反白选择的地方
                              d删除反白选择的地方
                              ~对反白选择的地方切换大小写
                              +

                              多文件编辑

                              + + + + + + + + + + + + + + + + + + +
                              快捷键功能
                              :n编辑下一个文件
                              :N编辑上一个文件
                              :files列出当前 Vim 打开的所有文件
                              +

                              多窗口功能

                              + + + + + + + + + + + + + + + + + + + + + + +
                              快捷键功能
                              :sp [filename]打开一个新窗口
                              Ctrl + w + j
                              Ctrl + w +
                              光标移动到下方的窗口
                              Ctrl + w + k
                              Ctrl + w +
                              光标移动到上方的窗口
                              Ctrl + w + q
                              :q
                              :close
                              关闭窗口
                              +

                              关键词自动补全

                              + + + + + + + + + + + + + + + + + + +
                              快捷键功能
                              Ctrl + x + Ctrl + n使用当前文件的内容文字作为关键词,予以补齐
                              Ctrl + x + Ctrl + f使用当前目录的文件名作为关键词,予以补齐
                              Ctrl + x + Ctrl + o使用扩展名作为语法补充,以 Vim 内置的关键词,予以补齐
                              +

                              环境配置

                              + + + + + + + + + + + + +
                              设置参数功能
                              :set nu
                              :set nonu
                              设置和取消行号
                              :syntax on
                              :syntax off
                              是否依据程序相关语法显示不同颜色
                              -
                                -
                              • 注意:存储的所谓“线程 ID”并非平时所说的线程 ID,该值左移可以得到 jstack 的返回结果中的 tidjol-core 打印了一个名为 biased 的值与之相同
                              • -
                              • 在离开同步方法块后,Mark Word 不变
                              • -
                              -
                              偏向锁->轻量级锁

                              测试当拥有偏向锁的线程已经离开同步块,其他线程尝试获取偏向锁(弱竞争),锁将升级为轻量级锁。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              Scanner scanner = new Scanner(System.in);
                              log.info("测试:当持有偏向锁的线程已经离开同步块,其他线程尝试获取偏向锁时,将获得轻量级锁");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.SECONDS.sleep(4);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              Thread thread = new Thread(() -> {
                              log.info("第二个线程 {} 尝试获取锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              synchronized (lock) {
                              log.info("第二个线程 {} 获取锁 =====> 轻量级锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread1");
                              thread.start();
                              thread.join();

                              log.info("离开同步块后轻量级锁释放 =====> 无锁状态(可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              有相关资料提到在拥有偏向锁的线程死亡后,锁可以偏向新的线程,但是验证失败。

                              -
                              public static void main(String[] args) throws IOException, InterruptedException {
                              log.info("测试:之前获得偏向锁的线程已死时,新线程获得的仍然是偏向锁");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              Thread thread1 = new Thread(() -> {
                              synchronized (lock) {
                              log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread1");
                              thread1.start();

                              Thread thread2 = new Thread(() -> {
                              try {
                              thread1.join();
                              } catch (InterruptedException e) {
                              throw new RuntimeException(e);
                              }
                              boolean alive = thread1.isAlive();
                              log.info("第一个线程 {} 是否存活 {}", thread1.getName(), alive);
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              synchronized (lock) {
                              log.info("即使第一个线程已死亡,第二个线程 {} 获取锁 =====> 轻量级锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread2");
                              thread2.start();
                              thread2.join();

                              log.info("离开同步块后轻量级锁释放 =====> 无锁状态(可偏向的)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              偏向锁->重量级锁

                              测试当拥有偏向锁的线程尚未离开同步块,其他线程尝试获取偏向锁(强竞争),锁将升级为重量级锁。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              Scanner scanner = new Scanner(System.in);
                              log.info("测试:当持有偏向锁的线程尚未离开同步块,其他线程尝试获取偏向锁时,将升级为重量级锁");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.SECONDS.sleep(4);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              Thread thread1 = new Thread(() -> {
                              synchronized (lock) {
                              log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              log.info("暂停,输入任意字符回车继续");
                              scanner.next();

                              log.info("第一个线程 {} 持有偏向锁,在同步块内发生竞争 =====> 升级为重量级锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              log.info("第一个线程 {} 结束", Thread.currentThread().getName());
                              }, "thread1");
                              thread1.start();

                              TimeUnit.SECONDS.sleep(1);

                              Thread thread2 = new Thread(() -> {
                              log.info("第二个线程 {} 尝试获取偏向锁失败", Thread.currentThread().getName());
                              synchronized (lock) {
                              log.info("第二个线程 {} 获取锁 =====> 重量级锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread2");
                              thread2.start();
                              thread2.join();

                              log.info("即使离开同步块后 =====> 重量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              偏向锁->偏向锁(特例)

                              这是一个很奇怪的测试用例,它是在测试中唯一发生不同线程对同一个锁获得偏向锁的情况。但是排查过程中发现两个线程的 tid 相同,猜测是局部变量表槽位复用时有什么优化机制。

                              -

                              卡了我好久,也没有探究到实质的新信息。

                              +

                              可以通过 vim ~/.vimrc 修改配置文件。

                              -
                              public static void main(String[] args) throws IOException, InterruptedException {
                              log.info("测试:之前获得偏向锁的线程已死时,新线程获得的仍然是偏向锁");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              Thread thread1 = new Thread(() -> {
                              synchronized (lock) {
                              log.info("第一个线程 {} 获取锁 =====> 偏向锁", Thread.currentThread().getName());
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread1");
                              thread1.start();
                              thread1.join();

                              Thread thread2 = new Thread(() -> {
                              synchronized (lock) {
                              log.info("第二个线程 {} 获取锁,=====> 偏向锁", Thread.currentThread().getName());
                              log.info("震惊!!!为什么两个 tid 相同啊,有什么复用机制吗");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              }, "thread2");
                              thread2.start();
                              thread2.join();

                              log.info("偏向锁等到竞争出现才释放锁,因此离开同步方法块后,Mark Word 不变");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              匿名偏向锁状态计算 hashCode

                              在匿名偏向锁状态计算 hashCode,锁将变为无锁状态。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:在匿名偏向锁状态计算 hashCode");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              int hashCode = lock.hashCode();
                              log.info("在计算 hashCode 后:Mark Word =====> 无锁状态(hash|age|0|01)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("获取锁 =====> 轻量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              log.info("离开同步块后轻量级锁释放 =====> 无锁状态(hash|age|0|01)");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              偏向锁状态无锁时计算 hashCode

                              在偏向锁状态无锁时计算 hashCode,锁将变为无锁状态。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:在偏向锁状态无锁时计算 hashCode");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("获取锁 =====> 偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              int hashCode = lock.hashCode();
                              log.info("离开同步块后再计算 hashCode:Mark Word =====> 无锁状态");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -
                              偏向锁状态加锁时计算 hashCode

                              在偏向锁状态加锁时计算 hashCode,锁将升级为重量级锁状态。

                              -
                              public static void main(String[] args) throws InterruptedException {
                              log.info("测试:在偏向锁状态计算 hashCode");

                              log.info("sleep 4000ms,等待偏向锁激活");
                              TimeUnit.MILLISECONDS.sleep(4000);

                              Object lock = new Object();
                              log.info("Mark Word 初始为 =====> 匿名偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              synchronized (lock) {
                              log.info("获取锁 =====> 偏向锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());

                              int hashCode = lock.hashCode();
                              log.info("在计算 hashCode 后:Mark Word =====> 重量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }

                              log.info("即使离开同步块后 =====> 重量级锁");
                              log.info(ClassLayout.parseInstance(lock).toPrintable());
                              }
                              - -

                              轻量级锁

                              轻量级锁加锁

                              获取偏向锁失败后续处理一(是否是偏向锁为 0):

                              -
                                -
                              • 检测锁标志位是否为 01 或者 00
                                  -
                                • 如果否,则说明是重量级锁状态 =====> 获取轻量级锁失败后续处理一
                                • -
                                • 如果是,则说明是无锁状态或者轻量级锁状态,尝试通过 CAS 操作设置轻量级锁
                                    -
                                  • 如果成功,说明获得轻量级锁
                                  • -
                                  • 如果失败,说明发生竞争 =====> 获取轻量级锁失败后续处理二
                                  • -
                                  -
                                • -
                                -
                              • +

                                参考文章

                                -
                                -

                                通过 CAS 操作设置轻量级锁中,Compare 操作是“测试 Mark Word 的锁标志位是否为 01,代表处于无锁状态”,Swap 操作是将 Mark Word 复制到栈中锁记录,并将指向栈中锁记录的指针设置到 Mark Word 的相应位置以及修改锁标志位。所谓“栈中锁记录”又称为 Displaced Mark WordJVM 会在当前线程的栈帧中创建用于存储锁记录的空间,用于在轻量级锁状态下临时存放 Mark Word

                                +]]> + + linux + vim + + + + k3s 的安装和使用 + /2024/01/30/installation-and-use-of-k3s/ + 本文记录了 k3s 的安装和使用,相较于 minikube,前者是一个完全兼容的 Kubernetes 发行版,安装和使用的体验更佳。

                                + + +

                                安装

                                +

                                参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!

                                +

                                步骤

                                  +
                                1. 获取并运行 k3s 安装脚本。官方为中国用户提供了镜像加速支持。
                                  $ curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -                                
                                  [sudo] password for moralok:
                                  [INFO] Finding release for channel stable
                                  [INFO] Using v1.28.5+k3s1 as release
                                  [INFO] Downloading hash rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/sha256sum-amd64.txt
                                  [INFO] Downloading binary rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/k3s
                                  [INFO] Verifying binary download
                                  [INFO] Installing k3s to /usr/local/bin/k3s
                                  [INFO] Skipping installation of SELinux RPM
                                  [INFO] Creating /usr/local/bin/kubectl symlink to k3s
                                  [INFO] Creating /usr/local/bin/crictl symlink to k3s
                                  [INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
                                  [INFO] Creating killall script /usr/local/bin/k3s-killall.sh
                                  [INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
                                  [INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
                                  [INFO] systemd: Creating service file /etc/systemd/system/k3s.service
                                  sh: 1014: restorecon: not found
                                  sh: 1015: restorecon: not found
                                  [INFO] systemd: Enabling k3s unit
                                  Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
                                  [INFO] systemd: Starting k3s
                                2. +
                                3. 可以通过使用 kubectl 确认安装成功。刚安装的时候使用 kubectl 需要 root 权限。
                                  $ sudo kubectl get node
                                  NAME STATUS ROLES AGE VERSION
                                  ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1
                                4. +
                                5. 实际上安装的就是一个 k3s 可执行文件,kubectlcrictl 只是软链接,指向 k3s
                                  $ ls /usr/local/bin/
                                  crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl
                                6. +
                                -

                                在轻量级锁状态下,明确提及了锁记录的作用,但偏向锁状态下,提及锁记录却并未加以解释。

                                +

                                安装的信息中显示了 k3sservice fileenvironment file 的路径,后续修改启动参数和环境变量需要用到。

                                -

                                获取偏向锁失败后续处理一:

                                -
                                  -
                                • 已经升级为重量级锁
                                • -
                                -

                                获取偏向锁失败后续处理二(通过 CAS 加偏向锁失败):

                                -
                                  -
                                • 获取锁失败的线程将锁升级为重量级锁,修改 Mark Word指向互斥量(重量级锁)的指针|10(这个操作将影响到持有轻量级锁的线程的解锁)
                                • -
                                • 线程阻塞,等待唤醒
                                • -
                                -

                                补充思考:

                                -
                                  -
                                • 有相关资料提及偏向锁并非直接升级到重量级锁,无法验证是否总是有轻量级锁作为中间状态
                                • -
                                • 轻量级锁面临竞争时升级为重量级锁的过程相比于偏向锁面临竞争时的升级过程,更加容易理解,后者好多细节没有找到令人信服的答案。
                                • -
                                -

                                轻量级锁解锁

                                轻量级锁解锁时,会通过 CAS 操作解锁,Compare 操作是“测试 Mark Word 的锁标志位是否为 00,代表处于轻量级锁状态,Swap 操作是将栈中锁记录 Dispaced Mark Word 替换回对象头的 Mark Word 以及修改锁标志位。
                                如果 Compare 操作失败,则代表发生竞争,此时锁已经被其他线程升级为重量级锁以及 Mark Word 被修改为指向互斥量(重量级锁)的指针|10。持有轻量级锁的线程会释放锁(直接将 Dispaced Mark Word 替换回 Mark Word?)并唤醒等待的线程,开启新的一轮争抢。

                                -

                                测试轻量级锁升级

                                无锁->轻量级锁

                                测试在无锁状态下获取锁,锁将变成轻量级锁状态。

                                -
                                public static void main(String[] args) throws IOException, InterruptedException {
                                Scanner scanner = new Scanner(System.in);
                                log.info("轻量级锁基础测试:无锁状态 -> 轻量级锁");

                                Object lock = new Object();
                                log.info("在偏向锁激活之前创建的对象为 =====> 无锁状态(可偏向额)");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());

                                synchronized (lock) {
                                log.info("即使是单线程无竞争获取锁,=====> 轻量级锁");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                log.info("暂停,回车继续");
                                scanner.nextLine();
                                }

                                log.info("离开同步块后,-> 无锁状态(可偏向的)");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                }
                                +

                                配置文件权限问题

                                在刚安装完 k3s 的时候,使用 kubectl 需要 root 权限。根据报错信息可知,是因为非 root 用户无法读取配置文件 /etc/rancher/k3s/k3s.yaml

                                +
                                $ kubectl get node                                                                                                                                            
                                WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode to modify kube config permissions
                                error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
                                -
                                无锁状态计算 hashCode

                                在无锁状态计算 hashCode,仍然是无锁状态。

                                -
                                public static void main(String[] args) throws InterruptedException {
                                log.info("测试:在无锁状态计算 hashCode");

                                Object lock = new Object();
                                log.info("Mark Word 初始为 =====> 无锁状态");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());

                                int hashCode = lock.hashCode();
                                log.info("在计算 hashCode 后:Mark Word =====> 无锁状态(hash|age|0|01)");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());

                                synchronized (lock) {
                                log.info("获取锁 =====> 轻量级锁");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                }

                                log.info("离开同步块后轻量级锁释放 =====> 无锁状态(hash|age|0|01)");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                }
                                +

                                查看配置文件的信息可知其权限配置为 600,只有 root 用户具有读写权限。

                                +
                                $ ll /etc/rancher/k3s/k3s.yaml                                                                                               
                                -rw------- 1 root root 2961 Jan 30 18:58 /etc/rancher/k3s/k3s.yaml
                                -
                                轻量级锁加锁时计算 hashCode

                                在轻量级锁状态加锁时计算 hashCode,锁将升级为重量级锁状态。

                                -
                                public static void main(String[] args) throws InterruptedException {
                                log.info("测试:在轻量级锁状态计算 hashCode");

                                Object lock = new Object();
                                log.info("Mark Word 初始为 =====> 无锁状态");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());

                                synchronized (lock) {
                                log.info("获取锁 =====> 轻量级锁");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());

                                int hashCode = lock.hashCode();
                                log.info("在计算 hashCode 后:Mark Word =====> 重量级锁");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                }

                                log.info("即使离开同步块后 =====> 重量级锁");
                                log.info(ClassLayout.parseInstance(lock).toPrintable());
                                }
                                +

                                一般来说,我们希望能够通过非 root 用户使用 kubectl,避免通过 root 用户或者通过 sudo 加输入密码的形式来使用 kubectl。那么如何解决这个问题呢?本质上这是一个 Linux 的文件权限问题,似乎修改文件的权限配置就可以解决。但是提示信息给出的解决方案并不是那么直接,它告诉我们通过修改 k3s server 的启动参数来达到修改配置文件权限的目的。这是因为 k3s 服务在每次重启时会根据启动参数和环境变量重置配置文件 /etc/rancher/k3s/k3s.yaml,手动修改文件的权限配置并不能优雅地解决这个问题,一旦服务重启,修改就会丢失。

                                +
                                +

                                k3s 的 Github Discussions 中讨论了这个问题,并链接了文档 管理 Kubeconfig 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。

                                +
                                +

                                修改启动参数

                                第一种方式是修改启动参数。

                                +
                                  +
                                1. sudo vim /etc/systemd/system/k3s.service 添加 k3s 启动参数 --write-kubeconfig-mode 644
                                  ExecStart=/usr/local/bin/k3s \
                                  server --write-kubeconfig-mode 644 \
                                2. +
                                3. systemctl daemon-reload 重新加载 systemd 配置
                                4. +
                                5. systemctl restart k3s.service 重启服务
                                6. +
                                7. 验证修改生效
                                  $ ll /etc/rancher/k3s/k3s.yaml
                                  -rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml
                                8. +
                                +

                                修改环境变量

                                第二种方式是修改环境变量。

                                +
                                  +
                                1. sudo vim /etc/systemd/system/k3s.service.env 添加环境变量 K3S_KUBECONFIG_MODE=644
                                  K3S_KUBECONFIG_MODE=644
                                2. +
                                3. systemctl restart k3s.service 重启服务
                                4. +
                                +

                                修改配置文件路径

                                第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。

                                +
                                  +
                                1. 设置环境变量 export KUBECONFIG=~/.kube/config
                                2. +
                                3. 创建文件夹 mkdir ~/.kube 2> /dev/null
                                4. +
                                5. 复制配置信息 sudo k3s kubectl config view --raw > "$KUBECONFIG"
                                6. +
                                7. 修改配置文件的权限 chmod 600 "$KUBECONFIG"
                                8. +
                                +

                                配置代理

                                涉及 k8s,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及 k3s 安装脚本会自动使用当前 shell 中的 HTTP_PROXYHTTPS_PROXYNO_PROXY,以及 CONTAINERD_HTTP_PROXYCONTAINERD_HTTPS_PROXYCONTAINERD_NO_PROXY 变量(如果存在),并将它们写入 systemd 服务的环境文件。比如我设置过 shell 变量 HTTP_PROXYHTTPS_PROXYNO_PROXY/etc/systemd/system/k3s.service.env 如下,你也可以自行编辑修改。

                                +
                                http_proxy='http://127.0.0.1:7890'
                                https_proxy='http://127.0.0.1:7890'
                                no_proxy='localhost,127.0.0.1'
                                K3S_KUBECONFIG_MODE=644
                                +

                                使用

                                +

                                k8s 基础教程可参考官方文档 Kubernetes 基础

                                +
                                +

                                创建 Deployment

                                  +
                                1. 使用 kubectl create 命令创建管理 PodDeployment。该 Pod 根据提供的 Docker 镜像运行容器。
                                  kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
                                2. +
                                3. 查看 Deployment
                                  $ kubectl get pods
                                  NAME READY STATUS RESTARTS AGE
                                  hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h
                                4. +
                                5. 查看 Pod 中容器的应用程序日志。
                                  $ kubectl logs hello-node-ccf4b9788-d8k9b
                                  I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
                                  I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
                                6. +
                                +

                                创建 Service

                                  +
                                1. 使用 kubectl expose 命令将 Pod 暴露给公网:
                                  kubectl expose deployment hello-node --type=LoadBalancer --port=8080
                                2. +
                                3. 查看你创建的 Service
                                  $ kubectl get services
                                  NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
                                  kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 15h
                                  hello-node LoadBalancer 10.43.37.170 192.168.46.128 8080:32117/TCP 15h
                                4. +
                                5. 使用 curl 发起请求:
                                  $ curl http://localhost:8080
                                  NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511
                                6. +
                                7. 再次查看 Pod 中容器的应用程序日志。
                                  $ kubectl logs hello-node-ccf4b9788-d8k9b
                                  I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
                                  I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
                                  I0130 19:32:21.074992 1 log.go:195] GET /
                                8. +
                                +

                                清理

                                  +
                                1. 删除 Service
                                  kubectl delete service hello-node
                                2. +
                                3. 删除 Deployment
                                  kubectl delete deployment hello-node
                                4. +

                                参考文章

                                ]]>
                                - java - lock - synchronized + k8s + k3s
                                diff --git a/tags/index.html b/tags/index.html index 6afaa598..5ee43efb 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - +