From 6ba71ea24dead75ac86f6f9d5522f5d0be15b6d3 Mon Sep 17 00:00:00 2001
From: moralok
-
+
diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html
index 37adb678..89dc9ba9 100644
--- a/2020/08/19/docker-frequently-used-commands/index.html
+++ b/2020/08/19/docker-frequently-used-commands/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 d96ba79f..3c0b64d9 100644
--- a/2020/08/27/Linux-frequently-used-commands/index.html
+++ b/2020/08/27/Linux-frequently-used-commands/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 b6e645a8..b5839368 100644
--- a/2020/09/04/MySQL-frequently-used-commands/index.html
+++ b/2020/09/04/MySQL-frequently-used-commands/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 9d70feb3..7425902a 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
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -230,7 +230,7 @@
-
+
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 650773ec..bb7e3b28 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
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -230,7 +230,7 @@
-
+
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 032010fc..7b2a5b13 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
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -230,7 +230,7 @@
-
+
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 2c2a6813..db64fe3c 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
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -230,7 +230,7 @@
-
+
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 97dc5a8d..7f4397ca 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
@@ -3,7 +3,7 @@
-
+
@@ -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 1f27eb7c..236efbf6 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
@@ -3,7 +3,7 @@
-
+
@@ -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 4673c435..ce701cd0 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
@@ -3,7 +3,7 @@
-
+
@@ -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 53697a86..65d9f2a3 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
@@ -3,7 +3,7 @@
-
+
@@ -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 57a5eea3..8a37c595 100644
--- a/2023/06/29/tmux-frequently-used-commands/index.html
+++ b/2023/06/29/tmux-frequently-used-commands/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 8fd96762..88daf256 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
@@ -3,7 +3,7 @@
-
+
@@ -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 fa9026a9..e928d49a 100644
--- a/2023/08/04/Spring-application-context-refresh-process/index.html
+++ b/2023/08/04/Spring-application-context-refresh-process/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 00d3f1b0..cc37253d 100644
--- a/2023/08/10/how-does-Spring-load-beans/index.html
+++ b/2023/08/10/how-does-Spring-load-beans/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 71e06ac6..c4c7b848 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
@@ -3,7 +3,7 @@
-
+
@@ -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 a1273583..a45ca24e 100644
--- a/2023/11/03/testing-and-analysis-of-StringTable/index.html
+++ b/2023/11/03/testing-and-analysis-of-StringTable/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 ab096732..999ed1f6 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
@@ -3,7 +3,7 @@
-
+
@@ -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 148672cd..abe10117 100644
--- a/2023/11/07/garbage-collection-in-Java/index.html
+++ b/2023/11/07/garbage-collection-in-Java/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 fe567ea6..12f316e8 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
@@ -3,7 +3,7 @@
-
+
@@ -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 238e9dbd..82760b3f 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
@@ -3,7 +3,7 @@
-
+
@@ -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 cce4d484..b2712e68 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
@@ -3,7 +3,7 @@
-
+
@@ -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 a7e2e0ca..8c42e928 100644
--- a/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html
+++ b/2023/11/19/JDK-dynamic-proxy-and-CGLib/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 bc25585d..00126813 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
@@ -3,7 +3,7 @@
-
+
@@ -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 5ec730a1..f6b7456f 100644
--- a/2023/11/22/circular-dependencies-in-Spring/index.html
+++ b/2023/11/22/circular-dependencies-in-Spring/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 39f0b604..bdee0ae9 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
@@ -3,7 +3,7 @@
-
+
@@ -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 a3404a54..52ae02ae 100644
--- a/2023/11/28/how-does-Dubbo-SPI-works/index.html
+++ b/2023/11/28/how-does-Dubbo-SPI-works/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 930c769a..2f4067ac 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
@@ -3,7 +3,7 @@
-
+
@@ -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 151ed25f..9db97dba 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
@@ -3,7 +3,7 @@
-
+
@@ -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 3817a1d1..1b0e8d65 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
@@ -3,7 +3,7 @@
-
+
@@ -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 4ce6bc07..a0955568 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
@@ -3,7 +3,7 @@
-
+
@@ -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 c17e4851..e6366943 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
@@ -3,7 +3,7 @@
-
+
@@ -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 21dc0992..046fccbf 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
@@ -3,7 +3,7 @@
-
+
@@ -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 e5465667..cb02ddfa 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
@@ -3,7 +3,7 @@
-
+
@@ -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 6089e4c2..8a8c8c2f 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
@@ -3,7 +3,7 @@
-
+
@@ -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 060584f1..d42c2f26 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
@@ -3,7 +3,7 @@
-
+
@@ -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 89901539..c15af93f 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
@@ -3,7 +3,7 @@
-
+
@@ -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 3f61ec98..b36db012 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
@@ -3,7 +3,7 @@
-
+
@@ -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 0bd53b0e..b52a8c99 100644
--- a/2023/12/14/install-ELK-using-Docker-Compose/index.html
+++ b/2023/12/14/install-ELK-using-Docker-Compose/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 9550b433..3445253e 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
@@ -3,7 +3,7 @@
-
+
@@ -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 b63eaf45..22884ae3 100644
--- a/2023/12/25/Unsafe-an-anti-Java-class/index.html
+++ b/2023/12/25/Unsafe-an-anti-Java-class/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 04c273e7..c3c6c57d 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
@@ -3,7 +3,7 @@
-
+
@@ -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 67fefb37..5e13ee31 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
@@ -3,7 +3,7 @@
-
+
@@ -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 94d132ab..da6c8b89 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
@@ -3,7 +3,7 @@
-
+
@@ -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 f3e25570..2234b1d5 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
@@ -3,7 +3,7 @@
-
+
@@ -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 c017ad82..413651ea 100644
--- a/2024/01/18/use-vim/index.html
+++ b/2024/01/18/use-vim/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 0ec632b8..12eeb7ce 100644
--- a/2024/01/30/installation-and-use-of-k3s/index.html
+++ b/2024/01/30/installation-and-use-of-k3s/index.html
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -230,7 +230,7 @@
-
+
diff --git a/2024/03/11/representating-and-manipulating-information/index.html b/2024/03/11/representating-and-manipulating-information/index.html
index 755bd565..2433d6b6 100644
--- a/2024/03/11/representating-and-manipulating-information/index.html
+++ b/2024/03/11/representating-and-manipulating-information/index.html
@@ -3,7 +3,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -340,7 +340,7 @@
-
+
diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html
index 320c5eeb..7887afff 100644
--- a/archives/2020/08/index.html
+++ b/archives/2020/08/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2020/09/index.html b/archives/2020/09/index.html
index 85070666..503cfbd0 100644
--- a/archives/2020/09/index.html
+++ b/archives/2020/09/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2020/index.html b/archives/2020/index.html
index 3a32fae7..34cf4775 100644
--- a/archives/2020/index.html
+++ b/archives/2020/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html
index 3f4a74ff..6bd069df 100644
--- a/archives/2023/05/index.html
+++ b/archives/2023/05/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html
index f437963f..a2b4978d 100644
--- a/archives/2023/06/index.html
+++ b/archives/2023/06/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html
index 5938cbba..ccd03baf 100644
--- a/archives/2023/07/index.html
+++ b/archives/2023/07/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html
index 7f3e26fc..81d27605 100644
--- a/archives/2023/08/index.html
+++ b/archives/2023/08/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html
index 3d96b3d3..d5862e1d 100644
--- a/archives/2023/11/index.html
+++ b/archives/2023/11/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/11/page/2/index.html b/archives/2023/11/page/2/index.html
index baa587d9..eacc4264 100644
--- a/archives/2023/11/page/2/index.html
+++ b/archives/2023/11/page/2/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/12/index.html b/archives/2023/12/index.html
index 592a64d2..485db735 100644
--- a/archives/2023/12/index.html
+++ b/archives/2023/12/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/12/page/2/index.html b/archives/2023/12/page/2/index.html
index ae78b14c..2a215c5a 100644
--- a/archives/2023/12/page/2/index.html
+++ b/archives/2023/12/page/2/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/index.html b/archives/2023/index.html
index b1efaa7a..d65cd405 100644
--- a/archives/2023/index.html
+++ b/archives/2023/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html
index 74efb4b9..67628397 100644
--- a/archives/2023/page/2/index.html
+++ b/archives/2023/page/2/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html
index b32bad61..3b8da5c3 100644
--- a/archives/2023/page/3/index.html
+++ b/archives/2023/page/3/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html
index 85f30238..9584b706 100644
--- a/archives/2023/page/4/index.html
+++ b/archives/2023/page/4/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2024/01/index.html b/archives/2024/01/index.html
index d967f14f..5e7670b2 100644
--- a/archives/2024/01/index.html
+++ b/archives/2024/01/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2024/03/index.html b/archives/2024/03/index.html
index 1e75c755..8cc498ad 100644
--- a/archives/2024/03/index.html
+++ b/archives/2024/03/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/2024/index.html b/archives/2024/index.html
index 4138296d..158e7140 100644
--- a/archives/2024/index.html
+++ b/archives/2024/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/index.html b/archives/index.html
index 36fa986b..32f40026 100644
--- a/archives/index.html
+++ b/archives/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/page/2/index.html b/archives/page/2/index.html
index cf19e6ca..0eeda2fb 100644
--- a/archives/page/2/index.html
+++ b/archives/page/2/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/page/3/index.html b/archives/page/3/index.html
index 9ce33e4b..33842044 100644
--- a/archives/page/3/index.html
+++ b/archives/page/3/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/page/4/index.html b/archives/page/4/index.html
index 07d59e2f..6bcba0be 100644
--- a/archives/page/4/index.html
+++ b/archives/page/4/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/archives/page/5/index.html b/archives/page/5/index.html
index 59bccd43..49abc5a4 100644
--- a/archives/page/5/index.html
+++ b/archives/page/5/index.html
@@ -3,7 +3,7 @@
-
+
diff --git a/index.html b/index.html
index 61ecc28b..9b2e97de 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
-
+
@@ -336,7 +336,7 @@
-
+
@@ -441,7 +441,7 @@
-
+
@@ -546,7 +546,7 @@
-
+
@@ -651,7 +651,7 @@
-
+
@@ -756,7 +756,7 @@
-
+
@@ -861,7 +861,7 @@
-
+
@@ -966,7 +966,7 @@
-
+
@@ -1071,7 +1071,7 @@
-
+
@@ -1176,7 +1176,7 @@
-
+
@@ -1281,7 +1281,7 @@
-
+
diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json
index 6c935c4a..45c9ace1 100644
--- a/leancloud_counter_security_urls.json
+++ b/leancloud_counter_security_urls.json
@@ -1 +1 @@
-[{"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":"在 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":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"Spring Bean 加载过程","url":"/2023/08/10/how-does-Spring-load-beans/"},{"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":"Spring 应用 context 刷新流程","url":"/2023/08/04/Spring-application-context-refresh-process/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"使用 Grafana 和 Prometheus 搭建监控","url":"/2023/11/18/setup-monitoring-using-grafana-and-prometheus/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"Spring AOP 如何创建代理 beans","url":"/2023/11/19/how-does-Spring-AOP-create-proxy-beans/"},{"title":"JDK 动态代理和 CGLib","url":"/2023/11/19/JDK-dynamic-proxy-and-CGLib/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"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":"Spring 中的循环依赖","url":"/2023/11/22/circular-dependencies-in-Spring/"},{"title":"使用 logrotate 滚动 Docker 容器内的 Nginx 的日志","url":"/2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/"},{"title":"Spring @Configuration 注解的源码分析","url":"/2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/"},{"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":"Dubbo SPI 自适应拓展的工作原理","url":"/2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/"},{"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":"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":"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/"},{"title":"探索 Java 类 Cleaner 和 Finalizer","url":"/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/"},{"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":"信息的表示和处理","url":"/2024/03/11/representating-and-manipulating-information/"},{"title":"Spring 中 @Import 注解的使用和源码分析","url":"/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/"}]
\ 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":"MySQL 常用命令","url":"/2020/09/04/MySQL-frequently-used-commands/"},{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"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":"使用 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":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"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":"Spring 应用 context 刷新流程","url":"/2023/08/04/Spring-application-context-refresh-process/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"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":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"Spring 中的循环依赖","url":"/2023/11/22/circular-dependencies-in-Spring/"},{"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":"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":"Dubbo SPI 的工作原理","url":"/2023/11/28/how-does-Dubbo-SPI-works/"},{"title":"Spring Boot SPI 的工作原理","url":"/2023/12/06/how-does-Spring-Boot-SPI-works/"},{"title":"Spring Boot 自定义 starter 和自动配置的工作原理","url":"/2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/"},{"title":"Spring 中 @Import 注解的使用和源码分析","url":"/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/"},{"title":"Spring 中 @PropertySource 注解的使用和源码分析","url":"/2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/"},{"title":"当 MySQL 以 skip-name-resolve 模式启动时如何使用 grant 命令","url":"/2023/12/13/how-to-grant-when-MySQL-started-with-skip-name-resolve-mode/"},{"title":"ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗","url":"/2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/"},{"title":"使用 Docker Compose 安装 ELK","url":"/2023/12/14/install-ELK-using-Docker-Compose/"},{"title":"ComponentScan 扫描路径覆盖的真相","url":"/2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/"},{"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":"synchronized 锁机制的分析和验证","url":"/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/"},{"title":"Spring AutowiredAnnotationBeanPostProcessor 的源码分析","url":"/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/"},{"title":"使用 Vim","url":"/2024/01/18/use-vim/"},{"title":"不使用 GParted 的情况下为 VMware 中的 Ubuntu Server 增大磁盘空间","url":"/2024/01/14/increase-disk-space-of-Ubuntu-server-on-VMware-without-using-GParted/"},{"title":"k3s 的安装和使用","url":"/2024/01/30/installation-and-use-of-k3s/"},{"title":"谈谈 MySQL 事务的隔离性","url":"/2024/01/06/talk-about-isolation-of-MySQL-transactions/"},{"title":"信息的表示和处理","url":"/2024/03/11/representating-and-manipulating-information/"}]
\ No newline at end of file
diff --git a/page/2/index.html b/page/2/index.html
index 678d60d0..dd89103b 100644
--- a/page/2/index.html
+++ b/page/2/index.html
@@ -3,7 +3,7 @@
-
+
@@ -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 0cd34f10..463df535 100644
--- a/page/3/index.html
+++ b/page/3/index.html
@@ -3,7 +3,7 @@
-
+
@@ -224,7 +224,7 @@
-
+
@@ -329,7 +329,7 @@
-
+
@@ -434,7 +434,7 @@
-
+
@@ -539,7 +539,7 @@
-
+
@@ -644,7 +644,7 @@
-
+
@@ -872,7 +872,7 @@
-
+
@@ -1074,7 +1074,7 @@
-
+
@@ -1179,7 +1179,7 @@
-
+
@@ -1333,7 +1333,7 @@
-
+
@@ -1501,7 +1501,7 @@
-
+
diff --git a/page/4/index.html b/page/4/index.html
index d1fb8a76..60d6b149 100644
--- a/page/4/index.html
+++ b/page/4/index.html
@@ -3,7 +3,7 @@
-
+
@@ -224,7 +224,7 @@
-
+
@@ -457,7 +457,7 @@
-
+
@@ -657,7 +657,7 @@
-
+
@@ -819,7 +819,7 @@
-
+
@@ -979,7 +979,7 @@
-
+
@@ -1184,7 +1184,7 @@
-
+
@@ -1470,7 +1470,7 @@
-
+
@@ -1575,7 +1575,7 @@
-
+
@@ -1680,7 +1680,7 @@
-
+
@@ -1785,7 +1785,7 @@
-
+
diff --git a/page/5/index.html b/page/5/index.html
index 158aef70..f0cd665a 100644
--- a/page/5/index.html
+++ b/page/5/index.html
@@ -3,7 +3,7 @@
-
+
@@ -224,7 +224,7 @@
-
+
@@ -364,7 +364,7 @@
-
+
@@ -469,7 +469,7 @@
-
+
@@ -574,7 +574,7 @@
-
+
@@ -679,7 +679,7 @@
-
+
@@ -784,7 +784,7 @@
-
+
@@ -889,7 +889,7 @@
-
+
@@ -994,7 +994,7 @@
-
+
@@ -1099,7 +1099,7 @@
-
+
diff --git a/search.xml b/search.xml
index e4cad3b9..10b796e1 100644
--- a/search.xml
+++ b/search.xml
@@ -924,59 +924,6 @@
Ubuntu
上安装 Clash
,以供各类程序在必要的时候使用代理。
有些时候我们面对特定资源会遇到下载速度极其缓慢甚至无法下载的情况,像是使用 Github
,下载 k8s
相关镜像,还有访问特定网站等等。也许有些 Dirty Hack 的方式可以短暂地绕开限制,比如修改 hosts
文件,但这并不总是见效。也许添加国内的镜像仓库地址可以满足很多人的需求,但是说不准就会遇到镜像更新不及时甚至镜像被污染的情况。总之,如果你有一把不错的梯子,使用代理辅助开发肯定会拥有更好的体验。
当然,为 Git
设置代理,为 Terminal
设置代理,为 Docker Engine
设置代理,为 Docker
容器设置代理,为 “还有一些你尚未知道原来这还需要这样设置代理的地方” 设置代理,仍然是一件痛苦的事情。如果你有条件为全屋设备配置透明代理,肯定能避免踩非常多的坑。
--注意:多个
-Clash
相关的仓库已经 GG
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
--本人最新的安装是通过直接拷贝旧服务器上的二进制可执行文件以及全球
-IP
库文件来完成的,保留已有安装的相关文件在短时间内仍然可以满足重新安装的需求。注意到原作者Dreamacro
的Docker
镜像仓库仍然保留,也许通过Docker
运行Clash
是个更好的选择。
docker-compose.yml
version: "1.0" |
/root/.config/clash/config.yaml
--长远来看,使用无法得到更新的旧版本仍然可能在未来导致安全问题的发生,以后再说吧 =_=。
-
--本方式仅限仍然可获得可靠安全的二进制可执行文件的人使用。
-
wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz |
gzip
解压压缩包 clash-linux-amd64-v1.16.0.gz
得到 clash-linux-amd64-v1.16.0
。忽略提示。gzip -d clash-linux-amd64-v1.16.0.gz |
/usr/local/bin
并且重命名为 clash
。sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash |
clash -v
查看 Clash
的版本信息。clash -v |
clash
启动,可以看到日志。:~$ clash |
Clash
时,Clash
会在 ~/.config
下创建目录 clash
,并在其中创建 3
个文件。其中 config.yaml
是 Clash
的配置文件,Country.mmdb
是全球 IP
库,可以实现各个国家的 IP
信息解析和地理定位。cache.db
用于缓存。CDN
已失效 ):~/.config/clash$ ls |
Clash
,直接订阅;否则需要手动修改配置文件。http
)---
参考官方文档 Clash as a service。( 已404
)
IP
库到 /etc/clash
~/.config/clash$ sudo cp config.yaml /etc/clash/ |
/etc/systemd/system/clash.service
创建 systemd
配置文件[Unit] |
systemd
systemctl daemon-reload |
Clash
systemctl enable clash |
Clash
systemctl start clash |
Clash
的健康状态和日志systemctl status clash |
Ubuntu
上安装 Clash
,以供各类程序在必要的时候使用代理。Github
,下载 k8s
相关镜像,还有访问特定网站等等。也许有些 Dirty Hack 的方式可以短暂地绕开限制,比如修改 hosts
文件,但这并不总是见效。也许添加国内的镜像仓库地址可以满足很多人的需求,但是说不准就会遇到镜像更新不及时甚至镜像被污染的情况。总之,如果你有一把不错的梯子,使用代理辅助开发肯定会拥有更好的体验。Git
设置代理,为 Terminal
设置代理,为 Docker Engine
设置代理,为 Docker
容器设置代理,为 “还有一些你尚未知道原来这还需要这样设置代理的地方” 设置代理,仍然是一件痛苦的事情。如果你有条件为全屋设备配置透明代理,肯定能避免踩非常多的坑。
+
+
+++注意:多个
+Clash
相关的仓库已经 GG
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
++本人最新的安装是通过直接拷贝旧服务器上的二进制可执行文件以及全球
+IP
库文件来完成的,保留已有安装的相关文件在短时间内仍然可以满足重新安装的需求。注意到原作者Dreamacro
的Docker
镜像仓库仍然保留,也许通过Docker
运行Clash
是个更好的选择。
docker-compose.yml
version: "1.0" |
/root/.config/clash/config.yaml
++长远来看,使用无法得到更新的旧版本仍然可能在未来导致安全问题的发生,以后再说吧 =_=。
+
++本方式仅限仍然可获得可靠安全的二进制可执行文件的人使用。
+
wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz |
gzip
解压压缩包 clash-linux-amd64-v1.16.0.gz
得到 clash-linux-amd64-v1.16.0
。忽略提示。gzip -d clash-linux-amd64-v1.16.0.gz |
/usr/local/bin
并且重命名为 clash
。sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash |
clash -v
查看 Clash
的版本信息。clash -v |
clash
启动,可以看到日志。:~$ clash |
Clash
时,Clash
会在 ~/.config
下创建目录 clash
,并在其中创建 3
个文件。其中 config.yaml
是 Clash
的配置文件,Country.mmdb
是全球 IP
库,可以实现各个国家的 IP
信息解析和地理定位。cache.db
用于缓存。CDN
已失效 ):~/.config/clash$ ls |
Clash
,直接订阅;否则需要手动修改配置文件。http
)+++
参考官方文档 Clash as a service。( 已404
)
IP
库到 /etc/clash
~/.config/clash$ sudo cp config.yaml /etc/clash/ |
/etc/systemd/system/clash.service
创建 systemd
配置文件[Unit] |
systemd
systemctl daemon-reload |
Clash
systemctl enable clash |
Clash
systemctl start clash |
Clash
的健康状态和日志systemctl status clash |
有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wget
和 curl
。
这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxy
、https_proxy
和 no_proxy
。
仅为当前会话设置,执行命令:
export http_proxy=http://proxyAddress:port |
永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc
或 ~/.zshrc
)添加:
export http_proxy=http://proxyAddress:port |
重新启动一个会话或者执行命令 source ~/.bashrc
使其在当前会话立即生效。
在搜索过程中发现还可以在 wget
的配置文件 ~/.wgetrc
中添加:
use_proxy = on |
如果你以为为终端设置代理后 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
执行的。
如果你的 docker daemon
是通过 systemd
管理的,那么你可以通过设置 docker.service
服务的环境变量来设置代理。
执行命令查看 docker.service
信息,得知配置文件位置 /lib/systemd/system/docker.service
。
systemctl status docker.service |
在 docker.service
的 [Service]
模块添加:
Environment=HTTP_PROXY=http://proxyAddress:port |
重新加载配置文件并重启服务:
-systemctl daemon-reload |
还可以修改 dockerd
配置文件,添加:
export http_proxy="http://proxyAddress:port" |
然后重启 docker daemon
即可。
--国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。
-
为 docker daemon
进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。
创建或修改 ~/.docker/config.json
,添加:
{ |
此后创建的新容器,会自动设置环境变量来使用代理。
-在启动容器时使用 -e
手动注入环境变量 http_proxy
。这意味着进入容器使用 export
设置环境变量的方式也是可行的。
--注意:如果代理是使用宿主机的代理,当网络为
-bridge
模式,proxyAddress 需要填写宿主机的 IP;如果使用host
模式,proxyAddress 可以填写 127.0.0.1。
不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:
-举一反三,像 apt
和 git
这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。
--原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。
-
Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置
有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wget
和 curl
。
这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxy
、https_proxy
和 no_proxy
。
仅为当前会话设置,执行命令:
export http_proxy=http://proxyAddress:port |
永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc
或 ~/.zshrc
)添加:
export http_proxy=http://proxyAddress:port |
重新启动一个会话或者执行命令 source ~/.bashrc
使其在当前会话立即生效。
在搜索过程中发现还可以在 wget
的配置文件 ~/.wgetrc
中添加:
use_proxy = on |
如果你以为为终端设置代理后 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
执行的。
如果你的 docker daemon
是通过 systemd
管理的,那么你可以通过设置 docker.service
服务的环境变量来设置代理。
执行命令查看 docker.service
信息,得知配置文件位置 /lib/systemd/system/docker.service
。
systemctl status docker.service |
在 docker.service
的 [Service]
模块添加:
Environment=HTTP_PROXY=http://proxyAddress:port |
重新加载配置文件并重启服务:
+systemctl daemon-reload |
还可以修改 dockerd
配置文件,添加:
export http_proxy="http://proxyAddress:port" |
然后重启 docker daemon
即可。
++国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。
+
为 docker daemon
进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。
创建或修改 ~/.docker/config.json
,添加:
{ |
此后创建的新容器,会自动设置环境变量来使用代理。
+在启动容器时使用 -e
手动注入环境变量 http_proxy
。这意味着进入容器使用 export
设置环境变量的方式也是可行的。
++注意:如果代理是使用宿主机的代理,当网络为
+bridge
模式,proxyAddress 需要填写宿主机的 IP;如果使用host
模式,proxyAddress 可以填写 127.0.0.1。
不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:
+举一反三,像 apt
和 git
这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。
++原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。
+
Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置
使用 Vmware Workstation 通过 ubuntu-20.04.6-live-server-amd64.iso
安装。需要满足条件如下:
SSH
连接 Github
和免密登录服务器作为备忘笔记,主要在新建虚拟机或重装云服务器系统时使用。
+
+
+打开终端,输入 ls -al ~/.ssh
以查看是否存在现有的 SSH
密钥。
ls -al ~/.ssh |
检查目录列表以查看是否已经有 SSH
公钥。 默认情况下,GitHub
的一个支持的公钥的文件名是以下之一。
id_rsa.pub
id_ecdsa.pub
id_ed25519.pub
参考官方文档:Install Docker Engine on Ubuntu。
-参考官方文档:minikube start。
-参考官方文档:在 Linux 系统中安装并设置 kubectl。
-创建集群:
-minikube start |
如果没有密钥,就需要生成新的 SSH
密钥;如果已有,跳到上传已有密钥环节。
打开终端,粘贴下面的文本(替换为你的 GitHub
电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH
密钥。
一直 yes
确定选择默认即可。
ssh-keygen -t ed25519 -C "your_email@example.com" |
不加 sudo
的时候,创建集群失败,提示无法选择默认 driver。可能是 docker 处于不健康状态或者用户权限不足。
$ minikube start |
将 SSH
公钥复制到剪贴板,在 Github
上的 Settings -> Access -> SSH and GPG keys -> New SSH key
,粘贴即可。
cat ~/.ssh/id_ed25519.pub |
使用 sudo
的时候,会提示不建议通过 root
权限使用 docker
,如果还是想要继续,可以使用选项 --force
。
sudo minikube start |
git config --global user.name "your_username" |
考虑到仅用于测试,尝试通过 sudo minikube start --force
启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。
sudo minikube start --force |
成功启动集群后,使用 kubectl get pod
测试,提示连接被拒绝。
kubectl get pod |
使用现成的密钥,将 ~/.ssh/id_ed25519.pub
的内容追加到服务端的 ~/.ssh/authorized_keys
中,使用 ssh user@host
成功免密登录。这样一来,远程连接服务器或者使用 VScode Remote Explorer
时,不用每次输入密码了。
在更新 VMware Workstation 17 Pro
后,发现虚拟机的 IP
从 192.168.46.135
重置 为 192.168.46.128
,即使更新配置文件 C:\Users\moralok\.ssh\config
中的 IP
VScode Remote Explorer
仍然无法连接,但是通过 Xshell
使用账号密码可以登录。查看报错信息发现 known_hosts
中 192.168.46.128
对应的 ECDSA key
有问题,应该记录的是之前占用该 IP
的虚拟机的 ECDSA key
,删除该行后重新连接成功。
[18:09:59.973] > @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
使用 minikube dashboard
启动控制台,在访问时同样提示连接被拒绝:dial tcp 127.0.0.1:8080: connect: connection refused
。
考虑到可能还有别的问题,决定采用官方建议将用户添加到 docker
用户组。
使用 sudo usermod -aG docker $USER && newgrp docker
将当前用户添加到 docker
用户组并切换当前用户组到 docker
用户组后,正常启动集群。
minikube start |
在解决问题的过程中,发现有人存在下载镜像失败的情况。从启动日志可以看到,由于 minikube 下载 gcr.io/k8s-minikube/kicbase:v0.0.39
镜像失败,自动下载 docker.io/kicbase/stable:v0.0.39
镜像作为备选。如果从 docker.io
下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。
使用 SSH
协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github
还可以使用 SSH
密钥对提交进行签名。
SSH
的使用(非对称加密)需要生成公钥 public key
和私钥 private key
。常用的算法有 rsa
、ecdsa
和 ed25519
,相对应的公钥默认文件名即 id_XXX.pub
。ed25519
的安全性介于 rsa 2048
和 rsa 4096
之间,但性能却提升数十倍。
在生成密钥时,会要求你 Enter passphrase (empty for no passphrase):
,可以输入一个口令保护私钥的使用。不为空的情况下,正常使用是需要输入这个口令的,很多人认为麻烦,因此留空。
公钥的权限必须是 644
,私钥的权限必须是 600
,否则 SSH
认为其不可靠。
私钥是要安全保管在客户端不能泄露的,公钥则要提供给远程服务器或服务。服务端的 ~/.ssh/authorized_keys
里面存储着可以登录的客户端的公钥。我们将公钥粘贴到 Github
的过程就是对应于此。
ssh-keygen -t rsa -b 4096 -f my_id -C "email@example.com" |
-t
表示算法,如 rsa
。-b
表示 rsa
密钥长度,默认 2048 bit
,ed25519
不需要指定。-f
表示文件名。-C
表示在公钥文件中添加注释,可修改Client
将自己的公钥存放到服务端,追加到 authorized_keys
文件。Server
收到 Client
的连接请求后,会在 authorized_keys
文件中匹配到 Client
传过来的公钥,并生成随机数 R
,用公钥对随机数加密得到 pubKey(R)
。Client
收到后通过私钥解密得到随机数 R
,然后对随机数 R
和本次会话的 sessionKey
使用 MD5
生成摘要 Digest1
,发送给服务端。Server
会对随机数 R
和会话的 sessionKey
同样使用 MD5
生成摘要 Digest2
,对比相同即完成认证过程。SSH
通过口令确认避免中间人攻击,如果用户第一次登录 Server
,系统会提示:
ssh -T git@github.com |
Server
需要在其网站上公示其公钥的指纹,Github
的公钥指纹在这里。
确认匹配后,客户端会在 ~/.ssh/known_hosts
中记录,下次登录不再警告。
使用 SSH 进行连接 Github
Git 多台电脑共用SSH Key
SSH协议登录过程详解
GitHub 的 SSH 密钥指纹
使用 Ed25519 算法生成你的 SSH 密钥
使用 Vmware Workstation 通过 ubuntu-20.04.6-live-server-amd64.iso
安装。需要满足条件如下:
参考官方文档:Install Docker Engine on Ubuntu。
+参考官方文档:minikube start。
+参考官方文档:在 Linux 系统中安装并设置 kubectl。
+创建集群:
+minikube start |
不加 sudo
的时候,创建集群失败,提示无法选择默认 driver。可能是 docker 处于不健康状态或者用户权限不足。
$ minikube start |
使用 sudo
的时候,会提示不建议通过 root
权限使用 docker
,如果还是想要继续,可以使用选项 --force
。
sudo minikube start |
考虑到仅用于测试,尝试通过 sudo minikube start --force
启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。
sudo minikube start --force |
成功启动集群后,使用 kubectl get pod
测试,提示连接被拒绝。
kubectl get pod |
使用 minikube dashboard
启动控制台,在访问时同样提示连接被拒绝:dial tcp 127.0.0.1:8080: connect: connection refused
。
考虑到可能还有别的问题,决定采用官方建议将用户添加到 docker
用户组。
使用 sudo usermod -aG docker $USER && newgrp docker
将当前用户添加到 docker
用户组并切换当前用户组到 docker
用户组后,正常启动集群。
minikube start |
在解决问题的过程中,发现有人存在下载镜像失败的情况。从启动日志可以看到,由于 minikube 下载 gcr.io/k8s-minikube/kicbase:v0.0.39
镜像失败,自动下载 docker.io/kicbase/stable:v0.0.39
镜像作为备选。如果从 docker.io
下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。
minikube start --help | grep repo |
启动控制台:
@@ -1712,297 +1767,99 @@当 Java
程序启动的时候,Java
虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String)
加载 main
方法所在的类。
public Class<?> loadClass(String name) throws ClassNotFoundException { |
public class JvmGcTest_1 { |
根据注释可知,此方法加载具有指定二进制名称的类,它由 Java
虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)
。
protected Class<?> loadClass(String name, boolean resolve) |
Heap |
根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean)
同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:
findLoadedClass(String)
以检查该类是否已加载。loadClass
方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。findClass(String)
方法来查找该类。如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 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
。
需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK
的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader
对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader
对象。
简单地说,如果 ClassLoader
对象注册为具备并行能力,那么一个 name
一个锁对象,已创建的锁对象保存在 ConcurrentHashMap
类型的 parallelLockMap
中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader
对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?
protected Object getClassLoadingLock(String className) { |
根据打印的信息,组成如下:
+Heap
: 堆。def new generation
: 新生代。tenured generation
: 老年代。Metaspace
: 元空间,实际上并不属于堆, -XX:+PrintGCDetails
将它的信息一起输出。新生代中的空间占比 eden:from:to
在默认情况下是 8:1:1
,与观察到的数据 8192K:1024K:1024K
一致。
新生代的空间 eden + from + to
为 10240K,符合 -Xmn10M
设置的大小。total
显示为 9216K,即 eden + from
的大小,是因为 to
的空间不计算在内。新生代可用的空间只有 eden + from
,to
空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden
中已用 2010K,约占 eden
空间的 24%。
内存地址为 16 位的 16 进制的数字,64 位机器。[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000)
,老年代 [0x00000000ff600000, 0x0000000100000000)
,计算可得空间大小均为 10MB。eden
中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8)
,空间大小为 2058440 byte,约等于 2010K。
显而易见,新生代和老生代是一片完全连续的地址空间。
+public static void main(String[] args) { |
ClassLoader
对象注册为具有并行能力”呢?AppClassLoader
中有一段 static
代码。事实上 java.lang.ClassLoader#registerAsParallelCapable
是将 ClassLoader
对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader
都需要调用一次该方法。
static { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
java.lang.ClassLoader#registerAsParallelCapable
方法有一个注解 @CallerSensitive
,这是因为它的代码中调用的 native
方法 sun.reflect.Reflection#getCallerClass()
方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:
Object
类除外)都注册为具有并行能力。static
代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。Java
虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static
代码块,因此父类的 static
代码块总是先于子类的 static
代码块。你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader
均在 static
代码块实现注册,以保证满足以上两个条件。
简单地说就是保存了类加载器所属 Class
的 Set
。
|
Allocation Failure
,正常情况下,新对象总是分配在 Eden,分配空间失败,eden
的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
。[DefNew: 2013K->721K(9216K), 0.0105099 secs]
,新生代在垃圾回收前后空间的占用变化和耗时。2013K->721K(19456K), 0.0105455 secs
,整个堆在垃圾回收前后空间的占用变化和耗时。
from
的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0)
,空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden
区域存活的对象全部转移到了原 to
空间,from
和 to
空间的角色相互转换(从地址空间的信息可以看到此时 to
的地址指针比 from
的地址指针小)。eden
的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0)
,空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden
区域除了 byte[]
对象外,还存储了其他对象,比如为了创建 List<byte[]>
对象而新加载的类对象。
public static void main(String[] args) { |
方法 java.lang.ClassLoader.ParallelLoaders#register
。ParallelLoaders
封装了一组具有并行能力的加载器类型。就是持有 ClassLoader
的 Class
实例的集合,并保证添加时加同步锁。
// private 修饰,只有其外部类 ClassLoader 才可以使用 |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader
提供了三个构造器方法:
private ClassLoader(Void unused, ClassLoader parent) { |
由于 eden
区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。eden
区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0)
,空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16
。显然第二次添加时,不再因为创建 List<byte[]>
而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh。
public static void main(String[] args) { |
ClassLoader
的构造器方法最终都调用 private
修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader)
,又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader
的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。
使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable
为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader
还是 getClassLoadingLock
都是由 protect
修饰,允许子类重写,来自定义并行加载类的能力。
--todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。
-
加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native
方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?
protected final Class<?> findLoadedClass(String name) { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent
属性的值——另一个类加载器实例。一层一层向上委派直到 parent
为 null
,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader
处理,然后由 bootstrap class loader
首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader
,它的 parent
为 ExtClassLoader
,ExtClassLoader
的 parent
为 null
。
--在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。
-
这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object
类,设计者不希望编写代码的人重新写一个 Object
类并加载到 Java
虚拟机中,但是加载类的本质就是读取字节数据传递给 Java
虚拟机创建一个 Class
实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。
通常流程如下:
-AppClassLoader
调用 loadClass
方法,先委派给 ExtClassLoader
。ExtClassLoader
调用 loadClass
方法,先委派给 bootstrap class loader
。bootstrap class loader
在其设置的类路径中无法找到 BananaTest
类,抛出 ClassNotFoundException
异常。ExtClassLoader
捕获异常,然后自己调用 findClass
方法尝试进行加载。ExtClassLoader
在其设置的类路径中无法找到 BananaTest
类,抛出 ClassNotFoundException
异常。AppClassLoader
捕获异常,然后自己调用 findClass
方法尝试进行加载。注释中提到鼓励重写 findClass
方法而不是 loadClass
,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass
实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass
;其次是因为该方法中涉及并行加载类的机制。
默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass
方法,该方法由子类重写。AppClassLoader
和 ExtClassLoader
都是继承 URLClassLoader
,而 URLClassLoader
重写了 findClass
方法。根据注释可知,该方法会从 URL
搜索路径查找并加载具有指定名称的类。任何引用 Jar
文件的 URL
都会根据需要加载并打开,直到找到该类。
过程如下:
-name
转换为 path
,比如 com.example.BananaTest
转换为 com/example/BananaTest.class
。URL
搜索路径 URLClassPath
和 path
中获取 Resource
,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。URLClassLoader
的私有方法 defineClass
,该方法调用父类 SecureClassLoader
的 defineClass
方法。protected Class<?> findClass(final String name) |
在第三次添加时,由于 eden
空间不足,因此又发生了第二次垃圾回收。[DefNew: 8565K->512K(9216K), 0.0046378 secs]
,新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
在第二次添加完成后,eden
[0x00000000fec00000, 0x00000000ff3bd8d0)
和 from
[0x00000000ff500000, 0x00000000ff5b45f0)
占用的空间为 8116432 + 738800 = 8855232
约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。8565K->8396K(19456K), 0.0046540 secs
,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。eden
中加入了第三次添加时的对象,大于 512K 不少。
此时 eden
、from
、tenured
中均有不好确认成分的空间占用,比如 from 中多了 56 字节。
public static void main(String[] args) { |
URLClassLoader
拥有一个 URLClassPath
类型的属性 ucp
。由注释可知,URLClassPath
类用于维护一个 URL
的搜索路径,以便从 Jar
文件和目录中加载类和资源。URLClassPath
的核心构造器方法:
public URLClassPath(URL[] urls, |
Heap |
URLClassLoader
调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean)
方法获取指定名称对应的资源。根据注释,该方法会查找 URL
搜索路径上的第一个资源,如果找不到资源,则返回 null
。
显然,这里的 Loader
不是我们前面提到的类加载器。Loader
是 URLClassPath
的内部类,用于表示根据一个基本 URL
创建的资源和类的加载器。也就是说一个基本 URL
对应一个 Loader
。
public Resource getResource(String name, boolean check) { |
在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。
+public static void main(String[] args) { |
获取下一个 Loader
,其实根据 index
从一个存放已创建 Loader
的 ArrayList
中获取。
private synchronized Loader getNextLoader(int[] cache, int index) { |
waiting... |
index
到存放已创建 Loader
的列表中去获取(调用方传入的 index
从 0
开始不断递增直到超过范围)。index
超过范围,说明已有的 Loader
都找不到目标 Resource
,需要到未打开的 URL
中查找。URL
中取出(pop
)一个来创建 Loader
,如果 urls
已经为空,则返回 null
。private synchronized Loader getLoader(int index) { |
当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError
。
public static void main(String[] args) { |
根据指定的 URL
创建 Loader
,不同类型的 URL
会返回不同具体实现的 Loader
。
URL
不是以 /
结尾,认为是 Jar
文件,则返回 JarLoader
类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
。URL
以 /
结尾,且协议为 file
,则返回 FileLoader
类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
。URL
以 /
结尾,且协议不会 file
,则返回 Loader
类型。private Loader getLoader(final URL url) throws IOException { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
以 FileLoader
的 getResource
为例,如果文件找到了,就会将文件包装成一个 FileInputStream
,再将 FileInputStream
包装成一个 Resource
返回。
Resource getResource(final String name, boolean check) { |
当 Thread-0
发生 OutOfMemoryError
后,main
线程仍然正常运行。
当创建的大对象 + 对象头的容量小于等于 eden
,如果 GC 后的存活对象可以放入 to
,那么还是会先在 eden
中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。
public static void main(String[] args) { |
从上文可知,ClassLoader
调用 findClass
方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader
有不同的类路径。
以下是通过 IDEA
启动 Java
程序时的命令,可以看到其中通过 -classpath
指定了应用·类加载器 AppClassLoader
的类路径,该类路径除了包含常规的 JRE
的文件路径外,还额外添加了当前 maven
工程编译生成的 target\classes
目录。
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest |
[GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
启动·类加载器 bootstrap class loader
,加载核心类库,即 <JRE_HOME>/lib
目录中的部分类库,如 rt.jar
,只有名字符合要求的 jar
才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath
修改默认的类路径,有三种使用方式:
-Xbootclasspath:
:完全覆盖核心类库的类路径,不常用,除非重写核心类库。-Xbootclasspath/a:
以后缀的方式拼接在原搜索路径后面,常用。-Xbootclasspath/p:
以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。
+在 IDEA
中编辑启动配置,添加 VM
选项,-Xbootclasspath:C:\Software
,里面没有类文件,启动虚拟机失败,提示:
Error occurred during initialization of VM |
尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。
+]]>扩展·类加载器 ExtClassLoader
,加载 <JRE_HOME>/lib/ext/
目录中的类库。启动 Java
虚拟机时可以通过选项 -Djava.ext.dirs
修改默认的类路径。显然修改不当同样可能会引起 Java
程序的异常。
应用·类加载器 AppClassLoader
,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH
的值,也可以在启动 Java 虚拟机时通过选项 -classpath
修改。
CLASSPATH
在 Windows
中,多个文件路径使用分号 ;
分隔,而 Linux
中则使用冒号 :
分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。
.;C:\path\to\classes
.:/path/to/classes
事实上,AppClassLoader
最终的类路径,不仅仅包含 -classpath
的值,还会包含 -javaagent
指定的值。
方法 defineClass
,顾名思义,就是定义类,将字节数据转换为 Class
实例。在 ClassLoader
以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name
和字节数据等参数,调用 native
方法获得一个 Class
实例。
以下是定义类时最终可能调用的 native
方法。
private native Class<?> defineClass0(String name, byte[] b, int off, int len, |
其方法参数有:
-name
,目标类的名称。byte[]
或 ByteBuffer
类型的字节数据,off
和 len
只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。ProtectionDomain
,保护域,todo:source
,CodeSource
的位置。defineClass
方法的调用过程,其实就是从 URLClassLoader
开始,一层一层处理后再调用父类的 defineClass
方法,分别经过了 SecureClassLoader
和 ClassLoader
。
此方法是再 URLClassLoader
的 findClass
方法中,获得正确的 Resource
之后调用的,由 private
修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。
private Class<?> defineClass(String name, Resource res) throws IOException { |
public void refresh() throws BeansException, IllegalStateException { |
Resource
类提供了 getBytes
方法,此方法以字节数组的形式返回字节数据。
public byte[] getBytes() throws IOException { |
准备此 context 以供刷新,设置其启动日期和活动标志以及执行属性源的初始化。
+++编写管理资源的容器时,可以参考。
+
protected void prepareRefresh() { |
在 getByteBuffer
之后会缓存 InputStream
以便调用 getBytes
时使用,方法由 synchronized
修饰。
private synchronized InputStream cachedInputStream() throws IOException { |
在这个例子中,Resource
的实例是 URLClassPath
中的匿名类 FileLoader
以 Resource
的匿名类的方式创建的。
public InputStream getInputStream() throws IOException |
URLClassLoader
继承自 SecureClassLoader
,SecureClassLoader
提供并重载了 defineClass
方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[]
类型或者 ByteBuffer
类型)转换为 Class
类型的实例,有一个可选的 CodeSource
类型的参数。
protected final Class<?> defineClass(String name, |
方法中只是简单地将 CodeSource
类型的参数转换成 ProtectionDomain
类型,就调用 ClassLoader
的 defineClass
方法。
private ProtectionDomain getProtectionDomain(CodeSource cs) { |
根据注释可知,此方法会返回给定 CodeSource
对象的权限。此方法由 protect
修饰,AppClassLoader
和 URLClassLoader
都有重写。当前 ClassLoader
是 AppClassLoader
。
AppClassLoader#getPermissions
,添加允许从类路径加载的任何类退出 VM的权限。
protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions
,添加一个读文件或读目录的权限。
protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions
,延迟设置权限,在创建 ProtectionDomain
时再设置。
protected PermissionCollection getPermissions(CodeSource codesource) |
ProtectionDomain
的相关构造器参数:
CodeSource
PermissionCollection
,如果不为 null
,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。ClassLoader
Principal[]
这样看来,SecureClassLoader
为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain
的映射作为缓存。
抽象类 ClassLoader
中最终用于定义类的 native
方法 define0
,define1
,define2
都是由 private
修饰的,ClassLoader
提供并重载了 defineClass
方法作为使用它们的入口,这些 defineClass
方法都由 protect
final
修饰,这意味着这些方法只能被子类使用,并且不能被重写。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
主要步骤:
-preDefineClass
前置处理defineClassX
postDefineClass
后置处理确定保护域 ProtectionDomain
,并检查:
java.*
类package
)中其余类的签名者相匹配private ProtectionDomain preDefineClass(String name, |
确定 Class
的 CodeSource
位置。
private String defineClassSourceLocation(ProtectionDomain pd) |
这些 native
方法使用了 name
,字节数据,ProtectionDomain
和 source
等参数,像黑盒一样,在虚拟机中定义了一个类。
在定义类后使用 ProtectionDomain
中的 certs
补充 Class
实例的 signer
信息,猜测在 native
方法 defineClassX
方法中,对 ProtectionDomain
做了一些修改。事实上,从代码上看,将 CodeSource
包装为 ProtectionDomain
传入后,除了 defineClassX
方法外,其他地方都是取出 CodeSource
使用。
private void postDefineClass(Class<?> c, ProtectionDomain pd) |
public class JvmGcTest_1 { |
Heap |
根据打印的信息,组成如下:
-Heap
: 堆。def new generation
: 新生代。tenured generation
: 老年代。Metaspace
: 元空间,实际上并不属于堆, -XX:+PrintGCDetails
将它的信息一起输出。新生代中的空间占比 eden:from:to
在默认情况下是 8:1:1
,与观察到的数据 8192K:1024K:1024K
一致。
新生代的空间 eden + from + to
为 10240K,符合 -Xmn10M
设置的大小。total
显示为 9216K,即 eden + from
的大小,是因为 to
的空间不计算在内。新生代可用的空间只有 eden + from
,to
空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden
中已用 2010K,约占 eden
空间的 24%。
内存地址为 16 位的 16 进制的数字,64 位机器。[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000)
,老年代 [0x00000000ff600000, 0x0000000100000000)
,计算可得空间大小均为 10MB。eden
中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8)
,空间大小为 2058440 byte,约等于 2010K。
显而易见,新生代和老生代是一片完全连续的地址空间。
-public static void main(String[] args) { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
Allocation Failure
,正常情况下,新对象总是分配在 Eden,分配空间失败,eden
的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
。[DefNew: 2013K->721K(9216K), 0.0105099 secs]
,新生代在垃圾回收前后空间的占用变化和耗时。2013K->721K(19456K), 0.0105455 secs
,整个堆在垃圾回收前后空间的占用变化和耗时。
from
的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0)
,空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden
区域存活的对象全部转移到了原 to
空间,from
和 to
空间的角色相互转换(从地址空间的信息可以看到此时 to
的地址指针比 from
的地址指针小)。eden
的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0)
,空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden
区域除了 byte[]
对象外,还存储了其他对象,比如为了创建 List<byte[]>
对象而新加载的类对象。
public static void main(String[] args) { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
由于 eden
区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。eden
区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0)
,空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16
。显然第二次添加时,不再因为创建 List<byte[]>
而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh。
public static void main(String[] args) { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
在第三次添加时,由于 eden
空间不足,因此又发生了第二次垃圾回收。[DefNew: 8565K->512K(9216K), 0.0046378 secs]
,新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
在第二次添加完成后,eden
[0x00000000fec00000, 0x00000000ff3bd8d0)
和 from
[0x00000000ff500000, 0x00000000ff5b45f0)
占用的空间为 8116432 + 738800 = 8855232
约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。8565K->8396K(19456K), 0.0046540 secs
,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。eden
中加入了第三次添加时的对象,大于 512K 不少。
此时 eden
、from
、tenured
中均有不好确认成分的空间占用,比如 from 中多了 56 字节。
public static void main(String[] args) { |
Heap |
在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。
-public static void main(String[] args) { |
waiting... |
当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError
。
public static void main(String[] args) { |
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
当 Thread-0
发生 OutOfMemoryError
后,main
线程仍然正常运行。
当创建的大对象 + 对象头的容量小于等于 eden
,如果 GC 后的存活对象可以放入 to
,那么还是会先在 eden
中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。
public static void main(String[] args) { |
[GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。
-尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。
-]]>public void refresh() throws BeansException, IllegalStateException { |
准备此 context 以供刷新,设置其启动日期和活动标志以及执行属性源的初始化。
---编写管理资源的容器时,可以参考。
-
protected void prepareRefresh() { |
告诉子类刷新内部 bean 工厂并返回,返回的实例类型为 DefaultListableBeanFactory。在这里完成了配置文件的读取,初步注册了 bean 定义。
---我大概这辈子都不会想理清楚这里面关于 XML 文件的解析过程,但是我知道可以在这里观察到 beanFactory 因为配置文件注册了哪些 bean。
-
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { |
告诉子类刷新内部 bean 工厂并返回,返回的实例类型为 DefaultListableBeanFactory。在这里完成了配置文件的读取,初步注册了 bean 定义。
+++我大概这辈子都不会想理清楚这里面关于 XML 文件的解析过程,但是我知道可以在这里观察到 beanFactory 因为配置文件注册了哪些 bean。
+
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { |
配置 BeanFactory 以供在此 context 中使用,例如 context 的类加载器和一些后处理器,手动注册一些单例。
很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。
-本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。
-setnx lock:user1 true |
如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。
- - -对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。
-setnx lock:user1 true |
事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。
- - -本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。
-如果 setnx 和 expire 可以用一个原子指令实现就好了。
- - -在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。
-set lock:user1 true ex 5 nx |
除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。
-if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then |
基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。
- - -如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。
-如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。
-与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。
-如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。
-为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。
-private void scheduleRenewal() { |
但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。
-可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。
-tag = random.nextint() |
但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。
- - -if redis.call("get", KEYS[1]) == ARGV[1] then |
可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。
-Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。
-private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0); |
还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。
-if (redis.call('exists', KEYS[1]) == 0) then |
书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。
-Dockers Compose
安装 Grafana
和 Prometheus
在局域网中配合各类 exporter
为主机和诸多内部服务搭建监控。
-
+ version: "1.0" |
很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。
+本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。
+setnx lock:user1 true |
在通过 nginx
代理 Grafana
后,出现 "Origin not allowed"
报错信息。
如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。
+ -问题的原因参考官方社区:After update to 8.3.5: ‘Origin not allowed’ behind proxy
-在 nginx
配置文件的 proxy
配置上方添加 proxy_set_header Host $http_host
,然后重启 nginx
恢复正常。
location / { |
对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。
+setnx lock:user1 true |
version: "1.0" |
事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。
+ -global: |
本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。
+如果 setnx 和 expire 可以用一个原子指令实现就好了。
+ -node_exporter
被设计为监控主机系统,因为它需要访问主机系统,所以不推荐使用 Docker
容器部署。
node_exporter
二进制可执行文件。chmod u+x node_exporter
./node_exporter
启动Prometheus
配置文件global: |
/etc/systemd/system
在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。
+set lock:user1 true ex 5 nx |
除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。
+if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then |
基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。
+ + +如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。
+如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。
+与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。
+如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。
+为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。
+private void scheduleRenewal() { |
但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。
+可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。
+tag = random.nextint() |
但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。
+ + +if redis.call("get", KEYS[1]) == ARGV[1] then |
可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。
+Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。
+private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0); |
还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。
+if (redis.call('exists', KEYS[1]) == 0) then |
书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。
+Dockers Compose
安装 Grafana
和 Prometheus
在局域网中配合各类 exporter
为主机和诸多内部服务搭建监控。
+
+
+version: "1.0" |
在通过 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 / { |
version: "1.0" |
global: |
node_exporter
被设计为监控主机系统,因为它需要访问主机系统,所以不推荐使用 Docker
容器部署。
node_exporter
二进制可执行文件。chmod u+x node_exporter
./node_exporter
启动Prometheus
配置文件global: |
/etc/systemd/system
node_exporter.service
[Unit] |
每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。
垃圾收集(Garbage Collection,简称 GC)需要考虑什么?
-Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。
+使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。
+ +为什么要去了解垃圾收集和内存分配?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。
在 Java 中,垃圾收集需要关注哪些内存区域?
程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。
哪些对象是还存活着,哪些已经死亡?
--对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?
+根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。
优点:
+代理对象的类和实现的接口:
HelloService.java
+public interface HelloService { |
HelloService.java
+public class HelloServiceImpl implements HelloService { |
缺点:
+public class UserServiceInvocationHandler implements InvocationHandler { |
public class JdkProxyTest { |
do sth. before invocation |
<dependencies> |
public class UserServiceMethodInterceptor implements MethodInterceptor { |
public class CglibTest { |
do sth. before invocation |
使用以下语句,将在工作目录下生成代理类的 Class 文件。
+System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); |
public final class $Proxy0 extends Proxy implements HelloService { |
使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。
+System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class"); |
继承了被代理类。
+public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory { |
static { |
MethodProxy 稍后再做介绍。
+构造器方法内,调用了绑定回调(Callbacks)方法。
+public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() { |
CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。
+final void CGLIB$sayHello$0(String var1) { |
CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。
+如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。
+MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。
+CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0"); |
CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。
+private static class CreateInfo |
MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。
+// invoke 方法的代码相似 |
private void init() |
CGLIB$sayHello$0
方法在生成的 FastClass 中的索引。sayHello
方法在生成的 FastClass 中的索引。invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。
--提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?
+CGLib 完全有能力获得
CGLIB$sayHello$0
的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。
private static class FastClassInfo |
那么可作为 GC Roots 的对象有哪些呢?
固定的 GC Roots,主要是在全局性引用和执行上下文中:
以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511
为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0
方法。
临时性的GC Roots:
除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。
-- +比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
-
当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { |
对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。
怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。
--曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
+
当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?
虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。
-public class ReferenceTest_3 { |
public int getIndex(Signature var1) { |
--在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
-
这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。
--了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。
-
--有趣的知识点,无趣的面试考点。
-
方法区的垃圾收集主要回收两部分:
+两者在使用上是相仿的。
如何判定一个常量是否废弃?
没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。
--“虚拟机中也没有其他地方引用这个字面量”怎么理解?
+借着梳理 Spring 的机会回头再看,又感觉轻松不少。
如何判定一个类型是否可卸载?
+]]>Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。
--条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
-
“无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
Class 对象没有被引用时,会被回收吗?
卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?
分类:
-<dependency> |
public class MathCalculator { |
|
|
public class AopTest { |
beanName.equals("mathCalculator") |
其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。
+mathCalculator 以 getBean 方法为起点,开始创建的过程。
+
|
在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。
+可是,究竟是哪一个后置处理器做的呢?
+protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { |
在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。
+那么,它是什么时候,怎么加入到 beanFactory 中呢?
+PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。
+postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。
+AbstractAutoProxyCreator#postProcessAfterInitialization
就是创建代理对象的入口。public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport |
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { |
AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。
+protected Object createProxy( |
ProxyFactory#getProxy,创建一个 AopProxy 并委托它实现 getProxy。
+++AopProxy 的含义与职责从字面上有点不好理解。
+
public Object getProxy(ClassLoader classLoader) { |
ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。
+protected final synchronized AopProxy createAopProxy() { |
AopProxyFactory#createAopProxy。
+这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。
+public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
AopProxy 视角,获取代理。
+JdkDynamicAopProxy。
+
|
根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)
可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。
|
ObjenesisCglibAopProxy。
+
|
你可能会注意到 Spring 中并没有直接依赖 CGLib,像 Enhancer 所在的包是 org.springframework.cglib.proxy
。根据文档:
++从 spring 3.2 开始,不再需要将 cglib 添加到类路径中,因为 cglib 类在 org.springframework 下重新打包并分布在 spring-core jar 中。 这样做既是为了方便,也是为了避免与使用不同版本 cglib 的其他项目发生潜在冲突。
+
在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。
+Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。
+
|
从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。
+ + +从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。
+ + +注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。
+
|
在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。
+class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { |
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) { |
进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。
+Object bean = resolveBeforeInstantiation(beanName, mbdToUse)
,在实例化前进行解析。Object beanInstance = doCreateBean(beanName, mbdToUse, args)
,创建 Bean 的具体过程。
|
根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。
+protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { |
应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。
+protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) { |
AnnotationAwareAspectJAutoProxyCreator 不仅仅是一个 BeanPostProcessor,它还是一个 InstantiationAwareBeanPostProcessor。
+public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { |
和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。
+ + +++注意:以下方法应注意是否被子类重写。
+
org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip
+protected boolean shouldSkip(Class<?> beanClass, String beanName) { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
+protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
+protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { |
容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。
+org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
+protected List<Advisor> findCandidateAdvisors() { |
org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors
+public List<Advisor> buildAspectJAdvisors() { |
可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache
观察 advisors 的查找情况。
每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。
垃圾收集(Garbage Collection,简称 GC)需要考虑什么?
+为什么要去了解垃圾收集和内存分配?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。
在 Java 中,垃圾收集需要关注哪些内存区域?
程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。
哪些对象是还存活着,哪些已经死亡?
+++对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?
+
优点:
+缺点:
+++提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?
+
那么可作为 GC Roots 的对象有哪些呢?
固定的 GC Roots,主要是在全局性引用和执行上下文中:
临时性的GC Roots:
除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。
++ + +比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
+
当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。
对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。
++曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
+
当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。
虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。
+public class ReferenceTest_3 { |
++在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
+
这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。
++了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。
+
++有趣的知识点,无趣的面试考点。
+
方法区的垃圾收集主要回收两部分:
+如何判定一个常量是否废弃?
没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。
++“虚拟机中也没有其他地方引用这个字面量”怎么理解?
+
如何判定一个类型是否可卸载?
+Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。
++条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
+
“无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
Class 对象没有被引用时,会被回收吗?
卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?
分类:
+分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
@@ -2942,453 +3055,355 @@当 Java
程序启动的时候,Java
虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String)
加载 main
方法所在的类。
public Class<?> loadClass(String name) throws ClassNotFoundException { |
<dependency> |
public class MathCalculator { |
|
|
public class AopTest { |
beanName.equals("mathCalculator") |
其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。
-mathCalculator 以 getBean 方法为起点,开始创建的过程。
-
|
在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。
-可是,究竟是哪一个后置处理器做的呢?
-protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) { |
在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。
-那么,它是什么时候,怎么加入到 beanFactory 中呢?
-PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。
-postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。
-AbstractAutoProxyCreator#postProcessAfterInitialization
就是创建代理对象的入口。public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport |
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { |
根据注释可知,此方法加载具有指定二进制名称的类,它由 Java
虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)
。
protected Class<?> loadClass(String name, boolean resolve) |
AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。
+根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean)
同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:
findLoadedClass(String)
以检查该类是否已加载。loadClass
方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。findClass(String)
方法来查找该类。protected Object createProxy( |
ProxyFactory#getProxy,创建一个 AopProxy 并委托它实现 getProxy。
+如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true
,则此方法将对生成的 Class
对象调用 resolveClass(Class)
方法。鼓励 ClassLoader
的子类重写 findClass(String)
,而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock
方法的结果进行同步。
--AopProxy 的含义与职责从字面上有点不好理解。
+注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的
parent
属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的parent
值为null
时,它的父·类加载器是引导类加载器(bootstrap class loader
),但是当看到findBootstrapClassOrNull
方法时,我有点困惑,因为我以为会看到语义类似于loadClassByBootstrapClassLoader
这样的方法名。从注释和代码的语义上看,bootstrap class loader
不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给parent
属性。findBootstrapClassOrNull
方法的语义更接近于:当一个类加载器的父·类加载器为null
时,将准备加载的目标类先当作启动类(Bootstrap Class
)尝试查找,如果找不到就返回null
。
public Object getProxy(ClassLoader classLoader) { |
ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。
-protected final synchronized AopProxy createAopProxy() { |
需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK
的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader
对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader
对象。
简单地说,如果 ClassLoader
对象注册为具备并行能力,那么一个 name
一个锁对象,已创建的锁对象保存在 ConcurrentHashMap
类型的 parallelLockMap
中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader
对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?
protected Object getClassLoadingLock(String className) { |
AopProxyFactory#createAopProxy。
-这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。
-public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
ClassLoader
对象注册为具有并行能力”呢?AppClassLoader
中有一段 static
代码。事实上 java.lang.ClassLoader#registerAsParallelCapable
是将 ClassLoader
对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader
都需要调用一次该方法。
static { |
AopProxy 视角,获取代理。
-JdkDynamicAopProxy。
-
|
java.lang.ClassLoader#registerAsParallelCapable
方法有一个注解 @CallerSensitive
,这是因为它的代码中调用的 native
方法 sun.reflect.Reflection#getCallerClass()
方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:
Object
类除外)都注册为具有并行能力。static
代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。Java
虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static
代码块,因此父类的 static
代码块总是先于子类的 static
代码块。你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader
均在 static
代码块实现注册,以保证满足以上两个条件。
简单地说就是保存了类加载器所属 Class
的 Set
。
|
根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)
可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。
|
方法 java.lang.ClassLoader.ParallelLoaders#register
。ParallelLoaders
封装了一组具有并行能力的加载器类型。就是持有 ClassLoader
的 Class
实例的集合,并保证添加时加同步锁。
// private 修饰,只有其外部类 ClassLoader 才可以使用 |
ObjenesisCglibAopProxy。
-
|
但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader
提供了三个构造器方法:
private ClassLoader(Void unused, ClassLoader parent) { |
你可能会注意到 Spring 中并没有直接依赖 CGLib,像 Enhancer 所在的包是 org.springframework.cglib.proxy
。根据文档:
ClassLoader
的构造器方法最终都调用 private
修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader)
,又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader
的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。
使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable
为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader
还是 getClassLoadingLock
都是由 protect
修饰,允许子类重写,来自定义并行加载类的能力。
--从 spring 3.2 开始,不再需要将 cglib 添加到类路径中,因为 cglib 类在 org.springframework 下重新打包并分布在 spring-core jar 中。 这样做既是为了方便,也是为了避免与使用不同版本 cglib 的其他项目发生潜在冲突。
+todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。
在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。
-Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。
-
|
加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native
方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?
protected final Class<?> findLoadedClass(String name) { |
从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。
- +正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent
属性的值——另一个类加载器实例。一层一层向上委派直到 parent
为 null
,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader
处理,然后由 bootstrap class loader
首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader
,它的 parent
为 ExtClassLoader
,ExtClassLoader
的 parent
为 null
。
++在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。
+
这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object
类,设计者不希望编写代码的人重新写一个 Object
类并加载到 Java
虚拟机中,但是加载类的本质就是读取字节数据传递给 Java
虚拟机创建一个 Class
实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。
通常流程如下:
+AppClassLoader
调用 loadClass
方法,先委派给 ExtClassLoader
。ExtClassLoader
调用 loadClass
方法,先委派给 bootstrap class loader
。bootstrap class loader
在其设置的类路径中无法找到 BananaTest
类,抛出 ClassNotFoundException
异常。ExtClassLoader
捕获异常,然后自己调用 findClass
方法尝试进行加载。ExtClassLoader
在其设置的类路径中无法找到 BananaTest
类,抛出 ClassNotFoundException
异常。AppClassLoader
捕获异常,然后自己调用 findClass
方法尝试进行加载。注释中提到鼓励重写 findClass
方法而不是 loadClass
,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass
实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass
;其次是因为该方法中涉及并行加载类的机制。
默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass
方法,该方法由子类重写。AppClassLoader
和 ExtClassLoader
都是继承 URLClassLoader
,而 URLClassLoader
重写了 findClass
方法。根据注释可知,该方法会从 URL
搜索路径查找并加载具有指定名称的类。任何引用 Jar
文件的 URL
都会根据需要加载并打开,直到找到该类。
过程如下:
+name
转换为 path
,比如 com.example.BananaTest
转换为 com/example/BananaTest.class
。URL
搜索路径 URLClassPath
和 path
中获取 Resource
,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。URLClassLoader
的私有方法 defineClass
,该方法调用父类 SecureClassLoader
的 defineClass
方法。protected Class<?> findClass(final String name) |
从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。
- +URLClassLoader
拥有一个 URLClassPath
类型的属性 ucp
。由注释可知,URLClassPath
类用于维护一个 URL
的搜索路径,以便从 Jar
文件和目录中加载类和资源。URLClassPath
的核心构造器方法:
public URLClassPath(URL[] urls, |
注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。
-
|
URLClassLoader
调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean)
方法获取指定名称对应的资源。根据注释,该方法会查找 URL
搜索路径上的第一个资源,如果找不到资源,则返回 null
。
显然,这里的 Loader
不是我们前面提到的类加载器。Loader
是 URLClassPath
的内部类,用于表示根据一个基本 URL
创建的资源和类的加载器。也就是说一个基本 URL
对应一个 Loader
。
public Resource getResource(String name, boolean check) { |
在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。
-class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { |
获取下一个 Loader
,其实根据 index
从一个存放已创建 Loader
的 ArrayList
中获取。
private synchronized Loader getNextLoader(int[] cache, int index) { |
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) { |
index
到存放已创建 Loader
的列表中去获取(调用方传入的 index
从 0
开始不断递增直到超过范围)。index
超过范围,说明已有的 Loader
都找不到目标 Resource
,需要到未打开的 URL
中查找。URL
中取出(pop
)一个来创建 Loader
,如果 urls
已经为空,则返回 null
。private synchronized Loader getLoader(int index) { |
进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。
+根据指定的 URL
创建 Loader
,不同类型的 URL
会返回不同具体实现的 Loader
。
Object bean = resolveBeforeInstantiation(beanName, mbdToUse)
,在实例化前进行解析。Object beanInstance = doCreateBean(beanName, mbdToUse, args)
,创建 Bean 的具体过程。URL
不是以 /
结尾,认为是 Jar
文件,则返回 JarLoader
类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
。URL
以 /
结尾,且协议为 file
,则返回 FileLoader
类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
。URL
以 /
结尾,且协议不会 file
,则返回 Loader
类型。
|
private Loader getLoader(final URL url) throws IOException { |
根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。
-protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { |
以 FileLoader
的 getResource
为例,如果文件找到了,就会将文件包装成一个 FileInputStream
,再将 FileInputStream
包装成一个 Resource
返回。
Resource getResource(final String name, boolean check) { |
应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。
-protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) { |
从上文可知,ClassLoader
调用 findClass
方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader
有不同的类路径。
以下是通过 IDEA
启动 Java
程序时的命令,可以看到其中通过 -classpath
指定了应用·类加载器 AppClassLoader
的类路径,该类路径除了包含常规的 JRE
的文件路径外,还额外添加了当前 maven
工程编译生成的 target\classes
目录。
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest |
AnnotationAwareAspectJAutoProxyCreator 不仅仅是一个 BeanPostProcessor,它还是一个 InstantiationAwareBeanPostProcessor。
-public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { |
启动·类加载器 bootstrap class loader
,加载核心类库,即 <JRE_HOME>/lib
目录中的部分类库,如 rt.jar
,只有名字符合要求的 jar
才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath
修改默认的类路径,有三种使用方式:
-Xbootclasspath:
:完全覆盖核心类库的类路径,不常用,除非重写核心类库。-Xbootclasspath/a:
以后缀的方式拼接在原搜索路径后面,常用。-Xbootclasspath/p:
以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。在 IDEA
中编辑启动配置,添加 VM
选项,-Xbootclasspath:C:\Software
,里面没有类文件,启动虚拟机失败,提示:
Error occurred during initialization of VM |
和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。
- +扩展·类加载器 ExtClassLoader
,加载 <JRE_HOME>/lib/ext/
目录中的类库。启动 Java
虚拟机时可以通过选项 -Djava.ext.dirs
修改默认的类路径。显然修改不当同样可能会引起 Java
程序的异常。
应用·类加载器 AppClassLoader
,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH
的值,也可以在启动 Java 虚拟机时通过选项 -classpath
修改。
CLASSPATH
在 Windows
中,多个文件路径使用分号 ;
分隔,而 Linux
中则使用冒号 :
分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。
.;C:\path\to\classes
.:/path/to/classes
事实上,AppClassLoader
最终的类路径,不仅仅包含 -classpath
的值,还会包含 -javaagent
指定的值。
方法 defineClass
,顾名思义,就是定义类,将字节数据转换为 Class
实例。在 ClassLoader
以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name
和字节数据等参数,调用 native
方法获得一个 Class
实例。
以下是定义类时最终可能调用的 native
方法。
private native Class<?> defineClass0(String name, byte[] b, int off, int len, |
--注意:以下方法应注意是否被子类重写。
-
org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip
-protected boolean shouldSkip(Class<?> beanClass, String beanName) { |
其方法参数有:
+name
,目标类的名称。byte[]
或 ByteBuffer
类型的字节数据,off
和 len
只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。ProtectionDomain
,保护域,todo:source
,CodeSource
的位置。defineClass
方法的调用过程,其实就是从 URLClassLoader
开始,一层一层处理后再调用父类的 defineClass
方法,分别经过了 SecureClassLoader
和 ClassLoader
。
此方法是再 URLClassLoader
的 findClass
方法中,获得正确的 Resource
之后调用的,由 private
修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。
private Class<?> defineClass(String name, Resource res) throws IOException { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
-protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) { |
Resource
类提供了 getBytes
方法,此方法以字节数组的形式返回字节数据。
public byte[] getBytes() throws IOException { |
org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
-protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { |
在 getByteBuffer
之后会缓存 InputStream
以便调用 getBytes
时使用,方法由 synchronized
修饰。
private synchronized InputStream cachedInputStream() throws IOException { |
容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。
-org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors
-protected List<Advisor> findCandidateAdvisors() { |
在这个例子中,Resource
的实例是 URLClassPath
中的匿名类 FileLoader
以 Resource
的匿名类的方式创建的。
public InputStream getInputStream() throws IOException |
org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors
-public List<Advisor> buildAspectJAdvisors() { |
URLClassLoader
继承自 SecureClassLoader
,SecureClassLoader
提供并重载了 defineClass
方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[]
类型或者 ByteBuffer
类型)转换为 Class
类型的实例,有一个可选的 CodeSource
类型的参数。
protected final Class<?> defineClass(String name, |
可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache
观察 advisors 的查找情况。
方法中只是简单地将 CodeSource
类型的参数转换成 ProtectionDomain
类型,就调用 ClassLoader
的 defineClass
方法。
private ProtectionDomain getProtectionDomain(CodeSource cs) { |
根据注释可知,此方法会返回给定 CodeSource
对象的权限。此方法由 protect
修饰,AppClassLoader
和 URLClassLoader
都有重写。当前 ClassLoader
是 AppClassLoader
。
AppClassLoader#getPermissions
,添加允许从类路径加载的任何类退出 VM的权限。
protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions
,添加一个读文件或读目录的权限。
protected PermissionCollection getPermissions(CodeSource codesource) |
SecureClassLoader#getPermissions
,延迟设置权限,在创建 ProtectionDomain
时再设置。
protected PermissionCollection getPermissions(CodeSource codesource) |
ProtectionDomain
的相关构造器参数:
CodeSource
PermissionCollection
,如果不为 null
,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。ClassLoader
Principal[]
这样看来,SecureClassLoader
为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain
的映射作为缓存。
抽象类 ClassLoader
中最终用于定义类的 native
方法 define0
,define1
,define2
都是由 private
修饰的,ClassLoader
提供并重载了 defineClass
方法作为使用它们的入口,这些 defineClass
方法都由 protect
final
修饰,这意味着这些方法只能被子类使用,并且不能被重写。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
主要步骤:
+preDefineClass
前置处理defineClassX
postDefineClass
后置处理确定保护域 ProtectionDomain
,并检查:
java.*
类package
)中其余类的签名者相匹配private ProtectionDomain preDefineClass(String name, |
确定 Class
的 CodeSource
位置。
private String defineClassSourceLocation(ProtectionDomain pd) |
这些 native
方法使用了 name
,字节数据,ProtectionDomain
和 source
等参数,像黑盒一样,在虚拟机中定义了一个类。
在定义类后使用 ProtectionDomain
中的 certs
补充 Class
实例的 signer
信息,猜测在 native
方法 defineClassX
方法中,对 ProtectionDomain
做了一些修改。事实上,从代码上看,将 CodeSource
包装为 ProtectionDomain
传入后,除了 defineClassX
方法外,其他地方都是取出 CodeSource
使用。
private void postDefineClass(Class<?> c, ProtectionDomain pd) |
Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。
-使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。
- -Spring
中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring
的具体处理过程,并在最后给出笔者的个人思考。
+
+
+当 Bean A
依赖另一个 Bean B
,Bean B
也依赖了 Bean A
,我们就称之为循环依赖:
Bean A -> Bean B -> Bean A |
首先,我们应该将循环依赖和 “Spring
中的循环依赖问题”分开看待。循环依赖是一个正常的现象,一个 employee 依赖他的 department,department 拥有许多 employee。先实例化 employee 后实例化 department,然后先后为它们设置依赖,这样并不会发生什么问题。
当 Spring
加载所有的 Bean
时,会进行依赖注入处理。Spring
并不是先将所有的 Bean
实例化,再去进行依赖注入,而是实例化一个 Bean
后,立即对它进行依赖注入,为此它会递归地实例化 Bean
的依赖。仔细思考,即使在存在循环依赖问题的时候,以上的过程同样并不会产生什么大问题,在实例化和依赖注入分成两个阶段的情况下,你可以轻而易举地保存和获取已经实例化的 Bean
。唯一的问题是,获取的已经实例化的 Bean
可能尚未初始化完毕(比如它的依赖尚未全部注入),那么你只需要确保它在初始化完毕前不被使用即可。
按照上述思路,你可以使用两个 map
,一个保存已经初始化完毕、可以使用的完成品 Bean
,一个保存尚未初始化完毕、不可以被使用的半成品 Bean
。
--根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。
+在一些资料中,你会看到有人特地强调如果只是解决常规的循环引用问题,那么只需要两个缓存。
代理对象的类和实现的接口:
+ + +但是问题并不总是那么简单,如果实例化和依赖注入不能分为两个阶段,如果 B 依赖的不再是简单的 A 对象,而是 A 的代理,那么上述方案就不再适用了。
+如果 A 的构造器方法需要 B,B 的构造器方法需要 A,那么在 A 的实例化阶段就需要 B 的实例,B 的实例化阶段又需要 A,这就陷入了死循环。虽然我们常说 Spring
解决了循环依赖问题,但实际上,Spring
并没有解决所有情形的循环依赖问题。
HelloService.java
-public interface HelloService { |
HelloService.java
-public class HelloServiceImpl implements HelloService { |
public class UserServiceInvocationHandler implements InvocationHandler { |
public class JdkProxyTest { |
do sth. before invocation |
<dependencies> |
public class UserServiceMethodInterceptor implements MethodInterceptor { |
public class CglibTest { |
do sth. before invocation |
@Lazy
注解告诉 Spring
,延迟 Bean
的初始化。在这时候,被标注的参数注入的不是一个立即创建的实例,而是一个代理对象。prototype
类型的 Bean
发生循环依赖,Spring
会抛出异常,因为每次都创建新的 Bean
必然会导致无限循环。使用以下语句,将在工作目录下生成代理类的 Class 文件。
-System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); |
public final class $Proxy0 extends Proxy implements HelloService { |
使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。
-System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class"); |
Spring
鼎鼎大名的核心功能,除了 IOC
,还有 AOP
。在 AOP
的场景中,Bean A
的完成品不是简单的 A 对象,而是一个 A 的代理。这时候又该如何应对呢?似乎不能再简单地将保存的 A 的实例交给 B,否则 B 持有的就不是最终的 A 的代理。
如果你没有被 Spring
影响思路的话,其实并不难。既然需要 A 的代理,那么在获取 B 依赖的 A 时,直接根据已有的半成品 A 创建代理就好了。
当我们脱离 Spring
的具体方案和代码讨论循环依赖问题,我们会发现解决的思路是简单、清晰和理所当然的。事实上 Spring
的解决方案也是如此,当然其中会有很多值得深思的细节。回顾循环依赖问题的解决思路,你会发现:
Spring
依赖注入时,虽然 Bean B
依赖的 Bean A
尚未初始化完毕,但是已经实例化,可以用来赋值Spring AOP
中,既然 Bean B
依赖的 Bean A
需要是 A 对象的代理,那么就在那时候创建代理,用来赋值即可在开始之前我们先放一张循环引用的处理流程图,用于在后续分析过程中进行对照。
+ +以下是测试用例的代码:
CircularA
public class CircularA { |
CircularB
public class CircularB { |
circular-reference-test.xml
<beans> |
public class CircularReferenceTest { |
继承了被代理类。
-public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory { |
static { |
MethodProxy 稍后再做介绍。
-构造器方法内,调用了绑定回调(Callbacks)方法。
-public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() { |
CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。
-final void CGLIB$sayHello$0(String var1) { |
CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。
-如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。
-MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。
-CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0"); |
CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。
-private static class CreateInfo |
MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。
-// invoke 方法的代码相似 |
调用 doGetBean(circularA)
方法第一次获取:
circularA
(先不看方法内的具体代码,在第一次进入该方法时,必定返回 null
)circularA
protected <T> T doGetBean( |
private void init() |
在真正创建 circularA
之前,会调用 getSingleton(String, ObjectFactory)
再次尝试从缓存中获取(构成双重校验),这个方法内部通过 ObjectFactory
调用创建 Bean
的方法,并且在一前一后分别添加和移除 “Bean
是否正在创建中”的标志。在后续 circularB
获取 circularA
时就是依据该标志判断 circularA
正在创建中。
isSingletonCurrentlyInCreation(beanName) |
CGLIB$sayHello$0
方法在生成的 FastClass 中的索引。sayHello
方法在生成的 FastClass 中的索引。invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。
--CGLib 完全有能力获得
+CGLIB$sayHello$0
的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。这里的“是否正在创建中”,并不是狭义地指一个
Bean
是否已经实例化,而是指一个Bean
是否已经实例化和初始化。circular A
在初始化阶段,去获取circularB
,在circularB
视角中,circular A
仍处于正在创建中。示意图如下。
private static class FastClassInfo |
以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511
为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0
方法。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
createBean
方法被包装在 ObjectFactory
中。创建的工作分为两个部分:
circularA
circularA
circularA
进行依赖注入时:getBean(circularB)
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { |
怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。
---在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?
-
public int getIndex(Signature var1) { |
两者在使用上是相仿的。
+很重要的是,在实例化 circularA
之后,尚未进行初始化工作之前,如果 circularA
满足早期暴露的条件,将会被包装为 ObjectFactory
缓存到 singletonFactory
(三级缓存) 中。
值得注意的是:
circularA
最终不需要早期暴露,那么这个 ObjectFactory
是会被直接抛弃的circularA
需要早期暴露,即它依赖的 circularB
同时依赖它,到时候将调用 getEarlyBeanReference
方法获得 circularA
的早期 Bean
引用。--]]>借着梳理 Spring 的机会回头再看,又感觉轻松不少。
+刚开始看
ObjectFactory
匿名类的用法可能有点不适应,可以多读几次,帮助理解getObject
和getEarlyBeanReference
的语义。
SSH
连接 Github
和免密登录服务器作为备忘笔记,主要在新建虚拟机或重装云服务器系统时使用。
-
-
-打开终端,输入 ls -al ~/.ssh
以查看是否存在现有的 SSH
密钥。
ls -al ~/.ssh |
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) |
检查目录列表以查看是否已经有 SSH
公钥。 默认情况下,GitHub
的一个支持的公钥的文件名是以下之一。
id_rsa.pub
id_ecdsa.pub
id_ed25519.pub
如果没有密钥,就需要生成新的 SSH
密钥;如果已有,跳到上传已有密钥环节。
打开终端,粘贴下面的文本(替换为你的 GitHub
电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH
密钥。
一直 yes
确定选择默认即可。
ssh-keygen -t ed25519 -C "your_email@example.com" |
填充 Bean
属性的 populateBean
方法很复杂,我们只关注对 circularA
的依赖注入将间接地调用 getBean(circularB)
进入获取 circularB
的过程。
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { |
将 SSH
公钥复制到剪贴板,在 Github
上的 Settings -> Access -> SSH and GPG keys -> New SSH key
,粘贴即可。
cat ~/.ssh/id_ed25519.pub |
AbstractBeanFactory#doGetBean(circularB)
获取 circularB
将经过和 circularA
一样的流程,进入 populateBean(circularB)
方法进行依赖注入,进而再次去获取 circularA
。
调用 doGetBean(circularA)
方法第二次获取 circularA
时,仍然先尝试从缓存中获取,这次将从缓存中得到先前创建的 circularA
。
protected <T> T doGetBean( |
git config --global user.name "your_username" |
这个 getSingleton
方法正是在第一次获取 circularA
时我们跳过没有查看的方法。方法中代码的逻辑并不复杂,但是要理解为什么这么做,却需要回过头来反复品味和思考。这里也是经常被拿来说的“三级缓存”问题的核心。
singletonObjects
(一级缓存) 获取 circularA
,不存在circularA
是正在创建中,从 earlySingletonObjects
(二级缓存) 获取,仍然不存在allowEarlyReference
为真,从 singletonFactories
(三级缓存) 获取 ObjectFactory
getObject
间接调用 getEarlyBeanReference
获得早期 Bean
引用protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
请注意这次在调用 getObject
方法时,并不是直接返回 Bean
的实例,而是间接调用 getEarlyBeanReference
方法,顾名思义是获取早期 Bean
引用。处理逻辑是如果存在 SmartInstantiationAwareBeanPostProcessor
,将使用这些后处理器处理以获得早期 Bean
引用。
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
使用现成的密钥,将 ~/.ssh/id_ed25519.pub
的内容追加到服务端的 ~/.ssh/authorized_keys
中,使用 ssh user@host
成功免密登录。这样一来,远程连接服务器或者使用 VScode Remote Explorer
时,不用每次输入密码了。
在更新 VMware Workstation 17 Pro
后,发现虚拟机的 IP
从 192.168.46.135
重置 为 192.168.46.128
,即使更新配置文件 C:\Users\moralok\.ssh\config
中的 IP
VScode Remote Explorer
仍然无法连接,但是通过 Xshell
使用账号密码可以登录。查看报错信息发现 known_hosts
中 192.168.46.128
对应的 ECDSA key
有问题,应该记录的是之前占用该 IP
的虚拟机的 ECDSA key
,删除该行后重新连接成功。
[18:09:59.973] > @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
++关于创建代理的分析请参考Spring AOP 如何创建代理 beans。
+
通过后处理器的 getEarlyBeanReference
方法获取早期 Bean
引用时,可能返回的就是 circularA
对象,但是如果 circularA
需要创建代理,就会在这时候为它创建代理,而在之后 BeanPostProcessor
处理时就不会再创建代理了。
以 AbstractAutoProxyCreator
为例,它是自动代理创建者的抽象类,同时实现了 SmartInstantiationAwareBeanPostProcessor
和 BeanPostProcessor
接口。
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { |
如果已经在获取 circularA
的早期引用时就将其包装为代理,则不再创建代理。
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
使用 SSH
协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github
还可以使用 SSH
密钥对提交进行签名。
SSH
的使用(非对称加密)需要生成公钥 public key
和私钥 private key
。常用的算法有 rsa
、ecdsa
和 ed25519
,相对应的公钥默认文件名即 id_XXX.pub
。ed25519
的安全性介于 rsa 2048
和 rsa 4096
之间,但性能却提升数十倍。
在生成密钥时,会要求你 Enter passphrase (empty for no passphrase):
,可以输入一个口令保护私钥的使用。不为空的情况下,正常使用是需要输入这个口令的,很多人认为麻烦,因此留空。
公钥的权限必须是 644
,私钥的权限必须是 600
,否则 SSH
认为其不可靠。
私钥是要安全保管在客户端不能泄露的,公钥则要提供给远程服务器或服务。服务端的 ~/.ssh/authorized_keys
里面存储着可以登录的客户端的公钥。我们将公钥粘贴到 Github
的过程就是对应于此。
ssh-keygen -t rsa -b 4096 -f my_id -C "email@example.com" |
著名的“三级缓存”,实际上就是三个存放 Bean
的 map
:
singletonObjects
earlySingletonObjects
singletonFactories
在很多网上的资料中,都称 Spring
通过使用三级缓存的设计解决了循环引用问题。同时我也看到有人反思,这样翻译对学习者造成了很大的困扰,代码中并没有多级 cache
的意味,称之为“三个缓存”比“三级缓存”更合理也更容易理解。三个存放 Bean
的 map
事实上是相互独立的,甚至它们是互斥的,一个 Bean 在同一时间最多只能存在于其中一个 map
中。
对我个人而言,我对反对者的观点深有同感,如果我没有看过面经,即使我熟读并理解代码,我可能都无法回答 Spring 中的三级缓存是什么。甚至我会被三级缓存这个名词所震慑,在了解它之前在心里放大它的复杂性。
但是在不断阅读的过程中(可能也有已有记忆的加持),我也会感受到称之为“三级缓存”的合理性。这里的分级含义更多体现的是 Bean 的“晋升”过程。
网上很多资料在讨论 Bean
在缓存中的添加和删除时,大多一笔带过,并没有谈到细节。但是 Bean
并不是在这三个缓存中依次晋级,甚至有时候,添加和移除的都不是一个对象,比如三级缓存中的 ObjectFactory
可能被直接抛弃。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) |
-t
表示算法,如 rsa
。-b
表示 rsa
密钥长度,默认 2048 bit
,ed25519
不需要指定。-f
表示文件名。-C
表示在公钥文件中添加注释,可修改ObjectFactory
,但是也是没有用到的。Bean
直接保存到一级缓存中。Bean
仍然是同一个对象,Bean
仍然是直接保存到一级缓存,再删掉二级缓存。Client
将自己的公钥存放到服务端,追加到 authorized_keys
文件。Server
收到 Client
的连接请求后,会在 authorized_keys
文件中匹配到 Client
传过来的公钥,并生成随机数 R
,用公钥对随机数加密得到 pubKey(R)
。Client
收到后通过私钥解密得到随机数 R
,然后对随机数 R
和本次会话的 sessionKey
使用 MD5
生成摘要 Digest1
,发送给服务端。Server
会对随机数 R
和会话的 sessionKey
同样使用 MD5
生成摘要 Digest2
,对比相同即完成认证过程。SSH
通过口令确认避免中间人攻击,如果用户第一次登录 Server
,系统会提示:
ssh -T git@github.com |
Server
需要在其网站上公示其公钥的指纹,Github
的公钥指纹在这里。
确认匹配后,客户端会在 ~/.ssh/known_hosts
中记录,下次登录不再警告。
使用 SSH 进行连接 Github
Git 多台电脑共用SSH Key
SSH协议登录过程详解
GitHub 的 SSH 密钥指纹
使用 Ed25519 算法生成你的 SSH 密钥
网上有很多资料在分析为什么需要三个缓存,才能解决在需要创建代理的情况下发生的循环依赖问题。但是个人觉得有些分析缺乏逻辑,也有点违和感。将当前的解决方案套到只有两个缓存的情况下去分析不太合理,就像你把四轮机动车卸掉一个轮子,说机动车必须要四个轮子才可以,不然不平衡,事实上三个轮子的机动车设计是存在且可行的。
+在分析两个缓存如何解决在需要创建代理的情况下发生的循环依赖问题时,应该抛开现有的处理逻辑,回归本质问题:既然 circularA
需要创建代理,如果 circularA
依赖的 circularB
也依赖了 circular A
,在为它获取依赖 circularA
时立即创建代理即可。
一个 map
必须用于存放完成品,另一个 map
用于存放半成品。创建的代理作为升级版的半成品,完全可以覆盖原始的半成品继续存放在第二个 map
中。为了避免重复创建代理,只要能够标识半成品是已经经过代理包装的即可。BeanDefinition
、Bean
自身、创建代理的地方,都有能力实现标识一个 Bean
的半成品是否经过包装,最不济使用一个 map
存放标识(但是这也就等同于使用三个 map
了)。你甚至可以将半成品 circularA
直接尝试包装成代理再存放入半成品 map
中,这个方案本质上是将创建代理的步骤从初始化 Bean
中分离到初始化 Bean
之前。
综上,使用两个 map
解决在技术上是没有问题的,很多分析中考虑的问题相当于把 Spring
现有的处理逻辑当成枷锁限制了自己。既然你都在问不这么打地基可不可以,我难道不得考虑挪一挪上面的砖墙吗?当然我不能保证这么设计不会破坏 Spring
现有全部功能的兼容性和扩展性,但是这并不是代理为循环依赖引入的问题。
SPI
作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI
被广泛应用于各类技术框架中,例如 JDBC
驱动、Spring
和 Dubbo
等等。Dubbo
并未使用原生的 Java SPI
,而是重新实现了一套更加强大的 Dubbo SPI
。本文将简单介绍 SPI
的设计理念,通过示例带你体会 SPI
的作用,通过 Dubbo
获取拓展的流程图和源码分析带你理解 Dubbo SPI
的工作原理。深入了解 Dubbo SPI
,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。
+ Cloudflare Tunnel
访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare
已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare
统计中缺少的新,决定使用 Nginx
反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts
文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port
的形式去访问。
-SPI
的全称是 Service Provider Interface
,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI
机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI
具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 Java
的 JDBC
驱动为例,JDK 提供了 java.sql.Driver
接口,各个数据库厂商,例如 MySQL
、Oracle
提供具体的实现。
+++
Cloudflare Tunnel
相较于Zerotier
和OpenVPN
,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。
客户端发起请求,请求经 Cloudflare
转发到局域网中的 Tunnel
。原先,Tunnel
如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx
,由 Nginx
反向代理,发向目标服务。
目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:
-META-INF/services/full.qualified.interface.name
META-INF/dubbo/full.qualified.interface.name
(还有其他目录可供选择)META-INF/spring.factories
定义一个接口 Animal
。
public interface Animal { |
Nginx
和 Tunnel
还有其他内部服务应处于同一个网络中。
version: "1.0" |
定义两个实现类 Dog
和 Cat
。
public class Dog implements Animal { |
在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel
的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx
服务。
user nginx; |
在 META-INF/services
文件夹下创建一个文件,名称为 Animal
的全限定名 com.moralok.dubbo.spi.test.Animal
,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。
com.moralok.dubbo.spi.test.Dog |
进行测试。
-public class JavaSPITest { |
测试结果
-Java SPI |
Dubbo
并未使用原生的 Java SPI
,而是重新实现了一套功能更加强大的 SPI
机制。Dubbo SPI
的配置文件放在 META-INF/dubbo
文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI
注解。
dog = com.moralok.dubbo.spi.test.Dog |
进行测试。
-public class DubboSPITest { |
测试结果
-Dubbo SPI |
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64); |
这个方法包含了如下步骤:
-EXTENSION_LOADERS
中获取与拓展类对应的 ExtensionLoader
,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。--“从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的
-getOrCreate
的处理模式在Dubbo
的源码中经常出现。
EXTENSION_LOADERS
是 ExtensionLoader
的静态变量,保存了“拓展类->ExtensionLoader
”的映射关系。
public T getExtension(String name) { |
这个方法中获取 Holder
和获取拓展实例都是使用 getOrCreate
的模式。
Holder
用于持有拓展实例。cachedInstances
是 ExtensionLoader
的成员变量,保存了“name->Holder
(拓展实例)”的映射关系。
private T createExtension(String name, boolean wrap) { |
这个方法包含如下步骤:
-getExtensionClasses
获取所有拓展类Wrapper
对象中第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOC
和 AOP
的具体实现。
最后拓展实例的结构如下图。
- - -private Map<String, Class<?>> getExtensionClasses() { |
代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/
、META-INF/dubbo/
、META-INF/services/
三个目录下的配置文件,获取拓展类。
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); |
新版本使用原生的 Java SPI
加载 LoadingStrategy
,允许用户自定义加载策略。
DubboInternalLoadingStrategy
,目录 META-INF/dubbo/internal/
,优先级最高DubboLoadingStrategy
,目录 META-INF/dubbo/
,优先级普通ServicesLoadingStrategy
,目录 META-INF/services/
,优先级最低private static volatile LoadingStrategy[] strategies = loadLoadingStrategies(); |
LoadingStrategy
的 Java SPI
配置文件
loadDirectory
方法先通过 classLoader
获取所有的资源链接,然后再通过 loadResource
方法加载资源。
新版本中 extensionLoaderClassLoaderFirst
可以设置是否优先使用 ExtensionLoader's ClassLoader
获取资源链接。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, |
loadResource
方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass
方法进行其他操作。loadClass
方法用于操作缓存。
新版本中 excludedPackages
可以设置将指定包内的类都排除。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, |
loadClass
方法设置了多个缓存,比如 cachedAdaptiveClass
、cachedWrapperClasses
、cachedNames
和 cachedClasses
。
新版本中 overridden
可以设置是否覆盖 cachedAdaptiveClass
、cachedClasses
的 name->clazz
。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, |
Dubbo IOC
是通过 setter
方法注入依赖。Dubbo
首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter
方法特征并满足条件,若有,则通过 objectFactory
获取依赖对象,最后通过反射调用 setter
方法将依赖设置到目标对象中。
--与
-Spring IOC
相比,Dubbo IOC
实现的依赖注入功能更加简单,代码也更加容易理解。
private T injectExtension(T instance) { |
objectFactory
是 ExtensionFactory
的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader
,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理。
public <T> T getExtension(Class<T> type, String name) { |
Cloudflare Tunnel
访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare
已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare
统计中缺少的新,决定使用 Nginx
反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts
文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port
的形式去访问。
-
-
----
Cloudflare Tunnel
相较于Zerotier
和OpenVPN
,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。
客户端发起请求,请求经 Cloudflare
转发到局域网中的 Tunnel
。原先,Tunnel
如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx
,由 Nginx
反向代理,发向目标服务。
Nginx
和 Tunnel
还有其他内部服务应处于同一个网络中。
version: "1.0" |
在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel
的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx
服务。
user nginx; |
本目录下,配置 server 块。
-server { |
本目录下,配置 server 块。
+server { |
代理(Proxy)也称为网络代理,是一种特殊的网络服务,允许一个终端通过这个服务与另一个终端进行非直接的连接。一般认为代理服务有利于保障网络终端的隐私或安全,在一定程度上能够阻止网络攻击。
@@ -3445,138 +3460,6 @@Spring
中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring
的具体处理过程,并在最后给出笔者的个人思考。
-
-
-当 Bean A
依赖另一个 Bean B
,Bean B
也依赖了 Bean A
,我们就称之为循环依赖:
Bean A -> Bean B -> Bean A |
首先,我们应该将循环依赖和 “Spring
中的循环依赖问题”分开看待。循环依赖是一个正常的现象,一个 employee 依赖他的 department,department 拥有许多 employee。先实例化 employee 后实例化 department,然后先后为它们设置依赖,这样并不会发生什么问题。
当 Spring
加载所有的 Bean
时,会进行依赖注入处理。Spring
并不是先将所有的 Bean
实例化,再去进行依赖注入,而是实例化一个 Bean
后,立即对它进行依赖注入,为此它会递归地实例化 Bean
的依赖。仔细思考,即使在存在循环依赖问题的时候,以上的过程同样并不会产生什么大问题,在实例化和依赖注入分成两个阶段的情况下,你可以轻而易举地保存和获取已经实例化的 Bean
。唯一的问题是,获取的已经实例化的 Bean
可能尚未初始化完毕(比如它的依赖尚未全部注入),那么你只需要确保它在初始化完毕前不被使用即可。
按照上述思路,你可以使用两个 map
,一个保存已经初始化完毕、可以使用的完成品 Bean
,一个保存尚未初始化完毕、不可以被使用的半成品 Bean
。
-- - -在一些资料中,你会看到有人特地强调如果只是解决常规的循环引用问题,那么只需要两个缓存。
-
但是问题并不总是那么简单,如果实例化和依赖注入不能分为两个阶段,如果 B 依赖的不再是简单的 A 对象,而是 A 的代理,那么上述方案就不再适用了。
-如果 A 的构造器方法需要 B,B 的构造器方法需要 A,那么在 A 的实例化阶段就需要 B 的实例,B 的实例化阶段又需要 A,这就陷入了死循环。虽然我们常说 Spring
解决了循环依赖问题,但实际上,Spring
并没有解决所有情形的循环依赖问题。
@Lazy
注解告诉 Spring
,延迟 Bean
的初始化。在这时候,被标注的参数注入的不是一个立即创建的实例,而是一个代理对象。prototype
类型的 Bean
发生循环依赖,Spring
会抛出异常,因为每次都创建新的 Bean
必然会导致无限循环。Spring
鼎鼎大名的核心功能,除了 IOC
,还有 AOP
。在 AOP
的场景中,Bean A
的完成品不是简单的 A 对象,而是一个 A 的代理。这时候又该如何应对呢?似乎不能再简单地将保存的 A 的实例交给 B,否则 B 持有的就不是最终的 A 的代理。
如果你没有被 Spring
影响思路的话,其实并不难。既然需要 A 的代理,那么在获取 B 依赖的 A 时,直接根据已有的半成品 A 创建代理就好了。
当我们脱离 Spring
的具体方案和代码讨论循环依赖问题,我们会发现解决的思路是简单、清晰和理所当然的。事实上 Spring
的解决方案也是如此,当然其中会有很多值得深思的细节。回顾循环依赖问题的解决思路,你会发现:
Spring
依赖注入时,虽然 Bean B
依赖的 Bean A
尚未初始化完毕,但是已经实例化,可以用来赋值Spring AOP
中,既然 Bean B
依赖的 Bean A
需要是 A 对象的代理,那么就在那时候创建代理,用来赋值即可在开始之前我们先放一张循环引用的处理流程图,用于在后续分析过程中进行对照。
- - -以下是测试用例的代码:
-CircularA
public class CircularA { |
CircularB
public class CircularB { |
circular-reference-test.xml
<beans> |
public class CircularReferenceTest { |
调用 doGetBean(circularA)
方法第一次获取:
circularA
(先不看方法内的具体代码,在第一次进入该方法时,必定返回 null
)circularA
protected <T> T doGetBean( |
在真正创建 circularA
之前,会调用 getSingleton(String, ObjectFactory)
再次尝试从缓存中获取(构成双重校验),这个方法内部通过 ObjectFactory
调用创建 Bean
的方法,并且在一前一后分别添加和移除 “Bean
是否正在创建中”的标志。在后续 circularB
获取 circularA
时就是依据该标志判断 circularA
正在创建中。
isSingletonCurrentlyInCreation(beanName) |
-- - -这里的“是否正在创建中”,并不是狭义地指一个
-Bean
是否已经实例化,而是指一个Bean
是否已经实例化和初始化。circular A
在初始化阶段,去获取circularB
,在circularB
视角中,circular A
仍处于正在创建中。示意图如下。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
createBean
方法被包装在 ObjectFactory
中。创建的工作分为两个部分:
circularA
circularA
circularA
进行依赖注入时:getBean(circularB)
很重要的是,在实例化 circularA
之后,尚未进行初始化工作之前,如果 circularA
满足早期暴露的条件,将会被包装为 ObjectFactory
缓存到 singletonFactory
(三级缓存) 中。
值得注意的是:
-circularA
最终不需要早期暴露,那么这个 ObjectFactory
是会被直接抛弃的circularA
需要早期暴露,即它依赖的 circularB
同时依赖它,到时候将调用 getEarlyBeanReference
方法获得 circularA
的早期 Bean
引用。--刚开始看
-ObjectFactory
匿名类的用法可能有点不适应,可以多读几次,帮助理解getObject
和getEarlyBeanReference
的语义。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) |
填充 Bean
属性的 populateBean
方法很复杂,我们只关注对 circularA
的依赖注入将间接地调用 getBean(circularB)
进入获取 circularB
的过程。
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { |
AbstractBeanFactory#doGetBean(circularB)
获取 circularB
将经过和 circularA
一样的流程,进入 populateBean(circularB)
方法进行依赖注入,进而再次去获取 circularA
。
调用 doGetBean(circularA)
方法第二次获取 circularA
时,仍然先尝试从缓存中获取,这次将从缓存中得到先前创建的 circularA
。
protected <T> T doGetBean( |
这个 getSingleton
方法正是在第一次获取 circularA
时我们跳过没有查看的方法。方法中代码的逻辑并不复杂,但是要理解为什么这么做,却需要回过头来反复品味和思考。这里也是经常被拿来说的“三级缓存”问题的核心。
singletonObjects
(一级缓存) 获取 circularA
,不存在circularA
是正在创建中,从 earlySingletonObjects
(二级缓存) 获取,仍然不存在allowEarlyReference
为真,从 singletonFactories
(三级缓存) 获取 ObjectFactory
getObject
间接调用 getEarlyBeanReference
获得早期 Bean
引用protected Object getSingleton(String beanName, boolean allowEarlyReference) { |
请注意这次在调用 getObject
方法时,并不是直接返回 Bean
的实例,而是间接调用 getEarlyBeanReference
方法,顾名思义是获取早期 Bean
引用。处理逻辑是如果存在 SmartInstantiationAwareBeanPostProcessor
,将使用这些后处理器处理以获得早期 Bean
引用。
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
--关于创建代理的分析请参考Spring AOP 如何创建代理 beans。
-
通过后处理器的 getEarlyBeanReference
方法获取早期 Bean
引用时,可能返回的就是 circularA
对象,但是如果 circularA
需要创建代理,就会在这时候为它创建代理,而在之后 BeanPostProcessor
处理时就不会再创建代理了。
以 AbstractAutoProxyCreator
为例,它是自动代理创建者的抽象类,同时实现了 SmartInstantiationAwareBeanPostProcessor
和 BeanPostProcessor
接口。
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { |
如果已经在获取 circularA
的早期引用时就将其包装为代理,则不再创建代理。
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
著名的“三级缓存”,实际上就是三个存放 Bean
的 map
:
singletonObjects
earlySingletonObjects
singletonFactories
在很多网上的资料中,都称 Spring
通过使用三级缓存的设计解决了循环引用问题。同时我也看到有人反思,这样翻译对学习者造成了很大的困扰,代码中并没有多级 cache
的意味,称之为“三个缓存”比“三级缓存”更合理也更容易理解。三个存放 Bean
的 map
事实上是相互独立的,甚至它们是互斥的,一个 Bean 在同一时间最多只能存在于其中一个 map
中。
对我个人而言,我对反对者的观点深有同感,如果我没有看过面经,即使我熟读并理解代码,我可能都无法回答 Spring 中的三级缓存是什么。甚至我会被三级缓存这个名词所震慑,在了解它之前在心里放大它的复杂性。
但是在不断阅读的过程中(可能也有已有记忆的加持),我也会感受到称之为“三级缓存”的合理性。这里的分级含义更多体现的是 Bean 的“晋升”过程。
网上很多资料在讨论 Bean
在缓存中的添加和删除时,大多一笔带过,并没有谈到细节。但是 Bean
并不是在这三个缓存中依次晋级,甚至有时候,添加和移除的都不是一个对象,比如三级缓存中的 ObjectFactory
可能被直接抛弃。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) |
ObjectFactory
,但是也是没有用到的。Bean
直接保存到一级缓存中。Bean
仍然是同一个对象,Bean
仍然是直接保存到一级缓存,再删掉二级缓存。网上有很多资料在分析为什么需要三个缓存,才能解决在需要创建代理的情况下发生的循环依赖问题。但是个人觉得有些分析缺乏逻辑,也有点违和感。将当前的解决方案套到只有两个缓存的情况下去分析不太合理,就像你把四轮机动车卸掉一个轮子,说机动车必须要四个轮子才可以,不然不平衡,事实上三个轮子的机动车设计是存在且可行的。
-在分析两个缓存如何解决在需要创建代理的情况下发生的循环依赖问题时,应该抛开现有的处理逻辑,回归本质问题:既然 circularA
需要创建代理,如果 circularA
依赖的 circularB
也依赖了 circular A
,在为它获取依赖 circularA
时立即创建代理即可。
一个 map
必须用于存放完成品,另一个 map
用于存放半成品。创建的代理作为升级版的半成品,完全可以覆盖原始的半成品继续存放在第二个 map
中。为了避免重复创建代理,只要能够标识半成品是已经经过代理包装的即可。BeanDefinition
、Bean
自身、创建代理的地方,都有能力实现标识一个 Bean
的半成品是否经过包装,最不济使用一个 map
存放标识(但是这也就等同于使用三个 map
了)。你甚至可以将半成品 circularA
直接尝试包装成代理再存放入半成品 map
中,这个方案本质上是将创建代理的步骤从初始化 Bean
中分离到初始化 Bean
之前。
综上,使用两个 map
解决在技术上是没有问题的,很多分析中考虑的问题相当于把 Spring
现有的处理逻辑当成枷锁限制了自己。既然你都在问不这么打地基可不可以,我难道不得考虑挪一挪上面的砖墙吗?当然我不能保证这么设计不会破坏 Spring
现有全部功能的兼容性和扩展性,但是这并不是代理为循环依赖引入的问题。
Configuration
注解是 Spring
中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean
注解将创建的 bean
交给 Spring
容器管理。神奇的是,被 Bean
注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug
时注意过配置类的实例,你会发现配置类的 Class
名称中携带 EnhancerBySpringCGLIB
。本文将从源码角度,分析 Configuration
注解是如何工作的。
+ Dubbo SPI
自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo
的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI
自适应拓展的相关源码和工作原理。
-
|
|
通常情况下,我们称被 Configuration
注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。
++站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。
+
在 Dubbo
中,很多拓展都是通过 SPI
机制动态加载的,比如 Protocol
、Cluster
和 LoadBalance
等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。
定义一个接口 Animal
。
public interface Animal { |
定义两个实现类 Dog
和 Cat
。
public class Dog implements Animal { |
在运行时根据参数动态地加载拓展。
+public void bark(String type) { |
是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:
+Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type); |
这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter)
这样重复的代码;引入了 ExtensionLoader
这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo
和 动物 Animal
举例。
在非动态加载情况下,我们可能会这样写:
+public class Zoo { |
在动态加载情况下,我们可能会这样写。在这种情况下,Zoo
没有合适的方式直接持有 Animal
,而是通过 ExtensionLoader
间接地持有。
public class Zoo { |
我们更想要以下这种直接持有 Animal
的方式,在运行时 animal
可以是 Dog
,也可以是 Cat
,还可以是其他的动物。
public class Zoo { |
Dubbo
采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal
的自适应拓展,就像下面这样:
public class AdaptiveAnimal implements Animal { |
当然,我们不希望需要手动地为每一个拓展编写 Adaptive
代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark
方法的参数列表中新增了 type
类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo
的另一个设计规避了这一缺点,Dubbo
在公共契约中提到:所有扩展点参数都包含 URL
参数,URL
作为上下文信息贯穿整个扩展点设计体系。因此围绕着 Dubbo
以 URL
为中心的拓展体系,你很难设计出 Animal.bark(URL url)
这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo
并未完全抛弃手工编写自适应拓展的方式,而是予以保留。
在在 Dubbo
中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL
作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory
的自适应拓展,当你调用它的 getExtension
方法时,它就是将工作全权委托给 factory.getExtension(type, name)
完成的,而 factories
在创建 AdaptiveExtensionFactory
时就已经获取了。
|
至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。
+Adaptive
注解是一个与自适应拓展息息相关的注解,该定义如下:
|
根据 Target
注解的 value
可知,Adaptive
注解可标注在类或者方法上。当 Adaptive
注解标注在类上时,Dubbo
不会为该类生成代理类。当 Adaptive
注解标注在接口方法上时,Dubbo
则会为该方法生成代理逻辑。Adaptive
注解在类上的情况很少,在 Dubbo
中,仅有两个类被 Adaptive
注解标注,分别是 AdaptiveCompiler
和 AdaptiveExtensionFactory
。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive
注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。
获取自适应拓展的入口方法是 getAdaptiveExtension
,使用 getOrCreate
的模式获取。
public T getAdaptiveExtension() { |
当缓存为空时,就会通过 createAdaptiveExtension
方法创建。方法包含以下三个处理逻辑:
getAdaptiveExtensionClass
方法获取自适应拓展的 Class
对象。injectExtension
方法对拓展实例进行依赖注入。++手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展。
+
private T createAdaptiveExtension() { |
获取自适应拓展类的 getAdaptiveExtensionClass
方法包含以下三个处理逻辑:
getExtensionClasses
方法获取所有拓展类。cachedAdaptiveClass
,如果不为 null
,则返回缓存。null
,则调用 createAdaptiveExtensionClass
创建自适应拓展类(代理类)。在Dubbo SPI 的工作原理中我们分析过 getExtensionClasses
方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive
注解标注了,那么该类就会被赋值给 cachedAdaptiveClass
变量。“原理”部分介绍的 AdaptiveExtensionFactory
就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive
注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。
private Class<?> getAdaptiveExtensionClass() { |
++到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用
+getOrCreate
的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。
生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。
+Class
对象。++在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。
+
private Class<?> createAdaptiveExtensionClass() { |
为了更直观地了解代码生成的效果及其实现的功能,以 Protocol
为例,生成的完整代码(已经经过格式化)展示如下。
package org.apache.dubbo.rpc; |
生成的代理类需完成以下功能:
+adaptive
方法,直接抛出异常。adaptive
方法:URL
对象,结合 URL
对象和默认拓展名得到最终的拓展名 extName
。ExtensionLoader
,再根据拓展名 extName
获取拓展,最后调用拓展的同名方法。以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo
会直接或间接地从参数列表中查找 URL
对象,所谓直接就是 URL
对象直接在参数列表中,所谓间接就是 URL
对象是其中一个参数的属性。在得到 URL
对象后,Dubbo
会尝试以 Adaptive
注解的 value
为 key
,从 URL
中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName
。实际的实现更加复杂,需要耐心阅读和测试。
新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 type
和 defaultExtName
,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。
public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) { |
在生成代理类源码之前,generate
方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive
注解,若不满足,就会抛出异常。
++流式编程使用得当的话很有可读性啊。
+
private boolean hasAdaptiveMethod() { |
生成代理类源码的顺序和普通 Java
类文件中内容的顺序一致:
先忽略“生成方法”的部分,以 Dubbo
的 Protocol
拓展为例,生成的代码如下:
package org.apache.dubbo.rpc; |
生成方法的过程同样被抽象为几个命名含义清晰的方法,包含以下五个部分:
+private String generateMethod(Method method) { |
除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive
注解标注。
private String generateMethodContent(Method method) { |
对于无 Adaptive
注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。
private String generateUnsupported(Method method) { |
以 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 注解标注的方法,
+// 查找 URL 类型的参数 |
直接从方法的参数类型列表中查找第一个 URL
类型的参数,返回其索引。
private int getUrlTypeIndex(Method method) { |
间接从方法的参数类型列表中,查找 URL
类型的参数,并生成判空检查和赋值代码。
private String generateUrlAssignmentIndirectly(Method method) { |
以 Protocol
的 refer
和 export
方法为例,生成的内容如下:
// refer |
private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) { |
检测是否有 Invocation
类型的参数,并生成判空检查代码和赋值代码。从 Invocation
可以获得 methodName
。
private boolean hasInvocationArgument(Method method) { |
以 LoadBalance
的 select
方法为例,生成的内容如下:
if (arg2 == null) throw new IllegalArgumentException("invocation == null"); |
本方法用于根据 SPI
和 Adaptive
注解的 value
生成“获取拓展名”的代码,同时生成逻辑还受 Invocation
影响,因此相对复杂。总结的规则如下:
private String generateExtNameAssignment(String[] value, boolean hasInvocation) { |
private String generateExtensionAssignment() { |
以 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) { |
以 Protocol
接口的 refer
方法为例,生成的内容如下:
return extension.refer(arg0, arg1); |
++新版本通过提炼方法、使用流式编程和使用
+String.format()
代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。
Configuration
注解是 Spring
中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean
注解将创建的 bean
交给 Spring
容器管理。神奇的是,被 Bean
注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug
时注意过配置类的实例,你会发现配置类的 Class
名称中携带 EnhancerBySpringCGLIB
。本文将从源码角度,分析 Configuration
注解是如何工作的。
+
+
+
|
|
通常情况下,我们称被 Configuration
注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。
ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class); |
本文不详细介绍配置类本身如何注册到 BeanFactory
中。当 BeanConfig
被传递给 AnnotationConfigApplicationContext
,自身会先被解析为 BeanDefinition
注册到 beanFactory
中。有两点需要注意:
starter
联系在一起,本文将介绍如何创建一个自定义的 starter
并从源码角度分析 Spring Boot
自动配置的工作原理。
+ SPI
作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI
被广泛应用于各类技术框架中,例如 JDBC
驱动、Spring
和 Dubbo
等等。Dubbo
并未使用原生的 Java SPI
,而是重新实现了一套更加强大的 Dubbo SPI
。本文将简单介绍 SPI
的设计理念,通过示例带你体会 SPI
的作用,通过 Dubbo
获取拓展的流程图和源码分析带你理解 Dubbo SPI
的工作原理。深入了解 Dubbo SPI
,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。
-一个 library 的完整 Spring Boot starter
可能包含以下组件:
starter
之后应该足以开始使用这个 library。--如果你不需要将自动配置的代码和依赖项管理分开,你可以将它们合并到一个模块中。
-
spring-boot
开头命名模块,即使你使用的是不同的 Maven groupId,因为 Spring
可能在将来提供官方的自动配置支持。自定义 starter
约定俗成的命名方式是 xxx-spring-boot-starter
。starter
提供了配置属性的定义,请选择适当的命名空间,避免使用 Spring Boot
的命名空间,否则他们未来的修改可能破坏你的配置。以下将通过一款基于 Redis
实现的分布式锁 redis-lock 的 starter
介绍如何创建一个自定义的 Spring Boot starter
。注意:实际上项目中的的 redis-lock-spring-boot-starter
合并了自动配置模块和启动模块。
自动配置模块包含开始使用 library 所需要的一切配置。它还可能包含配置键定义(@ConfigurationProperties
)和任何其他可用于进一步自定义组件初始化方式的回调接口。
按照惯例,模块命名为 redis-lock-spring-boot-autoconfigure
。
自动配置模块需要添加以下依赖。
-<dependency> |
SPI
的全称是 Service Provider Interface
,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI
机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI
具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 Java
的 JDBC
驱动为例,JDK 提供了 java.sql.Driver
接口,各个数据库厂商,例如 MySQL
、Oracle
提供具体的实现。
和平常在 Spring
中使用一个 library 时一样,创建配置类并配置好使用它所需要的 Bean
。
目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:
Configuration
注解,标识为配置类Bean
注解,配置所需要的 Bean
EnableConfigurationProperties
注解,启用配置属性(可选)META-INF/services/full.qualified.interface.name
META-INF/dubbo/full.qualified.interface.name
(还有其他目录可供选择)META-INF/spring.factories
|
定义一个接口 Animal
。
public interface Animal { |
你可能需要定义一些配置属性来设置使用 library 所需要的属性。
-
|
定义两个实现类 Dog
和 Cat
。
public class Dog implements Animal { |
在 src/main/resources/META-INF
目录中添加一个 spring.factories
文件,文件内容如下。键为 EnableAutoConfiguration
的全限定名,值为配置类的全限定名,如果需要配置多个配置类,可以用逗号分隔。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
在 META-INF/services
文件夹下创建一个文件,名称为 Animal
的全限定名 com.moralok.dubbo.spi.test.Animal
,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。
com.moralok.dubbo.spi.test.Dog |
starter
实际上是一个空的 jar
,它唯一的目的就是提供使用 library
所需要的依赖项。
按照惯例,模块命名为 redis-lock-spring-boot-starter
。
需要引入以下依赖:
-<!-- library 的依赖项 --> |
进行测试。
+public class JavaSPITest { |
这样就创建了一个自定义 starter
。在项目中引入 starter
后,无需进一步配置,即可使用 RedisLockManager
和 RedisClient
。
<dependency> |
测试结果
+Java SPI |
从自定义 starter
的过程来看,使用 library
所需要的配置类和依赖项并没有“凭空消失”,而是由 starter
的编写者提供。然而在正常情况下,第三方的 jar
中的配置类并不在 Spring
扫描 Bean
的范围内,那么 starter
中的配置类是如何被注册到 Spring
容器中呢?我们做的事情中,看起来比较特别的一件事情是添加了 spring.factories
文件。
在 Spring Boot
的启动类(也是 Spring context
的最初配置类)上,标注了 SpringBootApplication
注解。该注解上标注了 EnableAutoConfiguration
注解,它的全限定名正是 spring.factories
文件中配置的键。注解的名字表明它用于启用自动配置功能。
|
Dubbo
并未使用原生的 Java SPI
,而是重新实现了一套功能更加强大的 SPI
机制。Dubbo SPI
的配置文件放在 META-INF/dubbo
文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI
注解。
dog = com.moralok.dubbo.spi.test.Dog |
EnableAutoConfiguration
注解用于启用自动配置功能。该注解上标注了 Import
注解,导入了 AutoConfigurationImportSelector
。很多形似 EnableXXX
的注解都是通过 Import
注解导入(注册)一些配置类,达到启用 XXX
功能的目的。Import
注解的功能详见之前的文章:
|
进行测试。
+public class DubboSPITest { |
导入选择器 ImportSelector
的 selectImports
方法返回要导入的类的全限定名。AutoConfigurationImportSelector
的名字含义是自动配置导入选择器,顾名思义它返回的应该是要导入的自动配置类。自动配置类这个说法有点容易让人误解,好像这个配置类本身具备“自动”的特性,实际上它就是一个普通的配置类。自动配置描述的是一种机制,想象一下,如果我们在 selectImports
方法中返回 starter
中的配置类 RedisLockAutoConfiguration
,是不是就为 redis-lock
完成了自动配置。事实上,selectImports
方法的作用就是找到并返回那些需要被自动配置的配置类。
public String[] selectImports(AnnotationMetadata annotationMetadata) { |
测试结果
+Dubbo SPI |
可以通过环境变量 spring.boot.enableautoconfiguration
覆盖是否启用自动配置功能。
protected boolean isEnabled(AnnotationMetadata metadata) { |
我们前面提到过,自动配置类本身只是普通的配置类,那么有什么标记或特征表明目标是一个自动配置类吗?有的,凡是配置在 spring.factories
文件中 EnableAutoConfiguration
(org.springframework.boot.autoconfigure.EnableAutoConfiguration
) 键下的类,就是候选的自动配置类。getCandidateConfigurations
方法用于获取候选的配置类。该方法运用了 Spring
的 SPI
机制,通过 SpringFactoriesLoader
获得所有配置在 spring.factories
文件中,org.springframework.boot.autoconfigure.EnableAutoConfiguration
键下的类,其中就包括了 RedisLockAutoConfiguration
。这样就完成了自动配置。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, |
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64); |
基于 Spring Boot SPI
机制获取配置在 spring.factories
文件中的自动配置类的过程我们不再分析,可以参见以下文章:
在平时开发时你可能会注意到,有时候在配置文件 application.properties
或 application.yml
中编写配置时,IDEA
会自动提示我们存在哪些配置,默认值是什么。
这个方法包含了如下步骤:
+EXTENSION_LOADERS
中获取与拓展类对应的 ExtensionLoader
,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。++“从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的
+getOrCreate
的处理模式在Dubbo
的源码中经常出现。
EXTENSION_LOADERS
是 ExtensionLoader
的静态变量,保存了“拓展类->ExtensionLoader
”的映射关系。
public T getExtension(String name) { |
只需要添加以下依赖,在编译项目时,就会自动调用该处理器 spring-boot-configuration-processor
为你的项目中被 ConfigurationProperties
注解标注的类生成配置元数据文件。
<dependency> |
这个方法中获取 Holder
和获取拓展实例都是使用 getOrCreate
的模式。
Holder
用于持有拓展实例。cachedInstances
是 ExtensionLoader
的成员变量,保存了“name->Holder
(拓展实例)”的映射关系。
private T createExtension(String name, boolean wrap) { |
这个方法包含如下步骤:
+getExtensionClasses
获取所有拓展类Wrapper
对象中第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOC
和 AOP
的具体实现。
最后拓展实例的结构如下图。
+ + +private Map<String, Class<?>> getExtensionClasses() { |
代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/
、META-INF/dubbo/
、META-INF/services/
三个目录下的配置文件,获取拓展类。
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); |
新版本使用原生的 Java SPI
加载 LoadingStrategy
,允许用户自定义加载策略。
DubboInternalLoadingStrategy
,目录 META-INF/dubbo/internal/
,优先级最高DubboLoadingStrategy
,目录 META-INF/dubbo/
,优先级普通ServicesLoadingStrategy
,目录 META-INF/services/
,优先级最低private static volatile LoadingStrategy[] strategies = loadLoadingStrategies(); |
LoadingStrategy
的 Java SPI
配置文件
loadDirectory
方法先通过 classLoader
获取所有的资源链接,然后再通过 loadResource
方法加载资源。
新版本中 extensionLoaderClassLoaderFirst
可以设置是否优先使用 ExtensionLoader's ClassLoader
获取资源链接。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, |
loadResource
方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass
方法进行其他操作。loadClass
方法用于操作缓存。
新版本中 excludedPackages
可以设置将指定包内的类都排除。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, |
loadClass
方法设置了多个缓存,比如 cachedAdaptiveClass
、cachedWrapperClasses
、cachedNames
和 cachedClasses
。
新版本中 overridden
可以设置是否覆盖 cachedAdaptiveClass
、cachedClasses
的 name->clazz
。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, |
Dubbo IOC
是通过 setter
方法注入依赖。Dubbo
首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter
方法特征并满足条件,若有,则通过 objectFactory
获取依赖对象,最后通过反射调用 setter
方法将依赖设置到目标对象中。
-- +注意:不要盲目手打相信智能提示弄错了依赖,谁能想到
+Spring
有好几个命名这么像的processor
,偏偏网上还有各种复制粘贴的文章解答在多模块项目中spring-boot-configuration-processor
出现的问题——来自Debug
到深夜的人的怨念。与
Spring IOC
相比,Dubbo IOC
实现的依赖注入功能更加简单,代码也更加容易理解。
private T injectExtension(T instance) { |
你几乎总是希望在自动配置类中包含一个或者多个 Conditional
注解。ConditionalOnMissingBean
是一个常用的注解,允许开发人员在对默认设置不满意时覆盖自动配置。
不要对添加 starter
的项目做出假设,如果你的 starter
需要用到别的 starter
,也请提到它们。为你的 library 的典型用法选择一组适当的默认依赖,避免引入不必要的依赖项,尽管当可选的依赖项很多时这可能有些困难。
Spring Boot
的自动配置在底层是通过标准的 Configuration
注解实现的,配合 Conditional
注解限制何时应用自动配置。“自动”的特性是基于两个重要的机制:
SPI
机制,从 spring.factories
文件中,获取自动配置类的全限定类名Import
机制,导入从 ImportSelector
返回的类工作原理的示意图如下:
- +objectFactory
是 ExtensionFactory
的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader
,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理。
public <T> T getExtension(Class<T> type, String name) { |
@PropertySource
注解提供了一种方便的声明性机制,用于将 PropertySource
添加到 Spring
容器的 Environment
环境中。该注解通常搭配 @Configuration
注解一起使用。本文将介绍如何使用 @PropertySource
注解,并通过分析源码解释外部配置文件是如何被解析进入 Spring
的 Environment
中。
+ starter
联系在一起,本文将介绍如何创建一个自定义的 starter
并从源码角度分析 Spring Boot
自动配置的工作原理。
-@Configuration
注解表示这是一个配置类,Spring
在处理配置类时,会解析并处理配置类上的 @PropertySource
注解,将对应的配置文件解析为 PropertySource
,添加到 Spring
容器的 Environment
环境中。这样就可以在其他的 Bean
中,使用 @Value
注解使用这些配置
|
一个 library 的完整 Spring Boot starter
可能包含以下组件:
starter
之后应该足以开始使用这个 library。++如果你不需要将自动配置的代码和依赖项管理分开,你可以将它们合并到一个模块中。
+
spring-boot
开头命名模块,即使你使用的是不同的 Maven groupId,因为 Spring
可能在将来提供官方的自动配置支持。自定义 starter
约定俗成的命名方式是 xxx-spring-boot-starter
。starter
提供了配置属性的定义,请选择适当的命名空间,避免使用 Spring Boot
的命名空间,否则他们未来的修改可能破坏你的配置。以下将通过一款基于 Redis
实现的分布式锁 redis-lock 的 starter
介绍如何创建一个自定义的 Spring Boot starter
。注意:实际上项目中的的 redis-lock-spring-boot-starter
合并了自动配置模块和启动模块。
自动配置模块包含开始使用 library 所需要的一切配置。它还可能包含配置键定义(@ConfigurationProperties
)和任何其他可用于进一步自定义组件初始化方式的回调接口。
按照惯例,模块命名为 redis-lock-spring-boot-autoconfigure
。
自动配置模块需要添加以下依赖。
+<dependency> |
配置文件
-player.nickname=Tom |
和平常在 Spring
中使用一个 library 时一样,创建配置类并配置好使用它所需要的 Bean
。
Configuration
注解,标识为配置类Bean
注解,配置所需要的 Bean
EnableConfigurationProperties
注解,启用配置属性(可选)
|
测试类
-public class PropertySourceTest { |
你可能需要定义一些配置属性来设置使用 library 所需要的属性。
+
|
测试结果
-Player{name='null', age=null, nickname='Tom'} |
在 src/main/resources/META-INF
目录中添加一个 spring.factories
文件,文件内容如下。键为 EnableAutoConfiguration
的全限定名,值为配置类的全限定名,如果需要配置多个配置类,可以用逗号分隔。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
关于 Spring
是如何处理配置类的请参见之前的文章:
starter
实际上是一个空的 jar
,它唯一的目的就是提供使用 library
所需要的依赖项。
按照惯例,模块命名为 redis-lock-spring-boot-starter
。
需要引入以下依赖:
+<!-- library 的依赖项 --> |
这样就创建了一个自定义 starter
。在项目中引入 starter
后,无需进一步配置,即可使用 RedisLockManager
和 RedisClient
。
<dependency> |
从自定义 starter
的过程来看,使用 library
所需要的配置类和依赖项并没有“凭空消失”,而是由 starter
的编写者提供。然而在正常情况下,第三方的 jar
中的配置类并不在 Spring
扫描 Bean
的范围内,那么 starter
中的配置类是如何被注册到 Spring
容器中呢?我们做的事情中,看起来比较特别的一件事情是添加了 spring.factories
文件。
在 Spring Boot
的启动类(也是 Spring context
的最初配置类)上,标注了 SpringBootApplication
注解。该注解上标注了 EnableAutoConfiguration
注解,它的全限定名正是 spring.factories
文件中配置的键。注解的名字表明它用于启用自动配置功能。
|
EnableAutoConfiguration
注解用于启用自动配置功能。该注解上标注了 Import
注解,导入了 AutoConfigurationImportSelector
。很多形似 EnableXXX
的注解都是通过 Import
注解导入(注册)一些配置类,达到启用 XXX
功能的目的。Import
注解的功能详见之前的文章:
Spring
在解析配置类构建配置模型时,会对配置类上的 @PropertySource
注解进行处理。Spring
将获取所有的 @PropertySource
注解属性,并遍历进行处理。
|
导入选择器 ImportSelector
的 selectImports
方法返回要导入的类的全限定名。AutoConfigurationImportSelector
的名字含义是自动配置导入选择器,顾名思义它返回的应该是要导入的自动配置类。自动配置类这个说法有点容易让人误解,好像这个配置类本身具备“自动”的特性,实际上它就是一个普通的配置类。自动配置描述的是一种机制,想象一下,如果我们在 selectImports
方法中返回 starter
中的配置类 RedisLockAutoConfiguration
,是不是就为 redis-lock
完成了自动配置。事实上,selectImports
方法的作用就是找到并返回那些需要被自动配置的配置类。
public String[] selectImports(AnnotationMetadata annotationMetadata) { |
可以通过环境变量 spring.boot.enableautoconfiguration
覆盖是否启用自动配置功能。
protected boolean isEnabled(AnnotationMetadata metadata) { |
我们前面提到过,自动配置类本身只是普通的配置类,那么有什么标记或特征表明目标是一个自动配置类吗?有的,凡是配置在 spring.factories
文件中 EnableAutoConfiguration
(org.springframework.boot.autoconfigure.EnableAutoConfiguration
) 键下的类,就是候选的自动配置类。getCandidateConfigurations
方法用于获取候选的配置类。该方法运用了 Spring
的 SPI
机制,通过 SpringFactoriesLoader
获得所有配置在 spring.factories
文件中,org.springframework.boot.autoconfigure.EnableAutoConfiguration
键下的类,其中就包括了 RedisLockAutoConfiguration
。这样就完成了自动配置。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, |
基于 Spring Boot SPI
机制获取配置在 spring.factories
文件中的自动配置类的过程我们不再分析,可以参见以下文章:
@PropertySource
注解是可重复的,一个类上可以标注多个@PropertySources
注解包含 @PropertySource
注解protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) |
在平时开发时你可能会注意到,有时候在配置文件 application.properties
或 application.yml
中编写配置时,IDEA
会自动提示我们存在哪些配置,默认值是什么。
使用 IDEA
查看 AnnotationAttributes
:
只需要添加以下依赖,在编译项目时,就会自动调用该处理器 spring-boot-configuration-processor
为你的项目中被 ConfigurationProperties
注解标注的类生成配置元数据文件。
<dependency> |
@PropertySource
注解属性的信息,如名称、编码和位置等等location
查找资源PropertySourceFactory
使用资源创建属性源 PropertySource
Environment
--注意属性源
+PropertySource
不是@PropertySource
注解,而是表示name/value
属性对的源的抽象基类。注意:不要盲目手打相信智能提示弄错了依赖,谁能想到
Spring
有好几个命名这么像的processor
,偏偏网上还有各种复制粘贴的文章解答在多模块项目中spring-boot-configuration-processor
出现的问题——来自Debug
到深夜的人的怨念。
private void processPropertySource(AnnotationAttributes propertySource) throws IOException { |
将属性源添加到 Environment
中有以下几个规则:
你几乎总是希望在自动配置类中包含一个或者多个 Conditional
注解。ConditionalOnMissingBean
是一个常用的注解,允许开发人员在对默认设置不满意时覆盖自动配置。
不要对添加 starter
的项目做出假设,如果你的 starter
需要用到别的 starter
,也请提到它们。为你的 library 的典型用法选择一组适当的默认依赖,避免引入不必要的依赖项,尽管当可选的依赖项很多时这可能有些困难。
Spring Boot
的自动配置在底层是通过标准的 Configuration
注解实现的,配合 Conditional
注解限制何时应用自动配置。“自动”的特性是基于两个重要的机制:
@PropertySource
注解加入的属性源,name
都会添加到 propertySourceNames
propertySourceNames
为空时,代表这是第一个通过 @PropertySource
注解加入的属性源,添加到最后(前面有系统属性源)propertySourceNames
不为空时,添加到上一个添加到 propertySourceNames
中的属性源的前面(后来居上)propertySources
的方法中都是先尝试移除,后添加(代表可能有顺序调整,具体场景不知)@PropertySource
注解加入的属性源,则扩展为 CompositePropertySource
,里面包含多个同名属性源(后来居上)SPI
机制,从 spring.factories
文件中,获取自动配置类的全限定类名Import
机制,导入从 ImportSelector
返回的类private void addPropertySource(PropertySource<?> propertySource) { |
可以适当地将添加属性源和使用属性分开看待,Environment
是它们产生联系的枢纽,@PropertySource
注解的处理过程是 @Configuration
注解的处理过程的一部分,在文件中的配置转换成为 Environment
中的 PropertySource
后,如何使用它们是独立的一件事情。
工作原理的示意图如下:
+ -关于搭配使用的 @Value
注解是如何工作的,可以参考文章:
Dubbo SPI
自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo
的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI
自适应拓展的相关源码和工作原理。
+ Import
注解是 Spring
基于 Java
注解配置的重要组成部分,处理 Import
注解是处理 Configuration
注解的子过程之一,本文将介绍 Import
注解的 3
种使用方式,然后通过分析源码和处理过程示意图解释它是如何导入(注册) BeanDefinition
的。
---站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。
-
在 Dubbo
中,很多拓展都是通过 SPI
机制动态加载的,比如 Protocol
、Cluster
和 LoadBalance
等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。
定义一个接口 Animal
。
public interface Animal { |
定义两个实现类 Dog
和 Cat
。
public class Dog implements Animal { |
@Import
是处理 @Configuration
过程的一部分。Import
注解有 3
种导入(注册) BeanDefinition
的方式:
Import
将目标类的 Class
对象,解析为 BeanDefinition
并注册。Import
配合 ImportSelector
的实现类,将 selectImports
方法返回的所有全限定类名字符串,解析为 BeanDefinition
并注册。Import
配合 ImportBeanDefinitionRegistra
r 的实现类,在 registerBeanDefinitions
方法中,直接向 BeanDefinitionRegistry
中注册 BeanDefinition
。测试了使用 Import
注解的 3
种方式:
Import
直接导入(注册) Red
。ImportBeanDefinitionRegistrar
间接注册 Color
。ImportSelector
间接导入(注册) Blue
。用例中的特别地测试了以下两种情况:
+Import
直接导入和配合 ImportSelector
间接导入相同的类 Red
只会注册一个 BeanDefinition
。MyImportSelector
书面顺序在 MyImportBeanDefinitionRegistrar
之后,但是 MyImportBeanDefinitionRegistrar
判断 registry
是否包含在 MyImportSelector
导入的类 Blue
时,不受顺序影响。
|
在运行时根据参数动态地加载拓展。
-public void bark(String type) { |
测试结果
+...... |
是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:
-Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type); |
这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter)
这样重复的代码;引入了 ExtensionLoader
这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo
和 动物 Animal
举例。
在非动态加载情况下,我们可能会这样写:
-public class Zoo { |
在动态加载情况下,我们可能会这样写。在这种情况下,Zoo
没有合适的方式直接持有 Animal
,而是通过 ExtensionLoader
间接地持有。
public class Zoo { |
我们更想要以下这种直接持有 Animal
的方式,在运行时 animal
可以是 Dog
,也可以是 Cat
,还可以是其他的动物。
public class Zoo { |
Dubbo
采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal
的自适应拓展,就像下面这样:
public class AdaptiveAnimal implements Animal { |
当然,我们不希望需要手动地为每一个拓展编写 Adaptive
代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark
方法的参数列表中新增了 type
类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo
的另一个设计规避了这一缺点,Dubbo
在公共契约中提到:所有扩展点参数都包含 URL
参数,URL
作为上下文信息贯穿整个扩展点设计体系。因此围绕着 Dubbo
以 URL
为中心的拓展体系,你很难设计出 Animal.bark(URL url)
这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo
并未完全抛弃手工编写自适应拓展的方式,而是予以保留。
在在 Dubbo
中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL
作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory
的自适应拓展,当你调用它的 getExtension
方法时,它就是将工作全权委托给 factory.getExtension(type, name)
完成的,而 factories
在创建 AdaptiveExtensionFactory
时就已经获取了。
|
至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。
-Adaptive
注解是一个与自适应拓展息息相关的注解,该定义如下:
|
根据 Target
注解的 value
可知,Adaptive
注解可标注在类或者方法上。当 Adaptive
注解标注在类上时,Dubbo
不会为该类生成代理类。当 Adaptive
注解标注在接口方法上时,Dubbo
则会为该方法生成代理逻辑。Adaptive
注解在类上的情况很少,在 Dubbo
中,仅有两个类被 Adaptive
注解标注,分别是 AdaptiveCompiler
和 AdaptiveExtensionFactory
。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive
注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。
获取自适应拓展的入口方法是 getAdaptiveExtension
,使用 getOrCreate
的模式获取。
public T getAdaptiveExtension() { |
当缓存为空时,就会通过 createAdaptiveExtension
方法创建。方法包含以下三个处理逻辑:
getAdaptiveExtensionClass
方法获取自适应拓展的 Class
对象。injectExtension
方法对拓展实例进行依赖注入。关于 Import
注解的源码分析需要建立在对关于 Configuration
注解的源码的了解基础上,因为前者是 Spring
解析配置类处理过程的一部分,可以参考文章:
在 doProcessConfigurationClass
方法中处理配置类构建配置模型时,会调用 processImports
方法处理 Import
注解。在进入方法前,会调用 getImports
方法从 sourceClass
获取要导入的目标。
--手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展。
+注意:目标不仅仅来自直接标注在
sourceClass
上的Import
注解,因为sourceClass
上可能还有其他的注解,这些注解自身可能标注了Import
注解,因此需要递归地遍历所有注解,找到所有的Import
注解。
private T createAdaptiveExtension() { |
获取自适应拓展类的 getAdaptiveExtensionClass
方法包含以下三个处理逻辑:
getExtensionClasses
方法获取所有拓展类。cachedAdaptiveClass
,如果不为 null
,则返回缓存。null
,则调用 createAdaptiveExtensionClass
创建自适应拓展类(代理类)。在Dubbo SPI 的工作原理中我们分析过 getExtensionClasses
方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive
注解标注了,那么该类就会被赋值给 cachedAdaptiveClass
变量。“原理”部分介绍的 AdaptiveExtensionFactory
就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive
注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。
private Class<?> getAdaptiveExtensionClass() { |
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) |
collectImports
方法是一种常见的递归写法(深度优先遍历)。imports
存放要导入的目标,visited
存放已经访问过的 sourceClass
。sourceClass
在入口处包装了一个普通的 Class
,在递归的过程中包装的都是一个注解 Class
。
--到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用
-getOrCreate
的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。
生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。
-Class
对象。--在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。
+注意:这里还没有检测循环导入的情况并抛出异常,但
visited
保证了只会遍历一次。
private Class<?> createAdaptiveExtensionClass() { |
为了更直观地了解代码生成的效果及其实现的功能,以 Protocol
为例,生成的完整代码(已经经过格式化)展示如下。
package org.apache.dubbo.rpc; |
// 获取 Import 注解 value 中的 Class 对象,并包装为 SourceClass 返回 |
生成的代理类需完成以下功能:
-adaptive
方法,直接抛出异常。adaptive
方法:URL
对象,结合 URL
对象和默认拓展名得到最终的拓展名 extName
。ExtensionLoader
,再根据拓展名 extName
获取拓展,最后调用拓展的同名方法。这时候,并不区分要导入的目标的 Class
有什么特别之处,Import
注解的语义,此时宽泛地说就是:“将 value
中的类导入”。但是显而易见,这样的方式不够灵活,因此才有了另外两种更有灵活性的导入方式:ImportSelector
和 ImportBeanDefinitionRegistrar
,Spring
最终不会真的注册这两种类,而是注册它们“介绍”的类,相当于把确定导入什么类的工作委托给它们。
processImports
方法是处理 Import
注解的核心方法,这里的处理逻辑就对应着 Import
注解的三种使用方式。主要步骤如下:
ImportSelector
类型,调用 selectImports
方法获取新的要导入的目标,递归调用 processImports
处理ImportBeanDefinitionRegistrar
类型,添加到配置模型 configClass
(出口 1
)2
)以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo
会直接或间接地从参数列表中查找 URL
对象,所谓直接就是 URL
对象直接在参数列表中,所谓间接就是 URL
对象是其中一个参数的属性。在得到 URL
对象后,Dubbo
会尝试以 Adaptive
注解的 value
为 key
,从 URL
中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName
。实际的实现更加复杂,需要耐心阅读和测试。
新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 type
和 defaultExtName
,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。
public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) { |
在生成代理类源码之前,generate
方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive
注解,若不满足,就会抛出异常。
--流式编程使用得当的话很有可读性啊。
-
private boolean hasAdaptiveMethod() { |
生成代理类源码的顺序和普通 Java
类文件中内容的顺序一致:
先忽略“生成方法”的部分,以 Dubbo
的 Protocol
拓展为例,生成的代码如下:
package org.apache.dubbo.rpc; |
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, |
生成方法的过程同样被抽象为几个命名含义清晰的方法,包含以下五个部分:
+如果要导入的目标是 ImportSelector
类型,那么 Spring
将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。具体步骤是:
Class
对象ImportSelector
实例selectImports
方法,该方法返回的是类的全限定名,这样就得到了真正要导入的目标processImports
ImportSelector
就像它名字的含义一样,本质上是一种导入选择器,是一种更加灵活的 getImports
方法。由于返回的目标可能属于三种情形中的任意一种,所以对这些目标的处理还是要回到 processImports
方法。可以说 ImportSelector
类型本身不是 processImports
方法的出口,它最终会转换为 ImportBeanDefinitionRegistrar
或其他剩余情况。
ImportSelector
灵活性的来源:
selectImports
的 AnnotationMetadata
参数,为它提供了根据注解信息返回要导入的目标的能力ImportSelector
可以实现 Aware
接口,用以感知到一些容器级别的资源,如 BeanFactory
,这为它提供了根据这些资源中的信息返回要导入的目标的能力private String generateMethod(Method method) { |
除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive
注解标注。
private String generateMethodContent(Method method) { |
对于无 Adaptive
注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。
private String generateUnsupported(Method method) { |
以 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 注解标注的方法,
-// 查找 URL 类型的参数 |
直接从方法的参数类型列表中查找第一个 URL
类型的参数,返回其索引。
private int getUrlTypeIndex(Method method) { |
间接从方法的参数类型列表中,查找 URL
类型的参数,并生成判空检查和赋值代码。
private String generateUrlAssignmentIndirectly(Method method) { |
以 Protocol
的 refer
和 export
方法为例,生成的内容如下:
// refer |
private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) { |
检测是否有 Invocation
类型的参数,并生成判空检查代码和赋值代码。从 Invocation
可以获得 methodName
。
private boolean hasInvocationArgument(Method method) { |
以 LoadBalance
的 select
方法为例,生成的内容如下:
if (arg2 == null) throw new IllegalArgumentException("invocation == null"); |
本方法用于根据 SPI
和 Adaptive
注解的 value
生成“获取拓展名”的代码,同时生成逻辑还受 Invocation
影响,因此相对复杂。总结的规则如下:
如果要导入的目标是 ImportBeanDefinitionRegistrar
,它会和 ImportSelector
有些相似却又有所不同。Spring
同样将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。
Class
对象ImportBeanDefinitionRegistrar
实例configClass
的 importBeanDefinitionRegistrars
属性private String generateExtNameAssignment(String[] value, boolean hasInvocation) { |
ImportBeanDefinitionRegistrar
不像 ImportSelector
需要进一步处理,它本身就代表着一个返回出口,成为了配置模型的一部分。但是请注意,registerBeanDefinitions
方法此时并没有被调用。
ImportBeanDefinitionRegistrar
灵活性的来源:
registerBeanDefinitions
的 AnnotationMetadata
参数,为它提供了根据注解信息决定注册 BeanDefinition
的能力registerBeanDefinitions
的 BeanDefinitionRegistry
参数,为它提供了根据 BeanDefinitionRegistry
中的信息决定注册 BeanDefinition
的能力ImportBeanDefinitionRegistrar
可以实现 Aware
接口,用以感知到一些容器级别的资源,如 BeanFactory
,这为它提供了根据这些资源中的信息返回要导入的目标的能力如果要导入的目标属于既不是 ImportSelector
也不是 ImportBeanDefinitionRegistrar
的其他剩余情况,那么 Spring
将其视为被 Configuration
注解标注的配置类进行处理。这里的处理逻辑是,Import
注解导入的类可能不是一个普通的类,而是一个配置类,因此需要回到 processConfigurationClass
进行处理。processConfigurationClass
方法正是本文开头的 doProcessConfigurationClass
方法的调用方,这里有两个地方值得注意:
Import
注解产生的 ConfigurationClass
根据不同的情况需要合并或者被抛弃,显式声明比 Import 导入的优先级更高。parser
的 configurationClasses
属性。protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { |
private String generateExtensionAssignment() { |
在解析完每一批(注释中说“全部”)的配置类后,会统一调用 DeferredImportSelector
。它作为一个标记接口推迟了 selectImports
的时机,打破了处理顺序的限制,在方法被调用时,可以得到更加完整的信息。注释中说“在选择导入的目标是 @Conditional
时,这个类型的选择器会很有用”,但是我不太理解,因为这个时候,处理配置类得到的信息尚未转换为 ImportSelector
可以感知到的信息,不像 ImportBeanDefinitionRegistrar
,它被调用的时机在最后,也因此可以感知到更多的信息。
public void parse(Set<BeanDefinitionHolder> configCandidates) { |
以 Protocol
接口的 refer
方法为例,生成的内容如下:
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); |
ConfigurationClassPostProcessor
在每次解析得到新的一批配置模型后,都会调用 ConfigurationClassBeanDefinitionReader
的 loadBeanDefinitions
方法加载 BeanDefinition
,在这过程的最后会从 ImportBeanDefinitionRegistrar
加载 BeanDefinition
。这代表在处理同一批配置类时,在 registerBeanDefinitions
方法中总是能感知到以其他方式注册到 BeanDefinitionRegistry
中的 BeanDefinition
,不论书面定义的顺序如何。
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { |
生成方法调用语句,如有必要,返回结果。
-private String generateReturnAndInvocation(Method method) { |
在处理导入的目标前将配置类放入 importStack
,处理完毕移除。如果要导入的目标属于其他剩余情况时,注册被导入类->所有导入类集合的映射关系。
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, |
以 Protocol
接口的 refer
方法为例,生成的内容如下:
return extension.refer(arg0, arg1); |
检测是否发生循环导入。以当前类开始,循环向上查找最近一个导入自身的类,如果找到自身,说明发生循环导入。
+private boolean isChainedImportOnStack(ConfigurationClass configClass) { |
--新版本通过提炼方法、使用流式编程和使用
-String.format()
代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。
+ | ImportSelector |
+ImportBeanDefinitionRegistrar |
+其他剩余情况 | +
---|---|---|---|
灵活性 | +中 | +高 | +低 | +
处理结果 | ++ | 转换为配置模型的一部分 | +转换为一个配置模型 | +
方法调用时机 | +立即(或解析配置类的最后) | +加载 BeanDefinition 的最后 |
++ |
方法的结果 | +获取 Import 目标 |
+直接注册 BeanDefinition |
++ |
@ConfigurationProperties
和 @EnableConfigurationProperties
是 Spring Boot
中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot
的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机和工作原理。
+ @PropertySource
注解提供了一种方便的声明性机制,用于将 PropertySource
添加到 Spring
容器的 Environment
环境中。该注解通常搭配 @Configuration
注解一起使用。本文将介绍如何使用 @PropertySource
注解,并通过分析源码解释外部配置文件是如何被解析进入 Spring
的 Environment
中。
-@Import
的工作原理对阅读本文的源码有非常大的帮助,可以参考Spring 中 @Import 注解的使用和源码分析。ConfigurationProperties
是用于外部化配置的注解。如果你想绑定和验证某些外部属性(例如来自 .properties
文件),就将其添加到类定义或 @Configuration
类中的 @Bean
方法。请注意,和 @Value
相反,SpEL
表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties
注解的源码可知,该注解主要起到标记和存储一些信息的作用。
|
@Configuration
注解表示这是一个配置类,Spring
在处理配置类时,会解析并处理配置类上的 @PropertySource
注解,将对应的配置文件解析为 PropertySource
,添加到 Spring
容器的 Environment
环境中。这样就可以在其他的 Bean
中,使用 @Value
注解使用这些配置
|
查看 EnableConfigurationProperties
的源码,我们注意到它通过 @Import
导入了 EnableConfigurationPropertiesImportSelector
。
|
配置文件
+player.nickname=Tom |
查看 EnableConfigurationPropertiesImportSelector
的源码,关注 selectImports
方法。该方法返回了 ConfigurationPropertiesBeanRegistrar
和 ConfigurationPropertiesBindingPostProcessorRegistrar
的全限定类名,Spring
将注册它们。
class EnableConfigurationPropertiesImportSelector implements ImportSelector { |
测试类
+public class PropertySourceTest { |
ConfigurationPropertiesBeanRegistrar
是一个内部类,查看 ConfigurationPropertiesBeanRegistrar
的源码,关注 registerBeanDefinitions
方法。注册的目标来自于:
@EnableConfigurationProperties
的 value
所指定的类中@ConfigurationProperties
的类public static class ConfigurationPropertiesBeanRegistrar |
查看 ConfigurationPropertiesBindingPostProcessorRegistrar
的源码,关注 registerBeanDefinitions
方法。该方法注册了 ConfigurationPropertiesBindingPostProcessor
和 ConfigurationBeanFactoryMetadata
。
ConfigurationProperties
的绑定Bean
工厂初始化期间记住 @Bean
定义元数据的实用程序类public class ConfigurationPropertiesBindingPostProcessorRegistrar |
测试结果
+Player{name='null', age=null, nickname='Tom'} |
ConfigurationPropertiesBindingPostProcessor
是用于 ConfigurationProperties
绑定的后处理器,关注 afterPropertiesSet
方法还有核心方法 postProcessBeforeInitialization
。
关于 Spring
是如何处理配置类的请参见之前的文章:
afterPropertiesSet
方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
。postProcessBeforeInitialization
方法中,先获取 @ConfigurationProperties
,再进行绑定。public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, |
ConfigurationBeanFactoryMetadata
是用于在 Bean
工厂初始化期间记住 @Bean
定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties
不仅可以添加到类定义,还可以用于标注 @Bean
方法,ConfigurationBeanFactoryMetadata
正是应用于在后者这类情况下获取 @ConfigurationProperties
。
public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor { |
@EnableConfigurationProperties
的目的有两个:
Spring
在解析配置类构建配置模型时,会对配置类上的 @PropertySource
注解进行处理。Spring
将获取所有的 @PropertySource
注解属性,并遍历进行处理。
Bean
初始化工作时,介入进行绑定@PropertySource
注解是可重复的,一个类上可以标注多个@PropertySources
注解包含 @PropertySource
注解尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties
类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties
类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties
注册的,因此你需要保证至少有一个 @EnableConfigurationProperties
标注的类被注册(并被处理了 @Import
)。
在 Spring Boot
中,@SpringBootApplication
通过 @EnableAutoConfiguration
启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfiguration
,ConfigurationPropertiesAutoConfiguration
标注了 @EnableConfigurationProperties
。因此,对于 Spring Boot
而言,扫描范围内的所有 ConfigurationProperties
类,其实都不需要 @EnableAutoConfiguration
。事实上,由于默认生成的 beanName
不同,多余的配置还会重复注册两个 Bean
定义。
|
Spring
中,AutowiredAnnotationBeanPostProcessor
是一个非常重要的后处理器,它可以自动装配标注注解的字段和方法,默认使用 @Autowired
和 @Value
注解,可以支持 JSR-330
的 @Inject
注解。本文通过分析源码介绍它的调用时机和工作原理。
-
-
-AutowiredAnnotationBeanPostProcessor
顾名思义,是自动装配注解的 BeanPostProcessor
,但是它处理的不仅仅是 @Autowired
这一个注解。个人认为 Autowired Annotation
的意思更接近“用于标注目标是被自动装配的注解”。使用“目标”是为了表达注解标注的目标不仅仅限于字段,更是包括构造函数、方法、方法参数以及注解;使用“被自动装配”是为了表达注解描述的是目标的特征或者被处理的结果,体现出被动的语义更准确;使用“注解”是为了表达注解的种类不仅仅限于 @Autowired
,还包括 @Value
和 @Inject
,它们都指示目标需要被自动装配处理。
通过 AutowiredAnnotationBeanPostProcessor
的构造函数可以看到 @Inject
注解的特别之处,为了使用它,需要在 Maven
配置中额外引入 javax.inject
依赖。
public AutowiredAnnotationBeanPostProcessor() { |
populateBean
方法我们在Spring Bean 加载过程中介绍过为 bean
填充属性值发生在 populateBean
方法中。我们也将直接从这里开始跟踪代码的处理过程。
--个人认为宽松地讲,“填充属性”等于“注入属性”等于“自动装配”,前两者更侧重处理的结果,后者更侧重过程的特征,但请注意在具体的代码上下文中应辨析区别。例如为
-bean
填充属性是Spring
的重要目标之一,基于Autowired Annotation
进行自动装配某一个后处理器的功能,是Spring
实现目标的其中一个具体方式。
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { |
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) |
--有时候在
-Spring
中看到BeanPostProcessor
并不能代表将目光转向该接口的方法实现。不同BeanPostProcessor
的子接口存在不同的调用时机。AutowiredAnnotationBeanPostProcessor
间接实现了InstantiationAwareBeanPostProcessor
并直接实现了MergedBeanDefinitionPostProcessor
,这是我们今天要关注的两个重点接口。
AutowiredAnnotationBeanPostProcessor
是什么时候注册的呢?
以 AnnotationConfigApplicationContext
为例,它在构造函数中创建了 AnnotatedBeanDefinitionReader
,AnnotatedBeanDefinitionReader
又在构造函数中注册了基于注解配置的处理器:
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); |
使用 IDEA
查看 AnnotationAttributes
:
其中就包括 AutowiredAnnotationBeanPostProcessor
。
AutowiredAnnotationBeanPostProcessor
实现了 InstantiationAwareBeanPostProcessor
接口,该接口关注 bean
的实例化:
postProcessBeforeInstantiation
(实例化前)postProcessAfterInstantiation
(实例化后)postProcessPropertyValues
(实例化后)@PropertySource
注解属性的信息,如名称、编码和位置等等location
查找资源PropertySourceFactory
使用资源创建属性源 PropertySource
Environment
--+
postProcessPropertyValues
方法在工厂将给定属性值应用到给定bean
之前对给定属性值进行后处理。允许检查是否满足所有依赖关系,例如基于bean
属性setters
上的@Required
注解进行检查。还允许替换要应用的属性值,通常是通过基于原始PropertyValues
创建新的MutablePropertyValues
实例,并添加或删除特定值。注意属性源
PropertySource
不是@PropertySource
注解,而是表示name/value
属性对的源的抽象基类。
postProcessPropertyValues
方法做了两件事情:
|
private void processPropertySource(AnnotationAttributes propertySource) throws IOException { |
--这部分代码体现了注入(Injection)和自动装配(Autowiring)的等价性。
-InjectionMetadata
和AutowiringMetadata
的含义是用于注入(自动装配)的元数据。
InjectionMetadata
是用于管理注入元数据的内部类,不适合直接在应用程序中使用。它和 Class
是一对一的关系,封装了需要被注入的元素 InjectedElement
。一个 InjectedElement
对应着一个字段(Field
)或一个方法(Method
),分别对应着两个实现类 AutowiredFieldElement
和 AutowiredMethodElement
。这里再次体现了被注入、被自动装配的语义。
查找自动装配元数据的过程如下:
+将属性源添加到 Environment
中有以下几个规则:
@PropertySource
注解加入的属性源,name
都会添加到 propertySourceNames
propertySourceNames
为空时,代表这是第一个通过 @PropertySource
注解加入的属性源,添加到最后(前面有系统属性源)propertySourceNames
不为空时,添加到上一个添加到 propertySourceNames
中的属性源的前面(后来居上)propertySources
的方法中都是先尝试移除,后添加(代表可能有顺序调整,具体场景不知)@PropertySource
注解加入的属性源,则扩展为 CompositePropertySource
,里面包含多个同名属性源(后来居上)--注意:在
-postProcessPropertyValues
第一次调用findAutowiringMetadata
缓存中就已经有结果了。什么时候构建并存入缓存的呢?
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, PropertyValues pvs) { |
构建自动装配元数据只需要给定一个 Class
,沿着给定的 Class
的父类向上循环查找直到 Object
类。在每个循环中,先遍历当前类声明的所有属性,找到标注了自动装配注解的属性,为其创建 AutowiredFieldElement
并添加到临时集合,再遍历当前类声明的所有方法,找到标注了自动装配注解的方法,为其创建 AutowiredMethodElement
并添加到临时集合。最后汇总 InjectedElement
封装到 InjectionMetadata
中。
--这个处理顺序意味着在注入时方法的优先级高于字段,前者会覆盖后者。
-
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) { |
private void addPropertySource(PropertySource<?> propertySource) { |
对于字段来说,注入意味着将一个解析得到的 value
通过反射设置到字段中;对于方法来说,注入意味着解析得到方法参数的 value
,然后通过反射调用方法。
InjectionMetadata
的 inject
方法比较简单,内部会遍历并调用 InjectedElement
的 inject
方法,AutowiredFieldElement
和 AutowiredMethodElement
各自实现了 inject
方法。
public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable { |
可以适当地将添加属性源和使用属性分开看待,Environment
是它们产生联系的枢纽,@PropertySource
注解的处理过程是 @Configuration
注解的处理过程的一部分,在文件中的配置转换成为 Environment
中的 PropertySource
后,如何使用它们是独立的一件事情。
不论是 AutowiredFieldElement
还是 AutowiredMethodElement
,inject
的过程都比较相似:
关于搭配使用的 @Value
注解是如何工作的,可以参考文章:
DependencyDescriptor
描述即将被注入的特定依赖项,DependencyDescriptor
包装了构造函数参数、方法参数或者字段,允许以统一的方式访问它们的元数据DependencyDescriptor
bean
,用于判断是否使用 DependencyDescriptor
的变体 ShortcutDependencyDescriptor
优化缓存beanFactory.resolveDependency
解析依赖private class AutowiredFieldElement extends InjectionMetadata.InjectedElement { |
--不理解缓存
-DependencyDescriptor
代码上的注释:Shortcut for avoiding synchronization…
private class AutowiredMethodElement extends InjectionMetadata.InjectedElement { |
--为什么在这里
-beanFactory.resolveDependency
需要的参数和未缓存时不一样啊?虽然内部会通过相同的方式获得typeConverter
,但是很奇怪啊。
private Object resolvedCachedArgument(String beanName, Object cachedArgument) { |
解析依赖的过程暂不深入。
-public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName, |
public Object doResolveDependency(DependencyDescriptor descriptor, String beanName, |
你在 Debug
的时候也许会注意到,在第一次进入 postProcessPropertyValues
方法,查找自动装配元数据时,就已经是从缓存中获取的了。那么究竟是什么时候构建自动装配元数据并放入缓存的呢?这就需要我们目前一直没有讲到的 MergedBeanDefinitionPostProcessor
派上用场了。在 postProcessMergedBeanDefinition
方法中,也调用了 findAutowiringMetadata
方法,这才是真正的第一次查找自动装配元数据。
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { |
那么 MergedBeanDefinitionPostProcessor
又是什么时候被调用的呢?在 doCreateBean
方法中,创建实例后,填充属性前。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) |
---
configMember
这个命名不太理解,指的是通过配置实现注入的Member
(Filed
和Method
的父类)吗?
检测配置成员,如果不是外部管理的配置成员,则注册为外部管理的配置成员。在合并后的 bean
定义中,externallyManagedConfigMembers
保存了外部管理的配置成员,用于标记一个配置成员是外部管理的。例如当一个字段同时标注了 @Resource
和 @Autowired
注解,当 @Resouce
注解被处理后,该字段已经被标记,当 @Autowired
注解被处理时,就会跳过该字段,避免重复注入造成冲突。
--这里的外部管理感觉有点指向依赖注入的控制反转思想。
-
public void checkConfigMembers(RootBeanDefinition beanDefinition) { |
--如果不了解具体的场景,可能会比较难想象这个标记的用处是什么。
-
-]]>尽管
-@Autowired
注解配合@Value
注解可以很灵活,但是应尽量采取清晰明了的配置方式,让注入的结果一眼就能看出来。
@ConfigurationProperties
和 @EnableConfigurationProperties
是 Spring Boot
中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot
的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机和工作原理。
+
+
+@Import
的工作原理对阅读本文的源码有非常大的帮助,可以参考Spring 中 @Import 注解的使用和源码分析。ConfigurationProperties
是用于外部化配置的注解。如果你想绑定和验证某些外部属性(例如来自 .properties
文件),就将其添加到类定义或 @Configuration
类中的 @Bean
方法。请注意,和 @Value
相反,SpEL
表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties
注解的源码可知,该注解主要起到标记和存储一些信息的作用。
|
查看 EnableConfigurationProperties
的源码,我们注意到它通过 @Import
导入了 EnableConfigurationPropertiesImportSelector
。
|
查看 EnableConfigurationPropertiesImportSelector
的源码,关注 selectImports
方法。该方法返回了 ConfigurationPropertiesBeanRegistrar
和 ConfigurationPropertiesBindingPostProcessorRegistrar
的全限定类名,Spring
将注册它们。
class EnableConfigurationPropertiesImportSelector implements ImportSelector { |
ConfigurationPropertiesBeanRegistrar
是一个内部类,查看 ConfigurationPropertiesBeanRegistrar
的源码,关注 registerBeanDefinitions
方法。注册的目标来自于:
@EnableConfigurationProperties
的 value
所指定的类中@ConfigurationProperties
的类public static class ConfigurationPropertiesBeanRegistrar |
查看 ConfigurationPropertiesBindingPostProcessorRegistrar
的源码,关注 registerBeanDefinitions
方法。该方法注册了 ConfigurationPropertiesBindingPostProcessor
和 ConfigurationBeanFactoryMetadata
。
ConfigurationProperties
的绑定Bean
工厂初始化期间记住 @Bean
定义元数据的实用程序类public class ConfigurationPropertiesBindingPostProcessorRegistrar |
ConfigurationPropertiesBindingPostProcessor
是用于 ConfigurationProperties
绑定的后处理器,关注 afterPropertiesSet
方法还有核心方法 postProcessBeforeInitialization
。
afterPropertiesSet
方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
。postProcessBeforeInitialization
方法中,先获取 @ConfigurationProperties
,再进行绑定。public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor, |
ConfigurationBeanFactoryMetadata
是用于在 Bean
工厂初始化期间记住 @Bean
定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties
不仅可以添加到类定义,还可以用于标注 @Bean
方法,ConfigurationBeanFactoryMetadata
正是应用于在后者这类情况下获取 @ConfigurationProperties
。
public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor { |
@EnableConfigurationProperties
的目的有两个:
Bean
初始化工作时,介入进行绑定尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties
类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties
类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties
注册的,因此你需要保证至少有一个 @EnableConfigurationProperties
标注的类被注册(并被处理了 @Import
)。
在 Spring Boot
中,@SpringBootApplication
通过 @EnableAutoConfiguration
启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfiguration
,ConfigurationPropertiesAutoConfiguration
标注了 @EnableConfigurationProperties
。因此,对于 Spring Boot
而言,扫描范围内的所有 ConfigurationProperties
类,其实都不需要 @EnableAutoConfiguration
。事实上,由于默认生成的 beanName
不同,多余的配置还会重复注册两个 Bean
定义。
|
Unsafe
类位于 sun.misc
包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe
类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CAS
(Compare and Swap
)操作等。本文将介绍这个“反 Java
”的类及其方法的典型使用场景。
+ @ComponentScan
注解是 Spring
中很常用的注解,用于扫描并加载指定类路径下的 Bean
,而 Spring Boot
为了便捷使用 @SpringBootApplication
组合注解集成了 @ComponentScan
的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication
可以间接设置 @ComponentScan
属性的原因?本文从源码角度分析 @ComponentScan
的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。
+@ComponentScan
是处理 @Configuration
过程的一部分。对于标注了 @ComponentScan
注解的配置类,处理过程如下:
@ComponentScan
的注解属性Bean
定义Bean
定义中有任何其他配置类,将递归解析(处理配置类)-+由于
+Unsafe
类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过Java
语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用Unsafe
类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用Unsafe
类来进行一些性能优化。这里和处理
@Import
的过程很像,都出现了递归解析新获得的配置类。
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) |
我们先跳过“获取 @ComponentScan
的注解属性”的过程,来看“扫描获取 Bean
定义”的过程。扫描是通过 ComponentScanAnnotationParser
的 parse
方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner
设置一些来自 @ComponentScan
的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner
顾名思义是基于类路径的 Bean
定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages
的设置:
basePackages
值并添加basePackageClasses
值,转换为它们所在的包名并添加--尽管在生产中需要谨慎使用
-Unsafe
,但是可以在测试中使用它来更真实地接触Java
对象在内存中的存储结构,验证自己的理论知识。
--在
+Java 9
及之后的版本中,Unsafe
类中的getUnsafe()
方法被标记为不安全(Unsafe
),不再允许普通的Java
应用程序代码通过此方法获取Unsafe
实例。这是为了提高Java
的安全性,防止滥用Unsafe
类的功能。最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。
在正常的 Java
应用程序中,获取 Unsafe
实例是不被推荐的,因为它违反了 Java
语言的安全性和封装原则。Unsafe
类的设计本意是为了 Java
库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe
对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe
对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。
获取 Unsafe
实例的静态方法如下:
|
Unsafe
使用单例模式,可以通过静态方法 getUnsafe
获取 Unsafe
实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe
实例有以下两种可行方案:
-Xbootclasspath/a:${path}
把调用方法的类所在的 jar
包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径Unsafe
类中的 Unsafe
实例private static Unsafe getUnsafe() { |
Unsafe
类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java
语言的内置安全性和类型检查。以下是一些常见的 Unsafe
类中关于内存操作的方法:
allocateMemory
: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。public native long allocateMemory(long bytes); |
reallocateMemory
: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。public native long reallocateMemory(long address, long bytes); |
freeMemory
: 释放之前由 allocateMemory
或 reallocateMemory
分配的内存。public native void freeMemory(long address); |
setMemory
: 将给定内存块中的所有字节设置为固定值(通常为零)。public native void setMemory(Object o, long offset, long bytes, byte value); |
copyMemory
: 复制指定长度的内存块public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); |
putXxx
: 将指定偏移量处的内存设置为指定的值,其中 Xxx
可以是 Object
、int
、long
、float
和 double
等。public native void putObject(Object o, long offset, Object x); |
getXxx
: 从指定偏移量处的内存读取值,其中 Xxx
可以是 Object
、int
、long
、float
和 double
等。public native Object getObject(Object o, long offset); |
putXxx
和 getXxx
也提供了按绝对基地址操作内存的方法。public native byte getByte(long address); |
从内存读取值时,除非满足以下情况之一,否则结果不确定:
-objectFieldOffset
从字段的 Field
对象获取的,o
指向的对象的类与字段所属的类兼容。o
指向的对象(无论是否为 null
)分别是通过 staticFieldOffset
和 staticFieldBase
从 Field
对象获得的。o
指向的是一个数组,偏移量是一个形式为 B+N*S
的整数,其中 N
是数组的有效索引,B
和 S
分别是通过 arrayBaseOffset
和 arrayIndexScale
获得的值。--做一些“不确定”的测试,比如使用
-byte
相关的方法操作int
所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。
在 Java NIO
(New I/O
)中,分配堆外内存使用了 Unsafe
类的 allocateMemory
方法。堆外内存是一种在 Java
虚拟机之外分配的内存,它不受 Java
堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O
操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java
堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting
)的机制来处理。
DirectByteBuffer(int cap) { |
当 DirectByteBuffer
对象仅被 Cleaner
对象(虚引用)引用时,它可以在任意一次 GC
中被垃圾回收。在 DirectByteBuffer
对象被垃圾回收后,Cleaner
对象会被加入到引用队列,ReferenceHandler
线程将调用 Deallocator
对象的 run
方法,从而实现本地内存的自动释放。
private static class Deallocator implements Runnable { |
Unsafe
提供了 3
个 CAS
相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU
会自动将该位置更新为新值,否则,CPU
不做任何操作。这些方法的底层实现对应着 CPU
指令 cmpxchg
。
// 如果 Java 变量当前符合预期,则自动将其更新为 x。 |
在 AtomicInteger
的实现中,静态字段 valueOffset
即为字段 value
的内存偏移地址,valueOffset
的值在 AtomicInteger
初始化时,在静态代码块中通过 Unsafe
的 objectFieldOffset
方法获取。
public class AtomicInteger extends Number implements java.io.Serializable { |
CAS 更新变量的值的内存变化如下:
- - -配合 ClassLayout
打印 AtomicInteger
的内部结构更直观地感受 offset
的含义:
java.util.concurrent.atomic.AtomicInteger object internals: |
@ComponentScan
注解是 Spring
中很常用的注解,用于扫描并加载指定类路径下的 Bean
,而 Spring Boot
为了便捷使用 @SpringBootApplication
组合注解集成了 @ComponentScan
的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication
可以间接设置 @ComponentScan
属性的原因?本文从源码角度分析 @ComponentScan
的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。
-
-
-@ComponentScan
是处理 @Configuration
过程的一部分。对于标注了 @ComponentScan
注解的配置类,处理过程如下:
@ComponentScan
的注解属性Bean
定义Bean
定义中有任何其他配置类,将递归解析(处理配置类)--这里和处理
-@Import
的过程很像,都出现了递归解析新获得的配置类。
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) |
我们先跳过“获取 @ComponentScan
的注解属性”的过程,来看“扫描获取 Bean
定义”的过程。扫描是通过 ComponentScanAnnotationParser
的 parse
方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner
设置一些来自 @ComponentScan
的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner
顾名思义是基于类路径的 Bean
定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages
的设置:
basePackages
值并添加basePackageClasses
值,转换为它们所在的包名并添加--最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。
-
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { |
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) { |
parse
方法与其说是解析,不如说是封装了一些设置并最终调用 ClassPathBeanDefinitionScanner
,而设置的属性值来源于 @ComponentScan
的注解属性。关于获取 @ComponentScan
的注解属性的方法 AnnotationConfigUtils.attributesForRepeatable
在分析 @PropertySource
时也曾经遇到过,顾名思义我们知道它应该是用于获取可重复的注解的属性。可是它和直接获取注解对象有什么区别呢?
@@ -4932,6 +4892,78 @@spring boot
Unsafe
类位于 sun.misc
包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe
类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CAS
(Compare and Swap
)操作等。本文将介绍这个“反 Java
”的类及其方法的典型使用场景。
+
+
+++由于
+Unsafe
类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过Java
语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用Unsafe
类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用Unsafe
类来进行一些性能优化。
++尽管在生产中需要谨慎使用
+Unsafe
,但是可以在测试中使用它来更真实地接触Java
对象在内存中的存储结构,验证自己的理论知识。
++在
+Java 9
及之后的版本中,Unsafe
类中的getUnsafe()
方法被标记为不安全(Unsafe
),不再允许普通的Java
应用程序代码通过此方法获取Unsafe
实例。这是为了提高Java
的安全性,防止滥用Unsafe
类的功能。
在正常的 Java
应用程序中,获取 Unsafe
实例是不被推荐的,因为它违反了 Java
语言的安全性和封装原则。Unsafe
类的设计本意是为了 Java
库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe
对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe
对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。
获取 Unsafe
实例的静态方法如下:
|
Unsafe
使用单例模式,可以通过静态方法 getUnsafe
获取 Unsafe
实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe
实例有以下两种可行方案:
-Xbootclasspath/a:${path}
把调用方法的类所在的 jar
包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径Unsafe
类中的 Unsafe
实例private static Unsafe getUnsafe() { |
Unsafe
类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java
语言的内置安全性和类型检查。以下是一些常见的 Unsafe
类中关于内存操作的方法:
allocateMemory
: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。public native long allocateMemory(long bytes); |
reallocateMemory
: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。public native long reallocateMemory(long address, long bytes); |
freeMemory
: 释放之前由 allocateMemory
或 reallocateMemory
分配的内存。public native void freeMemory(long address); |
setMemory
: 将给定内存块中的所有字节设置为固定值(通常为零)。public native void setMemory(Object o, long offset, long bytes, byte value); |
copyMemory
: 复制指定长度的内存块public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); |
putXxx
: 将指定偏移量处的内存设置为指定的值,其中 Xxx
可以是 Object
、int
、long
、float
和 double
等。public native void putObject(Object o, long offset, Object x); |
getXxx
: 从指定偏移量处的内存读取值,其中 Xxx
可以是 Object
、int
、long
、float
和 double
等。public native Object getObject(Object o, long offset); |
putXxx
和 getXxx
也提供了按绝对基地址操作内存的方法。public native byte getByte(long address); |
从内存读取值时,除非满足以下情况之一,否则结果不确定:
+objectFieldOffset
从字段的 Field
对象获取的,o
指向的对象的类与字段所属的类兼容。o
指向的对象(无论是否为 null
)分别是通过 staticFieldOffset
和 staticFieldBase
从 Field
对象获得的。o
指向的是一个数组,偏移量是一个形式为 B+N*S
的整数,其中 N
是数组的有效索引,B
和 S
分别是通过 arrayBaseOffset
和 arrayIndexScale
获得的值。++做一些“不确定”的测试,比如使用
+byte
相关的方法操作int
所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。
在 Java NIO
(New I/O
)中,分配堆外内存使用了 Unsafe
类的 allocateMemory
方法。堆外内存是一种在 Java
虚拟机之外分配的内存,它不受 Java
堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O
操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java
堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting
)的机制来处理。
DirectByteBuffer(int cap) { |
当 DirectByteBuffer
对象仅被 Cleaner
对象(虚引用)引用时,它可以在任意一次 GC
中被垃圾回收。在 DirectByteBuffer
对象被垃圾回收后,Cleaner
对象会被加入到引用队列,ReferenceHandler
线程将调用 Deallocator
对象的 run
方法,从而实现本地内存的自动释放。
private static class Deallocator implements Runnable { |
Unsafe
提供了 3
个 CAS
相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU
会自动将该位置更新为新值,否则,CPU
不做任何操作。这些方法的底层实现对应着 CPU
指令 cmpxchg
。
// 如果 Java 变量当前符合预期,则自动将其更新为 x。 |
在 AtomicInteger
的实现中,静态字段 valueOffset
即为字段 value
的内存偏移地址,valueOffset
的值在 AtomicInteger
初始化时,在静态代码块中通过 Unsafe
的 objectFieldOffset
方法获取。
public class AtomicInteger extends Number implements java.io.Serializable { |
CAS 更新变量的值的内存变化如下:
+ + +配合 ClassLayout
打印 AtomicInteger
的内部结构更直观地感受 offset
的含义:
java.util.concurrent.atomic.AtomicInteger object internals: |
Java
中 synchronized
锁的机制、存储结构、优化措施以及升级过程,并通过 jol-core
演示 Mark Word
的变化来验证锁升级的多个 case
。
+ Java
类 Cleaner
和 Finalizer
都实现了一种 finalization
机制,前者更轻量和强大,你可能在了解 NIO
的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize
方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是 Reference
的具体子类。Reference
作为和 GC
紧密联系的类,你可能从很多文字描述中了解过 SoftReference
、WeakReference
还有 PhantomReference
但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从 Root
对象可达”这条规则再面对 Reference
的子类时是否产生过割裂感;你是否好奇过 Finalizer
如何和重写 finalize
方法的类产生联系,本文将从 Cleaner
和 Finalizer
的源码揭示一些你可能已知的结论背后的朴素原理。
--待完善
+本文的写作动机继承自 Java 类 Reference 的源码分析,有时候也会自我怀疑研究一个涉及大家极力劝阻使用的
finalize
是否浪费精力,只能说确实如此!要不是半途而废会膈应难受肯定就停了!只能说这个过程确实帮助自己对Java
引用和GC
对其的处理有更加深刻的理解。
利用 synchronized
实现同步的基础:Java
中的每一个对象都可以作为锁。具体表现为以下 3
种形式。
PhantomReference
对象在垃圾收集器确定其关联对象
可以被回收时或可以被回收后一段时间,将被入队。“可以被回收”更明确的描述是“虚引用的关联对象
变成 phantom reachable
,即只有虚引用引用了它”。但是和软引用和弱引用不同,当虚引用入队时并不会被垃圾收集器自动清理(其关联对象)。一个 phantom reachable
的对象会一直维持原样直到所有虚引用被清理或者它们自身变得不可达。
PhantomReference
的代码非常简单:
PhantomReference
仅提供了一个 public
构造函数,必须提供 ReferenceQueue
参数。它不像 SoftReference
和 WeakReference
可以离开 ReferenceQueue
单独使用,尽管 queue
可以为 null
,但是这样做并没有意义。get()
返回 null
,这意味着不能通过 PhantomReference
获取其关联的对象 referent
。+++
get()
返回null
并不是可以随意忽略的事情,它保证了phantom reachable
对象不会被重新触达和修改(这是为清理工作留出时间吗)。
public class PhantomReference<T> extends Reference<T> { |
通过以下示例验证 GC
不会自动清理虚引用的关联对象:
public static void main(String[] args) throws InterruptedException { |
虚引用最常用于以比 finalization
更灵活的方式安排清理工作,比如其子类 Cleaner
就是一种基于虚引用的清理器,它比 finalization
更轻量但更强大。Cleaner
追踪其关联对象
并封装任意的清理代码,在 GC
检测到其关联对象
变成 phantom reachable
后一段时间,Reference-Handler
线程将运行清理代码。同时 Cleaner
可以被直接调用,它是线程安全的并且可以保证清理代码最多运行一次。但是 Cleaner
不是 finalization
的替代品,为了避免阻塞 Reference-Handler
线程,清理代码应极其简单和直接。
Cleaner
的构造函数为 private
,仅可通过 create
方法创建实例。
Class
对象。synchronized
括号里配置的对象。referent
: 关联对象
dummyQueue
: 假队列,需要它仅仅是因为 PhantomReference
的构造函数需要一个 queue
参数,但是这个 queue
完全没用,在 Reference
中 Reference-Handler
线程会显式调用 cleaners
而不会执行入队操作。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
+public class Cleaner extends PhantomReference<Object> { |
cleaner
synchronized
同步Cleaner
自身维护一个双向链表存储 cleaners
,通过静态变量 first
存储头节点,以防止 cleaners
比其关联对象
更早被 GC
。// 头节点 |
在 Reference
中 Reference-Handler
线程对于 Cleaner
类型的对象,会显式地调用其 clean
方法并返回,而不会将其入队。
synchronized
同步,从双链表上移除自身thunk
的 run
方法public void clean() { |
Cleaner
对象被 Cleaner
类的双链表直接或间接引用(强引用),因此不会被垃圾回收GC
特殊地对待虚引用的关联对象,当关联对象从 reachable
变成 phantom reachable
,GC
将 Cleaner
对象将加入 pending-list
Reference-Handler
线程又将其移除并调用 clean
方法Cleaner
对象变成 unreachable
并最终被垃圾回收,其关联对象也被垃圾回收++ + +注意,Cleaner 对象本身在被调用完毕之前始终是被静态变量引用,是
+reachable
的,我们讨论的被判定为可回收的、变成phantom reachable
状态的是关联对象。
++事实上,个人猜测“虚引用的关联对象不像软引用和弱引用会被自动清理”描述的仅仅是一个表象,判断是否要被垃圾回收的根本法则仍然是“对象是否从
+Root
对象可达”,软引用和弱引用的关联对象
之所以会被垃圾回收是因为它们在加入pending-list
时被从引用对象
断开,否则当引用对象
被添加到引用队列
时,引用队列
如果从Root
对象可达,将导致关联对象
也从Root
对象可达。在Reference
的clear()
的注释中提及该方法只被Java
代码调用,GC
不需要调用该方法就可以直接清理,肯定是GC
有直接清理关联对象
的场景。同时Reference
类有一句注释“GC
在检测到关联对象
有特定的可达性变化后,将把引用对象
添加到引用队列
”,它并未将特定的可达性变化直接描述为关联对象
变为不可达。目前尚未从JVM
源代码验证该猜测。
FinalReference
用于实现 finalization
,其代码很简单。
class FinalReference<T> extends Reference<T> { |
其子类 Finalizer
继承自 FinalReference
,Cleaner
在代码设计上和它非常相似。
Finalizer
的构造函数为 private
,仅可通过 register
方法创建实例。
JVM
层面,synchronized
锁是基于进入和退出 Monitor
来实现的,每一个对象都有一个 Monitor
与之相关联。monitorenter
和 monitorexit
指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。finalizee
: 关联对象
,即重写了 finalize
方法的类的实例queue
: 引用队列-锁存在哪里呢?锁里面又会存储什么信息呢?
++-根据注释
register
由VM
调用,我们可以合理猜测,这里就是重写了finalize
方法的类的实例和Finalizer
对象关联的起点。对象头
-
synchronized
用的锁是存在Java
对象头(object header
)里的。如果对象是数组类型,则虚拟机用3
字宽(Word
)存储对象头,如果对象是非数组类型,则用2
字宽存储对象头。在32
位虚拟机中,1
字宽等于4
字节,即32bit
。在64
位虚拟机中,1
字宽等于8
字节,即64bit
。-
Java
对象头的组成结构如下:- -
- - -长度 -内容 -说明 -- -- 32/64bit
- Mark Word
存储对象的 -hashCode
或锁信息- -- 32/64bit
- Class Metadata Address
存储指向对象类型数据的指针 -- +- 32/64bit
- Array length
数组的长度(如果当前对象是数组) -+ +
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;
}+
SharedSecrets
的javaLangAccess
通过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();
}
});
}+
System
的setJavaLangAccess
方法在initializeSystemClass
方法中被调用。这里正对应着FinalizerThread
的run
方法中等待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");
}
protected void finalize() throws Throwable {
System.out.println("finalized");
save = this;
}
public void echo() {
System.out.println("I am alive.");
}
}
}参考文章
+]]> ++ + +java ++ synchronized 锁机制的分析和验证 +/2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/ +本文详细介绍了 Java
中synchronized
锁的机制、存储结构、优化措施以及升级过程,并通过jol-core
演示Mark Word
的变化来验证锁升级的多个case
。 + + +++待完善
+利用
+synchronized
实现同步的基础:Java
中的每一个对象都可以作为锁。具体表现为以下3
种形式。+
+- 对于普通同步方法,锁是当前实例对象。
+- 对于静态同步方法,锁是当前类的
+Class
对象。- 对于同步方法块,锁是
+synchronized
括号里配置的对象。当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
++
+- 在
+JVM
层面,synchronized
锁是基于进入和退出Monitor
来实现的,每一个对象都有一个Monitor
与之相关联。- 在字节码层面,同步方法块是使用
+monitorenter
和monitorexit
指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。存储结构
++锁存在哪里呢?锁里面又会存储什么信息呢?
+对象头
+
synchronized
用的锁是存在Java
对象头(object header
)里的。如果对象是数组类型,则虚拟机用3
字宽(Word
)存储对象头,如果对象是非数组类型,则用2
字宽存储对象头。在32
位虚拟机中,1
字宽等于4
字节,即32bit
。在64
位虚拟机中,1
字宽等于8
字节,即64bit
。+
Java
对象头的组成结构如下:+ +
+ + +长度 +内容 +说明 ++ ++ 32/64bit
+ Mark Word
存储对象的 +hashCode
或锁信息+ ++ 32/64bit
+ Class Metadata Address
存储指向对象类型数据的指针 ++ + 32/64bit
+ Array length
数组的长度(如果当前对象是数组) +Mark Word
Java
对象头里的Mark Word
里默认存储对象的HashCode
,分代年龄和锁标记位。在运行期间,Mark Word
里存储的数据会随着锁标志位的变化而变化。Mark Word
可能变化为另外4
种数据。以
@@ -5454,494 +5610,545 @@32
位虚拟机为例:- +探索 Java 类 Cleaner 和 Finalizer -/2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/ -Java
类Cleaner
和Finalizer
都实现了一种finalization
机制,前者更轻量和强大,你可能在了解NIO
的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize
方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是Reference
的具体子类。Reference
作为和GC
紧密联系的类,你可能从很多文字描述中了解过SoftReference
、WeakReference
还有PhantomReference
但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从Root
对象可达”这条规则再面对Reference
的子类时是否产生过割裂感;你是否好奇过Finalizer
如何和重写finalize
方法的类产生联系,本文将从Cleaner
和Finalizer
的源码揭示一些你可能已知的结论背后的朴素原理。 +Spring AutowiredAnnotationBeanPostProcessor 的源码分析 +/2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/ +在 +Spring
中,AutowiredAnnotationBeanPostProcessor
是一个非常重要的后处理器,它可以自动装配标注注解的字段和方法,默认使用@Autowired
和@Value
注解,可以支持JSR-330
的@Inject
注解。本文通过分析源码介绍它的调用时机和工作原理。 +介绍
+
AutowiredAnnotationBeanPostProcessor
顾名思义,是自动装配注解的BeanPostProcessor
,但是它处理的不仅仅是@Autowired
这一个注解。个人认为Autowired Annotation
的意思更接近“用于标注目标是被自动装配的注解”。使用“目标”是为了表达注解标注的目标不仅仅限于字段,更是包括构造函数、方法、方法参数以及注解;使用“被自动装配”是为了表达注解描述的是目标的特征或者被处理的结果,体现出被动的语义更准确;使用“注解”是为了表达注解的种类不仅仅限于@Autowired
,还包括@Value
和@Inject
,它们都指示目标需要被自动装配处理。通过
+AutowiredAnnotationBeanPostProcessor
的构造函数可以看到@Inject
注解的特别之处,为了使用它,需要在Maven
配置中额外引入javax.inject
依赖。+ +
public AutowiredAnnotationBeanPostProcessor() {
this.autowiredAnnotationTypes.add(Autowired.class);
this.autowiredAnnotationTypes.add(Value.class);
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
logger.info("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}入口:
populateBean
方法我们在Spring Bean 加载过程中介绍过为
bean
填充属性值发生在populateBean
方法中。我们也将直接从这里开始跟踪代码的处理过程。--本文的写作动机继承自 Java 类 Reference 的源码分析,有时候也会自我怀疑研究一个涉及大家极力劝阻使用的
+finalize
是否浪费精力,只能说确实如此!要不是半途而废会膈应难受肯定就停了!只能说这个过程确实帮助自己对Java
引用和GC
对其的处理有更加深刻的理解。个人认为宽松地讲,“填充属性”等于“注入属性”等于“自动装配”,前两者更侧重处理的结果,后者更侧重过程的特征,但请注意在具体的代码上下文中应辨析区别。例如为
bean
填充属性是Spring
的重要目标之一,基于Autowired Annotation
进行自动装配某一个后处理器的功能,是Spring
实现目标的其中一个具体方式。虚引用之 Cleaner
虚引用介绍
-
PhantomReference
对象在垃圾收集器确定其关联对象
可以被回收时或可以被回收后一段时间,将被入队。“可以被回收”更明确的描述是“虚引用的关联对象
变成phantom reachable
,即只有虚引用引用了它”。但是和软引用和弱引用不同,当虚引用入队时并不会被垃圾收集器自动清理(其关联对象)。一个phantom reachable
的对象会一直维持原样直到所有虚引用被清理或者它们自身变得不可达。-
PhantomReference
的代码非常简单:-
+- -
PhantomReference
仅提供了一个public
构造函数,必须提供ReferenceQueue
参数。它不像SoftReference
和WeakReference
可以离开ReferenceQueue
单独使用,尽管queue
可以为null
,但是这样做并没有意义。- -
get()
返回null
,这意味着不能通过PhantomReference
获取其关联的对象referent
。+
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
// ...
// 如果存在 InstantiationAwareBeanPostProcessor 或者需要检查依赖
if (hasInstAwareBpps || needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
// 如果存在 InstantiationAwareBeanPostProcessor
if (hasInstAwareBpps) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
// AutowiredAnnotationBeanPostProcessor 实现了 InstantiationAwareBeanPostProcessor 接口
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 调用 postProcessPropertyValues 方法
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
// ...
}--+
get()
返回null
并不是可以随意忽略的事情,它保证了phantom reachable
对象不会被重新触达和修改(这是为清理工作留出时间吗)。有时候在
Spring
中看到BeanPostProcessor
并不能代表将目光转向该接口的方法实现。不同BeanPostProcessor
的子接口存在不同的调用时机。AutowiredAnnotationBeanPostProcessor
间接实现了InstantiationAwareBeanPostProcessor
并直接实现了MergedBeanDefinitionPostProcessor
,这是我们今天要关注的两个重点接口。- -
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();
}+
AutowiredAnnotationBeanPostProcessor
是什么时候注册的呢?以
+AnnotationConfigApplicationContext
为例,它在构造函数中创建了AnnotatedBeanDefinitionReader
,AnnotatedBeanDefinitionReader
又在构造函数中注册了基于注解配置的处理器:-
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);Cleaner 介绍
虚引用最常用于以比
-finalization
更灵活的方式安排清理工作,比如其子类Cleaner
就是一种基于虚引用的清理器,它比finalization
更轻量但更强大。Cleaner
追踪其关联对象
并封装任意的清理代码,在GC
检测到其关联对象
变成phantom reachable
后一段时间,Reference-Handler
线程将运行清理代码。同时Cleaner
可以被直接调用,它是线程安全的并且可以保证清理代码最多运行一次。但是Cleaner
不是finalization
的替代品,为了避免阻塞Reference-Handler
线程,清理代码应极其简单和直接。构造函数
+
Cleaner
的构造函数为private
,仅可通过create
方法创建实例。其中就包括
+AutowiredAnnotationBeanPostProcessor
。后处理 PropertyValues
AutowiredAnnotationBeanPostProcessor
实现了InstantiationAwareBeanPostProcessor
接口,该接口关注bean
的实例化:-
-- -
referent
:关联对象
- +
dummyQueue
: 假队列,需要它仅仅是因为PhantomReference
的构造函数需要一个queue
参数,但是这个queue
完全没用,在Reference
中Reference-Handler
线程会显式调用cleaners
而不会执行入队操作。- +
postProcessBeforeInstantiation
(实例化前)- +
postProcessAfterInstantiation
(实例化后)postProcessPropertyValues
(实例化后)- -
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
。+++
postProcessPropertyValues
方法在工厂将给定属性值应用到给定bean
之前对给定属性值进行后处理。允许检查是否满足所有依赖关系,例如基于bean
属性setters
上的@Required
注解进行检查。还允许替换要应用的属性值,通常是通过基于原始PropertyValues
创建新的MutablePropertyValues
实例,并添加或删除特定值。+
postProcessPropertyValues
方法做了两件事情:+
- - -- 查找需要自动装配的元数据
+- 注入
- -
// 头节点
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 方法
在
-Reference
中Reference-Handler
线程对于Cleaner
类型的对象,会显式地调用其clean
方法并返回,而不会将其入队。-
-- 使用
-synchronized
同步,从双链表上移除自身- 调用
-thunk
的run
方法+
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;
}-
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
// 查找自动装配元数据
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
// 注入
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}Cleaner 处理流程
-
- 创建的
-Cleaner
对象被Cleaner
类的双链表直接或间接引用(强引用),因此不会被垃圾回收- 一切的起点仍然是
-GC
特殊地对待虚引用的关联对象,当关联对象从reachable
变成phantom reachable
,GC
将Cleaner
对象将加入pending-list
- -
Reference-Handler
线程又将其移除并调用clean
方法- 在调用完毕后,
+Cleaner
对象变成unreachable
并最终被垃圾回收,其关联对象也被垃圾回收查找自动装配元数据
++这部分代码体现了注入(Injection)和自动装配(Autowiring)的等价性。
+InjectionMetadata
和AutowiringMetadata
的含义是用于注入(自动装配)的元数据。+
InjectionMetadata
是用于管理注入元数据的内部类,不适合直接在应用程序中使用。它和Class
是一对一的关系,封装了需要被注入的元素InjectedElement
。一个InjectedElement
对应着一个字段(Field
)或一个方法(Method
),分别对应着两个实现类AutowiredFieldElement
和AutowiredMethodElement
。这里再次体现了被注入、被自动装配的语义。查找自动装配元数据的过程如下:
++
- 先从缓存中获取,如果存在且不需要刷新,则直接返回结果
+- 否则构建自动装配元数据并放入缓存
-- +注意,Cleaner 对象本身在被调用完毕之前始终是被静态变量引用,是
+reachable
的,我们讨论的被判定为可回收的、变成phantom reachable
状态的是关联对象。注意:在
postProcessPropertyValues
第一次调用findAutowiringMetadata
缓存中就已经有结果了。什么时候构建并存入缓存的呢?+
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, PropertyValues pvs) {
// 缓存 key,如果没有指定退化为使用全限定类名
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// 双重检查
// 先从缓存中获取
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
// 检测是否需要刷新
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
try {
// 构建自动装配元数据
metadata = buildAutowiringMetadata(clazz);
// 放入缓存
this.injectionMetadataCache.put(cacheKey, metadata);
}
catch (NoClassDefFoundError err) {
throw new IllegalStateException("Failed to introspect bean class [" + clazz.getName() +
"] for autowiring metadata: could not find class that it depends on", err);
}
}
}
}
return metadata;
}
public static boolean needsRefresh(InjectionMetadata metadata, Class<?> clazz) {
// metadata.targetClass != clazz 的场景是什么?
return (metadata == null || metadata.targetClass != clazz);
}构建自动装配元数据
构建自动装配元数据只需要给定一个
Class
,沿着给定的Class
的父类向上循环查找直到Object
类。在每个循环中,先遍历当前类声明的所有属性,找到标注了自动装配注解的属性,为其创建AutowiredFieldElement
并添加到临时集合,再遍历当前类声明的所有方法,找到标注了自动装配注解的方法,为其创建AutowiredMethodElement
并添加到临时集合。最后汇总InjectedElement
封装到InjectionMetadata
中。--事实上,个人猜测“虚引用的关联对象不像软引用和弱引用会被自动清理”描述的仅仅是一个表象,判断是否要被垃圾回收的根本法则仍然是“对象是否从
+Root
对象可达”,软引用和弱引用的关联对象
之所以会被垃圾回收是因为它们在加入pending-list
时被从引用对象
断开,否则当引用对象
被添加到引用队列
时,引用队列
如果从Root
对象可达,将导致关联对象
也从Root
对象可达。在Reference
的clear()
的注释中提及该方法只被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);
}
}-
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<InjectionMetadata.InjectedElement>();
Class<?> targetClass = clazz;
do {
final LinkedList<InjectionMetadata.InjectedElement> currElements =
new LinkedList<InjectionMetadata.InjectedElement>();
// 处理字段 Field -> AutowiredFieldElement
ReflectionUtils.doWithLocalFields(targetClass, new ReflectionUtils.FieldCallback() {
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
// 查找表示需被自动装配的注解:@Autowired、@Value、@Inject(可选)
AnnotationAttributes ann = findAutowiredAnnotation(field);
if (ann != null) {
if (Modifier.isStatic(field.getModifiers())) {
// 不支持静态字段
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation is not supported on static fields: " + field);
}
return;
}
// 确定 required
boolean required = determineRequiredStatus(ann);
// 根据 field 和 required 创建 AutowiredFieldElement 并添加
currElements.add(new AutowiredFieldElement(field, required));
}
}
});
// 处理方法 Method -> AutowiredMethodElement
ReflectionUtils.doWithLocalMethods(targetClass, new ReflectionUtils.MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
// 查找表示需被自动装配的注解:@Autowired、@Value、@Inject(可选)
AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
// 不支持静态方法
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation is not supported on static methods: " + method);
}
return;
}
if (method.getParameterTypes().length == 0) {
// 不支持无参数的方法
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation should only be used on methods with parameters: " +
method);
}
}
// 确定 required
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
// 创建 AutowiredMethodElement 并添加
currElements.add(new AutowiredMethodElement(method, required, pd));
}
}
});
elements.addAll(0, currElements);
// 向父类继续查找
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
// 封装为 InjectionMetadata 返回
return new InjectionMetadata(clazz, elements);
}其子类
-Finalizer
继承自FinalReference
,Cleaner
在代码设计上和它非常相似。构造函数
+
Finalizer
的构造函数为private
,仅可通过register
方法创建实例。注入
对于字段来说,注入意味着将一个解析得到的
+value
通过反射设置到字段中;对于方法来说,注入意味着解析得到方法参数的value
,然后通过反射调用方法。+
InjectionMetadata
的inject
方法比较简单,内部会遍历并调用InjectedElement
的inject
方法,AutowiredFieldElement
和AutowiredMethodElement
各自实现了inject
方法。+ +
public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable {
Collection<InjectedElement> elementsToIterate =
(this.checkedElements != null ? this.checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
boolean debug = logger.isDebugEnabled();
// 遍历(InjectedElement 包装的可能是字段,也可能是方法)
for (InjectedElement element : elementsToIterate) {
if (debug) {
logger.debug("Processing injected element of bean '" + beanName + "': " + element);
}
// 注入
element.inject(target, beanName, pvs);
}
}
}不论是
AutowiredFieldElement
还是AutowiredMethodElement
,inject
的过程都比较相似:-
-- -
finalizee
:关联对象
,即重写了finalize
方法的类的实例- +
queue
: 引用队列- 都使用
+DependencyDescriptor
描述即将被注入的特定依赖项,DependencyDescriptor
包装了构造函数参数、方法参数或者字段,允许以统一的方式访问它们的元数据- 都会缓存
+DependencyDescriptor
- 都会记录自动装配的
+bean
,用于判断是否使用DependencyDescriptor
的变体ShortcutDependencyDescriptor
优化缓存- 都通过
beanFactory.resolveDependency
解析依赖-根据注释
+register
由VM
调用,我们可以合理猜测,这里就是重写了finalize
方法的类的实例和Finalizer
对象关联的起点。字段注入
+ +
private class AutowiredFieldElement extends InjectionMetadata.InjectedElement {
// 是否必须
private final boolean required;
// 是否已缓存
private volatile boolean cached = false;
// Field 依赖描述符的缓存
private volatile Object cachedFieldValue;
public AutowiredFieldElement(Field field, boolean required) {
super(field, null);
this.required = required;
}
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
// 获取要注入的目标(Field 对象)
Field field = (Field) this.member;
// value
Object value;
if (this.cached) {
// 如果已缓存,解析已缓存的参数
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
}
else {
// 创建依赖描述符
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet<String>(1);
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
// 通过 beanFactory 解析依赖得到 value
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
}
synchronized (this) {
// 如果未缓存,则缓存
if (!this.cached) {
if (value != null || this.required) {
// 缓存 DependencyDescriptor
this.cachedFieldValue = desc;
// 注册依赖关系,用于控制销毁顺序
registerDependentBeans(beanName, autowiredBeanNames);
// 如果自动装配的 bean 刚好只有一个
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
// 检测工厂里存在 bean
if (beanFactory.containsBean(autowiredBeanName)) {
if (beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
// 替换为具有预先解析的目标 bean 名称的 DependencyDescriptor 变体
this.cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
}
}
else {
this.cachedFieldValue = null;
}
this.cached = true;
}
}
}
if (value != null) {
// 最后,通过反射将 value 设置到 field
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
}方法注入
+-不理解缓存
DependencyDescriptor
代码上的注释:Shortcut for avoiding synchronization…+
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);
}
}-
private class AutowiredMethodElement extends InjectionMetadata.InjectedElement {
// 是否必须
private final boolean required;
// 是否已缓存
private volatile boolean cached = false;
// Method 参数依赖描述符的缓存
private volatile Object[] cachedMethodArguments;
public AutowiredMethodElement(Method method, boolean required, PropertyDescriptor pd) {
super(method, pd);
this.required = required;
}
protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
if (checkPropertySkipping(pvs)) {
return;
}
// 获取要注入的目标(Method)
Method method = (Method) this.member;
// 方法的参数
Object[] arguments;
if (this.cached) {
// Shortcut for avoiding synchronization...
// 不理解这个注释
arguments = resolveCachedArguments(beanName);
}
else {
// 获取方法的参数类型数组
Class<?>[] paramTypes = method.getParameterTypes();
arguments = new Object[paramTypes.length];
DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
Set<String> autowiredBeans = new LinkedHashSet<String>(paramTypes.length);
TypeConverter typeConverter = beanFactory.getTypeConverter();
// 遍历
for (int i = 0; i < arguments.length; i++) {
MethodParameter methodParam = new MethodParameter(method, i);
// 为每个方法参数创建依赖描述符
DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
currDesc.setContainingClass(bean.getClass());
descriptors[i] = currDesc;
try {
// 通过 beanFactory 解析依赖得到 value
Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
if (arg == null && !this.required) {
arguments = null;
break;
}
// 赋值
arguments[i] = arg;
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
}
}
synchronized (this) {
// 如果未缓存,则缓存
if (!this.cached) {
if (arguments != null) {
this.cachedMethodArguments = new Object[paramTypes.length];
// 缓存 DependencyDescriptor
for (int i = 0; i < arguments.length; i++) {
this.cachedMethodArguments[i] = descriptors[i];
}
// 注册依赖关系
registerDependentBeans(beanName, autowiredBeans);
// 如果自动装配的 bean 数量等于参数的数量
if (autowiredBeans.size() == paramTypes.length) {
Iterator<String> it = autowiredBeans.iterator();
// 遍历
for (int i = 0; i < paramTypes.length; i++) {
String autowiredBeanName = it.next();
// 检测工厂里存在 bean
if (beanFactory.containsBean(autowiredBeanName)) {
if (beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
// 替换为具有预先解析的目标 bean 名称的 DependencyDescriptor 变体
this.cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
descriptors[i], autowiredBeanName, paramTypes[i]);
}
}
}
}
}
else {
this.cachedMethodArguments = null;
}
this.cached = true;
}
}
}
if (arguments != null) {
// 通过反射调用方法
try {
ReflectionUtils.makeAccessible(method);
method.invoke(bean, arguments);
}
catch (InvocationTargetException ex){
throw ex.getTargetException();
}
}
}
private Object[] resolveCachedArguments(String beanName) {
if (this.cachedMethodArguments == null) {
return null;
}
Object[] arguments = new Object[this.cachedMethodArguments.length];
// 遍历已缓存的方法参数
for (int i = 0; i < arguments.length; i++) {
// 解析已缓存的参数
arguments[i] = resolvedCachedArgument(beanName, this.cachedMethodArguments[i]);
}
return arguments;
}
}添加
Finalizer
-
- +- 使用
-synchronized
同步- -
Finalizer
自身维护一个双向链表存储finalizers
,通过静态变量unfinalized
存储头节点解析已缓存的方法参数或字段
++为什么在这里
+beanFactory.resolveDependency
需要的参数和未缓存时不一样啊?虽然内部会通过相同的方式获得typeConverter
,但是很奇怪啊。-
private Object resolvedCachedArgument(String beanName, Object cachedArgument) {
if (cachedArgument instanceof DependencyDescriptor) {
DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument;
return this.beanFactory.resolveDependency(descriptor, beanName, null, null);
}
else {
return cachedArgument;
}
}+
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;
}
}解析依赖
解析依赖的过程暂不深入。
+-
public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName,
Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (javaUtilOptionalClass == descriptor.getDependencyType()) {
return new OptionalDependencyFactory().createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330ProviderFactory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
// 如果依赖是懒加载,创建一个代理对象
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
// 一般情况
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}
Finalizer
线程- +
finalizers
的清理通常是由一条名为Finalizer
的线程处理。启动任意一个非常简单的Java
程序,通过JVM
相关的工具,比如JConsole
,你都能看到一个名为Finalizer
的线程。-
public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
try {
Object shortcut = descriptor.resolveShortcut(this);
if (shortcut != null) {
return shortcut;
}
Class<?> type = descriptor.getDependencyType();
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
// 如果 value 是 String 类型
if (value instanceof String) {
// 解析给定的嵌入值,例如替换占位符 ${},但不解析 SpEL 表达式
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
// 解析 SpEL 表达式
value = evaluateBeanDefinitionString(strVal, bd);
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
return (descriptor.getField() != null ?
converter.convertIfNecessary(value, type, descriptor.getField()) :
converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
if (isRequired(descriptor)) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
String autowiredBeanName;
Object instanceCandidate;
if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(type, matchingBeans);
}
else {
// In case of an optional Collection/Map, silently ignore a non-unique case:
// possibly it was meant to be an empty collection of multiple regular beans
// (before 4.3 in particular when we didn't even look for collection beans).
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
else {
// We have exactly one match.
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}
if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}
return (instanceCandidate instanceof Class ?
descriptor.resolveCandidate(autowiredBeanName, type, this) : instanceCandidate);
}
finally {
ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
}
}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) {
// 忽略并继续
}
}
}
}构建自动装配元数据的时机
你在
+Debug
的时候也许会注意到,在第一次进入postProcessPropertyValues
方法,查找自动装配元数据时,就已经是从缓存中获取的了。那么究竟是什么时候构建自动装配元数据并放入缓存的呢?这就需要我们目前一直没有讲到的MergedBeanDefinitionPostProcessor
派上用场了。在postProcessMergedBeanDefinition
方法中,也调用了findAutowiringMetadata
方法,这才是真正的第一次查找自动装配元数据。-
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
if (beanType != null) {
// 查找自动装配元数据
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
// 检查配置成员
metadata.checkConfigMembers(beanDefinition);
}
}创建和启动
-
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();
}那么
+MergedBeanDefinitionPostProcessor
又是什么时候被调用的呢?在doCreateBean
方法中,创建实例后,填充属性前。-
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
throws BeanCreationException {
// 创建实例
// 允许 post-processors 修改合并过的 bean definition
synchronized (mbd.postProcessingLock) {
// 如果尚未被 MergedBeanDefinitionPostProcessor 应用过
if (!mbd.postProcessed) {
try {
// 应用
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
// 修改为被应用过
mbd.postProcessed = true;
}
}
// 为 bean 填充属性值
}获取 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;
}
}检测配置成员
+++
configMember
这个命名不太理解,指的是通过配置实现注入的Member
(Filed
和Method
的父类)吗?检测配置成员,如果不是外部管理的配置成员,则注册为外部管理的配置成员。在合并后的
+bean
定义中,externallyManagedConfigMembers
保存了外部管理的配置成员,用于标记一个配置成员是外部管理的。例如当一个字段同时标注了@Resource
和@Autowired
注解,当@Resouce
注解被处理后,该字段已经被标记,当@Autowired
注解被处理时,就会跳过该字段,避免重复注入造成冲突。++这里的外部管理感觉有点指向依赖注入的控制反转思想。
+-
public void checkConfigMembers(RootBeanDefinition beanDefinition) {
Set<InjectedElement> checkedElements = new LinkedHashSet<InjectedElement>(this.injectedElements.size());
// 遍历需要被注入的元素
for (InjectedElement element : this.injectedElements) {
Member member = element.getMember();
// 如果不是外部管理的配置成员
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
// 注册为外部管理的配置成员
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isDebugEnabled()) {
logger.debug("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
// 在 `InjectionMetadata#inject` 方法中,迭代的集合将会是它
this.checkedElements = checkedElements;
}finalize 的调用原理
关于如何调用
-finalize
方法涉及不少平时接触不到的代码。+
// 获取 JavaLangAccess
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
// 通过 JavaLangAccess 调用 finalizee 的 finalize 方法
jla.invokeFinalize(finalizee);
public static void setJavaLangAccess(JavaLangAccess jla) {
javaLangAccess = jla;
}++如果不了解具体的场景,可能会比较难想象这个标记的用处是什么。
+++]]>尽管
+@Autowired
注解配合@Value
注解可以很灵活,但是应尽量采取清晰明了的配置方式,让注入的结果一眼就能看出来。+ +java +spring ++ -使用 Vim +/2024/01/18/use-vim/ +本文记录了 -Vim
常用的快捷键作为备忘清单。 + --
SharedSecrets
的javaLangAccess
通过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();
}
});
}-
System
的setJavaLangAccess
方法在initializeSystemClass
方法中被调用。这里正对应着FinalizerThread
的run
方法中等待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");
}
protected void finalize() throws Throwable {
System.out.println("finalized");
save = this;
}
public void echo() {
System.out.println("I am alive.");
}
}
}参考文章
-]]>- -java -- 谈谈 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
)等一个或多个现象。隔离级别越高,效率越低,因此很多时候,我们需要在二者之间寻找一个平衡点。+
常用快捷键
移动光标
+
- 隔离级别 -脏读 -不可重复读 -幻读 +快捷键 +功能 - 读未提交 -Y -Y -Y ++ h
,←
光标向左移动一个字符 - +读提交 -N -Y -Y ++ 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]
删除/复制/粘贴
+ +
++ + +快捷键 +功能 ++ ++ 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
进入插入模式,在光标所在行的上一行插入新的一行 - 可重复读 -N -N -Y ++ r
进入替换模式,只会替换光标所在的字符一次 - +串行化 -N -N -N ++ R
进入替换模式,替换光标所在的字符,直到通过Esc退出 ++ + Esc
退出编辑模式,回到一般命令模式 --读未提交和串行化很少在实际应用中使用。
-通过以下示例说明隔离级别的影响,
-V1
、V2
和V3
在不同隔离级别下的值有所不同。+
保存和退出
-
- 事务 A -事务 B -读未提交 -读提交 -可重复读 -串行化 +快捷键 +功能 - -开启事务 -开启事务 -- - - - - -查询得到值 1 -- - - - - - - 查询得到值 1 -- - - + + :w
保存文件 - - 修改值为 2 -- - - + + :w!
若文件为只读,强制保存 - 查询得到值 V1 -- 2(读到B未提交的修改) -1 -1 -1 ++ :q
退出 Vim,如果文件已修改,将退出失败 - - 提交事务 -- - - + + :q!
强制退出 Vim,不保存文件修改 - 查询得到值 V2 -- 2 -2(读到B已提交的修改) -1 -1 ++ :wq
保存文件并退出 Vim - 提交事务 -- - - - + + :w filename
另存为新文件 - 查询得到值 V3 -- 2 -2 -2(A在事务期间数据一致) -1 ++ ZZ
退出 Vim,若文件无修改,则不保存退出;如果文件已修改,保存并退出 - 补充说明 -- - - - B的修改阻塞至A提交 ++ :r filename
读入另一个文件的数据并添加到光标所在行之后 通过测试验证以上结论可以帮助你更直观地感受隔离级别的作用:
--
-- 新建连接
-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
中,事务隔离是通过lock
、undo log
和read view
共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。-
MySQL
各个事务隔离级别的实现原理简述如下:-
-- 串行化:读加共享锁,写加排他锁,读写互斥
-- 读未提交:写加排他锁,读不加锁
-- 可重复读:第一次读操作时创建快照,基于该快照进行读取
-- 读提交:每次读操作时重置快照,基于该快照进行读取
-前两者通过锁(
-lock
)实现比较容易理解;后两者通过多版本并发控制(MVCC
)实现。MVCC
是一种实现非阻塞并发读的设计思路,在InnoDB
引擎中主要通过undo log
和read view
实现。以下示意图表现了在
-InnoDB
引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC
),当你基于快照读取时可以获得旧版本的数据。-
- - -- 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
-- 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
-在接下来我们将通过锁、事务
-ID
、回滚日志和一致性视图逐步介绍InnoDB
事务隔离的实现原理。锁(lock)
事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。
-MySQL
正是通过共享锁和排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC
就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。--在
-InnoDB
的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议。理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及
-MVCC
的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。事务 ID
在
- - -InnoDB
引擎中,每个事务都有唯一的一个事务ID
,叫做transaction id
。它是在事务开始的时候向InnoDB
的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段trx_id
,记录了插入或更新该行数据的事务ID
。创建事务的时机
事务启动方式如下:
--
-- 显式启动事务语句是
-begin
或start transaction
,配套的提交语句是commit
,回滚语句是rollback
。- 隐式启动事务语句是
-set autocommit = 0
,该设置将关闭自动提交。当你执行select
,将自动启动一个事务,直到你主动commit
或rollback
。但注意,实际上不论是显式启动事务情况下的
-begin
或start transaction
,还是隐式启动事务情况下的commit
或rollback
都不会立即创建一个新事务,而是直到第一次操作InnoDB
表的语句执行时,才会真正创建一个新事务。可以通过以下语句查看当前“活跃”的事务进行验证:
-- -
select * from information_schema.innodb_trx;--只读事务的事务
-ID
和更新事务不同。--可以使用
-commit work and chain;
在提交的同时开启下一次事务,减少一次begin;
指令的交互开销。回滚日志(undo log)
在
-InnoDB
引擎中,每条记录在更新的时候都会同时记录一条回滚操作。记录的最新值,通过回滚操作,可以得到之前版本的值。它的作用是:-
-- 数据回滚:当事务回滚或者数据库崩溃时,通过
-undolog
进行数据回滚。- 多版本并发控制:当读取一行记录时,如果该行记录已经被其他事务修改,通过
-undo log
读取之前版本的数据,以此实现非阻塞并发读。实际上,每一行数据还有一个隐藏字段
- - -roll_ptr
。很多相关资料简单地描述“roll_ptr
用于指向该行数据的上一个版本”,但是该说法容易让人误解旧版本的数据是物理上真实存在的,好像有一张链表结构的历史记录表按顺序记录了每一个版本的数据。有些资料会特地强调旧版本的数据不是物理上真实存在的,
- - -undo log
是逻辑日志,记录了与实际操作语句相反的操作,旧版本的数据是通过undo log
计算得到的。--说实话,在不了解细节的前提下,通过计算得到旧版本的数据更加反直觉。总而言之,
-InnoDB
的数据总是存储最新版本,尽管该版本所属的事务可能尚未提交;任何事务其实都是从最新版本开始回溯,直到获得该事务认为可见的版本。回滚日志的删除时机
回滚日志不会一直保留,在没有事务需要的时候,系统会自动判断和删除。基于该结论,我们应该避免使用长事务。长事务意味着系统里面可能会存在很老的
-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
不在数组中,表示这个版本是已提交的事务生成的,可见-
InnoDB
利用“所有数据都有多个版本,每个版本都记录了所属事务ID
”这个特性,实现了“秒级创建快照”的能力。有了这个能力,系统里面随后发生的更新,就和当前事务可见的数据无关了,当前事务读取时也不必再加锁。--以上“具体实现”相较于之前的“需求描述”显得有些啰嗦和复杂,然而这里的细节是值得推敲的。即便是林晓斌老师在《MySQL 实战 45 讲》中的详细讲解也让部分读者包括我本人感到困惑。
-林晓斌老师的数据版本可见性示意图如下,容易让人产生误解的地方在于三段式的划分给人一种已提交的事务全都是小于低水位的错觉。
- - -事实上,已提交事务的分布可能如下,大部分人的疑问其实只是“在大于等于低水位小于高水位的范围中,为什么会有已提交的事务”。
- - -要理解该问题需要理解另外一个问题——“创建
-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
语句后再执行读取,熬到在它之前甚至之后创建的事务提交。有些人可能想到了前者,但对于后者存疑或者不知道如何验证,其实测试并不复杂:
-+
额外功能
可视模式
+
- 事务 A -事务 B +快捷键 +功能 - - begin;
+ begin;
+ v
字符选择,将光标经过的地方反白选择 - - update t set k = 2 where id = 2;
(创建事务)+ + V
行选择,将光标经过的行反白选择 - - + update t set k = 666 where id = 1;
(创建事务)+ Ctrl + v
区块选择,用矩形的方式反白选择 - +- + commit;
+ y
复制反白选择的地方 ++ ++ d
删除反白选择的地方 ++ ++ ~
对反白选择的地方切换大小写 +多文件编辑
+ +
-+ + +快捷键 +功能 ++ + :n
编辑下一个文件 - - select * from t where id = 1;
(创建read view
,k = 666)+ + :N
编辑上一个文件 - - commit;
+ + :files
列出当前 Vim 打开的所有文件 --因此,严谨地说,创建事务的时机和创建一致性视图的时机是不同的。通过
-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;
(自动提交事务)+ :sp [filename]
打开一个新窗口 - - update t set k = k + 1 where id = 1;
(当前读)+ + Ctrl + w
+j
Ctrl + w
+↓
光标移动到下方的窗口 - - select * from t where id = 1;
(k = 3)+ + Ctrl + w
+k
Ctrl + w
+↑
光标移动到上方的窗口 - - commit;
+ + Ctrl + w
+q
:q
:close
关闭窗口 其实,更新数据是先读后写的,并且是“当前读”。
--
-- 当前读:读取一行数据的最新版本,并保证在读取时其他事务不能修改该行数据,因此需要在读取时加锁。以下操作属于当前读的情况:
--
-- 共享锁:
-select lock in share mode
- 排他锁:
-select for update
,update
,insert
,delete
- 快照读:在不加锁的情况下通过
-select
读取一行数据,但和“读未提交”隔离级别下单纯地读取最新版本不同,它是基于一个“快照”进行读取。因此在事务 A 中更新时,读取到的是事务 B 更新后的最新值,在事务 A 更新后,依据
-read view
的可见性原则,它可以看到自身事务的更新后的最新值 3。如果事务 B 尚未提交的情况下,事务 A 发起更新,会如何呢?这时候就轮到“两阶段锁协议”派上用场了:
--
-- 事务 B 在更新时,对改行数据加排他锁,在事务 B 提交时才会释放
-- 当事务 A 发起更新,将阻塞直到事务 B 提交
-+
关键词自动补全
+
- 事务 A -事务 B +快捷键 +功能 - -- start transaction with consistent snapshot;
(k = 1)- - - + begin;
+ Ctrl + x
+Ctrl + n
使用当前文件的内容文字作为关键词,予以补齐 - - + update t set k = k + 1 where id = 1;
(排他锁)+ Ctrl + x
+Ctrl + f
使用当前目录的文件名作为关键词,予以补齐 - +- update t set k = k + 1 where id = 1;
(阻塞至 B 提交)+ + Ctrl + x
+Ctrl + o
使用扩展名作为语法补充,以 Vim 内置的关键词,予以补齐 环境配置
+
-- -- + commit;
设置参数 +功能 - +- select * from t where id = 1;
(k = 3)+ + + :set nu
:set nonu
设置和取消行号 - - commit;
+ + :syntax on
:syntax off
是否依据程序相关语法显示不同颜色 至此,我们将锁和
-MVCC
在事务隔离的实现原理中串联起来了。两者是互相独立又互相协作的两个机制,前者实现了“当前读”,后者实现了“快照读”。总结
卡壳好几天,想到有不少好的文章却仍然会给读者留下困惑,想到自己在当初学习时对一些不严谨的表达抓耳挠腮想不通为什么,就有点不知道如何下笔。最终围绕着自己当初的一些困惑,一点一点修修补补完了。
++可以通过
+vim ~/.vimrc
修改配置文件。参考文章
]]>- mysql +linux +vim @@ -5999,515 +6206,455 @@ - 使用 Vim -/2024/01/18/use-vim/ -本文记录了 Vim
常用的快捷键作为备忘清单。 +k3s 的安装和使用 +/2024/01/30/installation-and-use-of-k3s/ +本文记录了 k3s
的安装和使用,相较于minikube
,前者是一个完全兼容的Kubernetes
发行版,安装和使用的体验更佳。 -常用快捷键
移动光标
- -
-- - -快捷键 -功能 -- -- 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
光标移动到上一个词 (以空格分隔的词) -查找和替换
+
安装
++参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!
+步骤
+
+- 获取并运行
+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- 可以通过使用
+kubectl
确认安装成功。刚安装的时候使用kubectl
需要root
权限。
$ sudo kubectl get node
NAME STATUS ROLES AGE VERSION
ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1- 实际上安装的就是一个
+k3s
可执行文件,kubectl
和crictl
只是软链接,指向k3s
。
$ ls /usr/local/bin/
crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl++安装的信息中显示了
+k3s
的service file
和environment 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 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。
+修改启动参数
第一种方式是修改启动参数。
++
+- +
sudo vim /etc/systemd/system/k3s.service
添加k3s
启动参数--write-kubeconfig-mode 644
ExecStart=/usr/local/bin/k3s \
server --write-kubeconfig-mode 644 \- +
systemctl daemon-reload
重新加载systemd
配置- +
systemctl restart k3s.service
重启服务- 验证修改生效
+
$ ll /etc/rancher/k3s/k3s.yaml
-rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml修改环境变量
第二种方式是修改环境变量。
++
+- +
sudo vim /etc/systemd/system/k3s.service.env
添加环境变量K3S_KUBECONFIG_MODE=644
K3S_KUBECONFIG_MODE=644- +
systemctl restart k3s.service
重启服务修改配置文件路径
第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。
++
+- 设置环境变量
+export KUBECONFIG=~/.kube/config
- 创建文件夹
+mkdir ~/.kube 2> /dev/null
- 复制配置信息
+sudo k3s kubectl config view --raw > "$KUBECONFIG"
- 修改配置文件的权限
+chmod 600 "$KUBECONFIG"
配置代理
涉及
+k8s
,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及k3s
安装脚本会自动使用当前shell
中的HTTP_PROXY
、HTTPS_PROXY
和NO_PROXY
,以及CONTAINERD_HTTP_PROXY
、CONTAINERD_HTTPS_PROXY
和CONTAINERD_NO_PROXY
变量(如果存在),并将它们写入systemd
服务的环境文件。比如我设置过shell
变量HTTP_PROXY
、HTTPS_PROXY
和NO_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
+
+- 使用
+kubectl create
命令创建管理Pod
的Deployment
。该Pod
根据提供的Docker
镜像运行容器。
kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080- 查看
+Deployment
:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h- 查看
+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创建 Service
+
+- 使用
+kubectl expose
命令将Pod
暴露给公网:
kubectl expose deployment hello-node --type=LoadBalancer --port=8080- 查看你创建的
+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- 使用
+curl
发起请求:
$ curl http://localhost:8080
NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511- 再次查看
+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 /清理
+
+- 删除
+Service
:
kubectl delete service hello-node- 删除
+Deployment
:
kubectl delete deployment hello-node卸载
+
+- 从 server 节点卸载 k3s
+
/usr/local/bin/k3s-uninstall.sh参考文章
+
+]]> +- 快速入门指南
+- 管理 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
++ + +k8s +k3s ++ 谈谈 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
)等一个或多个现象。隔离级别越高,效率越低,因此很多时候,我们需要在二者之间寻找一个平衡点。
- 快捷键 -功能 +隔离级别 +脏读 +不可重复读 +幻读 - -- /word
向光标之后搜索word -- -- ?word
向光标之前搜索word -- -- n
重复前一个查找操作 -- -- N
反向进行前一个查找操作 -- - :n1,n2s/original/replacement/g
在第n1行到第n2行之间查找original并替换为replacement +读未提交 +Y +Y +Y - - :1,$s/original/replacement/g
在第1行到最后一行之间查找original并替换为replacement +读提交 +N +Y +Y - - :1,$s/original/replacement/gc
在第1行到最后一行之间查找original并替换为replacement,替换前需确认 +可重复读 +N +N +Y - - :%s/original/replacement
在所有行中查找行中第一个出现的original并替换为replacement +串行化 +N +N +N --替换格式如下
+:[range]s/<pattern>/[string]/[flags] [count]
读未提交和串行化很少在实际应用中使用。
删除/复制/粘贴
- -
-- - -快捷键 -功能 -- -- 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
重做上一个操作 -- -- .
重复前一个操作 -进入编辑模式
+
通过以下示例说明隔离级别的影响,
+V1
、V2
和V3
在不同隔离级别下的值有所不同。-
- 快捷键 -功能 +事务 A +事务 B +读未提交 +读提交 +可重复读 +串行化 - -- i
进入插入模式,从光标所在处开始插入 -- -- I
进入插入模式,从光标所在行的第一个非空格开始插入 -- -- a
进入插入模式,从光标所在的下一个字符处开始插入 -- -- A
进入插入模式,从光标所在行的最后一个字符处开始插入 -- -- o
进入插入模式,在光标所在行的下一行插入新的一行 -- -- O
进入插入模式,在光标所在行的上一行插入新的一行 -- -- r
进入替换模式,只会替换光标所在的字符一次 -- - R
进入替换模式,替换光标所在的字符,直到通过Esc退出 +开启事务 +开启事务 ++ + + - -- Esc
退出编辑模式,回到一般命令模式 +查询得到值 1 ++ + + + 保存和退出
-
-- - -快捷键 -功能 -- - :w
保存文件 ++ 查询得到值 1 ++ + + - - :w!
若文件为只读,强制保存 ++ 修改值为 2 ++ + + - - :q
退出 Vim,如果文件已修改,将退出失败 +查询得到值 V1 ++ 2(读到B未提交的修改) +1 +1 +1 - - :q!
强制退出 Vim,不保存文件修改 ++ 提交事务 ++ + + - - :wq
保存文件并退出 Vim +查询得到值 V2 ++ 2 +2(读到B已提交的修改) +1 +1 - - :w filename
另存为新文件 +提交事务 ++ + + + - - ZZ
退出 Vim,若文件无修改,则不保存退出;如果文件已修改,保存并退出 +查询得到值 V3 ++ 2 +2 +2(A在事务期间数据一致) +1 - - :r filename
读入另一个文件的数据并添加到光标所在行之后 +补充说明 ++ + + + 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;
- 测试和验证
++ +
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
中,事务隔离是通过lock
、undo log
和read view
共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。+
MySQL
各个事务隔离级别的实现原理简述如下:+
+- 串行化:读加共享锁,写加排他锁,读写互斥
+- 读未提交:写加排他锁,读不加锁
+- 可重复读:第一次读操作时创建快照,基于该快照进行读取
+- 读提交:每次读操作时重置快照,基于该快照进行读取
+前两者通过锁(
+lock
)实现比较容易理解;后两者通过多版本并发控制(MVCC
)实现。MVCC
是一种实现非阻塞并发读的设计思路,在InnoDB
引擎中主要通过undo log
和read view
实现。以下示意图表现了在
+InnoDB
引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC
),当你基于快照读取时可以获得旧版本的数据。+
+ + +- 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
+- 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
+在接下来我们将通过锁、事务
+ID
、回滚日志和一致性视图逐步介绍InnoDB
事务隔离的实现原理。锁(lock)
事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。
+MySQL
正是通过共享锁和排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC
就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。++在
+InnoDB
的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议。理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及
+MVCC
的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。事务 ID
在
+ + +InnoDB
引擎中,每个事务都有唯一的一个事务ID
,叫做transaction id
。它是在事务开始的时候向InnoDB
的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段trx_id
,记录了插入或更新该行数据的事务ID
。创建事务的时机
事务启动方式如下:
++
+- 显式启动事务语句是
+begin
或start transaction
,配套的提交语句是commit
,回滚语句是rollback
。- 隐式启动事务语句是
+set autocommit = 0
,该设置将关闭自动提交。当你执行select
,将自动启动一个事务,直到你主动commit
或rollback
。但注意,实际上不论是显式启动事务情况下的
+begin
或start transaction
,还是隐式启动事务情况下的commit
或rollback
都不会立即创建一个新事务,而是直到第一次操作InnoDB
表的语句执行时,才会真正创建一个新事务。可以通过以下语句查看当前“活跃”的事务进行验证:
++ +
select * from information_schema.innodb_trx;++只读事务的事务
+ID
和更新事务不同。++可以使用
+commit work and chain;
在提交的同时开启下一次事务,减少一次begin;
指令的交互开销。回滚日志(undo log)
在
+InnoDB
引擎中,每条记录在更新的时候都会同时记录一条回滚操作。记录的最新值,通过回滚操作,可以得到之前版本的值。它的作用是:+
+- 数据回滚:当事务回滚或者数据库崩溃时,通过
+undolog
进行数据回滚。- 多版本并发控制:当读取一行记录时,如果该行记录已经被其他事务修改,通过
+undo log
读取之前版本的数据,以此实现非阻塞并发读。实际上,每一行数据还有一个隐藏字段
+ + +roll_ptr
。很多相关资料简单地描述“roll_ptr
用于指向该行数据的上一个版本”,但是该说法容易让人误解旧版本的数据是物理上真实存在的,好像有一张链表结构的历史记录表按顺序记录了每一个版本的数据。有些资料会特地强调旧版本的数据不是物理上真实存在的,
+ + +undo log
是逻辑日志,记录了与实际操作语句相反的操作,旧版本的数据是通过undo log
计算得到的。++说实话,在不了解细节的前提下,通过计算得到旧版本的数据更加反直觉。总而言之,
+InnoDB
的数据总是存储最新版本,尽管该版本所属的事务可能尚未提交;任何事务其实都是从最新版本开始回溯,直到获得该事务认为可见的版本。回滚日志的删除时机
回滚日志不会一直保留,在没有事务需要的时候,系统会自动判断和删除。基于该结论,我们应该避免使用长事务。长事务意味着系统里面可能会存在很老的
+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
不在数组中,表示这个版本是已提交的事务生成的,可见+
InnoDB
利用“所有数据都有多个版本,每个版本都记录了所属事务ID
”这个特性,实现了“秒级创建快照”的能力。有了这个能力,系统里面随后发生的更新,就和当前事务可见的数据无关了,当前事务读取时也不必再加锁。++以上“具体实现”相较于之前的“需求描述”显得有些啰嗦和复杂,然而这里的细节是值得推敲的。即便是林晓斌老师在《MySQL 实战 45 讲》中的详细讲解也让部分读者包括我本人感到困惑。
+林晓斌老师的数据版本可见性示意图如下,容易让人产生误解的地方在于三段式的划分给人一种已提交的事务全都是小于低水位的错觉。
+ + +事实上,已提交事务的分布可能如下,大部分人的疑问其实只是“在大于等于低水位小于高水位的范围中,为什么会有已提交的事务”。
+ + +要理解该问题需要理解另外一个问题——“创建
+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
语句后再执行读取,熬到在它之前甚至之后创建的事务提交。有些人可能想到了前者,但对于后者存疑或者不知道如何验证,其实测试并不复杂:
+-
- 快捷键 -功能 +事务 A +事务 B - - v
字符选择,将光标经过的地方反白选择 ++ begin;
begin;
- - V
行选择,将光标经过的行反白选择 ++ update t set k = 2 where id = 2;
(创建事务)- - Ctrl + v
区块选择,用矩形的方式反白选择 ++ update t set k = 666 where id = 1;
(创建事务)- - y
复制反白选择的地方 ++ commit;
- - d
删除反白选择的地方 ++ select * from t where id = 1;
(创建read view
,k = 666)- - ~
对反白选择的地方切换大小写 ++ commit;
多文件编辑
+
++因此,严谨地说,创建事务的时机和创建一致性视图的时机是不同的。通过
+start transaction with consistent snapshot;
可以在开启事务的同时立即创建read view
。当前读和快照读
现在我们知道在
+InnoDB
引擎中,一行数据存在多个版本。MVCC
使得在“可重复读”隔离级别下的事务好像与世无争。但是在以下示例中,事务 B 是在事务 A 的一致性视图之后创建和提交的,为什么事务 A 查询到的k
为 3 呢?-
- 快捷键 -功能 +事务 A +事务 B - -- :n
编辑下一个文件 -- -- :N
编辑上一个文件 -- -- :files
列出当前 Vim 打开的所有文件 ++ start transaction with consistent snapshot;
(k = 1)多窗口功能
-
-- - -快捷键 -功能 -- - :sp [filename]
打开一个新窗口 ++ update t set k = k + 1 where id = 1;
(自动提交事务)- - Ctrl + w
+j
Ctrl + w
+↓
光标移动到下方的窗口 ++ update t set k = k + 1 where id = 1;
(当前读)- - Ctrl + w
+k
Ctrl + w
+↑
光标移动到上方的窗口 ++ select * from t where id = 1;
(k = 3)- - Ctrl + w
+q
:q
:close
关闭窗口 ++ commit;
关键词自动补全
+
其实,更新数据是先读后写的,并且是“当前读”。
++
+- 当前读:读取一行数据的最新版本,并保证在读取时其他事务不能修改该行数据,因此需要在读取时加锁。以下操作属于当前读的情况:
++
+- 共享锁:
+select lock in share mode
- 排他锁:
+select for update
,update
,insert
,delete
- 快照读:在不加锁的情况下通过
+select
读取一行数据,但和“读未提交”隔离级别下单纯地读取最新版本不同,它是基于一个“快照”进行读取。因此在事务 A 中更新时,读取到的是事务 B 更新后的最新值,在事务 A 更新后,依据
+read view
的可见性原则,它可以看到自身事务的更新后的最新值 3。如果事务 B 尚未提交的情况下,事务 A 发起更新,会如何呢?这时候就轮到“两阶段锁协议”派上用场了:
++
+- 事务 B 在更新时,对改行数据加排他锁,在事务 B 提交时才会释放
+- 当事务 A 发起更新,将阻塞直到事务 B 提交
+-
- 快捷键 -功能 +事务 A +事务 B - - Ctrl + x
+Ctrl + n
使用当前文件的内容文字作为关键词,予以补齐 ++ start transaction with consistent snapshot;
(k = 1)- - Ctrl + x
+Ctrl + f
使用当前目录的文件名作为关键词,予以补齐 ++ begin;
- -- Ctrl + x
+Ctrl + o
使用扩展名作为语法补充,以 Vim 内置的关键词,予以补齐 ++ update t set k = k + 1 where id = 1;
(排他锁)环境配置
-
-- +设置参数 -功能 ++ update t set k = k + 1 where id = 1;
(阻塞至 B 提交)+ + - -+ commit;
- - :set nu
:set nonu
设置和取消行号 ++ + select * from t where id = 1;
(k = 3)- - :syntax on
:syntax off
是否依据程序相关语法显示不同颜色 ++ commit;
--可以通过
-vim ~/.vimrc
修改配置文件。参考文章
-
-]]> -- vim 程式編輯器
-- Vim 快捷键速查表
-- Vim 配置入门
-- - -linux -vim -- k3s 的安装和使用 -/2024/01/30/installation-and-use-of-k3s/ -本文记录了 k3s
的安装和使用,相较于minikube
,前者是一个完全兼容的Kubernetes
发行版,安装和使用的体验更佳。 - - -安装
--参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!
-步骤
-
-- 获取并运行
-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- 可以通过使用
-kubectl
确认安装成功。刚安装的时候使用kubectl
需要root
权限。
$ sudo kubectl get node
NAME STATUS ROLES AGE VERSION
ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1- 实际上安装的就是一个
-k3s
可执行文件,kubectl
和crictl
只是软链接,指向k3s
。
$ ls /usr/local/bin/
crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl--安装的信息中显示了
-k3s
的service file
和environment 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 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。
-修改启动参数
第一种方式是修改启动参数。
--
-- -
sudo vim /etc/systemd/system/k3s.service
添加k3s
启动参数--write-kubeconfig-mode 644
ExecStart=/usr/local/bin/k3s \
server --write-kubeconfig-mode 644 \- -
systemctl daemon-reload
重新加载systemd
配置- -
systemctl restart k3s.service
重启服务- 验证修改生效
-
$ ll /etc/rancher/k3s/k3s.yaml
-rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml修改环境变量
第二种方式是修改环境变量。
--
-- -
sudo vim /etc/systemd/system/k3s.service.env
添加环境变量K3S_KUBECONFIG_MODE=644
K3S_KUBECONFIG_MODE=644- -
systemctl restart k3s.service
重启服务修改配置文件路径
第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。
--
-- 设置环境变量
-export KUBECONFIG=~/.kube/config
- 创建文件夹
-mkdir ~/.kube 2> /dev/null
- 复制配置信息
-sudo k3s kubectl config view --raw > "$KUBECONFIG"
- 修改配置文件的权限
-chmod 600 "$KUBECONFIG"
配置代理
涉及
-k8s
,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及k3s
安装脚本会自动使用当前shell
中的HTTP_PROXY
、HTTPS_PROXY
和NO_PROXY
,以及CONTAINERD_HTTP_PROXY
、CONTAINERD_HTTPS_PROXY
和CONTAINERD_NO_PROXY
变量(如果存在),并将它们写入systemd
服务的环境文件。比如我设置过shell
变量HTTP_PROXY
、HTTPS_PROXY
和NO_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
-
-- 使用
-kubectl create
命令创建管理Pod
的Deployment
。该Pod
根据提供的Docker
镜像运行容器。
kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080- 查看
-Deployment
:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h- 查看
-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创建 Service
-
-- 使用
-kubectl expose
命令将Pod
暴露给公网:
kubectl expose deployment hello-node --type=LoadBalancer --port=8080- 查看你创建的
-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- 使用
-curl
发起请求:
$ curl http://localhost:8080
NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511- 再次查看
-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 /清理
-
-- 删除
-Service
:
kubectl delete service hello-node- 删除
-Deployment
:
kubectl delete deployment hello-node卸载
-
+- 从 server 节点卸载 k3s
-
/usr/local/bin/k3s-uninstall.sh至此,我们将锁和
+MVCC
在事务隔离的实现原理中串联起来了。两者是互相独立又互相协作的两个机制,前者实现了“当前读”,后者实现了“快照读”。总结
卡壳好几天,想到有不少好的文章却仍然会给读者留下困惑,想到自己在当初学习时对一些不严谨的表达抓耳挠腮想不通为什么,就有点不知道如何下笔。最终围绕着自己当初的一些困惑,一点一点修修补补完了。
参考文章
-
]]>- 快速入门指南
-- 管理 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
+- 03 | 事务隔离:为什么你改了我还看不见
+- 08 | 事务到底是隔离的还是不隔离的?
- k8s -k3s +mysql @@ -7020,151 +7167,4 @@ ]]> -- diff --git a/tags/aop/index.html b/tags/aop/index.html index 1d649008..76d96ef0 100644 --- a/tags/aop/index.html +++ b/tags/aop/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/auto-configuration/index.html b/tags/auto-configuration/index.html index df65110a..29dce76a 100644 --- a/tags/auto-configuration/index.html +++ b/tags/auto-configuration/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/bytecode/index.html b/tags/bytecode/index.html index 440ecdc5..a2d04011 100644 --- a/tags/bytecode/index.html +++ b/tags/bytecode/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/cglib/index.html b/tags/cglib/index.html index a072b622..df1b7e52 100644 --- a/tags/cglib/index.html +++ b/tags/cglib/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/clash/index.html b/tags/clash/index.html index fd63fa51..ce583ce9 100644 --- a/tags/clash/index.html +++ b/tags/clash/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/class-loader/index.html b/tags/class-loader/index.html index 9c712cab..81a1640e 100644 --- a/tags/class-loader/index.html +++ b/tags/class-loader/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/distributed-lock/index.html b/tags/distributed-lock/index.html index f3e00e12..17c2b1fb 100644 --- a/tags/distributed-lock/index.html +++ b/tags/distributed-lock/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/docker/index.html b/tags/docker/index.html index fb08a4e7..887c4a52 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/dubbo/index.html b/tags/dubbo/index.html index 208b2183..048b1754 100644 --- a/tags/dubbo/index.html +++ b/tags/dubbo/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/elasticsearch/index.html b/tags/elasticsearch/index.html index db81041e..2b97ae32 100644 --- a/tags/elasticsearch/index.html +++ b/tags/elasticsearch/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/grafana/index.html b/tags/grafana/index.html index c897d74b..4d357a60 100644 --- a/tags/grafana/index.html +++ b/tags/grafana/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/index.html b/tags/index.html index ed20b6f5..4718303f 100644 --- a/tags/index.html +++ b/tags/index.html @@ -3,7 +3,7 @@ - + @@ -27,7 +27,7 @@ - + diff --git a/tags/java/index.html b/tags/java/index.html index 7db2ba32..b5d34296 100644 --- a/tags/java/index.html +++ b/tags/java/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/java/page/2/index.html b/tags/java/page/2/index.html index b4416057..56aa0b46 100644 --- a/tags/java/page/2/index.html +++ b/tags/java/page/2/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/java/page/3/index.html b/tags/java/page/3/index.html index 73356f9d..7ac69048 100644 --- a/tags/java/page/3/index.html +++ b/tags/java/page/3/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/jdk-proxy/index.html b/tags/jdk-proxy/index.html index 9da3a9c7..1094a976 100644 --- a/tags/jdk-proxy/index.html +++ b/tags/jdk-proxy/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/jvm/index.html b/tags/jvm/index.html index 9f528146..eaf20dbf 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/k3s/index.html b/tags/k3s/index.html index 0efd1bdf..e53c44bd 100644 --- a/tags/k3s/index.html +++ b/tags/k3s/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/k8s/index.html b/tags/k8s/index.html index e2eac73a..5a7f9989 100644 --- a/tags/k8s/index.html +++ b/tags/k8s/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/kibana/index.html b/tags/kibana/index.html index 5834c141..a8564db7 100644 --- a/tags/kibana/index.html +++ b/tags/kibana/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/linux/index.html b/tags/linux/index.html index f0fc4ed9..9aaf9fee 100644 --- a/tags/linux/index.html +++ b/tags/linux/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/lock/index.html b/tags/lock/index.html index f916ea24..c183f159 100644 --- a/tags/lock/index.html +++ b/tags/lock/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/logrotate/index.html b/tags/logrotate/index.html index 6b6dd561..bf8bb6cd 100644 --- a/tags/logrotate/index.html +++ b/tags/logrotate/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/minikube/index.html b/tags/minikube/index.html index 634a4849..43c229f0 100644 --- a/tags/minikube/index.html +++ b/tags/minikube/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/mysql/index.html b/tags/mysql/index.html index 29ff2c42..f563254c 100644 --- a/tags/mysql/index.html +++ b/tags/mysql/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/nginx/index.html b/tags/nginx/index.html index d6ba4d3a..4d032d19 100644 --- a/tags/nginx/index.html +++ b/tags/nginx/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/openvpn/index.html b/tags/openvpn/index.html index 68c61563..0cdfe081 100644 --- a/tags/openvpn/index.html +++ b/tags/openvpn/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/prometheus/index.html b/tags/prometheus/index.html index ed9a0bdf..dd6003e0 100644 --- a/tags/prometheus/index.html +++ b/tags/prometheus/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/proxy/index.html b/tags/proxy/index.html index c1078e9a..340162d7 100644 --- a/tags/proxy/index.html +++ b/tags/proxy/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/redis/index.html b/tags/redis/index.html index 661d385e..f0edc27e 100644 --- a/tags/redis/index.html +++ b/tags/redis/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/reverse-proxy/index.html b/tags/reverse-proxy/index.html index d4a42f29..1e1aea25 100644 --- a/tags/reverse-proxy/index.html +++ b/tags/reverse-proxy/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/spi/index.html b/tags/spi/index.html index b54c89f9..9cdb6d16 100644 --- a/tags/spi/index.html +++ b/tags/spi/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/spring-boot/index.html b/tags/spring-boot/index.html index 2d0e4307..980c184e 100644 --- a/tags/spring-boot/index.html +++ b/tags/spring-boot/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/spring/index.html b/tags/spring/index.html index e2a6ddfa..05824506 100644 --- a/tags/spring/index.html +++ b/tags/spring/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/spring/page/2/index.html b/tags/spring/page/2/index.html index 4e1c2715..61b83099 100644 --- a/tags/spring/page/2/index.html +++ b/tags/spring/page/2/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/ssh/index.html b/tags/ssh/index.html index e9f0f1da..6cc7dcb4 100644 --- a/tags/ssh/index.html +++ b/tags/ssh/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/synchronized/index.html b/tags/synchronized/index.html index fdd88a0e..1e81ed39 100644 --- a/tags/synchronized/index.html +++ b/tags/synchronized/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/tmux/index.html b/tags/tmux/index.html index 836f5648..fb51f4f0 100644 --- a/tags/tmux/index.html +++ b/tags/tmux/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/ubuntu/index.html b/tags/ubuntu/index.html index 0975adc2..cd49ec99 100644 --- a/tags/ubuntu/index.html +++ b/tags/ubuntu/index.html @@ -3,7 +3,7 @@ - + diff --git a/tags/vim/index.html b/tags/vim/index.html index b2d56d71..6ba57c91 100644 --- a/tags/vim/index.html +++ b/tags/vim/index.html @@ -3,7 +3,7 @@ - +Spring 中 @Import 注解的使用和源码分析 -/2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/ -- Import
注解是Spring
基于Java
注解配置的重要组成部分,处理Import
注解是处理Configuration
注解的子过程之一,本文将介绍Import
注解的3
种使用方式,然后通过分析源码和处理过程示意图解释它是如何导入(注册)BeanDefinition
的。 - - --
-- 本文的写作动机继承自Spring @Configuration 注解的源码分析,处理
-@Import
是处理@Configuration
过程的一部分。使用方式
-
Import
注解有3
种导入(注册)BeanDefinition
的方式:-
-- 使用
-Import
将目标类的Class
对象,解析为BeanDefinition
并注册。- 使用
-Import
配合ImportSelector
的实现类,将selectImports
方法返回的所有全限定类名字符串,解析为BeanDefinition
并注册。- 使用
-Import
配合ImportBeanDefinitionRegistra
r 的实现类,在registerBeanDefinitions
方法中,直接向BeanDefinitionRegistry
中注册BeanDefinition
。测试用例
测试了使用
-Import
注解的3
种方式:-
-- 使用
-Import
直接导入(注册)Red
。- 配合
-ImportBeanDefinitionRegistrar
间接注册Color
。- 配合
-ImportSelector
间接导入(注册)Blue
。用例中的特别地测试了以下两种情况:
--
-- 使用
-Import
直接导入和配合ImportSelector
间接导入相同的类Red
只会注册一个BeanDefinition
。- 尽管
-MyImportSelector
书面顺序在MyImportBeanDefinitionRegistrar
之后,但是MyImportBeanDefinitionRegistrar
判断registry
是否包含在MyImportSelector
导入的类Blue
时,不受顺序影响。- -
public class ImportConfig {
}
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
boolean hasRed = registry.containsBeanDefinition("com.moralok.bean.Red");
boolean hasBlue = registry.containsBeanDefinition("com.moralok.bean.Blue");
if (hasRed && hasBlue) {
BeanDefinition beanDefinition = new RootBeanDefinition(Color.class);
registry.registerBeanDefinition("color", beanDefinition);
}
}
}
public class MyImportSelector implements ImportSelector {
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[] {"com.moralok.bean.Blue", "com.moralok.bean.Red"};
}
}
public class Color {
}
public class Red {
}
public class Blue {
}
public class IocTest {
public void importTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ImportConfig.class);
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String name : beanDefinitionNames) {
System.out.println("beanDefinitionName.........." + name);
}
}
}测试结果
-- -
......
beanDefinitionName..........importConfig
beanDefinitionName..........com.moralok.bean.Red
beanDefinitionName..........com.moralok.bean.Blue
beanDefinitionName..........color源码分析
关于
- -Import
注解的源码分析需要建立在对关于Configuration
注解的源码的了解基础上,因为前者是Spring
解析配置类处理过程的一部分,可以参考文章:获取要导入的目标
在
-doProcessConfigurationClass
方法中处理配置类构建配置模型时,会调用processImports
方法处理Import
注解。在进入方法前,会调用getImports
方法从sourceClass
获取要导入的目标。--注意:目标不仅仅来自直接标注在
-sourceClass
上的Import
注解,因为sourceClass
上可能还有其他的注解,这些注解自身可能标注了Import
注解,因此需要递归地遍历所有注解,找到所有的Import
注解。- -
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
// 前后省略 @PropertySource、@ComponentScan、@ImportSource、@Bean 等注解的处理
// 处理 Import 注解
processImports(configClass, sourceClass, getImports(sourceClass), true);
}-
collectImports
方法是一种常见的递归写法(深度优先遍历)。imports
存放要导入的目标,visited
存放已经访问过的sourceClass
。sourceClass
在入口处包装了一个普通的Class
,在递归的过程中包装的都是一个注解Class
。--注意:这里还没有检测循环导入的情况并抛出异常,但
-visited
保证了只会遍历一次。- -
// 获取 Import 注解 value 中的 Class 对象,并包装为 SourceClass 返回
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<SourceClass>();
Set<SourceClass> visited = new LinkedHashSet<SourceClass>();
collectImports(sourceClass, imports, visited);
return imports;
}
// 递归地收集要导入的目标(包装为 SourceClass)
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {
// 如果 sourceClass 尚未访问过
if (visited.add(sourceClass)) {
// 遍历 sourceClass 上的注解
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
// 只要注解的名称不是 java 开头或者不是 Import 注解
if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
// 将该注解作为 sourceClass 递归地调用
collectImports(annotation, imports, visited);
}
}
// 将 Import 注解的 value 的值转换为 sourceClass 加入 imports
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}这时候,并不区分要导入的目标的
-Class
有什么特别之处,Import
注解的语义,此时宽泛地说就是:“将value
中的类导入”。但是显而易见,这样的方式不够灵活,因此才有了另外两种更有灵活性的导入方式:ImportSelector
和ImportBeanDefinitionRegistrar
,Spring
最终不会真的注册这两种类,而是注册它们“介绍”的类,相当于把确定导入什么类的工作委托给它们。处理要导入的目标
-
processImports
方法是处理Import
注解的核心方法,这里的处理逻辑就对应着Import
注解的三种使用方式。主要步骤如下:-
-- 检测要导入的候选者不为空
-- 判断是否要检测循环导入以及是否存在循环导入
-- 处理要导入的候选者
--
-- 如果是
-ImportSelector
类型,调用selectImports
方法获取新的要导入的目标,递归调用processImports
处理- 如果是
-ImportBeanDefinitionRegistrar
类型,添加到配置模型configClass
(出口1
)- 如果是其他剩余情况,作为配置类处理(出口
-2
)- -
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {
// 如果要导入的目标为空,直接返回
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
// 如果要检查循环导入,且确实存在循环导入,则抛出异常
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
// 将配置模型放入 importStack,用于检查循环导入
this.importStack.push(configClass);
try {
// 遍历每一个准备导入的目标
for (SourceClass candidate : importCandidates) {
// 如果是 ImportSelector 类型,委托给它确定导入目标
if (candidate.isAssignable(ImportSelector.class)) {
// 加载类
Class<?> candidateClass = candidate.loadClass();
// 实例化得到 ImportSelector 实例
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
// 调用其 Aware 接口
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
// 如果是 DeferredImportSelector 类型,存入 deferredImportSelectors 推迟调用
this.deferredImportSelectors.add(
new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
}
else {
// 调用 selectImports 方法,返回要导入的目标的全限定类名
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
// 包装为 SourceClass
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 递归调用 processImports
// 从这里看,ImportSelector 本质上是更加灵活的 Import
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
// 如果是 ImportBeanDefinitionRegistrar 类型,委托给它注册额外的 BeanDefinitions
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 加载类
Class<?> candidateClass = candidate.loadClass();
// 实例化得到 ImportBeanDefinitionRegistrar 实例
ImportBeanDefinitionRegistrar registrar =
BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
// 调用其 Aware 接口
ParserStrategyUtils.invokeAwareMethods(
registrar, this.environment, this.resourceLoader, this.registry);
// 添加到配置模型
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
// 既不是 ImportSelector,也不是 ImportBeanDefinitionRegistrar 的其他剩余情况,将其视为被 Configuration 注解标注的配置类进行处理
else {
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// asConfigClass 方法建立了 candidate importBy configClass 的关系
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
// pop 配置模型
this.importStack.pop();
}
}
}类型一:ImportSelector
如果要导入的目标是
-ImportSelector
类型,那么Spring
将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。具体步骤是:-
-- 先获取
-Class
对象- 再实例化得到一个
-ImportSelector
实例- 调用
-selectImports
方法,该方法返回的是类的全限定名,这样就得到了真正要导入的目标- 再次递归调用
-processImports
-
ImportSelector
就像它名字的含义一样,本质上是一种导入选择器,是一种更加灵活的getImports
方法。由于返回的目标可能属于三种情形中的任意一种,所以对这些目标的处理还是要回到processImports
方法。可以说ImportSelector
类型本身不是processImports
方法的出口,它最终会转换为ImportBeanDefinitionRegistrar
或其他剩余情况。-
ImportSelector
灵活性的来源:-
-- -
selectImports
的AnnotationMetadata
参数,为它提供了根据注解信息返回要导入的目标的能力- -
ImportSelector
可以实现Aware
接口,用以感知到一些容器级别的资源,如BeanFactory
,这为它提供了根据这些资源中的信息返回要导入的目标的能力类型二:ImportBeanDefinitionRegistrar
如果要导入的目标是
-ImportBeanDefinitionRegistrar
,它会和ImportSelector
有些相似却又有所不同。Spring
同样将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。-
-- 先获取
-Class
对象- 再实例化得到一个
-ImportBeanDefinitionRegistrar
实例- 添加到配置模型
-configClass
的importBeanDefinitionRegistrars
属性-
ImportBeanDefinitionRegistrar
不像ImportSelector
需要进一步处理,它本身就代表着一个返回出口,成为了配置模型的一部分。但是请注意,registerBeanDefinitions
方法此时并没有被调用。-
ImportBeanDefinitionRegistrar
灵活性的来源:-
-- -
registerBeanDefinitions
的AnnotationMetadata
参数,为它提供了根据注解信息决定注册BeanDefinition
的能力- -
registerBeanDefinitions
的BeanDefinitionRegistry
参数,为它提供了根据BeanDefinitionRegistry
中的信息决定注册BeanDefinition
的能力- -
ImportBeanDefinitionRegistrar
可以实现Aware
接口,用以感知到一些容器级别的资源,如BeanFactory
,这为它提供了根据这些资源中的信息返回要导入的目标的能力类型三:其他剩余情况
如果要导入的目标属于既不是
-ImportSelector
也不是ImportBeanDefinitionRegistrar
的其他剩余情况,那么Spring
将其视为被Configuration
注解标注的配置类进行处理。这里的处理逻辑是,Import
注解导入的类可能不是一个普通的类,而是一个配置类,因此需要回到processConfigurationClass
进行处理。processConfigurationClass
方法正是本文开头的doProcessConfigurationClass
方法的调用方,这里有两个地方值得注意:-
-- -
Import
注解产生的ConfigurationClass
根据不同的情况需要合并或者被抛弃,显式声明比 Import 导入的优先级更高。- 其他剩余情况下,目标最终会转换为一个配置模型,添加到
-parser
的configurationClasses
属性。- -
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 判断是否跳过处理
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
// 如果配置模型已经存在
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
// 如果新的配置模型代表的类,是 Import 导入的
if (configClass.isImported()) {
// 如果已存在的配置模型也是 Import 导入的
if (existingClass.isImported()) {
// 合并它们的来源
// 比如一个类 A 既被 Config1 上的 Import 注解导入,也被 Config2 上的 Import 导入
existingClass.mergeImportedBy(configClass);
}
// 否则忽略新的因为 Import 导入而产生的配置模型
return;
}
else {
// 使用显式定义的代替 Import 导入的(显式定义的和 Import 导入的有什么不同吗)
this.configurationClasses.remove(configClass);
for (Iterator<ConfigurationClass> it = this.knownSuperclasses.values().iterator(); it.hasNext();) {
if (configClass.equals(it.next())) {
it.remove();
}
}
}
}
// 先递归地处理配置类和它的父类,因为配合各种注解,可能引入更多的类
SourceClass sourceClass = asSourceClass(configClass);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass);
}
while (sourceClass != null);
// 一个配置类,本身最终被解析成配置模型(配置模型在后续将会解析出 BeanDefinition)
this.configurationClasses.put(configClass, configClass);
}DeferredImportSelector 的调用时机
在解析完每一批(注释中说“全部”)的配置类后,会统一调用
-DeferredImportSelector
。它作为一个标记接口推迟了selectImports
的时机,打破了处理顺序的限制,在方法被调用时,可以得到更加完整的信息。注释中说“在选择导入的目标是@Conditional
时,这个类型的选择器会很有用”,但是我不太理解,因为这个时候,处理配置类得到的信息尚未转换为ImportSelector
可以感知到的信息,不像ImportBeanDefinitionRegistrar
,它被调用的时机在最后,也因此可以感知到更多的信息。- -
public void parse(Set<BeanDefinitionHolder> configCandidates) {
this.deferredImportSelectors = new LinkedList<DeferredImportSelectorHolder>();
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}
// 调用 DeferredImportSelectors
processDeferredImportSelectors();
}
private void processDeferredImportSelectors() {
// 获取处理这一批配置类获得的 DeferredImportSelectors
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
// 清空
this.deferredImportSelectors = null;
// 排序
Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
// 遍历
for (DeferredImportSelectorHolder deferredImport : deferredImports) {
ConfigurationClass configClass = deferredImport.getConfigurationClass();
try {
// 调用 selectImports 获取要导入的目标
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
// 调用 processImports 处理要导入的目标,这里不管循环导入?竟然是任由 StackOverFlow
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
}
}ImportBeanDefinitionRegistrar 的调用时机
-
ConfigurationClassPostProcessor
在每次解析得到新的一批配置模型后,都会调用ConfigurationClassBeanDefinitionReader
的loadBeanDefinitions
方法加载BeanDefinition
,在这过程的最后会从ImportBeanDefinitionRegistrar
加载BeanDefinition
。这代表在处理同一批配置类时,在registerBeanDefinitions
方法中总是能感知到以其他方式注册到BeanDefinitionRegistry
中的BeanDefinition
,不论书面定义的顺序如何。- -
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
// 遍历每一个配置模型
for (ConfigurationClass configClass : configurationModel) {
// 从配置模型中加载 BeanDefinistion
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass,
TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
// 如果配置模型本身是导入的,为自身注册 BeanDefinition
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
// 为 BeanMethod 加载 BeanDefinition(Bean 注解)
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
// 为 ImportResources 加载 BeanDefinition(ImportResource 注解)
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
// 从 ImportBeanDefinitionRegistrar 加载 BeanDefinition
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
// 遍历 ImportBeanDefinitionRegistrar 调用 registerBeanDefinitions 方法注册 BeanDefinition
for (Map.Entry<ImportBeanDefinitionRegistrar, AnnotationMetadata> entry : registrars.entrySet()) {
entry.getKey().registerBeanDefinitions(entry.getValue(), this.registry);
}
}循环导入的检测
在处理导入的目标前将配置类放入
-importStack
,处理完毕移除。如果要导入的目标属于其他剩余情况时,注册被导入类->所有导入类集合的映射关系。- -
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {
// ...
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
// 如果要检查循环导入,且确实存在循环导入,则抛出异常
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
// 将配置模型放入 importStack,用于检查循环导入
this.importStack.push(configClass);
try {
// 遍历每一个准备导入的目标
for (SourceClass candidate : importCandidates) {
// ...
else {
// 记录了被导入类->所有导入类集合的映射关系
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// asConfigClass 方法建立了 candidate importBy configClass 的关系
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
// ...
finally {
// pop 配置模型
this.importStack.pop();
}
}
}检测是否发生循环导入。以当前类开始,循环向上查找最近一个导入自身的类,如果找到自身,说明发生循环导入。
-- -
private boolean isChainedImportOnStack(ConfigurationClass configClass) {
// 如果 importStack 已存在该配置模型
if (this.importStack.contains(configClass)) {
String configClassName = configClass.getMetadata().getClassName();
// 获取最新一个导入 configClass 的类
AnnotationMetadata importingClass = this.importStack.getImportingClassFor(configClassName);
// 循环查找导入类的最近一个导入类,如果找到了自身,表示发生循环导入
while (importingClass != null) {
if (configClassName.equals(importingClass.getClassName())) {
return true;
}
importingClass = this.importStack.getImportingClassFor(importingClass.getClassName());
}
}
return false;
}总结
对比
- -
-- - -- - ImportSelector
- ImportBeanDefinitionRegistrar
其他剩余情况 -- -灵活性 -中 -高 -低 -- -处理结果 -- 转换为配置模型的一部分 -转换为一个配置模型 -- -方法调用时机 -立即(或解析配置类的最后) -加载 -BeanDefinition
的最后- - -方法的结果 -获取 -Import
目标直接注册 -BeanDefinition
- 处理过程示意图
-]]>- -java -spring -