From 6ba71ea24dead75ac86f6f9d5522f5d0be15b6d3 Mon Sep 17 00:00:00 2001 From: moralok Date: Fri, 26 Apr 2024 15:43:49 +0000 Subject: [PATCH] deploy: df0c9b12bb4879d7690b42326ac44b7e17ae8f95 --- 2020/08/18/install-Docker/index.html | 6 +- .../index.html | 6 +- .../Linux-frequently-used-commands/index.html | 6 +- .../MySQL-frequently-used-commands/index.html | 6 +- .../how-to-install-clash-on-ubuntu/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../tmux-frequently-used-commands/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../10/how-does-Spring-load-beans/index.html | 6 +- .../testing-and-analysis-of-jvm-gc/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../07/garbage-collection-in-Java/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../19/JDK-dynamic-proxy-and-CGLib/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../11/28/how-does-Dubbo-SPI-works/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../how-does-Spring-Boot-SPI-works/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../25/Unsafe-an-anti-Java-class/index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- .../index.html | 6 +- 2024/01/18/use-vim/index.html | 6 +- .../30/installation-and-use-of-k3s/index.html | 6 +- .../index.html | 6 +- archives/2020/08/index.html | 2 +- archives/2020/09/index.html | 2 +- archives/2020/index.html | 2 +- archives/2023/05/index.html | 2 +- archives/2023/06/index.html | 2 +- archives/2023/07/index.html | 2 +- archives/2023/08/index.html | 2 +- archives/2023/11/index.html | 2 +- archives/2023/11/page/2/index.html | 2 +- archives/2023/12/index.html | 2 +- archives/2023/12/page/2/index.html | 2 +- archives/2023/index.html | 2 +- archives/2023/page/2/index.html | 2 +- archives/2023/page/3/index.html | 2 +- archives/2023/page/4/index.html | 2 +- archives/2024/01/index.html | 2 +- archives/2024/03/index.html | 2 +- archives/2024/index.html | 2 +- archives/index.html | 2 +- archives/page/2/index.html | 2 +- archives/page/3/index.html | 2 +- archives/page/4/index.html | 2 +- archives/page/5/index.html | 2 +- index.html | 22 +- leancloud_counter_security_urls.json | 2 +- page/2/index.html | 22 +- page/3/index.html | 22 +- page/4/index.html | 22 +- page/5/index.html | 20 +- search.xml | 5146 ++++++++--------- tags/aop/index.html | 2 +- tags/auto-configuration/index.html | 2 +- tags/bytecode/index.html | 2 +- tags/cglib/index.html | 2 +- tags/clash/index.html | 2 +- tags/class-loader/index.html | 2 +- tags/distributed-lock/index.html | 2 +- tags/docker/index.html | 2 +- tags/dubbo/index.html | 2 +- tags/elasticsearch/index.html | 2 +- tags/grafana/index.html | 2 +- tags/index.html | 4 +- tags/java/index.html | 2 +- tags/java/page/2/index.html | 2 +- tags/java/page/3/index.html | 2 +- tags/jdk-proxy/index.html | 2 +- tags/jvm/index.html | 2 +- tags/k3s/index.html | 2 +- tags/k8s/index.html | 2 +- tags/kibana/index.html | 2 +- tags/linux/index.html | 2 +- tags/lock/index.html | 2 +- tags/logrotate/index.html | 2 +- tags/minikube/index.html | 2 +- tags/mysql/index.html | 2 +- tags/nginx/index.html | 2 +- tags/openvpn/index.html | 2 +- tags/prometheus/index.html | 2 +- tags/proxy/index.html | 2 +- tags/redis/index.html | 2 +- tags/reverse-proxy/index.html | 2 +- tags/spi/index.html | 2 +- tags/spring-boot/index.html | 2 +- tags/spring/index.html | 2 +- tags/spring/page/2/index.html | 2 +- tags/ssh/index.html | 2 +- tags/synchronized/index.html | 2 +- tags/tmux/index.html | 2 +- tags/ubuntu/index.html | 2 +- tags/vim/index.html | 2 +- 119 files changed, 2839 insertions(+), 2839 deletions(-) diff --git a/2020/08/18/install-Docker/index.html b/2020/08/18/install-Docker/index.html index 65437ffe..2e52d45c 100644 --- a/2020/08/18/install-Docker/index.html +++ b/2020/08/18/install-Docker/index.html @@ -3,7 +3,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -229,7 +229,7 @@

- + diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index 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 @@ linux - - 在 Ubuntu 上安装 Clash - /2023/05/27/how-to-install-clash-on-ubuntu/ - 本文记录了如何在 Ubuntu 上安装 Clash,以供各类程序在必要的时候使用代理。
有些时候我们面对特定资源会遇到下载速度极其缓慢甚至无法下载的情况,像是使用 Github,下载 k8s 相关镜像,还有访问特定网站等等。也许有些 Dirty Hack 的方式可以短暂地绕开限制,比如修改 hosts 文件,但这并不总是见效。也许添加国内的镜像仓库地址可以满足很多人的需求,但是说不准就会遇到镜像更新不及时甚至镜像被污染的情况。总之,如果你有一把不错的梯子,使用代理辅助开发肯定会拥有更好的体验。
当然,为 Git 设置代理,为 Terminal 设置代理,为 Docker Engine 设置代理,为 Docker 容器设置代理,为 “还有一些你尚未知道原来这还需要这样设置代理的地方” 设置代理,仍然是一件痛苦的事情。如果你有条件为全屋设备配置透明代理,肯定能避免踩非常多的坑。

- - -
-

注意:多个 Clash 相关的仓库已经 GG
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!

-
-
-

通过 Docker 运行 Clash

-

本人最新的安装是通过直接拷贝旧服务器上的二进制可执行文件以及全球 IP 库文件来完成的,保留已有安装的相关文件在短时间内仍然可以满足重新安装的需求。注意到原作者 DreamacroDocker 镜像仓库仍然保留,也许通过 Docker 运行 Clash 是个更好的选择

-
-
    -
  • docker-compose.yml
    version: "1.0"
    services:
    clash:
    image: dreamacro/clash:v1.18.0
    container_name: clash
    ports:
    - 9090:9090
    - 7890:7890
    mem_limit: ${CLASH_MEM_LIMIT:-64m}
    restart: unless-stopped
  • -
  • 配置文件在容器内的位置 /root/.config/clash/config.yaml
  • -
-
-

长远来看,使用无法得到更新的旧版本仍然可能在未来导致安全问题的发生,以后再说吧 =_=。

-
-
-

二进制安装 Clash

-

本方式仅限仍然可获得可靠安全的二进制可执行文件的人使用。

-
-

安装 Clash

    -
  1. GitHub 下载预构建的二进制文件。仓库已删除
    $ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
  2. -
  3. 使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。忽略提示。
    $ gzip -d clash-linux-amd64-v1.16.0.gz 
    gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
  4. -
  5. 移动二进制文件到目录 /usr/local/bin 并且重命名为 clash
    $ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
  6. -
  7. 通过 clash -v 查看 Clash 的版本信息。
    $ clash -v
    Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
  8. -
-

使用 Clash

    -
  1. 使用命令 clash 启动,可以看到日志。
    :~$ clash
    INFO[0000] Can't find config, create a initial config file
    INFO[0000] Can't find MMDB, start download
    INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
  2. -
  3. 在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。其中 config.yamlClash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。cache.db 用于缓存。CDN 已失效
    :~/.config/clash$ ls
    cache.db config.yaml Country.mmdb
  4. -
  5. 如果梯子的订阅支持 Clash,直接订阅;否则需要手动修改配置文件。
  6. -
  7. 可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。( 注意:使用 http
  8. -
-

设置 Clash 开机自动启动

-

参考官方文档 Clash as a service404

-
-
    -
  1. 拷贝配置文件和全球 IP 库到 /etc/clash
    ~/.config/clash$ sudo cp config.yaml /etc/clash/
    ~/.config/clash$ sudo cp Country.mmdb /etc/clash/
  2. -
  3. /etc/systemd/system/clash.service 创建 systemd 配置文件
    [Unit]
    Description=Clash daemon, A rule-based proxy in Go.
    After=network-online.target

    [Service]
    Type=simple
    Restart=always
    ExecStart=/usr/local/bin/clash -d /etc/clash

    [Install]
    WantedBy=multi-user.target
  4. -
  5. 重载 systemd
    systemctl daemon-reload
  6. -
  7. 设置系统开机时自动启动 Clash
    systemctl enable clash
  8. -
  9. 启动 Clash
    systemctl start clash
  10. -
  11. 检查 Clash 的健康状态和日志
    systemctl status clash
    journalctl -xe
  12. -
-]]>
- - clash - proxy - -
MySQL 常用命令 /2020/09/04/MySQL-frequently-used-commands/ @@ -1248,6 +1195,59 @@ mysql + + 在 Ubuntu 上安装 Clash + /2023/05/27/how-to-install-clash-on-ubuntu/ + 本文记录了如何在 Ubuntu 上安装 Clash,以供各类程序在必要的时候使用代理。
有些时候我们面对特定资源会遇到下载速度极其缓慢甚至无法下载的情况,像是使用 Github,下载 k8s 相关镜像,还有访问特定网站等等。也许有些 Dirty Hack 的方式可以短暂地绕开限制,比如修改 hosts 文件,但这并不总是见效。也许添加国内的镜像仓库地址可以满足很多人的需求,但是说不准就会遇到镜像更新不及时甚至镜像被污染的情况。总之,如果你有一把不错的梯子,使用代理辅助开发肯定会拥有更好的体验。
当然,为 Git 设置代理,为 Terminal 设置代理,为 Docker Engine 设置代理,为 Docker 容器设置代理,为 “还有一些你尚未知道原来这还需要这样设置代理的地方” 设置代理,仍然是一件痛苦的事情。如果你有条件为全屋设备配置透明代理,肯定能避免踩非常多的坑。

+ + +
+

注意:多个 Clash 相关的仓库已经 GG
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!
请谨慎甄别网上搜索到的所谓备份的可靠性和安全性!!!

+
+
+

通过 Docker 运行 Clash

+

本人最新的安装是通过直接拷贝旧服务器上的二进制可执行文件以及全球 IP 库文件来完成的,保留已有安装的相关文件在短时间内仍然可以满足重新安装的需求。注意到原作者 DreamacroDocker 镜像仓库仍然保留,也许通过 Docker 运行 Clash 是个更好的选择

+
+
    +
  • docker-compose.yml
    version: "1.0"
    services:
    clash:
    image: dreamacro/clash:v1.18.0
    container_name: clash
    ports:
    - 9090:9090
    - 7890:7890
    mem_limit: ${CLASH_MEM_LIMIT:-64m}
    restart: unless-stopped
  • +
  • 配置文件在容器内的位置 /root/.config/clash/config.yaml
  • +
+
+

长远来看,使用无法得到更新的旧版本仍然可能在未来导致安全问题的发生,以后再说吧 =_=。

+
+
+

二进制安装 Clash

+

本方式仅限仍然可获得可靠安全的二进制可执行文件的人使用。

+
+

安装 Clash

    +
  1. GitHub 下载预构建的二进制文件。仓库已删除
    $ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
  2. +
  3. 使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。忽略提示。
    $ gzip -d clash-linux-amd64-v1.16.0.gz 
    gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
  4. +
  5. 移动二进制文件到目录 /usr/local/bin 并且重命名为 clash
    $ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
  6. +
  7. 通过 clash -v 查看 Clash 的版本信息。
    $ clash -v
    Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
  8. +
+

使用 Clash

    +
  1. 使用命令 clash 启动,可以看到日志。
    :~$ clash
    INFO[0000] Can't find config, create a initial config file
    INFO[0000] Can't find MMDB, start download
    INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
  2. +
  3. 在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。其中 config.yamlClash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。cache.db 用于缓存。CDN 已失效
    :~/.config/clash$ ls
    cache.db config.yaml Country.mmdb
  4. +
  5. 如果梯子的订阅支持 Clash,直接订阅;否则需要手动修改配置文件。
  6. +
  7. 可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。( 注意:使用 http
  8. +
+

设置 Clash 开机自动启动

+

参考官方文档 Clash as a service404

+
+
    +
  1. 拷贝配置文件和全球 IP 库到 /etc/clash
    ~/.config/clash$ sudo cp config.yaml /etc/clash/
    ~/.config/clash$ sudo cp Country.mmdb /etc/clash/
  2. +
  3. /etc/systemd/system/clash.service 创建 systemd 配置文件
    [Unit]
    Description=Clash daemon, A rule-based proxy in Go.
    After=network-online.target

    [Service]
    Type=simple
    Restart=always
    ExecStart=/usr/local/bin/clash -d /etc/clash

    [Install]
    WantedBy=multi-user.target
  4. +
  5. 重载 systemd
    systemctl daemon-reload
  6. +
  7. 设置系统开机时自动启动 Clash
    systemctl enable clash
  8. +
  9. 启动 Clash
    systemctl start clash
  10. +
  11. 检查 Clash 的健康状态和日志
    systemctl status clash
    journalctl -xe
  12. +
+]]>
+ + clash + proxy + +
在 iOS 和 macOS 上安装 OpenVPN 客户端 /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ @@ -1353,58 +1353,6 @@ openvpn - - 如何为终端、docker 和容器设置代理 - /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ - Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

-

为终端设置代理

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

-

设置 Shell 环境变量

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

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

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

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

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

-

修改 wget 配置文件

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

-
use_proxy = on

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

为 docker 设置代理

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

-

通过 systemd 设置

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

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

docker.service[Service] 模块添加:

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

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

-
systemctl daemon-reload
systemctl restart docker.service
- -

修改 dockerd 配置文件

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

-
export http_proxy="http://proxyAddress:port"
-

然后重启 docker daemon 即可。

-
-

国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。

-
-

为 docker 容器设置代理

docker daemon 进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。

-

修改配置文件

创建或修改 ~/.docker/config.json,添加:

-
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
-

此后创建的新容器,会自动设置环境变量来使用代理。

-

为指定容器添加环境变量

在启动容器时使用 -e 手动注入环境变量 http_proxy。这意味着进入容器使用 export 设置环境变量的方式也是可行的。

-
-

注意:如果代理是使用宿主机的代理,当网络为 bridge 模式,proxyAddress 需要填写宿主机的 IP;如果使用 host 模式,proxyAddress 可以填写 127.0.0.1。

-
-

总结

不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:

-
    -
  1. 使用系统的环境变量
  2. -
  3. 修改软件的配置文件
  4. -
  5. 执行时注入参数
  6. -
-

举一反三,像 aptgit 这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。

-
-

原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。

-
-

参考链接

Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置

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

+

为终端设置代理

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

+

设置 Shell 环境变量

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

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

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

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

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

+

修改 wget 配置文件

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

+
use_proxy = on

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

为 docker 设置代理

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

+

通过 systemd 设置

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

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

docker.service[Service] 模块添加:

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

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

+
systemctl daemon-reload
systemctl restart docker.service
+ +

修改 dockerd 配置文件

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

+
export http_proxy="http://proxyAddress:port"
+

然后重启 docker daemon 即可。

+
+

国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。

+
+

为 docker 容器设置代理

docker daemon 进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。

+

修改配置文件

创建或修改 ~/.docker/config.json,添加:

+
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
+

此后创建的新容器,会自动设置环境变量来使用代理。

+

为指定容器添加环境变量

在启动容器时使用 -e 手动注入环境变量 http_proxy。这意味着进入容器使用 export 设置环境变量的方式也是可行的。

+
+

注意:如果代理是使用宿主机的代理,当网络为 bridge 模式,proxyAddress 需要填写宿主机的 IP;如果使用 host 模式,proxyAddress 可以填写 127.0.0.1。

+
+

总结

不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:

+
    +
  1. 使用系统的环境变量
  2. +
  3. 修改软件的配置文件
  4. +
  5. 执行时注入参数
  6. +
+

举一反三,像 aptgit 这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。

+
+

原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。

+
+

参考链接

Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置

+]]>
+ + docker + proxy + +
Ubuntu server 20.04 安装后没有分配全部磁盘空间 /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ @@ -1578,40 +1578,95 @@ - 如何在 Ubuntu 20.04 上安装 Minikube - /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ - 环境搭建

安装 Ubuntu 20.04

使用 Vmware Workstation 通过 ubuntu-20.04.6-live-server-amd64.iso 安装。需要满足条件如下:

+ 如何使用 SSH 连接 Github 和服务器 + /2023/06/28/how-to-use-ssh-to-connect-github-and-server/ + 本文介绍了如何使用 SSH 连接 Github 和免密登录服务器作为备忘笔记,主要在新建虚拟机或重装云服务器系统时使用。

+ + +

使用 SSH 连接 Github

检查现有 SSH 密钥

打开终端,输入 ls -al ~/.ssh 以查看是否存在现有的 SSH 密钥。

+
$ ls -al ~/.ssh
total 16
drwx------ 2 moralok moralok 4096 Jun 28 20:19 .
drwxr-xr-x 6 moralok moralok 4096 Jun 28 20:13 ..
-rw------- 1 moralok moralok 106 Jun 28 20:07 authorized_keys
-rw-r--r-- 1 moralok moralok 444 Jun 28 20:19 known_hosts
+ +

检查目录列表以查看是否已经有 SSH 公钥。 默认情况下,GitHub 的一个支持的公钥的文件名是以下之一。

-

安装 Docker

参考官方文档:Install Docker Engine on Ubuntu

-

安装 Minikube

参考官方文档:minikube start

-

安装 kubectl 并启动 kubectl 自动补全功能

参考官方文档:在 Linux 系统中安装并设置 kubectl

-

开始使用

创建集群:

-
minikube start
+

生成 SSH 密钥

如果没有密钥,就需要生成新的 SSH 密钥;如果已有,跳到上传已有密钥环节。
打开终端,粘贴下面的文本(替换为你的 GitHub 电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH 密钥。
一直 yes 确定选择默认即可。

+
$ ssh-keygen -t ed25519 -C "your_email@example.com"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/your-user/.ssh/id_ed25519):
-

创建集群时的权限问题

不加 sudo

不加 sudo 的时候,创建集群失败,提示无法选择默认 driver。可能是 docker 处于不健康状态或者用户权限不足。

-
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Unable to pick a default driver. Here is what was considered, in preference order:
- docker: Not healthy: "docker version --format {{.Server.Os}}-{{.Server.Version}}:{{.Server.Platform.Name}}" exit status 1: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/version": dial unix /var/run/docker.sock: connect: permission denied
- docker: Suggestion: Add your user to the 'docker' group: 'sudo usermod -aG docker $USER && newgrp docker' <https://docs.docker.com/engine/install/linux-postinstall/>
* Alternatively you could install one of these drivers:
- kvm2: Not installed: exec: "virsh": executable file not found in $PATH
- podman: Not installed: exec: "podman": executable file not found in $PATH
- qemu2: Not installed: exec: "qemu-system-x86_64": executable file not found in $PATH
- virtualbox: Not installed: unable to find VBoxManage in $PATH

X Exiting due to DRV_NOT_HEALTHY: Found driver(s) but none were healthy. See above for suggestions how to fix installed drivers.
+

上传 SSH 密钥

SSH 公钥复制到剪贴板,在 Github 上的 Settings -> Access -> SSH and GPG keys -> New SSH key,粘贴即可。

+
$ cat ~/.ssh/id_ed25519.pub
-
使用 sudo

使用 sudo 的时候,会提示不建议通过 root 权限使用 docker,如果还是想要继续,可以使用选项 --force

-
$ sudo minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: none, ssh
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/

X Exiting due to DRV_AS_ROOT: The "docker" driver should not be used with root privileges.
+

配置 Git

$ git config --global user.name "your_username"
$ git config --global user.email "your_email@example.com"
-
使用选项 –force

考虑到仅用于测试,尝试通过 sudo minikube start --force 启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。

-
$ sudo minikube start --force
* minikube v1.30.1 on Ubuntu 20.04
! minikube skips various validations when --force is supplied; this may lead to unexpected behavior
* Automatically selected the docker driver. Other choices: ssh, none
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 25.89 M
> index.docker.io/kicbase/sta...: 373.53 MiB / 373.53 MiB 100.00% 7.17 Mi
! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: default-storageclass, storage-provisioner
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
-

成功启动集群后,使用 kubectl get pod 测试,提示连接被拒绝。

-
$ kubectl get pod
E0622 21:55:27.400754 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.401000 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410464 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410951 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.412076 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?
+

使用 SSH 免密登录服务器

使用现成的密钥,将 ~/.ssh/id_ed25519.pub 的内容追加到服务端的 ~/.ssh/authorized_keys 中,使用 ssh user@host 成功免密登录。这样一来,远程连接服务器或者使用 VScode Remote Explorer 时,不用每次输入密码了。

+

VScode Remote Explorer 无法连接

在更新 VMware Workstation 17 Pro 后,发现虚拟机的 IP192.168.46.135 重置 为 192.168.46.128,即使更新配置文件 C:\Users\moralok\.ssh\config 中的 IP VScode Remote Explorer 仍然无法连接,但是通过 Xshell 使用账号密码可以登录。查看报错信息发现 known_hosts192.168.46.128 对应的 ECDSA key 有问题,应该记录的是之前占用该 IP 的虚拟机的 ECDSA key,删除该行后重新连接成功。

+
[18:09:59.973] > @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
> @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
> @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
> IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
> Someone could be eavesdropping on you right now (man-in-the-middle attack)!
> It is also possible that a host key has just been changed.
> The fingerprint for the ECDSA key sent by the remote host is
> SHA256:Q8ckiM7lmz2HBjsNPHFaVjhJfuTcfbgThP1NLv52H1Y.
> Please contact your system administrator.
> Add correct host key in C:\\Users\\moralok/.ssh/known_hosts to get rid of this message.
> Offending ECDSA key in C:\\Users\\moralok/.ssh/known_hosts:1
> ECDSA host key for 192.168.46.128 has changed and you have requested strict checking.
> Host key verification failed.
> 过程试图写入的管道不存在。
>
-

使用 minikube dashboard 启动控制台,在访问时同样提示连接被拒绝:dial tcp 127.0.0.1:8080: connect: connection refused

-

考虑到可能还有别的问题,决定采用官方建议将用户添加到 docker 用户组。

-
将用户添加到 docker 用户组

使用 sudo usermod -aG docker $USER && newgrp docker 将当前用户添加到 docker 用户组并切换当前用户组到 docker 用户组后,正常启动集群。

-
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: ssh, none
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 393.36 MiB / 397.02 MiB 99.08% 19.35 Mi! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 21.44 M
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: storage-provisioner, default-storageclass
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
-

创建集群时下载镜像失败

在解决问题的过程中,发现有人存在下载镜像失败的情况。从启动日志可以看到,由于 minikube 下载 gcr.io/k8s-minikube/kicbase:v0.0.39 镜像失败,自动下载 docker.io/kicbase/stable:v0.0.39 镜像作为备选。如果从 docker.io 下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。

+

SSH 的原理

关于 SSH

使用 SSH 协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github 还可以使用 SSH 密钥对提交进行签名。

+

公钥和私钥

SSH 的使用(非对称加密)需要生成公钥 public key 和私钥 private key。常用的算法有 rsaecdsaed25519,相对应的公钥默认文件名即 id_XXX.pubed25519 的安全性介于 rsa 2048rsa 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"
+ + +

SSH 公钥登录过程

    +
  1. Client 将自己的公钥存放到服务端,追加到 authorized_keys 文件。
  2. +
  3. Server 收到 Client 的连接请求后,会在 authorized_keys 文件中匹配到 Client 传过来的公钥,并生成随机数 R,用公钥对随机数加密得到 pubKey(R)
  4. +
  5. Client 收到后通过私钥解密得到随机数 R,然后对随机数 R 和本次会话的 sessionKey 使用 MD5 生成摘要 Digest1,发送给服务端。
  6. +
  7. Server 会对随机数 R 和会话的 sessionKey 同样使用 MD5 生成摘要 Digest2,对比相同即完成认证过程。
  8. +
+

避免中间人攻击

SSH 通过口令确认避免中间人攻击,如果用户第一次登录 Server,系统会提示:

+
$ ssh -T git@github.com
The authenticity of host 'github.com (20.205.243.166)' can't be established.
ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,20.205.243.166' (ECDSA) to the list of known hosts.
Hi ${username}! You've successfully authenticated, but GitHub does not provide shell access.
+ +

Server 需要在其网站上公示其公钥的指纹,Github 的公钥指纹在这里
确认匹配后,客户端会在 ~/.ssh/known_hosts 中记录,下次登录不再警告。

+

参考链接

使用 SSH 进行连接 Github
Git 多台电脑共用SSH Key
SSH协议登录过程详解
GitHub 的 SSH 密钥指纹
使用 Ed25519 算法生成你的 SSH 密钥

+]]>
+ + ssh + + + + 如何在 Ubuntu 20.04 上安装 Minikube + /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ + 环境搭建

安装 Ubuntu 20.04

使用 Vmware Workstation 通过 ubuntu-20.04.6-live-server-amd64.iso 安装。需要满足条件如下:

+
    +
  • 2 个或更多 CPU(2 CPU)。
  • +
  • 2GB 可用内存(4GB)。
  • +
  • 20GB 可用磁盘空间(30GB)。
  • +
  • 网络连接
  • +
  • 容器或虚拟机管理器(Docker)。
  • +
+

安装 Docker

参考官方文档:Install Docker Engine on Ubuntu

+

安装 Minikube

参考官方文档:minikube start

+

安装 kubectl 并启动 kubectl 自动补全功能

参考官方文档:在 Linux 系统中安装并设置 kubectl

+

开始使用

创建集群:

+
minikube start
+ +

创建集群时的权限问题

不加 sudo

不加 sudo 的时候,创建集群失败,提示无法选择默认 driver。可能是 docker 处于不健康状态或者用户权限不足。

+
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Unable to pick a default driver. Here is what was considered, in preference order:
- docker: Not healthy: "docker version --format {{.Server.Os}}-{{.Server.Version}}:{{.Server.Platform.Name}}" exit status 1: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/version": dial unix /var/run/docker.sock: connect: permission denied
- docker: Suggestion: Add your user to the 'docker' group: 'sudo usermod -aG docker $USER && newgrp docker' <https://docs.docker.com/engine/install/linux-postinstall/>
* Alternatively you could install one of these drivers:
- kvm2: Not installed: exec: "virsh": executable file not found in $PATH
- podman: Not installed: exec: "podman": executable file not found in $PATH
- qemu2: Not installed: exec: "qemu-system-x86_64": executable file not found in $PATH
- virtualbox: Not installed: unable to find VBoxManage in $PATH

X Exiting due to DRV_NOT_HEALTHY: Found driver(s) but none were healthy. See above for suggestions how to fix installed drivers.
+ +
使用 sudo

使用 sudo 的时候,会提示不建议通过 root 权限使用 docker,如果还是想要继续,可以使用选项 --force

+
$ sudo minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: none, ssh
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/

X Exiting due to DRV_AS_ROOT: The "docker" driver should not be used with root privileges.
+ +
使用选项 –force

考虑到仅用于测试,尝试通过 sudo minikube start --force 启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。

+
$ sudo minikube start --force
* minikube v1.30.1 on Ubuntu 20.04
! minikube skips various validations when --force is supplied; this may lead to unexpected behavior
* Automatically selected the docker driver. Other choices: ssh, none
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 25.89 M
> index.docker.io/kicbase/sta...: 373.53 MiB / 373.53 MiB 100.00% 7.17 Mi
! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: default-storageclass, storage-provisioner
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
+ +

成功启动集群后,使用 kubectl get pod 测试,提示连接被拒绝。

+
$ kubectl get pod
E0622 21:55:27.400754 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.401000 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410464 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410951 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.412076 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?
+ +

使用 minikube dashboard 启动控制台,在访问时同样提示连接被拒绝:dial tcp 127.0.0.1:8080: connect: connection refused

+

考虑到可能还有别的问题,决定采用官方建议将用户添加到 docker 用户组。

+
将用户添加到 docker 用户组

使用 sudo usermod -aG docker $USER && newgrp docker 将当前用户添加到 docker 用户组并切换当前用户组到 docker 用户组后,正常启动集群。

+
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: ssh, none
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 393.36 MiB / 397.02 MiB 99.08% 19.35 Mi! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 21.44 M
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: storage-provisioner, default-storageclass
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
+ +

创建集群时下载镜像失败

在解决问题的过程中,发现有人存在下载镜像失败的情况。从启动日志可以看到,由于 minikube 下载 gcr.io/k8s-minikube/kicbase:v0.0.39 镜像失败,自动下载 docker.io/kicbase/stable:v0.0.39 镜像作为备选。如果从 docker.io 下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。

$ minikube start --help | grep repo
--image-repository='':
Alternative image repository to pull docker images from. This can be used when you have limited access to gcr.io. Set it to "auto" to let minikube decide one for you. For Chinese mainland users, you may use local gcr.io mirrors such as registry.cn-hangzhou.aliyuncs.com/google_containers

$ minikube start --image-repository='registry.cn-hangzhou.aliyuncs.com/google_containers'

如何通过宿主机进行访问 minikube 控制台

启动控制台:

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

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

-
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
+ JVM GC 的测试和分析 + /2023/11/01/testing-and-analysis-of-jvm-gc/ + 堆的组成
public class JvmGcTest_1 {

public static final int _512KB = 512 * 1024;
public static final int _1MB = 1024 * 1024;
public static final int _6MB = 6 * 1024 * 1024;
public static final int _7MB = 7 * 1024 * 1024;
public static final int _8MB = 8 * 1024 * 1024;

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// -XX:+UseSerialGC 避免幸存区比例动态调整
public static void main(String[] args) {

}
}
-

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

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

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

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
+
Heap
def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf68c8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3288K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
-

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

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

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

-
-

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

-
-

怎么并行地加载类 getClassLoadingLock

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

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

根据打印的信息,组成如下:

+
    +
  • 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 + fromto 空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 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) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
-
什么是 “ClassLoader 对象注册为具有并行能力”呢?

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

-
static {
ClassLoader.registerAsParallelCapable();
}
+
[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] 
Heap
def new generation total 9216K, used 8135K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
-

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

-
    -
  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. -
  3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
  4. -
-
怎么保证这两个条件成立呢?
    -
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. -
  3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
  4. -
-

你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

-
注册工作做了什么?

简单地说就是保存了类加载器所属 ClassSet

-
@CallerSensitive
protected static boolean registerAsParallelCapable() {
// 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
// 注册调用者的 Class 为具有并行能力
return ParallelLoaders.register(callerClass);
}
+

Allocation Failure,正常情况下,新对象总是分配在 Eden,分配空间失败,eden 的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
[DefNew: 2013K->721K(9216K), 0.0105099 secs],新生代在垃圾回收前后空间的占用变化和耗时。
2013K->721K(19456K), 0.0105455 secs,整个堆在垃圾回收前后空间的占用变化和耗时。

+

GC 类型

    +
  • GC: minor GC。
  • +
  • Fulle GC: full GC。
  • +
+

from 和 to 的角色变换

from 的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0),空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden 区域存活的对象全部转移到了原 to 空间,fromto 空间的角色相互转换(从地址空间的信息可以看到此时 to 的地址指针比 from 的地址指针小)。
eden 的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0),空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden 区域除了 byte[] 对象外,还存储了其他对象,比如为了创建 List<byte[]> 对象而新加载的类对象。

+

eden 空间足够时不发生 GC

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
}
-

方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

-
// private 修饰,只有其外部类 ClassLoader 才可以使用
// static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
private static class ParallelLoaders {
// private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
private ParallelLoaders() {}

// the set of parallel capable loader types
// 使用 loaderTypes 时通过 synchronized 加同步锁
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
// todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 将 ClassLoader 本身注册为具有并行能力
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
// 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
// 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}

/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
+
[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] 
Heap
def new generation total 9216K, used 8647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 96% used [0x00000000fec00000, 0x00000000ff3bd8d0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
-
“注册”怎么和锁产生联系?

但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

-
private ClassLoader(Void unused, ClassLoader parent) {
// 由 private 修饰,不允许子类重写
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
// 如果类加载器已经注册为具有并行能力,则做一些赋值操作
parallelLockMap = new ConcurrentHashMap<>();
// 保存 package->certs 的 map 映射,相关的工作也可以并行进行
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}

// 由 protect 修饰,允许子类重写,传递了父·类加载器。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

// 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
+

由于 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) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
-

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) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

private native final Class<?> findLoadedClass0(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] 
[GC (Allocation Failure) [DefNew: 8565K->512K(9216K), 0.0046378 secs] 8565K->8396K(19456K), 0.0046540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 1350K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd1a20, 0x00000000ff400000)
from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7884K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb33a0, 0x00000000ffdb3400, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
-

保证核心类库的安全性:双亲委派模型

正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

-
-

在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

-
-
为什么要用这套奇怪的机制

这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

-

通常流程如下:

-
    -
  1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
  2. -
  3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
  4. -
  5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  6. -
  7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  8. -
  9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  10. -
  11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  12. -
-

注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

-

查找类资源:findClass

默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

-

过程如下:

-
    -
  1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
  2. -
  3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
  4. -
  5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
  6. -
-
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// todo:
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将 name 转换为 path
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 中查找 Resource
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
+

在第三次添加时,由于 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 不少。
此时 edenfromtenured 中均有不好确认成分的空间占用,比如 from 中多了 56 字节。

+

新生代空间不足,大对象直接在老年代创建

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
-

查找类的目录列表:URLClassPath

URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
URLClassPath 的核心构造器方法:

-
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
push(urls);
if (factory != null) {
// 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
jarHandler = factory.createURLStreamHandler("jar");
}
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
+
Heap
def new generation total 9216K, used 2177K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee20730, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3353K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
-
URLClassPath#getResource

URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

-
public Resource getResource(String name, boolean check) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + name + "\")");
}

Loader loader;
// 获取缓存(默认没有用)
int[] cache = getLookupCache(name);
// 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
Resource res = loader.getResource(name, check);
if (res != null) {
return res;
}
}
return null;
}
+

在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。

+

内存不足 OOM

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
-
URLClassPath#getNextLoader

获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

-
private synchronized Loader getNextLoader(int[] cache, int index) {
if (closed) {
return null;
}
if (cache != null) {
if (index < cache.length) {
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 获取 Loader
return getLoader(index);
}
}
+
waiting...
[GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)

Heap
def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
class space used 522K, capacity 558K, committed 640K, reserved 1048576K
-
URLClassPath#getLoader(int)
    -
  1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
  2. -
  3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
  4. -
  5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
  6. -
-
private synchronized Loader getLoader(int index) {
if (closed) {
return null;
}
// Expand URL search path until the request can be satisfied
// or the URL stack is empty.
while (loaders.size() < index + 1) {
// Pop the next URL from the URL stack
// 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
URL url;
synchronized (urls) {
if (urls.empty()) {
return null;
} else {
url = urls.pop();
}
}
// Skip this URL if it already has a Loader. (Loader
// may be null in the case where URL has not been opened
// but is referenced by a JAR index.)
String urlNoFragString = URLUtil.urlNoFragString(url);
if (lmap.containsKey(urlNoFragString)) {
continue;
}
// Otherwise, create a new Loader for the URL.
Loader loader;
try {
// 根据 URL 创建 Loader
loader = getLoader(url);
// If the loader defines a local class path then add the
// URLs to the list of URLs to be opened.
URL[] urls = loader.getClassPath();
if (urls != null) {
push(urls);
}
} catch (IOException e) {
// Silently ignore for now...
continue;
} catch (SecurityException se) {
// Always silently ignore. The context, if there is one, that
// this URLClassPath was given during construction will never
// have permission to access the URL.
if (DEBUG) {
System.err.println("Failed to access " + url + ", " + se );
}
continue;
}
// Finally, add the Loader to the search path.
validateLookupCache(loaders.size(), urlNoFragString);
loaders.add(loader);
lmap.put(urlNoFragString, loader);
}
if (DEBUG_LOOKUP_CACHE) {
System.out.println("NOCACHE: Loading from : " + index );
}
return loaders.get(index);
}
+

当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError

+

线程中发生内存不足,不会影响其他线程

public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();

new Thread(() -> {
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();

System.out.println("waiting...");
try {
System.in.read();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
-
URLClassPath#getLoader(java.net.URL)

根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

-
    -
  1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
  2. -
  3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
  4. -
  5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
  6. -
-
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}
+
[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] 
[Full GC (Allocation Failure) [Tenured: 8912K->8895K(10240K), 0.0011880 secs] 8912K->8895K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0012009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8895K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffeafce0, 0x00000000ffeafe00, 0x0000000100000000)
Metaspace used 3380K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 363K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.main(JvmGcTest.java:21)
-

URLClassPath.FileLoader#getResource

FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

-
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}

if (check)
URLClassPath.check(url);

final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}

if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}
+

Thread-0 发生 OutOfMemoryError 后,main 线程仍然正常运行。

+

大对象的划分指标

当创建的大对象 + 对象头的容量小于等于 eden,如果 GC 后的存活对象可以放入 to,那么还是会先在 eden 中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。

+
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB - 16]);
}
-

ClassLoader 的搜索路径

从上文可知,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] 
[GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
Heap
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
-
bootstrap class loader

启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

-
    -
  • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
  • -
  • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
  • -
  • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
  • +

    尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。

    +

    注意事项

      +
    • 正常情况下,新对象都是在 eden 中创建。
    • +
    • 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。
    • +
    • 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。
    • +
    • 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。
    • +
    • 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。
    -

    IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

    -
    Error occurred during initialization of VM
    java/lang/NoClassDefFoundError: java/lang/Object

    进程已结束,退出代码1
    +

    尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

    +]]> + + java + jvm + + + + Spring 应用 context 刷新流程 + /2023/08/04/Spring-application-context-refresh-process/ + context 刷新流程简单图解

    刷新流程

    -
    ExtClassLoader

    扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

    -
    AppClassLoader

    应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

    -

    CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

    -
      -
    • Windows:.;C:\path\to\classes
    • -
    • Linux:.:/path/to/classes
    • -
    -

    事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

    -

    字节数据转换为 Class 实例:defineClass

    方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
    以下是定义类时最终可能调用的 native 方法。

    -
    private native Class<?> defineClass0(String name, byte[] b, int off, int len,
    ProtectionDomain pd);

    private native Class<?> defineClass1(String name, byte[] b, int off, int len,
    ProtectionDomain pd, String source);

    private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
    int off, int len, ProtectionDomain pd,
    String source);
    +

    刷新流程中的组件

    -

    其方法参数有:

    -
      -
    • name,目标类的名称。
    • -
    • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
    • -
    • ProtectionDomain,保护域,todo:
    • -
    • sourceCodeSource 的位置。
    • -
    -

    defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

    -

    URLClassLoader#defineClass

    此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

    -
    private Class<?> defineClass(String name, Resource res) throws IOException {
    long t0 = System.nanoTime();
    // 获取最后一个 . 的位置
    int i = name.lastIndexOf('.');
    // 返回资源的 CodeSourceURL
    URL url = res.getCodeSourceURL();
    if (i != -1) {
    // 截取包名 com.example
    String pkgname = name.substring(0, i);
    // Check if package already loaded.
    Manifest man = res.getManifest();
    definePackageInternal(pkgname, man, url);
    }
    // Now read the class bytes and define the class
    // 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
    java.nio.ByteBuffer bb = res.getByteBuffer();
    if (bb != null) {
    // Use (direct) ByteBuffer:
    // 不常用
    CodeSigner[] signers = res.getCodeSigners();
    CodeSource cs = new CodeSource(url, signers);
    sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
    // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
    return defineClass(name, bb, cs);
    } else {
    // 以字节数组的形式返回资源数据
    byte[] b = res.getBytes();
    // must read certificates AFTER reading bytes.
    // 必须再读取字节数据后读取证书,todo:
    CodeSigner[] signers = res.getCodeSigners();
    // 根据 URL 和签名者创建 CodeSource
    CodeSource cs = new CodeSource(url, signers);
    sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
    // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
    return defineClass(name, b, 0, b.length, cs);
    }
    }
    +

    上下文刷新 AbstractApplicationContext#refresh

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

    Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

    -
    public byte[] getBytes() throws IOException {
    byte[] b;
    // Get stream before content length so that a FileNotFoundException
    // can propagate upwards without being caught too early
    // 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
    // 获取缓存的 InputStream
    InputStream in = cachedInputStream();

    // This code has been uglified to protect against interrupts.
    // Even if a thread has been interrupted when loading resources,
    // the IO should not abort, so must carefully retry, failing only
    // if the retry leads to some other IO exception.
    // 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
    // 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
    boolean isInterrupted = Thread.interrupted();
    int len;
    for (;;) {
    try {
    // 获取内容长度,顺利的话就跳出循环
    len = getContentLength();
    break;
    } catch (InterruptedIOException iioe) {
    // 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
    Thread.interrupted();
    isInterrupted = true;
    }
    }

    try {
    b = new byte[0];
    if (len == -1) len = Integer.MAX_VALUE;
    int pos = 0;
    while (pos < len) {
    int bytesToRead;
    if (pos >= b.length) { // Only expand when there's no room
    // 如果当前读取位置已经大于等于数组长度
    // 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
    bytesToRead = Math.min(len - pos, b.length + 1024);
    if (b.length < pos + bytesToRead) {
    // 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
    b = Arrays.copyOf(b, pos + bytesToRead);
    }
    } else {
    // 数组还有空间,待读取字节长度 = 数组剩余空间
    bytesToRead = b.length - pos;
    }
    int cc = 0;
    try {
    // 读取数据
    cc = in.read(b, pos, bytesToRead);
    } catch (InterruptedIOException iioe) {
    // 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
    Thread.interrupted();
    isInterrupted = true;
    }
    if (cc < 0) {
    // 如果读取返回值 < 0
    if (len != Integer.MAX_VALUE) {
    // 且长度并未无限,表示提前检测到 EOF,抛出异常
    throw new EOFException("Detect premature EOF");
    } else {
    // 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
    if (b.length != pos) {
    b = Arrays.copyOf(b, pos);
    }
    break;
    }
    }
    pos += cc;
    }
    } finally {
    try {
    in.close();
    } catch (InterruptedIOException iioe) {
    isInterrupted = true;
    } catch (IOException ignore) {}

    if (isInterrupted) {
    // 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
    Thread.currentThread().interrupt();
    }
    }
    return b;
    }
    +

    准备 context 以供刷新 prepareRefresh

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

    +
    +

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

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

    getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

    -
    private synchronized InputStream cachedInputStream() throws IOException {
    if (cis == null) {
    cis = getInputStream();
    }
    return cis;
    }
    - -

    在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

    -
    public InputStream getInputStream() throws IOException
    {
    // 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
    return new FileInputStream(file);
    }

    public int getContentLength() throws IOException
    {
    // 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
    return (int)file.length();
    };
    - -

    SecureClassLoader#defineClass

    URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
    由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

    -
    protected final Class<?> defineClass(String name,
    byte[] b, int off, int len,
    CodeSource cs)
    {
    return defineClass(name, b, off, len, getProtectionDomain(cs));
    }

    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
    CodeSource cs)
    {
    return defineClass(name, b, getProtectionDomain(cs));
    }
    - -

    方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

    -
    private ProtectionDomain getProtectionDomain(CodeSource cs) {
    // 如果 CodeSource 为 null,直接返回 null
    if (cs == null)
    return null;

    ProtectionDomain pd = null;
    synchronized (pdcache) {
    // 先从 Map 缓存中获取 ProtectionDomain
    pd = pdcache.get(cs);
    if (pd == null) {
    // 从 CodeSource 中获取 PermissionCollection
    PermissionCollection perms = getPermissions(cs);
    // 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
    pd = new ProtectionDomain(cs, perms, this, null);
    pdcache.put(cs, pd);
    if (debug != null) {
    debug.println(" getPermissions "+ pd);
    debug.println("");
    }
    }
    }
    return pd;
    }
    - -
    getPermissions

    根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

    -

    AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

    -
    protected PermissionCollection getPermissions(CodeSource codesource)
    {
    // 调用父类 URLClassLoader 的 getPermissions
    PermissionCollection perms = super.getPermissions(codesource);
    // 允许从类路径加载的任何类退出 VM的权限。
    // todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
    perms.add(new RuntimePermission("exitVM"));
    return perms;
    }
    - -

    SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

    -
    protected PermissionCollection getPermissions(CodeSource codesource)
    {
    // 调用父类 SecureClassLoader 的 getPermissions
    PermissionCollection perms = super.getPermissions(codesource);

    URL url = codesource.getLocation();

    Permission p;
    URLConnection urlConnection;

    try {
    // FileURLConnection 实例
    urlConnection = url.openConnection();
    // 允许 read 的 FilePermission 实例
    p = urlConnection.getPermission();
    } catch (java.io.IOException ioe) {
    p = null;
    urlConnection = null;
    }

    if (p instanceof FilePermission) {
    // if the permission has a separator char on the end,
    // it means the codebase is a directory, and we need
    // to add an additional permission to read recursively
    // 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
    String path = p.getName();
    if (path.endsWith(File.separator)) {
    path += "-";
    p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
    }
    } else if ((p == null) && (url.getProtocol().equals("file"))) {
    String path = url.getFile().replace('/', File.separatorChar);
    path = ParseUtil.decode(path);
    if (path.endsWith(File.separator))
    path += "-";
    p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
    } else {
    /**
    * Not loading from a 'file:' URL so we want to give the class
    * permission to connect to and accept from the remote host
    * after we've made sure the host is the correct one and is valid.
    */
    URL locUrl = url;
    if (urlConnection instanceof JarURLConnection) {
    locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
    }
    String host = locUrl.getHost();
    if (host != null && (host.length() > 0))
    p = new SocketPermission(host,
    SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
    }

    // make sure the person that created this class loader
    // would have this permission

    if (p != null) {
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
    final Permission fp = p;
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() throws SecurityException {
    sm.checkPermission(fp);
    return null;
    }
    }, acc);
    }
    perms.add(p);
    }
    return perms;
    }
    - -

    SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

    -
    protected PermissionCollection getPermissions(CodeSource codesource)
    {
    // 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
    // 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
    check();
    // ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
    return new Permissions(); // ProtectionDomain defers the binding
    }
    - -
    ProtectionDomain

    ProtectionDomain 的相关构造器参数:

    -
      -
    • CodeSource
    • -
    • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
    • -
    • ClassLoader
    • -
    • Principal[]
    • -
    -

    这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

    -

    ClassLoader#defineClass

    抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

    -
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
    {
    return defineClass(name, b, off, len, null);
    }

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
    ProtectionDomain protectionDomain)
    throws ClassFormatError
    {
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
    }

    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
    ProtectionDomain protectionDomain)
    throws ClassFormatError
    {
    int len = b.remaining();

    // Use byte[] if not a direct ByteBufer:
    if (!b.isDirect()) {
    if (b.hasArray()) {
    return defineClass(name, b.array(),
    b.position() + b.arrayOffset(), len,
    protectionDomain);
    } else {
    // no array, or read-only array
    byte[] tb = new byte[len];
    b.get(tb); // get bytes out of byte buffer.
    return defineClass(name, tb, 0, len, protectionDomain);
    }
    }

    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
    }
    - -

    主要步骤:

    -
      -
    1. preDefineClass 前置处理
    2. -
    3. defineClassX
    4. -
    5. postDefineClass 后置处理
    6. -
    -
    preDefineClass

    确定保护域 ProtectionDomain,并检查:

    -
      -
    1. 未定义 java.*
    2. -
    3. 该类的签名者与包(package)中其余类的签名者相匹配
    4. -
    -
    private ProtectionDomain preDefineClass(String name,
    ProtectionDomain pd)
    {
    // 检查 name 为 null 或者有可能是有效的二进制名称
    if (!checkName(name))
    throw new NoClassDefFoundError("IllegalName: " + name);

    // Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    // 如果 name 以 java. 开头,则抛出异常
    if ((name != null) && name.startsWith("java.")) {
    throw new SecurityException
    ("Prohibited package name: " +
    name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
    // 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
    pd = defaultDomain;
    }

    // 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
    // todo: certs
    if (name != null) checkCerts(name, pd.getCodeSource());

    return pd;
    }
    - -
    defineClassSourceLocation

    确定 ClassCodeSource 位置。

    -
    private String defineClassSourceLocation(ProtectionDomain pd)
    {
    CodeSource cs = pd.getCodeSource();
    String source = null;
    if (cs != null && cs.getLocation() != null) {
    source = cs.getLocation().toString();
    }
    return source;
    }
    - -
    defineClassX 方法

    这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

    -
    postDefineClass

    在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

    -
    private void postDefineClass(Class<?> c, ProtectionDomain pd)
    {
    if (pd.getCodeSource() != null) {
    // 获取证书
    Certificate certs[] = pd.getCodeSource().getCertificates();
    if (certs != null)
    setSigners(c, certs);
    }
    }
    ]]>
    - - java - class loader - -
    - - JVM GC 的测试和分析 - /2023/11/01/testing-and-analysis-of-jvm-gc/ - 堆的组成
    public class JvmGcTest_1 {

    public static final int _512KB = 512 * 1024;
    public static final int _1MB = 1024 * 1024;
    public static final int _6MB = 6 * 1024 * 1024;
    public static final int _7MB = 7 * 1024 * 1024;
    public static final int _8MB = 8 * 1024 * 1024;

    // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    // -XX:+UseSerialGC 避免幸存区比例动态调整
    public static void main(String[] args) {

    }
    }
    - -
    Heap
    def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf68c8, 0x00000000ff400000)
    from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
    Metaspace used 3288K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 348K, capacity 388K, committed 512K, reserved 1048576K
    - -

    根据打印的信息,组成如下:

    -
      -
    • 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 + fromto 空间只是在使用标记-复制算法进行垃圾回收时使用。
    老年代的空间为 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) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    }
    - -
    [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] 
    Heap
    def new generation total 9216K, used 8135K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
    from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
    to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
    Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 360K, capacity 388K, committed 512K, reserved 1048576K
    - -

    Allocation Failure,正常情况下,新对象总是分配在 Eden,分配空间失败,eden 的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
    [DefNew: 2013K->721K(9216K), 0.0105099 secs],新生代在垃圾回收前后空间的占用变化和耗时。
    2013K->721K(19456K), 0.0105455 secs,整个堆在垃圾回收前后空间的占用变化和耗时。

    -

    GC 类型

      -
    • GC: minor GC。
    • -
    • Fulle GC: full GC。
    • -
    -

    from 和 to 的角色变换

    from 的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0),空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden 区域存活的对象全部转移到了原 to 空间,fromto 空间的角色相互转换(从地址空间的信息可以看到此时 to 的地址指针比 from 的地址指针小)。
    eden 的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0),空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden 区域除了 byte[] 对象外,还存储了其他对象,比如为了创建 List<byte[]> 对象而新加载的类对象。

    -

    eden 空间足够时不发生 GC

    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
    }
    - -
    [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] 
    Heap
    def new generation total 9216K, used 8647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 96% used [0x00000000fec00000, 0x00000000ff3bd8d0, 0x00000000ff400000)
    from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
    to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
    Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 360K, capacity 388K, committed 512K, reserved 1048576K
    - -

    由于 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) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    list.add(new byte[_512KB]);
    list.add(new byte[_512KB]);
    }
    - -
    [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] 
    [GC (Allocation Failure) [DefNew: 8565K->512K(9216K), 0.0046378 secs] 8565K->8396K(19456K), 0.0046540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Heap
    def new generation total 9216K, used 1350K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd1a20, 0x00000000ff400000)
    from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
    to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    tenured generation total 10240K, used 7884K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb33a0, 0x00000000ffdb3400, 0x0000000100000000)
    Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 360K, capacity 388K, committed 512K, reserved 1048576K
    - -

    在第三次添加时,由于 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 不少。
    此时 edenfromtenured 中均有不好确认成分的空间占用,比如 from 中多了 56 字节。

    -

    新生代空间不足,大对象直接在老年代创建

    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
    }
    - -
    Heap
    def new generation total 9216K, used 2177K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee20730, 0x00000000ff400000)
    from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
    Metaspace used 3353K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 360K, capacity 388K, committed 512K, reserved 1048576K
    - -

    在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。

    -

    内存不足 OOM

    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB]);
    list.add(new byte[_8MB]);
    }
    - -
    waiting...
    [GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    [Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
    at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
    at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:750)

    Heap
    def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
    from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
    Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
    class space used 522K, capacity 558K, committed 640K, reserved 1048576K
    - -

    当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError

    -

    线程中发生内存不足,不会影响其他线程

    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();

    new Thread(() -> {
    list.add(new byte[_8MB]);
    list.add(new byte[_8MB]);
    }).start();

    System.out.println("waiting...");
    try {
    System.in.read();
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    - -
    [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] 
    [Full GC (Allocation Failure) [Tenured: 8912K->8895K(10240K), 0.0011880 secs] 8912K->8895K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0012009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
    Heap
    def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
    from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    tenured generation total 10240K, used 8895K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffeafce0, 0x00000000ffeafe00, 0x0000000100000000)
    Metaspace used 3380K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 363K, capacity 388K, committed 512K, reserved 1048576K
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at com.moralok.jvm.gc.JvmGcTest.main(JvmGcTest.java:21)
    - -

    Thread-0 发生 OutOfMemoryError 后,main 线程仍然正常运行。

    -

    大对象的划分指标

    当创建的大对象 + 对象头的容量小于等于 eden,如果 GC 后的存活对象可以放入 to,那么还是会先在 eden 中创建大对象。
    在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。

    -
    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_8MB - 16]);
    }
    - -
    [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 (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
    Heap
    def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
    from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
    Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
    class space used 354K, capacity 388K, committed 512K, reserved 1048576K
    - -

    尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。

    -

    注意事项

      -
    • 正常情况下,新对象都是在 eden 中创建。
    • -
    • 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。
    • -
    • 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。
    • -
    • 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。
    • -
    • 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。
    • -
    -

    尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

    -]]>
    - - java - jvm - -
    - - Spring 应用 context 刷新流程 - /2023/08/04/Spring-application-context-refresh-process/ - context 刷新流程简单图解

    刷新流程

    - -

    刷新流程中的组件

    - -

    上下文刷新 AbstractApplicationContext#refresh

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

    准备 context 以供刷新 prepareRefresh

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

    -
    -

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

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

    obtainFreshBeanFactory

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

    -
    -

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

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

    obtainFreshBeanFactory

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

    +
    +

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

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

    准备 beanFactory

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

      @@ -2292,73 +2149,6 @@ bytecode - - 基于 Redis 的分布式锁的简单实现 - /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ - 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

      - - -

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

      -

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

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

      -
      > setnx lock:user1 true
      OK

      ... do something

      > del lock:user1
      (integer) 1
      - -

      死锁问题

      问题一:异常引发死锁 1

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

      - - -

      改进一:设置超时时间

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

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

      ... do something

      > del lock:user1
      (integer) 1
      - -

      问题二:异常引发死锁 2

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

      - - -

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

      -

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

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

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

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

      -
      > set lock:user1 true ex 5 nx
      OK

      ... do something

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

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

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

      超时问题

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

      - - -

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

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

      -

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

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

      -

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

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

      -

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

      -

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

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

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

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

      -

      改进三:加锁时指定 tag

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

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

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

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

      可重入性

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

      -

      使用 ThreadLocal 实现锁计数

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

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

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

      使用 Redis hash 实现锁计数

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

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

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

      -

      代码实现

      redis-lock

      -

      参考文章

        -
      • 《Redis 深度历险,核心原理与应用实践》
      • -
      -]]>
      - - lock - distributed lock - redis - -
      JVM 内存区域的测试和分析 /2023/11/04/testing-and-analysis-of-jvm-memory-area/ @@ -2505,38 +2295,105 @@ - 使用 Grafana 和 Prometheus 搭建监控 - /2023/11/18/setup-monitoring-using-grafana-and-prometheus/ - 本文介绍如何通过 Dockers Compose 安装 GrafanaPrometheus 在局域网中配合各类 exporter 为主机和诸多内部服务搭建监控。

      - + 基于 Redis 的分布式锁的简单实现 + /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ + 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

      + -

      Grafana

      docker-compose 部署

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

      volumes:
      grafana-storage:
      driver: local

      networks:
      my-network:
      external: true
      +

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

      +

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

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

      +
      > setnx lock:user1 true
      OK

      ... do something

      > del lock:user1
      (integer) 1
      -

      问题记录

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

      -
      +

      死锁问题

      问题一:异常引发死锁 1

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

      + -

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

      -

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

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

      改进一:设置超时时间

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

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

      ... do something

      > del lock:user1
      (integer) 1
      -

      Prometheus

      docker-compose 部署

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

      volumes:
      prometheus-data:
      driver: local

      networks:
      my-network:
      external: true
      +

      问题二:异常引发死锁 2

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

      + -

      配置文件 prometheus.yml

      global:
      scrape_interval: 15s

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

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

      +

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

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

      + -

      Ubuntu 监控

      node_exporter

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

      - -

      步骤

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

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

      设置 node_exporter 自启动

        -
      1. 路径 /etc/systemd/system
      2. +
        基于原生指令的实现

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

        +
        > set lock:user1 true ex 5 nx
        OK

        ... do something

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

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

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

        超时问题

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

        + + +

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

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

        +

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

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

        +

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

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

        +

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

        +

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

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

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

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

        +

        改进三:加锁时指定 tag

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

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

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

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

        可重入性

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

        +

        使用 ThreadLocal 实现锁计数

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

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

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

        使用 Redis hash 实现锁计数

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

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

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

        +

        代码实现

        redis-lock

        +

        参考文章

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

        + + +

        Grafana

        docker-compose 部署

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

        volumes:
        grafana-storage:
        driver: local

        networks:
        my-network:
        external: true
        + +

        问题记录

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

        +
        + +

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

        +

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

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

        Prometheus

        docker-compose 部署

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

        volumes:
        prometheus-data:
        driver: local

        networks:
        my-network:
        external: true
        + +

        配置文件 prometheus.yml

        global:
        scrape_interval: 15s

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

        Ubuntu 监控

        node_exporter

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

        + +

        步骤

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

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

        设置 node_exporter 自启动

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

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

          [Install]
          WantedBy=multi-user.target
        @@ -2597,107 +2454,363 @@
        - Java 垃圾收集 - /2023/11/07/garbage-collection-in-Java/ - -

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

        - -

        概述

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

        -
          -
        • 哪些内存需要回收?
        • -
        • 什么时候回收?
        • -
        • 如何回收?
        • + JDK 动态代理和 CGLib + /2023/11/19/JDK-dynamic-proxy-and-CGLib/ + 介绍

          JDK 动态代理

          Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。

          +

          CGLib 动态代理

          使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。

          +

          cglib Github 仓库

          +

          适用场景

            +
          • JDK 动态代理适用于实现接口的类,对未实现接口的类无能为力。
          • +
          • CGLib 不要求类实现接口,但对 final 方法无能为力。
          • +
          +

          性能比较

            +
          • 在 JDK 8 以前,CGLib 性能更好
          • +
          • 从 JDK 8 开始,JDK 动态代理性能更好
          -

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

          -

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

          -

          哪些内存需要回收?

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

          -

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

          +

          根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。

          -

          引用计数算法(Reference Counting)

          优点:

          +

          使用

          代理对象的类和接口

          代理对象的类和实现的接口:

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

            +
            public interface HelloService {
            void sayHello(String name);
            }
            +
          • +
          • HelloService.java

            +
            public class HelloServiceImpl implements HelloService {
            @Override
            public void sayHello(String name) {
            if (name == null) {
            throw new IllegalArgumentException("name can not be null");
            }
            System.out.println("Hello " + name);
            }
            }
          -

          缺点:

          +

          JDK 动态代理示例

            +
          • 自定义 InvocationHandler
            public class UserServiceInvocationHandler implements InvocationHandler {

            private Object target;

            public UserServiceInvocationHandler(Object target) {
            this.target = target;
            }

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
            System.out.println("do sth. before invocation");
            Object ret = method.invoke(target, args);
            System.out.println("do sth. after invocation");
            return ret;
            } catch (Exception e) {
            System.out.println("do sth. when exception occurs");
            throw e;
            } finally {
            System.out.println("do sth. finally");
            }
            }
            }
          • +
          • 测试类
            public class JdkProxyTest {

            public static void main(String[] args) {
            HelloService target = new HelloServiceImpl();
            ClassLoader classLoader = target.getClass().getClassLoader();
            Class<?>[] interfaces = target.getClass().getInterfaces();
            UserServiceInvocationHandler invocationHandler = new UserServiceInvocationHandler(target);

            HelloService proxy = (HelloService) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
            proxy.sayHello("Tom");
            System.out.println("=================");
            proxy.sayHello(null);
            }
            }
          • +
          • 结果
            do sth. before invocation
            Hello Tom
            do sth. after invocation
            do sth. finally
            =================
            do sth. before invocation
            do sth. when exception occurs
            do sth. finally
            Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
            at com.sun.proxy.$Proxy0.sayHello(Unknown Source)
            at com.moralok.proxy.jdk.JdkProxyTest.main(JdkProxyTest.java:19)
            Caused by: java.lang.reflect.InvocationTargetException
            at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
            at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
            at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
            at java.lang.reflect.Method.invoke(Method.java:498)
            at com.moralok.proxy.jdk.UserServiceInvocationHandler.invoke(UserServiceInvocationHandler.java:18)
            ... 2 more
            Caused by: java.lang.IllegalArgumentException: name can not be null
            at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
          • +
          +

          CGLib 动态代理示例

            +
          • 引入依赖
            <dependencies>
            <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
            </dependency>
            </dependencies>
          • +
          • 自定义 MethodInterceptor
            public class UserServiceMethodInterceptor implements MethodInterceptor {

            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            try {
            System.out.println("do sth. before invocation");
            Object ret = proxy.invokeSuper(obj, args);
            System.out.println("do sth. after invocation");
            return ret;
            } catch (Exception e) {
            System.out.println("do sth. when exception occurs");
            throw e;
            } finally {
            System.out.println("do sth. finally");
            }
            }
            }
          • +
          • 测试类
            public class CglibTest {

            public static void main(String[] args) {
            UserServiceMethodInterceptor methodInterceptor = new UserServiceMethodInterceptor();
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(HelloServiceImpl.class);
            enhancer.setCallback(methodInterceptor);

            HelloService proxy = (HelloService) enhancer.create();
            proxy.sayHello("Tom");
            System.out.println("=================");
            proxy.sayHello(null);
            }
            }
          • +
          • 结果
            do sth. before invocation
            Hello Tom
            do sth. after invocation
            do sth. finally
            =================
            do sth. before invocation
            do sth. when exception occurs
            do sth. finally
            Exception in thread "main" java.lang.IllegalArgumentException: name can not be null
            at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
            at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.CGLIB$sayHello$0(<generated>)
            at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.invoke(<generated>)
            at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
            at com.moralok.proxy.cglib.UserServiceMethodInterceptor.intercept(UserServiceMethodInterceptor.java:14)
            at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.sayHello(<generated>)
            at com.moralok.proxy.cglib.CglibTest.main(CglibTest.java:18)
          • +
          +

          查看 JDK 生成的代理类

          使用以下语句,将在工作目录下生成代理类的 Class 文件。

          +
          System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
          + +
          public final class $Proxy0 extends Proxy implements HelloService {
          private static Method m1;
          private static Method m3;
          private static Method m2;
          private static Method m0;

          public $Proxy0(InvocationHandler var1) throws {
          // 将 InvocationHandler 传递给父类 Proxy
          super(var1);
          }

          public final boolean equals(Object var1) throws {
          try {
          return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
          } catch (RuntimeException | Error var3) {
          throw var3;
          } catch (Throwable var4) {
          throw new UndeclaredThrowableException(var4);
          }
          }

          // 代理方法调用 InvocationHandler 的 invoke 方法
          public final void sayHello(String var1) throws {
          try {
          super.h.invoke(this, m3, new Object[]{var1});
          } catch (RuntimeException | Error var3) {
          throw var3;
          } catch (Throwable var4) {
          throw new UndeclaredThrowableException(var4);
          }
          }

          public final String toString() throws {
          try {
          return (String)super.h.invoke(this, m2, (Object[])null);
          } catch (RuntimeException | Error var2) {
          throw var2;
          } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
          }
          }

          public final int hashCode() throws {
          try {
          return (Integer)super.h.invoke(this, m0, (Object[])null);
          } catch (RuntimeException | Error var2) {
          throw var2;
          } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
          }
          }

          // 静态代码块,初始化 Method 属性。
          static {
          try {
          m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
          m3 = Class.forName("com.moralok.proxy.HelloService").getMethod("sayHello", Class.forName("java.lang.String"));
          m2 = Class.forName("java.lang.Object").getMethod("toString");
          m0 = Class.forName("java.lang.Object").getMethod("hashCode");
          } catch (NoSuchMethodException var2) {
          throw new NoSuchMethodError(var2.getMessage());
          } catch (ClassNotFoundException var3) {
          throw new NoClassDefFoundError(var3.getMessage());
          }
          }
          }
          + +

          查看 CGLib 生成的子类

          使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。

          +
          System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class");
          +
            -
          • 例外情况多,需要额外处理(比如循环引用)
          • +
          • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.class,代理类
          • +
          • HelloServiceImpl$$FastClassByCGLIB$$a5654167.class,被代理类的 FastClass
          • +
          • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.class,代理类的 FastClass
          • +
          + + +

          代理类定义

          继承了被代理类。

          +
          public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory {
          }
          + +

          静态代码块

          static {
          // 调用静态钩子方法
          CGLIB$STATICHOOK1();
          }

          static void CGLIB$STATICHOOK1() {
          CGLIB$THREAD_CALLBACKS = new ThreadLocal();
          CGLIB$emptyArgs = new Object[0];
          Class var0 = Class.forName("com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31");
          Class var1;
          // 获取 Object 类的 equals、toString、hashCode、clone 这几个特定方法的 Method 对象
          Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
          // 还生成了相对应的 Method 属性保存(为了减少一次寻址吗?)
          CGLIB$equals$1$Method = var10000[0];
          // 为每一个 Method 创建一个 MethodProxy
          CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
          CGLIB$toString$2$Method = var10000[1];
          CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
          CGLIB$hashCode$3$Method = var10000[2];
          CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
          CGLIB$clone$4$Method = var10000[3];
          CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
          // 被代理类的方法也做相同处理
          CGLIB$sayHello$0$Method = ReflectUtils.findMethods(new String[]{"sayHello", "(Ljava/lang/String;)V"}, (var1 = Class.forName("com.moralok.proxy.HelloServiceImpl")).getDeclaredMethods())[0];
          CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");
          }
          + +

          MethodProxy 稍后再做介绍。

          +

          构造器方法

          构造器方法内,调用了绑定回调(Callbacks)方法。

          +
          public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() {
          CGLIB$BIND_CALLBACKS(this);
          }

          // 标识是否已经绑定过回调
          private boolean CGLIB$BOUND;

          private static final void CGLIB$BIND_CALLBACKS(Object var0) {
          HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 var1 = (HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31)var0;
          if (!var1.CGLIB$BOUND) {
          // 未绑定过回调则进行绑定,更新标识
          var1.CGLIB$BOUND = true;
          // 先获取 THREAD_CALLBACKS
          Object var10000 = CGLIB$THREAD_CALLBACKS.get();
          if (var10000 == null) {
          // 如果为 null,再获取 STATIC_CALLBACKS
          var10000 = CGLIB$STATIC_CALLBACKS;
          if (var10000 == null) {
          // 如果仍然为 null,直接返回
          return;
          }
          }

          // 每一个 Callback (像之前的 Method 一样)都有专门的属性保存
          var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
          }

          }
          + +

          生成的代理方法

          CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。

          +
          final void CGLIB$sayHello$0(String var1) {
          super.sayHello(var1);
          }

          public final void sayHello(String var1) {
          // 获取回调(拦截器)
          MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
          if (var10000 == null) {
          // 如果为 null,先进行回调绑定
          CGLIB$BIND_CALLBACKS(this);
          var10000 = this.CGLIB$CALLBACK_0;
          }

          if (var10000 != null) {
          // 如果回调(拦截器)不为 null,则调用 intercept 方法
          var10000.intercept(this, CGLIB$sayHello$0$Method, new Object[]{var1}, CGLIB$sayHello$0$Proxy);
          } else {
          // 否则直接调用父类方法
          super.sayHello(var1);
          }
          }
          + +

          CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。

          +

          分析 MethodProxy

          如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。

          +

          创建 MethodProxy

          MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。

          +
          CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");

          public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
          MethodProxy proxy = new MethodProxy();
          // sayHello 方法签名
          proxy.sig1 = new Signature(name1, desc);
          // CGLIB$sayHello$0 方法签名
          proxy.sig2 = new Signature(name2, desc);
          // 被代理类和代理类
          proxy.createInfo = new CreateInfo(c1, c2);
          return proxy;
          }
          + +

          CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。

          +
          private static class CreateInfo
          {
          // 被代理类
          Class c1;
          // 代理类
          Class c2;
          NamingPolicy namingPolicy;
          GeneratorStrategy strategy;
          boolean attemptLoad;

          public CreateInfo(Class c1, Class c2)
          {
          this.c1 = c1;
          this.c2 = c2;
          AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();
          if (fromEnhancer != null) {
          namingPolicy = fromEnhancer.getNamingPolicy();
          strategy = fromEnhancer.getStrategy();
          attemptLoad = fromEnhancer.getAttemptLoad();
          }
          }
          }
          + +

          FastClass 和方法索引对

          调用原始方法 invokeSuper

          MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。

          +
          // invoke 方法的代码相似
          public Object invokeSuper(Object obj, Object[] args) throws Throwable {
          try {
          // 初始化,生成 FastClassInfo
          init();
          FastClassInfo fci = fastClassInfo;
          // 调用原始(父类)方法
          return fci.f2.invoke(fci.i2, obj, args);
          } catch (InvocationTargetException e) {
          throw e.getTargetException();
          }
          }
          + +

          生成 FastClass 信息

          private void init()
          {
          // 双重校验锁,生成 FastClass 和方法索引对
          if (fastClassInfo == null)
          {
          synchronized (initLock)
          {
          if (fastClassInfo == null)
          {
          CreateInfo ci = createInfo;

          FastClassInfo fci = new FastClassInfo();
          // 生成 FastClass
          fci.f1 = helper(ci, ci.c1);
          fci.f2 = helper(ci, ci.c2);
          // 获取方法索引
          fci.i1 = fci.f1.getIndex(sig1);
          fci.i2 = fci.f2.getIndex(sig2);
          fastClassInfo = fci;
          createInfo = null;
          }
          }
          }
          }
          + +

          FastClass 信息

            +
          • f1 是被代理类的 FastClass 对象,i1 是 CGLIB$sayHello$0 方法在生成的 FastClass 中的索引。
          • +
          • f2 是代理类的 FastClass 对象,i2 是 sayHello 方法在生成的 FastClass 中的索引。
          +

          invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。

          -

          提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?

          +

          CGLib 完全有能力获得 CGLIB$sayHello$0 的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。

          -

          可达性分析算法(Reachability Analysis)

            -
          • 选取一系列称为“GC Roots”的根对象作为起始节点集。
          • -
          • 根据引用关系向下搜索。
          • -
          • 如果某个对象到 GC Roots 间没有任何引用链相连,即该对象不可能再被使用。用图论的话说,就是 GC Roots 到该对象不可达。
          • -
          - +
          private static class FastClassInfo
          {
          FastClass f1;
          FastClass f2;
          int i1;
          int i2;
          }
          -

          那么可作为 GC Roots 的对象有哪些呢?
          固定的 GC Roots,主要是在全局性引用和执行上下文中:

          +

          FastClass 的 invoke 方法

          以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511 为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0 方法。

            -
          1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
          2. -
          3. 在方法区中类静态属性引用的变量。
          4. -
          5. 在方法区中类常量引用的对象,比如字符串常量池(String Table)里的引用。
          6. -
          7. 在本地方法栈中 JNI,即 Native 方法引用的对象。
          8. -
          9. Java 虚拟机内部的引用,如基本类型的 Class 对象,常驻的异常类型,还有系统类加载器。
          10. -
          11. 所有被同步锁(synchronized)持有的对象
          12. -
          13. 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(不懂)。
          14. +
          15. 获取代理对象
          16. +
          17. 根据传入的方法索引,调用
          -

          临时性的GC Roots:
          除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。

          -
          -

          比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
          当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。

          -
          - +
          public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
          HelloServiceImpl..EnhancerByCGLIB..c51b2c31 var10000 = (HelloServiceImpl..EnhancerByCGLIB..c51b2c31)var2;
          int var10001 = var1;

          try {
          switch (var10001) {
          case 0:
          return new Boolean(var10000.equals(var3[0]));
          // ...
          case 16:
          var10000.CGLIB$sayHello$0((String)var3[0]);
          return null;
          // ...
          }
          } catch (Throwable var4) {
          throw new InvocationTargetException(var4);
          }

          throw new IllegalArgumentException("Cannot find matching method/constructor");
          }
          -

          “引用”的概念扩充

          对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
          缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。

          +

          获取方法索引

          怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。

          -

          曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
          当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。

          +

          在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?

          -

          引用的分类

            -
          • 强引用(Strongly Reference),只要强引用还在,绝不会回收。
          • -
          • 软引用(Soft Reference),只被软引用关联的对象,在系统发生 OOM 前,会被列入回收范围进行第二次回收。
          • -
          • 弱引用(Weak Reference),只被弱引用关联的对象,只能生存到下一次垃圾收集发生为止,无论内存是否 足够,都会回收。
          • -
          • 虚引用(Phantom Reference),一个对象是否有虚引用,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是为了在对象被回收时收到一个系统通知。
          • -
          -

          虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。

          -

          弱引用的测试

          public class ReferenceTest_3 {
          private static final int _4MB = 4 * 1024 * 1024;

          // -Xmx20m -XX:+PrintGCDetails -verbose:gc
          public static void main(String[] args) throws IOException {
          // list -> WeakReference -> byte[]
          List<WeakReference<byte[]>> list = new ArrayList<>();
          for (int i = 0; i < 10; i++) {
          WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
          list.add(ref);
          System.out.print(list.size() + " ");
          for (WeakReference<byte[]> w : list) {

          System.out.print(w.get() + " ");
          }
          System.out.println();
          }
          System.out.println("循环结束: " + list.size());
          System.in.read();
          }
          }
          +
          public int getIndex(Signature var1) {
          String var10000 = var1.toString();
          switch (var10000.hashCode()) {
          // ...
          case -1721191351:
          if (var10000.equals("CGLIB$sayHello$0(Ljava/lang/String;)V")) {
          return 16;
          }
          break;
          // ...
          }

          return -1;
          }
          -
          -

          在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
          这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。

          -
          -
          -

          了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。

          -
          -

          finalize 方法

          -

          有趣的知识点,无趣的面试考点。

          -
          -

          方法区的垃圾回收是什么样的?

            -
          • 《Java虚拟机规范》中提到可以不要求虚拟机在方法区实现垃圾收集
          • -
          • 确实有未实现或未完整实现方法区类型卸载的收集器
          • -
          • 原因是方法区垃圾收集的性价比通常比较低
          • -
          -

          方法区的垃圾收集主要回收两部分:

          +

          总结和思考

          两者在使用上是相仿的。

            -
          • 废弃的常量
          • -
          • 不再使用的类型
          • +
          • 对于两者的源码,读得不多。有时候会感慨,看这么多年前的代码,还是感觉吃力。有时候想,如果不好好看源码,心里不踏实;如果花很多时间理清楚了,但是发现更多只是知道了一些细节,于整体理解的提升不大,又会感觉不值得。
          • +
          • 但也提醒自己,不要太在意,用得本就不多,涉及源码的机会更是没有,如果方方面面都要细究,人生太短,智商不够,等涉足相关问题再回头研究。
          • +
          • 基础的用法和概念应该了解,不然看到 Spring AOP 源码时,分不清 Spring 封装的边界在哪里。
          -

          如何判定一个常量是否废弃?
          没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
          如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。

          -

          “虚拟机中也没有其他地方引用这个字面量”怎么理解?

          +

          借着梳理 Spring 的机会回头再看,又感觉轻松不少。

          -

          如何判定一个类型是否可卸载?

          +]]>
          + + java + jdk proxy + cglib + + + + Spring AOP 如何创建代理 beans + /2023/11/19/how-does-Spring-AOP-create-proxy-beans/ + Spring AOP 是基于代理实现的,它既支持 JDK 动态代理也支持 CGLib。

            -
          • 该类的所有的实例都已经被回收
          • -
          • 加载该类的类加载器已经被回收
          • -
          • 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
          • +
          • 在什么时候创建代理对象的?
          • +
          • 怎么创建代理对象的?
          -

          Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
          和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。

          -
          -

          条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
          “无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
          Class 对象没有被引用时,会被回收吗?
          卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?

          -
          -

          如何回收:垃圾收集算法

          分类:

          -
            -
          • 引用计数式垃圾收集(Reference Counting GC)
          • +

            过程简单图解

            + +

            准备工作

              +
            • 引入依赖
              <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-context</artifactId>
              <version>4.3.12.RELEASE</version>
              </dependency>
              <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-aspects</artifactId>
              <version>4.3.12.RELEASE</version>
              </dependency>
            • +
            • 目标对象类
              public class MathCalculator {

              public int div(int i, int j) {
              return i / j;
              }
              }
            • +
            • 切面类
              @Aspect
              public class LogAspects {

              @Pointcut("execution(public int com.moralok.aop.MathCalculator.*(..))")
              public void pointCut() {

              }

              @Before("pointCut()")
              public void logStart(JoinPoint joinPoint) {
              System.out.println(joinPoint.getSignature().getName() + "除法运行@Before。。。参数列表为 " + Arrays.asList(joinPoint.getArgs()) + "");
              }

              @After("pointCut()")
              public void logEnd(JoinPoint joinPoint) {
              System.out.println(joinPoint.getSignature().getName() + "除法结束@After。。。");
              }

              @AfterReturning(value = "pointCut()", returning = "result")
              public void logReturn(JoinPoint joinPoint, Object result) {
              System.out.println(joinPoint.getSignature().getName() + "除法正常返回@AfterReturning。。。运行结果 " + result);
              }

              @AfterThrowing(value = "pointCut()", throwing = "e")
              public void logException(JoinPoint joinPoint, Exception e) {
              System.out.println(joinPoint.getSignature().getName() + "除法异常@AfterThrowing。。。异常信息 " + e.getMessage());
              }

              @Around(value = "execution(public String com.moralok.bean.Car.getName(..))")
              public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
              System.out.println(joinPoint.getSignature().getName() + " @Around开始");
              Object proceed = joinPoint.proceed();
              System.out.println(joinPoint.getSignature().getName() + " @Around结束");
              return proceed;
              }
              }
            • +
            • 配置类
              @Configuration
              @EnableAspectJAutoProxy
              public class AopConfig {

              @Bean
              public MathCalculator mathCalculator() {
              return new MathCalculator();
              }

              @Bean
              public LogAspects logAspects() {
              return new LogAspects();
              }
              }
            • +
            • 测试类
              public class AopTest {

              @Test
              public void aopTest() {
              AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
              MathCalculator mathCalculator = ac.getBean(MathCalculator.class);
              mathCalculator.div(1, 1);
              mathCalculator.div(1, 0);
              ac.close();
              }
              }
            • +
            • Debug 断点的判断条件(可选)
              beanName.equals("mathCalculator")
            • +
            +

            创建代理 Bean 和创建普通 Bean 的区别

            其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。

            +

            常规的入口 getBean

            mathCalculator 以 getBean 方法为起点,开始创建的过程。

            +
            @Override
            public void preInstantiateSingletons() throws BeansException {
            // ...(mathCalculator)
            getBean(beanName);
            // ...
            }
            + +

            应用后置处理器

            在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。

            +

            可是,究竟是哪一个后置处理器做的呢

            +
            protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
            // ...
            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
            // ...
            invokeInitMethods(beanName, wrappedBean, mbd);
            // ...
            wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            return wrappedBean;
            }
            + +

            AnnotationAwareAspectJAutoProxyCreator

            在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。

            +

            那么,它是什么时候,怎么加入到 beanFactory 中呢

            +

            PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。

            +

            BeanPostProcessor 的方法

            postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。

            +
              +
            • AbstractAutoProxyCreator#postProcessAfterInitialization 就是创建代理对象的入口。
            • +
            • wrapIfNecessary 就是将 Bean 包装成代理 Bean 的入口方法
            • +
            +
            public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
            implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
            // ...
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) {
            // 什么都没做
            return bean;
            }
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 如有必要,将 bean 包装成代理对象
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            }
            return bean;
            }
            // ...
            }
            + +

            创建代理 Bean 的过程

            按需包装成代理 wrapIfNecessary

            protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
            // 判断是否直接返回 bean
            if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
            return bean;
            }
            if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
            }
            if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
            }

            // 如果有适用于当前 bean 的 advise 则为其创建代理
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
            if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
            }

            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
            }
            + +

            AbstractAutoProxyCreator 视角,创建代理

            AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。

            +
              +
            1. 创建一个代理工厂 ProxyFactory
            2. +
            3. 设置相关信息
            4. +
            5. 通过 ProxyFactory 获取代理
            6. +
            +
            protected Object createProxy(
            Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {

            if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
            AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
            }

            ProxyFactory proxyFactory = new ProxyFactory();
            proxyFactory.copyFrom(this);

            if (!proxyFactory.isProxyTargetClass()) {
            if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
            }
            else {
            evaluateProxyInterfaces(beanClass, proxyFactory);
            }
            }

            Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
            proxyFactory.addAdvisors(advisors);
            proxyFactory.setTargetSource(targetSource);
            customizeProxyFactory(proxyFactory);

            proxyFactory.setFrozen(this.freezeProxy);
            if (advisorsPreFiltered()) {
            proxyFactory.setPreFiltered(true);
            }

            return proxyFactory.getProxy(getProxyClassLoader());
            }
            + +

            ProxyFactory 视角,获取代理

            ProxyFactory#getProxy,创建一个 AopProxy 并委托它实现 getProxy。

            +
            +

            AopProxy 的含义与职责从字面上有点不好理解。

            +
            +
            public Object getProxy(ClassLoader classLoader) {
            return createAopProxy().getProxy(classLoader);
            }
            + +

            ProxyFactor视角,创建 AopProxy

            ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。

            +
            protected final synchronized AopProxy createAopProxy() {
            if (!this.active) {
            activate();
            }
            // 获取 AopProxy 工厂并创建一个 AopProxy
            return getAopProxyFactory().createAopProxy(this);
            }
            + +

            AopProxyFactory视角,创建 AopProxy

            AopProxyFactory#createAopProxy。

            +
              +
            • AopProxyFactory 有且仅有一个默认实现 DefaultAopProxyFactory。
            • +
            • createAopProxy 方法会根据配置信息,返回具体实现:开箱即用的有 JdkDynamicAopProxy 或者 ObjenesisCglibAopProxy。
            • +
            +

            这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。

            +
            public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

            @Override
            public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
            if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
            "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
            }
            else {
            return new JdkDynamicAopProxy(config);
            }
            }
            }
            + +

            获取代理 AopProxy#getProxy

            AopProxy 视角,获取代理。

            +

            JDK 动态代理

            JdkDynamicAopProxy。

            +
            @Override
            public Object getProxy(ClassLoader classLoader) {
            // ...
            // JDK 动态代理,已经和 Spring 无关
            return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
            }
            + +
            InvocationHandler 的 invoke 方法

            根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this) 可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。

            +
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // ...
            }
            + +

            CGLib 动态代理

            ObjenesisCglibAopProxy。

            +
            @Override
            public Object getProxy(ClassLoader classLoader) {
            // ...
            // CGLib 动态代理,已经和 Spring 无关
            Enhancer enhancer = createEnhancer();
            // ...
            return createProxyClassAndInstance(enhancer, callbacks);
            }
            + +
            为什么 Spring 中没有依赖 CGLib

            你可能会注意到 Spring 中并没有直接依赖 CGLib,像 Enhancer 所在的包是 org.springframework.cglib.proxy。根据文档:

            +
            +

            从 spring 3.2 开始,不再需要将 cglib 添加到类路径中,因为 cglib 类在 org.springframework 下重新打包并分布在 spring-core jar 中。 这样做既是为了方便,也是为了避免与使用不同版本 cglib 的其他项目发生潜在冲突。

            +
            +

            创建代理前的准备

            在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。

            +

            Bean 后置处理器 AspectJAwareAdvisorAutoProxyCreator 在什么时候,怎么加入到 beanFactory 中的?

            Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。

            +
            @Override
            public void refresh() throws BeansException, IllegalStateException {
            // ...
            invokeBeanFactoryPostProcessors(beanFactory);
            // ...
            finishBeanFactoryInitialization(beanFactory);
            // ...
            }
            + +

            从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。

            + + +

            从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。

            + + +

            @EnableXXX 的魔法

            注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。

            +
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Import(AspectJAutoProxyRegistrar.class)
            public @interface EnableAspectJAutoProxy {
            boolean proxyTargetClass() default false;
            boolean exposeProxy() default false;
            }
            + +

            在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。

            +
            class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
            @Override
            public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            // 如有必要注册 AspectJAnnotationAutoProxyCreator
            AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
            // 根据配置设置一些属性
            AnnotationAttributes enableAspectJAutoProxy =
            AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
            }
            }
            + +
            public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
            return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
            }
            + +

            切面类 LogAspect 的解析是在什么时候?

            进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。

            +
              +
            1. Object bean = resolveBeforeInstantiation(beanName, mbdToUse),在实例化前进行解析。
            2. +
            3. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
            4. +
            +
            @Override
            protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
            // ...
            try {
            Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
            if (bean != null) {
            return bean;
            }
            }
            // ...

            Object beanInstance = doCreateBean(beanName, mbdToUse, args);
            // ...
            return beanInstance;
            }
            + +

            入口方法 resolveBeforeInstantiation

            根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。

            +
            protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
            Object bean = null;
            if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
            if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            Class<?> targetType = determineTargetType(beanName, mbd);
            if (targetType != null) {
            // 注意,应用的是实例化前的处理
            bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
            if (bean != null) {
            // 注意,应用的是初始化后的处理
            bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
            }
            }
            }
            mbd.beforeInstantiationResolved = (bean != null);
            }
            return bean;
            }
            + +

            InstantiationAwareBeanPostProcessor

            应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。

            +
            protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            // 循环依次处理
            Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
            if (result != null) {
            return result;
            }
            }
            }
            return null;
            }
            + +

            AnnotationAwareAspectJAutoProxyCreator 不仅仅是一个 BeanPostProcessor,它还是一个 InstantiationAwareBeanPostProcessor。

            +
            public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
            Object cacheKey = getCacheKey(beanClass, beanName);

            if (beanName == null || !this.targetSourcedBeans.contains(beanName)) {
            if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
            }
            if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
            }
            }

            if (beanName != null) {
            TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
            if (targetSource != null) {
            this.targetSourcedBeans.add(beanName);
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
            Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
            }
            }

            return null;
            }
            + +

            和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。

            + + +
            +

            注意:以下方法应注意是否被子类重写

            +
            +

            org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip

            +
            protected boolean shouldSkip(Class<?> beanClass, String beanName) {
            // 查找并缓存 advisors
            List<Advisor> candidateAdvisors = findCandidateAdvisors();
            // ...
            }
            + +

            org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean

            +
            protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {
            List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
            // ...
            }
            + +

            org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

            +
            protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
            // 查找并缓存 advisors
            List<Advisor> candidateAdvisors = findCandidateAdvisors();
            // ...
            }
            + +

            容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。

            +

            查找并缓存 advisors

            org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

            +
            protected List<Advisor> findCandidateAdvisors() {
            List<Advisor> advisors = super.findCandidateAdvisors();
            advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
            return advisors;
            }
            + +

            org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

            +
            public List<Advisor> buildAspectJAdvisors() {
            List<String> aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
            // 第一次进入,没有缓存
            synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
            List<Advisor> advisors = new LinkedList<Advisor>();
            aspectNames = new LinkedList<String>();
            String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
            this.beanFactory, Object.class, true, false);
            for (String beanName : beanNames) {
            // ...
            // 如果是切面,解析得到 advisors
            if (this.advisorFactory.isAspect(beanType)) {
            aspectNames.add(beanName);
            // ...
            if (this.beanFactory.isSingleton(beanName)) {
            this.advisorsCache.put(beanName, classAdvisors);
            }
            else {
            this.aspectFactoryCache.put(beanName, factory);
            }
            }
            }
            this.aspectBeanNames = aspectNames;
            return advisors;
            }
            }
            }

            if (aspectNames.isEmpty()) {
            return Collections.emptyList();
            }
            // 以后进来读缓存
            List<Advisor> advisors = new LinkedList<Advisor>();
            for (String aspectName : aspectNames) {
            List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
            if (cachedAdvisors != null) {
            advisors.addAll(cachedAdvisors);
            }
            else {
            MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
            advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
            }
            return advisors;
            }
            + +

            可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache 观察 advisors 的查找情况。

            +]]> + + java + spring + aop + + + + Java 垃圾收集 + /2023/11/07/garbage-collection-in-Java/ + +

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

            + +

            概述

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

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

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

            +

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

            +

            哪些内存需要回收?

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

            +
            +

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

            +
            +

            引用计数算法(Reference Counting)

            优点:

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

            缺点:

            +
              +
            • 例外情况多,需要额外处理(比如循环引用)
            • +
            +
            +

            提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?

            +
            +

            可达性分析算法(Reachability Analysis)

              +
            • 选取一系列称为“GC Roots”的根对象作为起始节点集。
            • +
            • 根据引用关系向下搜索。
            • +
            • 如果某个对象到 GC Roots 间没有任何引用链相连,即该对象不可能再被使用。用图论的话说,就是 GC Roots 到该对象不可达。
            • +
            + + +

            那么可作为 GC Roots 的对象有哪些呢?
            固定的 GC Roots,主要是在全局性引用和执行上下文中:

            +
              +
            1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
            2. +
            3. 在方法区中类静态属性引用的变量。
            4. +
            5. 在方法区中类常量引用的对象,比如字符串常量池(String Table)里的引用。
            6. +
            7. 在本地方法栈中 JNI,即 Native 方法引用的对象。
            8. +
            9. Java 虚拟机内部的引用,如基本类型的 Class 对象,常驻的异常类型,还有系统类加载器。
            10. +
            11. 所有被同步锁(synchronized)持有的对象
            12. +
            13. 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(不懂)。
            14. +
            +

            临时性的GC Roots:
            除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。

            +
            +

            比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
            当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。

            +
            + + +

            “引用”的概念扩充

            对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
            缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。

            +
            +

            曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
            当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。

            +
            +

            引用的分类

              +
            • 强引用(Strongly Reference),只要强引用还在,绝不会回收。
            • +
            • 软引用(Soft Reference),只被软引用关联的对象,在系统发生 OOM 前,会被列入回收范围进行第二次回收。
            • +
            • 弱引用(Weak Reference),只被弱引用关联的对象,只能生存到下一次垃圾收集发生为止,无论内存是否 足够,都会回收。
            • +
            • 虚引用(Phantom Reference),一个对象是否有虚引用,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是为了在对象被回收时收到一个系统通知。
            • +
            +

            虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。

            +

            弱引用的测试

            public class ReferenceTest_3 {
            private static final int _4MB = 4 * 1024 * 1024;

            // -Xmx20m -XX:+PrintGCDetails -verbose:gc
            public static void main(String[] args) throws IOException {
            // list -> WeakReference -> byte[]
            List<WeakReference<byte[]>> list = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            System.out.print(list.size() + " ");
            for (WeakReference<byte[]> w : list) {

            System.out.print(w.get() + " ");
            }
            System.out.println();
            }
            System.out.println("循环结束: " + list.size());
            System.in.read();
            }
            }
            + +
            +

            在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
            这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。

            +
            +
            +

            了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。

            +
            +

            finalize 方法

            +

            有趣的知识点,无趣的面试考点。

            +
            +

            方法区的垃圾回收是什么样的?

              +
            • 《Java虚拟机规范》中提到可以不要求虚拟机在方法区实现垃圾收集
            • +
            • 确实有未实现或未完整实现方法区类型卸载的收集器
            • +
            • 原因是方法区垃圾收集的性价比通常比较低
            • +
            +

            方法区的垃圾收集主要回收两部分:

            +
              +
            • 废弃的常量
            • +
            • 不再使用的类型
            • +
            +

            如何判定一个常量是否废弃?
            没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
            如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。

            +
            +

            “虚拟机中也没有其他地方引用这个字面量”怎么理解?

            +
            +

            如何判定一个类型是否可卸载?

            +
              +
            • 该类的所有的实例都已经被回收
            • +
            • 加载该类的类加载器已经被回收
            • +
            • 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
            • +
            +

            Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
            和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。

            +
            +

            条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
            “无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
            Class 对象没有被引用时,会被回收吗?
            卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?

            +
            +

            如何回收:垃圾收集算法

            分类:

            +
              +
            • 引用计数式垃圾收集(Reference Counting GC)
            • 追踪式垃圾收集(Tracing GC)

            分代收集理论

            分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

            @@ -2942,453 +3055,355 @@
            - Spring AOP 如何创建代理 beans - /2023/11/19/how-does-Spring-AOP-create-proxy-beans/ - Spring AOP 是基于代理实现的,它既支持 JDK 动态代理也支持 CGLib。

            -
              -
            • 在什么时候创建代理对象的?
            • -
            • 怎么创建代理对象的?
            • -
            -

            过程简单图解

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

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

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

            准备工作

              -
            • 引入依赖
              <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-context</artifactId>
              <version>4.3.12.RELEASE</version>
              </dependency>
              <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-aspects</artifactId>
              <version>4.3.12.RELEASE</version>
              </dependency>
            • -
            • 目标对象类
              public class MathCalculator {

              public int div(int i, int j) {
              return i / j;
              }
              }
            • -
            • 切面类
              @Aspect
              public class LogAspects {

              @Pointcut("execution(public int com.moralok.aop.MathCalculator.*(..))")
              public void pointCut() {

              }

              @Before("pointCut()")
              public void logStart(JoinPoint joinPoint) {
              System.out.println(joinPoint.getSignature().getName() + "除法运行@Before。。。参数列表为 " + Arrays.asList(joinPoint.getArgs()) + "");
              }

              @After("pointCut()")
              public void logEnd(JoinPoint joinPoint) {
              System.out.println(joinPoint.getSignature().getName() + "除法结束@After。。。");
              }

              @AfterReturning(value = "pointCut()", returning = "result")
              public void logReturn(JoinPoint joinPoint, Object result) {
              System.out.println(joinPoint.getSignature().getName() + "除法正常返回@AfterReturning。。。运行结果 " + result);
              }

              @AfterThrowing(value = "pointCut()", throwing = "e")
              public void logException(JoinPoint joinPoint, Exception e) {
              System.out.println(joinPoint.getSignature().getName() + "除法异常@AfterThrowing。。。异常信息 " + e.getMessage());
              }

              @Around(value = "execution(public String com.moralok.bean.Car.getName(..))")
              public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
              System.out.println(joinPoint.getSignature().getName() + " @Around开始");
              Object proceed = joinPoint.proceed();
              System.out.println(joinPoint.getSignature().getName() + " @Around结束");
              return proceed;
              }
              }
            • -
            • 配置类
              @Configuration
              @EnableAspectJAutoProxy
              public class AopConfig {

              @Bean
              public MathCalculator mathCalculator() {
              return new MathCalculator();
              }

              @Bean
              public LogAspects logAspects() {
              return new LogAspects();
              }
              }
            • -
            • 测试类
              public class AopTest {

              @Test
              public void aopTest() {
              AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
              MathCalculator mathCalculator = ac.getBean(MathCalculator.class);
              mathCalculator.div(1, 1);
              mathCalculator.div(1, 0);
              ac.close();
              }
              }
            • -
            • Debug 断点的判断条件(可选)
              beanName.equals("mathCalculator")
            • -
            -

            创建代理 Bean 和创建普通 Bean 的区别

            其实创建代理 Bean 的过程和创建普通 Bean 的过程直到进行初始化处理(initializeBean)前都是一样的。更具体地说,如很多资料所言,Spring 创建代理对象的工作,是在应用后置处理器阶段完成的。

            -

            常规的入口 getBean

            mathCalculator 以 getBean 方法为起点,开始创建的过程。

            -
            @Override
            public void preInstantiateSingletons() throws BeansException {
            // ...(mathCalculator)
            getBean(beanName);
            // ...
            }
            - -

            应用后置处理器

            在正常地实例化 Bean 后,初始化 Bean 时,会对 Bean 实例应用后置处理器。

            -

            可是,究竟是哪一个后置处理器做的呢

            -
            protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
            // ...
            wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
            // ...
            invokeInitMethods(beanName, wrappedBean, mbd);
            // ...
            wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            return wrappedBean;
            }
            - -

            AnnotationAwareAspectJAutoProxyCreator

            在本示例中,创建代理的后置处理器就是 AnnotationAwareAspectJAutoProxyCreator,它继承自 AbstractAutoProxyCreator,AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口。

            -

            那么,它是什么时候,怎么加入到 beanFactory 中呢

            -

            PS: 显然,还有其他继承自 AbstractAutoProxyCreator 的后置处理器,暂时不谈。

            -

            BeanPostProcessor 的方法

            postProcessBeforeInitialization 和 postProcessAfterInitialization 方法,前者什么都没做,后者在必要时对 Bean 进行包装。

            -
              -
            • AbstractAutoProxyCreator#postProcessAfterInitialization 就是创建代理对象的入口。
            • -
            • wrapIfNecessary 就是将 Bean 包装成代理 Bean 的入口方法
            • -
            -
            public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
            implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
            // ...
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) {
            // 什么都没做
            return bean;
            }
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 如有必要,将 bean 包装成代理对象
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            }
            return bean;
            }
            // ...
            }
            - -

            创建代理 Bean 的过程

            按需包装成代理 wrapIfNecessary

            protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
            // 判断是否直接返回 bean
            if (beanName != null && this.targetSourcedBeans.contains(beanName)) {
            return bean;
            }
            if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
            }
            if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
            }

            // 如果有适用于当前 bean 的 advise 则为其创建代理
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
            if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
            }

            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
            }
            +

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

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

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

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

            AbstractAutoProxyCreator 视角,创建代理

            AbstractAutoProxyCreator#createProxy,创建一个 ProxyFactory,将工作交给它处理。

            +

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

              -
            1. 创建一个代理工厂 ProxyFactory
            2. -
            3. 设置相关信息
            4. -
            5. 通过 ProxyFactory 获取代理
            6. +
            7. 调用 findLoadedClass(String) 以检查该类是否已加载。
            8. +
            9. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
            10. +
            11. 调用 findClass(String) 方法来查找该类。
            -
            protected Object createProxy(
            Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {

            if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
            AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
            }

            ProxyFactory proxyFactory = new ProxyFactory();
            proxyFactory.copyFrom(this);

            if (!proxyFactory.isProxyTargetClass()) {
            if (shouldProxyTargetClass(beanClass, beanName)) {
            proxyFactory.setProxyTargetClass(true);
            }
            else {
            evaluateProxyInterfaces(beanClass, proxyFactory);
            }
            }

            Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
            proxyFactory.addAdvisors(advisors);
            proxyFactory.setTargetSource(targetSource);
            customizeProxyFactory(proxyFactory);

            proxyFactory.setFrozen(this.freezeProxy);
            if (advisorsPreFiltered()) {
            proxyFactory.setPreFiltered(true);
            }

            return proxyFactory.getProxy(getProxyClassLoader());
            }
            - -

            ProxyFactory 视角,获取代理

            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) {
            return createAopProxy().getProxy(classLoader);
            }
            - -

            ProxyFactor视角,创建 AopProxy

            ProxyFactory#createAopProxy,获取一个 AopProxyFactory 创建 AopProxy。

            -
            protected final synchronized AopProxy createAopProxy() {
            if (!this.active) {
            activate();
            }
            // 获取 AopProxy 工厂并创建一个 AopProxy
            return getAopProxyFactory().createAopProxy(this);
            }
            +

            怎么并行地加载类 getClassLoadingLock

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

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

            AopProxyFactory视角,创建 AopProxy

            AopProxyFactory#createAopProxy。

            -
              -
            • AopProxyFactory 有且仅有一个默认实现 DefaultAopProxyFactory。
            • -
            • createAopProxy 方法会根据配置信息,返回具体实现:开箱即用的有 JdkDynamicAopProxy 或者 ObjenesisCglibAopProxy。
            • -
            -

            这里的处理,决定了 Spring AOP 会使用哪一种动态代理实现。比如 Spring AOP 默认使用 JDK 动态代理,如果目标对象实现了接口 Spring 会使用 JDK 动态代理,这些结论的依据就在于此。

            -
            public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

            @Override
            public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
            if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
            "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
            }
            else {
            return new JdkDynamicAopProxy(config);
            }
            }
            }
            +
            什么是 “ClassLoader 对象注册为具有并行能力”呢?

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

            +
            static {
            ClassLoader.registerAsParallelCapable();
            }
            -

            获取代理 AopProxy#getProxy

            AopProxy 视角,获取代理。

            -

            JDK 动态代理

            JdkDynamicAopProxy。

            -
            @Override
            public Object getProxy(ClassLoader classLoader) {
            // ...
            // JDK 动态代理,已经和 Spring 无关
            return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
            }
            +

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

            +
              +
            1. 尚未创建调用者的实例(类加载器尚未实例化)
            2. +
            3. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
            4. +
            +
            怎么保证这两个条件成立呢?
              +
            1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
            2. +
            3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
            4. +
            +

            你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

            +
            注册工作做了什么?

            简单地说就是保存了类加载器所属 ClassSet

            +
            @CallerSensitive
            protected static boolean registerAsParallelCapable() {
            // 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
            Class<? extends ClassLoader> callerClass =
            Reflection.getCallerClass().asSubclass(ClassLoader.class);
            // 注册调用者的 Class 为具有并行能力
            return ParallelLoaders.register(callerClass);
            }
            -
            InvocationHandler 的 invoke 方法

            根据 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this) 可知,this 也就是 JdkDynamicAopProxy 同时也是一个 InvocationHandler,它必然实现了 invoke 方法,当代理对象调用方法时,就会进入到 invoke 方法中。

            -
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // ...
            }
            +

            方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

            +
            // private 修饰,只有其外部类 ClassLoader 才可以使用
            // static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
            private static class ParallelLoaders {
            // private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
            private ParallelLoaders() {}

            // the set of parallel capable loader types
            // 使用 loaderTypes 时通过 synchronized 加同步锁
            private static final Set<Class<? extends ClassLoader>> loaderTypes =
            Collections.newSetFromMap(
            // todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
            new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
            static {
            // 将 ClassLoader 本身注册为具有并行能力
            synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
            }

            /**
            * Registers the given class loader type as parallel capabale.
            * Returns {@code true} is successfully registered; {@code false} if
            * loader's super class is not registered.
            */
            static boolean register(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
            if (loaderTypes.contains(c.getSuperclass())) {
            // register the class loader as parallel capable
            // if and only if all of its super classes are.
            // Note: given current classloading sequence, if
            // the immediate super class is parallel capable,
            // all the super classes higher up must be too.
            // 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
            // 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
            loaderTypes.add(c);
            return true;
            } else {
            return false;
            }
            }
            }

            /**
            * Returns {@code true} if the given class loader type is
            * registered as parallel capable.
            */
            static boolean isRegistered(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
            return loaderTypes.contains(c);
            }
            }
            }
            -

            CGLib 动态代理

            ObjenesisCglibAopProxy。

            -
            @Override
            public Object getProxy(ClassLoader classLoader) {
            // ...
            // CGLib 动态代理,已经和 Spring 无关
            Enhancer enhancer = createEnhancer();
            // ...
            return createProxyClassAndInstance(enhancer, callbacks);
            }
            +
            “注册”怎么和锁产生联系?

            但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

            +
            private ClassLoader(Void unused, ClassLoader parent) {
            // 由 private 修饰,不允许子类重写
            this.parent = parent;
            if (ParallelLoaders.isRegistered(this.getClass())) {
            // 如果类加载器已经注册为具有并行能力,则做一些赋值操作
            parallelLockMap = new ConcurrentHashMap<>();
            // 保存 package->certs 的 map 映射,相关的工作也可以并行进行
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
            } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
            }
            }

            // 由 protect 修饰,允许子类重写,传递了父·类加载器。
            protected ClassLoader(ClassLoader parent) {
            this(checkCreateClassLoader(), parent);
            }

            // 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
            protected ClassLoader() {
            this(checkCreateClassLoader(), getSystemClassLoader());
            }
            -
            为什么 Spring 中没有依赖 CGLib

            你可能会注意到 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: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

            -

            创建代理前的准备

            在前面预留了一些问题,当初我在看网上的资料时就有这些困惑。

            -

            Bean 后置处理器 AspectJAwareAdvisorAutoProxyCreator 在什么时候,怎么加入到 beanFactory 中的?

            Debug 停留在 Spring 上下文刷新方法中的 finishBeanFactoryInitialization。

            -
            @Override
            public void refresh() throws BeansException, IllegalStateException {
            // ...
            invokeBeanFactoryPostProcessors(beanFactory);
            // ...
            finishBeanFactoryInitialization(beanFactory);
            // ...
            }
            +

            检查目标类是否已加载

            加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
            todo: 不同类加载器同一个类名会如何判定?

            +
            protected final Class<?> findLoadedClass(String name) {
            if (!checkName(name))
            return null;
            return findLoadedClass0(name);
            }

            private native final Class<?> findLoadedClass0(String name);
            -

            从 beanFatory 的 beanDefinitionMap 可以观察到,配置类 AopConfig 中的 MathCalculator 和 LogAspect 的信息已经就位。

            - +

            保证核心类库的安全性:双亲委派模型

            正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
            正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

            +
            +

            在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

            +
            +
            为什么要用这套奇怪的机制

            这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

            +

            通常流程如下:

            +
              +
            1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
            2. +
            3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
            4. +
            5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
            6. +
            7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
            8. +
            9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
            10. +
            11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
            12. +
            +

            注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

            +

            查找类资源:findClass

            默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

            +

            过程如下:

            +
              +
            1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
            2. +
            3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
            4. +
            5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
            6. +
            +
            protected Class<?> findClass(final String name)
            throws ClassNotFoundException
            {
            final Class<?> result;
            try {
            // todo:
            result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
            public Class<?> run() throws ClassNotFoundException {
            // 将 name 转换为 path
            String path = name.replace('.', '/').concat(".class");
            // 从 URLClassPath 中查找 Resource
            Resource res = ucp.getResource(path, false);
            if (res != null) {
            try {
            return defineClass(name, res);
            } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
            } catch (ClassFormatError e2) {
            if (res.getDataError() != null) {
            e2.addSuppressed(res.getDataError());
            }
            throw e2;
            }
            } else {
            return null;
            }
            }
            }, acc);
            } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
            }
            if (result == null) {
            throw new ClassNotFoundException(name);
            }
            return result;
            }
            -

            从 beanFactory 的 beanProcessor 可以观察到,AnnotationAwareAspectJAutoProxyCreator 已经就位。

            - +

            查找类的目录列表:URLClassPath

            URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
            URLClassPath 的核心构造器方法:

            +
            public URLClassPath(URL[] urls,
            URLStreamHandlerFactory factory,
            AccessControlContext acc) {
            // 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
            for (int i = 0; i < urls.length; i++) {
            path.add(urls[i]);
            }
            // 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
            push(urls);
            if (factory != null) {
            // 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
            jarHandler = factory.createURLStreamHandler("jar");
            }
            if (DISABLE_ACC_CHECKING)
            this.acc = null;
            else
            this.acc = acc;
            }
            -

            @EnableXXX 的魔法

            注解 @EnableXXX 往往伴随着注解 @Import,在 invokeBeanFactoryPostProcessors(beanFactory) 中,工厂后置处理器 ConfigurationClassPostProcessor 会处理它。

            -
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Import(AspectJAutoProxyRegistrar.class)
            public @interface EnableAspectJAutoProxy {
            boolean proxyTargetClass() default false;
            boolean exposeProxy() default false;
            }
            +
            URLClassPath#getResource

            URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
            显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

            +
            public Resource getResource(String name, boolean check) {
            if (DEBUG) {
            System.err.println("URLClassPath.getResource(\"" + name + "\")");
            }

            Loader loader;
            // 获取缓存(默认没有用)
            int[] cache = getLookupCache(name);
            // 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
            for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
            Resource res = loader.getResource(name, check);
            if (res != null) {
            return res;
            }
            }
            return null;
            }
            -

            在 ConfigurationClassPostProcessor 的处理中,因为 AspectJAutoProxyRegistrar 实现了 ImportBeanDefinitionRegistrar,registerBeanDefinitions 方法会被调用,AnnotationAwareAspectJAutoProxyCreator 的 beanDefinition 随之被注册到 beanFactory,因 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor 被提前创建。

            -
            class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
            @Override
            public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            // 如有必要注册 AspectJAnnotationAutoProxyCreator
            AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
            // 根据配置设置一些属性
            AnnotationAttributes enableAspectJAutoProxy =
            AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
            }
            }
            +
            URLClassPath#getNextLoader

            获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

            +
            private synchronized Loader getNextLoader(int[] cache, int index) {
            if (closed) {
            return null;
            }
            if (cache != null) {
            if (index < cache.length) {
            Loader loader = loaders.get(cache[index]);
            if (DEBUG_LOOKUP_CACHE) {
            System.out.println("HASCACHE: Loading from : " + cache[index]
            + " = " + loader.getBaseURL());
            }
            return loader;
            } else {
            return null; // finished iterating over cache[]
            }
            } else {
            // 获取 Loader
            return getLoader(index);
            }
            }
            -
            public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
            return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
            }
            +
            URLClassPath#getLoader(int)
              +
            1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
            2. +
            3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
            4. +
            5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
            6. +
            +
            private synchronized Loader getLoader(int index) {
            if (closed) {
            return null;
            }
            // Expand URL search path until the request can be satisfied
            // or the URL stack is empty.
            while (loaders.size() < index + 1) {
            // Pop the next URL from the URL stack
            // 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
            URL url;
            synchronized (urls) {
            if (urls.empty()) {
            return null;
            } else {
            url = urls.pop();
            }
            }
            // Skip this URL if it already has a Loader. (Loader
            // may be null in the case where URL has not been opened
            // but is referenced by a JAR index.)
            String urlNoFragString = URLUtil.urlNoFragString(url);
            if (lmap.containsKey(urlNoFragString)) {
            continue;
            }
            // Otherwise, create a new Loader for the URL.
            Loader loader;
            try {
            // 根据 URL 创建 Loader
            loader = getLoader(url);
            // If the loader defines a local class path then add the
            // URLs to the list of URLs to be opened.
            URL[] urls = loader.getClassPath();
            if (urls != null) {
            push(urls);
            }
            } catch (IOException e) {
            // Silently ignore for now...
            continue;
            } catch (SecurityException se) {
            // Always silently ignore. The context, if there is one, that
            // this URLClassPath was given during construction will never
            // have permission to access the URL.
            if (DEBUG) {
            System.err.println("Failed to access " + url + ", " + se );
            }
            continue;
            }
            // Finally, add the Loader to the search path.
            validateLookupCache(loaders.size(), urlNoFragString);
            loaders.add(loader);
            lmap.put(urlNoFragString, loader);
            }
            if (DEBUG_LOOKUP_CACHE) {
            System.out.println("NOCACHE: Loading from : " + index );
            }
            return loaders.get(index);
            }
            -

            切面类 LogAspect 的解析是在什么时候?

            进入创建 Bean 的方法 createBean 后,除了 doCreateBean,应额外留意 resolveBeforeInstantiation 方法。

            +
            URLClassPath#getLoader(java.net.URL)

            根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

              -
            1. Object bean = resolveBeforeInstantiation(beanName, mbdToUse),在实例化前进行解析。
            2. -
            3. Object beanInstance = doCreateBean(beanName, mbdToUse, args),创建 Bean 的具体过程。
            4. +
            5. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
            6. +
            7. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
            8. +
            9. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
            -
            @Override
            protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
            // ...
            try {
            Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
            if (bean != null) {
            return bean;
            }
            }
            // ...

            Object beanInstance = doCreateBean(beanName, mbdToUse, args);
            // ...
            return beanInstance;
            }
            +
            private Loader getLoader(final URL url) throws IOException {
            try {
            return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedExceptionAction<Loader>() {
            public Loader run() throws IOException {
            String file = url.getFile();
            if (file != null && file.endsWith("/")) {
            if ("file".equals(url.getProtocol())) {
            return new FileLoader(url);
            } else {
            return new Loader(url);
            }
            } else {
            return new JarLoader(url, jarHandler, lmap, acc);
            }
            }
            }, acc);
            } catch (java.security.PrivilegedActionException pae) {
            throw (IOException)pae.getException();
            }
            }
            -

            入口方法 resolveBeforeInstantiation

            根据注释,该方法给 BeanPostProcessors 一个机会提前返回一个代理对象。在本示例中,返回 null,但是方法在第一次执行后已经提前解析得到 advisors 并缓存。

            -
            protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
            Object bean = null;
            if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
            if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            Class<?> targetType = determineTargetType(beanName, mbd);
            if (targetType != null) {
            // 注意,应用的是实例化前的处理
            bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
            if (bean != null) {
            // 注意,应用的是初始化后的处理
            bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
            }
            }
            }
            mbd.beforeInstantiationResolved = (bean != null);
            }
            return bean;
            }
            +

            URLClassPath.FileLoader#getResource

            FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

            +
            Resource getResource(final String name, boolean check) {
            final URL url;
            try {
            URL normalizedBase = new URL(getBaseURL(), ".");
            url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

            if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
            // requested resource had ../..'s in path
            return null;
            }

            if (check)
            URLClassPath.check(url);

            final File file;
            if (name.indexOf("..") != -1) {
            file = (new File(dir, name.replace('/', File.separatorChar)))
            .getCanonicalFile();
            if ( !((file.getPath()).startsWith(dir.getPath())) ) {
            /* outside of base dir */
            return null;
            }
            } else {
            file = new File(dir, name.replace('/', File.separatorChar));
            }

            if (file.exists()) {
            return new Resource() {
            public String getName() { return name; };
            public URL getURL() { return url; };
            public URL getCodeSourceURL() { return getBaseURL(); };
            public InputStream getInputStream() throws IOException
            { return new FileInputStream(file); };
            public int getContentLength() throws IOException
            { return (int)file.length(); };
            };
            }
            } catch (Exception e) {
            return null;
            }
            return null;
            }
            -

            InstantiationAwareBeanPostProcessor

            应用 InstantiationAwareBeanPostProcessor 的 postProcessBeforeInstantiation。

            -
            protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
            // 循环依次处理
            Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
            if (result != null) {
            return result;
            }
            }
            }
            return null;
            }
            +

            ClassLoader 的搜索路径

            从上文可知,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 {
            Object cacheKey = getCacheKey(beanClass, beanName);

            if (beanName == null || !this.targetSourcedBeans.contains(beanName)) {
            if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
            }
            if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
            }
            }

            if (beanName != null) {
            TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
            if (targetSource != null) {
            this.targetSourcedBeans.add(beanName);
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
            Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
            }
            }

            return null;
            }
            +
            bootstrap class loader

            启动·类加载器 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
            java/lang/NoClassDefFoundError: java/lang/Object

            进程已结束,退出代码1
            -

            和 wrapIfNecessary 方法对比,容易发现两者有不少相似的处理。

            - +
            ExtClassLoader

            扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

            +
            AppClassLoader

            应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

            +

            CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

            +
              +
            • Windows:.;C:\path\to\classes
            • +
            • Linux:.:/path/to/classes
            • +
            +

            事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

            +

            字节数据转换为 Class 实例:defineClass

            方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
            以下是定义类时最终可能调用的 native 方法。

            +
            private native Class<?> defineClass0(String name, byte[] b, int off, int len,
            ProtectionDomain pd);

            private native Class<?> defineClass1(String name, byte[] b, int off, int len,
            ProtectionDomain pd, String source);

            private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
            int off, int len, ProtectionDomain pd,
            String source);
            -
            -

            注意:以下方法应注意是否被子类重写

            -
            -

            org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator#shouldSkip

            -
            protected boolean shouldSkip(Class<?> beanClass, String beanName) {
            // 查找并缓存 advisors
            List<Advisor> candidateAdvisors = findCandidateAdvisors();
            // ...
            }
            +

            其方法参数有:

            +
              +
            • name,目标类的名称。
            • +
            • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
            • +
            • ProtectionDomain,保护域,todo:
            • +
            • sourceCodeSource 的位置。
            • +
            +

            defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

            +

            URLClassLoader#defineClass

            此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

            +
            private Class<?> defineClass(String name, Resource res) throws IOException {
            long t0 = System.nanoTime();
            // 获取最后一个 . 的位置
            int i = name.lastIndexOf('.');
            // 返回资源的 CodeSourceURL
            URL url = res.getCodeSourceURL();
            if (i != -1) {
            // 截取包名 com.example
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
            }
            // Now read the class bytes and define the class
            // 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
            java.nio.ByteBuffer bb = res.getByteBuffer();
            if (bb != null) {
            // Use (direct) ByteBuffer:
            // 不常用
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
            return defineClass(name, bb, cs);
            } else {
            // 以字节数组的形式返回资源数据
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            // 必须再读取字节数据后读取证书,todo:
            CodeSigner[] signers = res.getCodeSigners();
            // 根据 URL 和签名者创建 CodeSource
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            // 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
            return defineClass(name, b, 0, b.length, cs);
            }
            }
            -

            org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean

            -
            protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {
            List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
            // ...
            }
            +

            Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

            +
            public byte[] getBytes() throws IOException {
            byte[] b;
            // Get stream before content length so that a FileNotFoundException
            // can propagate upwards without being caught too early
            // 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
            // 获取缓存的 InputStream
            InputStream in = cachedInputStream();

            // This code has been uglified to protect against interrupts.
            // Even if a thread has been interrupted when loading resources,
            // the IO should not abort, so must carefully retry, failing only
            // if the retry leads to some other IO exception.
            // 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
            // 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
            boolean isInterrupted = Thread.interrupted();
            int len;
            for (;;) {
            try {
            // 获取内容长度,顺利的话就跳出循环
            len = getContentLength();
            break;
            } catch (InterruptedIOException iioe) {
            // 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
            Thread.interrupted();
            isInterrupted = true;
            }
            }

            try {
            b = new byte[0];
            if (len == -1) len = Integer.MAX_VALUE;
            int pos = 0;
            while (pos < len) {
            int bytesToRead;
            if (pos >= b.length) { // Only expand when there's no room
            // 如果当前读取位置已经大于等于数组长度
            // 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
            bytesToRead = Math.min(len - pos, b.length + 1024);
            if (b.length < pos + bytesToRead) {
            // 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
            b = Arrays.copyOf(b, pos + bytesToRead);
            }
            } else {
            // 数组还有空间,待读取字节长度 = 数组剩余空间
            bytesToRead = b.length - pos;
            }
            int cc = 0;
            try {
            // 读取数据
            cc = in.read(b, pos, bytesToRead);
            } catch (InterruptedIOException iioe) {
            // 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
            Thread.interrupted();
            isInterrupted = true;
            }
            if (cc < 0) {
            // 如果读取返回值 < 0
            if (len != Integer.MAX_VALUE) {
            // 且长度并未无限,表示提前检测到 EOF,抛出异常
            throw new EOFException("Detect premature EOF");
            } else {
            // 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
            if (b.length != pos) {
            b = Arrays.copyOf(b, pos);
            }
            break;
            }
            }
            pos += cc;
            }
            } finally {
            try {
            in.close();
            } catch (InterruptedIOException iioe) {
            isInterrupted = true;
            } catch (IOException ignore) {}

            if (isInterrupted) {
            // 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
            Thread.currentThread().interrupt();
            }
            }
            return b;
            }
            -

            org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

            -
            protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
            // 查找并缓存 advisors
            List<Advisor> candidateAdvisors = findCandidateAdvisors();
            // ...
            }
            +

            getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

            +
            private synchronized InputStream cachedInputStream() throws IOException {
            if (cis == null) {
            cis = getInputStream();
            }
            return cis;
            }
            -

            容易注意到两者在创建代理前,都会调用 findCandidateAdvisors 方法查找候选的 advisors,其实这也是我们想要找的对切面类的解析处理所在。

            -

            查找并缓存 advisors

            org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors

            -
            protected List<Advisor> findCandidateAdvisors() {
            List<Advisor> advisors = super.findCandidateAdvisors();
            advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
            return advisors;
            }
            +

            在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

            +
            public InputStream getInputStream() throws IOException
            {
            // 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
            return new FileInputStream(file);
            }

            public int getContentLength() throws IOException
            {
            // 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
            return (int)file.length();
            };
            -

            org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder#buildAspectJAdvisors

            -
            public List<Advisor> buildAspectJAdvisors() {
            List<String> aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
            // 第一次进入,没有缓存
            synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
            List<Advisor> advisors = new LinkedList<Advisor>();
            aspectNames = new LinkedList<String>();
            String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
            this.beanFactory, Object.class, true, false);
            for (String beanName : beanNames) {
            // ...
            // 如果是切面,解析得到 advisors
            if (this.advisorFactory.isAspect(beanType)) {
            aspectNames.add(beanName);
            // ...
            if (this.beanFactory.isSingleton(beanName)) {
            this.advisorsCache.put(beanName, classAdvisors);
            }
            else {
            this.aspectFactoryCache.put(beanName, factory);
            }
            }
            }
            this.aspectBeanNames = aspectNames;
            return advisors;
            }
            }
            }

            if (aspectNames.isEmpty()) {
            return Collections.emptyList();
            }
            // 以后进来读缓存
            List<Advisor> advisors = new LinkedList<Advisor>();
            for (String aspectName : aspectNames) {
            List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
            if (cachedAdvisors != null) {
            advisors.addAll(cachedAdvisors);
            }
            else {
            MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
            advisors.addAll(this.advisorFactory.getAdvisors(factory));
            }
            }
            return advisors;
            }
            +

            SecureClassLoader#defineClass

            URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
            由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

            +
            protected final Class<?> defineClass(String name,
            byte[] b, int off, int len,
            CodeSource cs)
            {
            return defineClass(name, b, off, len, getProtectionDomain(cs));
            }

            protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
            CodeSource cs)
            {
            return defineClass(name, b, getProtectionDomain(cs));
            }
            -

            可以通过 beanFactory->beanPostProcessors->aspectJAdvisorsBuilder->advisorsCache 观察 advisors 的查找情况。

            -]]>
            +

            方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

            +
            private ProtectionDomain getProtectionDomain(CodeSource cs) {
            // 如果 CodeSource 为 null,直接返回 null
            if (cs == null)
            return null;

            ProtectionDomain pd = null;
            synchronized (pdcache) {
            // 先从 Map 缓存中获取 ProtectionDomain
            pd = pdcache.get(cs);
            if (pd == null) {
            // 从 CodeSource 中获取 PermissionCollection
            PermissionCollection perms = getPermissions(cs);
            // 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
            pd = new ProtectionDomain(cs, perms, this, null);
            pdcache.put(cs, pd);
            if (debug != null) {
            debug.println(" getPermissions "+ pd);
            debug.println("");
            }
            }
            }
            return pd;
            }
            + +
            getPermissions

            根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

            +

            AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

            +
            protected PermissionCollection getPermissions(CodeSource codesource)
            {
            // 调用父类 URLClassLoader 的 getPermissions
            PermissionCollection perms = super.getPermissions(codesource);
            // 允许从类路径加载的任何类退出 VM的权限。
            // todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
            perms.add(new RuntimePermission("exitVM"));
            return perms;
            }
            + +

            SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

            +
            protected PermissionCollection getPermissions(CodeSource codesource)
            {
            // 调用父类 SecureClassLoader 的 getPermissions
            PermissionCollection perms = super.getPermissions(codesource);

            URL url = codesource.getLocation();

            Permission p;
            URLConnection urlConnection;

            try {
            // FileURLConnection 实例
            urlConnection = url.openConnection();
            // 允许 read 的 FilePermission 实例
            p = urlConnection.getPermission();
            } catch (java.io.IOException ioe) {
            p = null;
            urlConnection = null;
            }

            if (p instanceof FilePermission) {
            // if the permission has a separator char on the end,
            // it means the codebase is a directory, and we need
            // to add an additional permission to read recursively
            // 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
            String path = p.getName();
            if (path.endsWith(File.separator)) {
            path += "-";
            p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
            }
            } else if ((p == null) && (url.getProtocol().equals("file"))) {
            String path = url.getFile().replace('/', File.separatorChar);
            path = ParseUtil.decode(path);
            if (path.endsWith(File.separator))
            path += "-";
            p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
            } else {
            /**
            * Not loading from a 'file:' URL so we want to give the class
            * permission to connect to and accept from the remote host
            * after we've made sure the host is the correct one and is valid.
            */
            URL locUrl = url;
            if (urlConnection instanceof JarURLConnection) {
            locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
            }
            String host = locUrl.getHost();
            if (host != null && (host.length() > 0))
            p = new SocketPermission(host,
            SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
            }

            // make sure the person that created this class loader
            // would have this permission

            if (p != null) {
            final SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
            final Permission fp = p;
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() throws SecurityException {
            sm.checkPermission(fp);
            return null;
            }
            }, acc);
            }
            perms.add(p);
            }
            return perms;
            }
            + +

            SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

            +
            protected PermissionCollection getPermissions(CodeSource codesource)
            {
            // 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
            // 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
            check();
            // ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
            return new Permissions(); // ProtectionDomain defers the binding
            }
            + +
            ProtectionDomain

            ProtectionDomain 的相关构造器参数:

            +
              +
            • CodeSource
            • +
            • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
            • +
            • ClassLoader
            • +
            • Principal[]
            • +
            +

            这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

            +

            ClassLoader#defineClass

            抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

            +
            protected final Class<?> defineClass(String name, byte[] b, int off, int len)
            throws ClassFormatError
            {
            return defineClass(name, b, off, len, null);
            }

            protected final Class<?> defineClass(String name, byte[] b, int off, int len,
            ProtectionDomain protectionDomain)
            throws ClassFormatError
            {
            protectionDomain = preDefineClass(name, protectionDomain);
            String source = defineClassSourceLocation(protectionDomain);
            Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
            postDefineClass(c, protectionDomain);
            return c;
            }

            protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
            ProtectionDomain protectionDomain)
            throws ClassFormatError
            {
            int len = b.remaining();

            // Use byte[] if not a direct ByteBufer:
            if (!b.isDirect()) {
            if (b.hasArray()) {
            return defineClass(name, b.array(),
            b.position() + b.arrayOffset(), len,
            protectionDomain);
            } else {
            // no array, or read-only array
            byte[] tb = new byte[len];
            b.get(tb); // get bytes out of byte buffer.
            return defineClass(name, tb, 0, len, protectionDomain);
            }
            }

            protectionDomain = preDefineClass(name, protectionDomain);
            String source = defineClassSourceLocation(protectionDomain);
            Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
            postDefineClass(c, protectionDomain);
            return c;
            }
            + +

            主要步骤:

            +
              +
            1. preDefineClass 前置处理
            2. +
            3. defineClassX
            4. +
            5. postDefineClass 后置处理
            6. +
            +
            preDefineClass

            确定保护域 ProtectionDomain,并检查:

            +
              +
            1. 未定义 java.*
            2. +
            3. 该类的签名者与包(package)中其余类的签名者相匹配
            4. +
            +
            private ProtectionDomain preDefineClass(String name,
            ProtectionDomain pd)
            {
            // 检查 name 为 null 或者有可能是有效的二进制名称
            if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

            // Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
            // relies on the fact that spoofing is impossible if a class has a name
            // of the form "java.*"
            // 如果 name 以 java. 开头,则抛出异常
            if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
            ("Prohibited package name: " +
            name.substring(0, name.lastIndexOf('.')));
            }
            if (pd == null) {
            // 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
            pd = defaultDomain;
            }

            // 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
            // todo: certs
            if (name != null) checkCerts(name, pd.getCodeSource());

            return pd;
            }
            + +
            defineClassSourceLocation

            确定 ClassCodeSource 位置。

            +
            private String defineClassSourceLocation(ProtectionDomain pd)
            {
            CodeSource cs = pd.getCodeSource();
            String source = null;
            if (cs != null && cs.getLocation() != null) {
            source = cs.getLocation().toString();
            }
            return source;
            }
            + +
            defineClassX 方法

            这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

            +
            postDefineClass

            在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

            +
            private void postDefineClass(Class<?> c, ProtectionDomain pd)
            {
            if (pd.getCodeSource() != null) {
            // 获取证书
            Certificate certs[] = pd.getCodeSource().getCertificates();
            if (certs != null)
            setSigners(c, certs);
            }
            }
            ]]>
            java - spring - aop + class loader
            - JDK 动态代理和 CGLib - /2023/11/19/JDK-dynamic-proxy-and-CGLib/ - 介绍

            JDK 动态代理

            Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。

            -

            CGLib 动态代理

            使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。

            -

            cglib Github 仓库

            -

            适用场景

              -
            • JDK 动态代理适用于实现接口的类,对未实现接口的类无能为力。
            • -
            • CGLib 不要求类实现接口,但对 final 方法无能为力。
            • -
            -

            性能比较

              -
            • 在 JDK 8 以前,CGLib 性能更好
            • -
            • 从 JDK 8 开始,JDK 动态代理性能更好
            • -
            + Spring 中的循环依赖 + /2023/11/22/circular-dependencies-in-Spring/ + Spring 中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring 的具体处理过程,并在最后给出笔者的个人思考。

            + + +

            循环依赖的介绍和讨论

            什么是循环依赖?

            Bean A 依赖另一个 Bean BBean B 也依赖了 Bean A,我们就称之为循环依赖:

            +
            Bean A -> Bean B -> Bean A
            + +

            首先,我们应该将循环依赖和 “Spring 中的循环依赖问题”分开看待。循环依赖是一个正常的现象,一个 employee 依赖他的 department,department 拥有许多 employee。先实例化 employee 后实例化 department,然后先后为它们设置依赖,这样并不会发生什么问题。

            +

            Spring 中的循环依赖问题

            常规的循环依赖问题

            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 {
              void sayHello(String name);
              }
              -
            • -
            • HelloService.java

              -
              public class HelloServiceImpl implements HelloService {
              @Override
              public void sayHello(String name) {
              if (name == null) {
              throw new IllegalArgumentException("name can not be null");
              }
              System.out.println("Hello " + name);
              }
              }
            • -
            -

            JDK 动态代理示例

              -
            • 自定义 InvocationHandler
              public class UserServiceInvocationHandler implements InvocationHandler {

              private Object target;

              public UserServiceInvocationHandler(Object target) {
              this.target = target;
              }

              @Override
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              try {
              System.out.println("do sth. before invocation");
              Object ret = method.invoke(target, args);
              System.out.println("do sth. after invocation");
              return ret;
              } catch (Exception e) {
              System.out.println("do sth. when exception occurs");
              throw e;
              } finally {
              System.out.println("do sth. finally");
              }
              }
              }
            • -
            • 测试类
              public class JdkProxyTest {

              public static void main(String[] args) {
              HelloService target = new HelloServiceImpl();
              ClassLoader classLoader = target.getClass().getClassLoader();
              Class<?>[] interfaces = target.getClass().getInterfaces();
              UserServiceInvocationHandler invocationHandler = new UserServiceInvocationHandler(target);

              HelloService proxy = (HelloService) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
              proxy.sayHello("Tom");
              System.out.println("=================");
              proxy.sayHello(null);
              }
              }
            • -
            • 结果
              do sth. before invocation
              Hello Tom
              do sth. after invocation
              do sth. finally
              =================
              do sth. before invocation
              do sth. when exception occurs
              do sth. finally
              Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
              at com.sun.proxy.$Proxy0.sayHello(Unknown Source)
              at com.moralok.proxy.jdk.JdkProxyTest.main(JdkProxyTest.java:19)
              Caused by: java.lang.reflect.InvocationTargetException
              at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
              at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
              at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
              at java.lang.reflect.Method.invoke(Method.java:498)
              at com.moralok.proxy.jdk.UserServiceInvocationHandler.invoke(UserServiceInvocationHandler.java:18)
              ... 2 more
              Caused by: java.lang.IllegalArgumentException: name can not be null
              at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
            • -
            -

            CGLib 动态代理示例

              -
            • 引入依赖
              <dependencies>
              <dependency>
              <groupId>cglib</groupId>
              <artifactId>cglib</artifactId>
              <version>3.3.0</version>
              </dependency>
              </dependencies>
            • -
            • 自定义 MethodInterceptor
              public class UserServiceMethodInterceptor implements MethodInterceptor {

              @Override
              public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
              try {
              System.out.println("do sth. before invocation");
              Object ret = proxy.invokeSuper(obj, args);
              System.out.println("do sth. after invocation");
              return ret;
              } catch (Exception e) {
              System.out.println("do sth. when exception occurs");
              throw e;
              } finally {
              System.out.println("do sth. finally");
              }
              }
              }
            • -
            • 测试类
              public class CglibTest {

              public static void main(String[] args) {
              UserServiceMethodInterceptor methodInterceptor = new UserServiceMethodInterceptor();
              Enhancer enhancer = new Enhancer();
              enhancer.setSuperclass(HelloServiceImpl.class);
              enhancer.setCallback(methodInterceptor);

              HelloService proxy = (HelloService) enhancer.create();
              proxy.sayHello("Tom");
              System.out.println("=================");
              proxy.sayHello(null);
              }
              }
            • -
            • 结果
              do sth. before invocation
              Hello Tom
              do sth. after invocation
              do sth. finally
              =================
              do sth. before invocation
              do sth. when exception occurs
              do sth. finally
              Exception in thread "main" java.lang.IllegalArgumentException: name can not be null
              at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
              at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.CGLIB$sayHello$0(<generated>)
              at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.invoke(<generated>)
              at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
              at com.moralok.proxy.cglib.UserServiceMethodInterceptor.intercept(UserServiceMethodInterceptor.java:14)
              at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.sayHello(<generated>)
              at com.moralok.proxy.cglib.CglibTest.main(CglibTest.java:18)
            • +
            • 要应对构造器方法的循环依赖,需要人为地介入,使用 @Lazy 注解告诉 Spring,延迟 Bean 的初始化。在这时候,被标注的参数注入的不是一个立即创建的实例,而是一个代理对象。
            • +
            • 此外,如果是 prototype 类型的 Bean 发生循环依赖,Spring 会抛出异常,因为每次都创建新的 Bean 必然会导致无限循环。
            -

            查看 JDK 生成的代理类

            使用以下语句,将在工作目录下生成代理类的 Class 文件。

            -
            System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
            - -
            public final class $Proxy0 extends Proxy implements HelloService {
            private static Method m1;
            private static Method m3;
            private static Method m2;
            private static Method m0;

            public $Proxy0(InvocationHandler var1) throws {
            // 将 InvocationHandler 传递给父类 Proxy
            super(var1);
            }

            public final boolean equals(Object var1) throws {
            try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
            throw var3;
            } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
            }
            }

            // 代理方法调用 InvocationHandler 的 invoke 方法
            public final void sayHello(String var1) throws {
            try {
            super.h.invoke(this, m3, new Object[]{var1});
            } catch (RuntimeException | Error var3) {
            throw var3;
            } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
            }
            }

            public final String toString() throws {
            try {
            return (String)super.h.invoke(this, m2, (Object[])null);
            } catch (RuntimeException | Error var2) {
            throw var2;
            } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
            }
            }

            public final int hashCode() throws {
            try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
            } catch (RuntimeException | Error var2) {
            throw var2;
            } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
            }
            }

            // 静态代码块,初始化 Method 属性。
            static {
            try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.moralok.proxy.HelloService").getMethod("sayHello", Class.forName("java.lang.String"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
            } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
            }
            }
            }
            - -

            查看 CGLib 生成的子类

            使用以下语句,将 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 的解决方案也是如此,当然其中会有很多值得深思的细节。回顾循环依赖问题的解决思路,你会发现:

            +
              +
            1. 循环依赖本身是普通的,一个手动可解决的问题
            2. +
            3. Spring 依赖注入时,虽然 Bean B 依赖的 Bean A 尚未初始化完毕,但是已经实例化,可以用来赋值
            4. +
            5. Spring AOP 中,既然 Bean B 依赖的 Bean A 需要是 A 对象的代理,那么就在那时候创建代理,用来赋值即可
            6. +
            +

            流程图和测试用例

            在开始之前我们先放一张循环引用的处理流程图,用于在后续分析过程中进行对照。

            +
            +

            以下是测试用例的代码:

              -
            • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.class,代理类
            • -
            • HelloServiceImpl$$FastClassByCGLIB$$a5654167.class,被代理类的 FastClass
            • -
            • HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.class,代理类的 FastClass
            • +
            • CircularA
              public class CircularA {
              private CircularB circularB;
              public CircularB getCircularB() {
              return circularB;
              }
              public void setCircularB(CircularB circularB) {
              this.circularB = circularB;
              }
              }
            • +
            • CircularB
              public class CircularB {
              private CircularA circularA;
              public CircularA getCircularA() {
              return circularA;
              }
              public void setCircularA(CircularA circularA) {
              this.circularA = circularA;
              }
              }
            • +
            • circular-reference-test.xml
              <beans>
              <bean id="circularA" class="com.moralok.bean.CircularA">
              <property name="circularB" ref="circularB"/>
              </bean>
              <bean id="circularB" class="com.moralok.bean.CircularB">
              <property name="circularA" ref="circularA"/>
              </bean>
              </beans>
            • +
            • 测试类
              public class CircularReferenceTest {
              @Test
              public void testRegular() {
              ApplicationContext applicationContext = new ClassPathXmlApplicationContext("circular-reference-test.xml");
              CircularA circularA = (CircularA) applicationContext.getBean("circularA");
              }
              }
            - - -

            代理类定义

            继承了被代理类。

            -
            public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory {
            }
            - -

            静态代码块

            static {
            // 调用静态钩子方法
            CGLIB$STATICHOOK1();
            }

            static void CGLIB$STATICHOOK1() {
            CGLIB$THREAD_CALLBACKS = new ThreadLocal();
            CGLIB$emptyArgs = new Object[0];
            Class var0 = Class.forName("com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31");
            Class var1;
            // 获取 Object 类的 equals、toString、hashCode、clone 这几个特定方法的 Method 对象
            Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
            // 还生成了相对应的 Method 属性保存(为了减少一次寻址吗?)
            CGLIB$equals$1$Method = var10000[0];
            // 为每一个 Method 创建一个 MethodProxy
            CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
            CGLIB$toString$2$Method = var10000[1];
            CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
            CGLIB$hashCode$3$Method = var10000[2];
            CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
            CGLIB$clone$4$Method = var10000[3];
            CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
            // 被代理类的方法也做相同处理
            CGLIB$sayHello$0$Method = ReflectUtils.findMethods(new String[]{"sayHello", "(Ljava/lang/String;)V"}, (var1 = Class.forName("com.moralok.proxy.HelloServiceImpl")).getDeclaredMethods())[0];
            CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");
            }
            - -

            MethodProxy 稍后再做介绍。

            -

            构造器方法

            构造器方法内,调用了绑定回调(Callbacks)方法。

            -
            public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() {
            CGLIB$BIND_CALLBACKS(this);
            }

            // 标识是否已经绑定过回调
            private boolean CGLIB$BOUND;

            private static final void CGLIB$BIND_CALLBACKS(Object var0) {
            HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 var1 = (HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31)var0;
            if (!var1.CGLIB$BOUND) {
            // 未绑定过回调则进行绑定,更新标识
            var1.CGLIB$BOUND = true;
            // 先获取 THREAD_CALLBACKS
            Object var10000 = CGLIB$THREAD_CALLBACKS.get();
            if (var10000 == null) {
            // 如果为 null,再获取 STATIC_CALLBACKS
            var10000 = CGLIB$STATIC_CALLBACKS;
            if (var10000 == null) {
            // 如果仍然为 null,直接返回
            return;
            }
            }

            // 每一个 Callback (像之前的 Method 一样)都有专门的属性保存
            var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
            }

            }
            - -

            生成的代理方法

            CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。

            -
            final void CGLIB$sayHello$0(String var1) {
            super.sayHello(var1);
            }

            public final void sayHello(String var1) {
            // 获取回调(拦截器)
            MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
            if (var10000 == null) {
            // 如果为 null,先进行回调绑定
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
            }

            if (var10000 != null) {
            // 如果回调(拦截器)不为 null,则调用 intercept 方法
            var10000.intercept(this, CGLIB$sayHello$0$Method, new Object[]{var1}, CGLIB$sayHello$0$Proxy);
            } else {
            // 否则直接调用父类方法
            super.sayHello(var1);
            }
            }
            - -

            CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。

            -

            分析 MethodProxy

            如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。

            -

            创建 MethodProxy

            MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。

            -
            CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0");

            public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
            MethodProxy proxy = new MethodProxy();
            // sayHello 方法签名
            proxy.sig1 = new Signature(name1, desc);
            // CGLIB$sayHello$0 方法签名
            proxy.sig2 = new Signature(name2, desc);
            // 被代理类和代理类
            proxy.createInfo = new CreateInfo(c1, c2);
            return proxy;
            }
            - -

            CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。

            -
            private static class CreateInfo
            {
            // 被代理类
            Class c1;
            // 代理类
            Class c2;
            NamingPolicy namingPolicy;
            GeneratorStrategy strategy;
            boolean attemptLoad;

            public CreateInfo(Class c1, Class c2)
            {
            this.c1 = c1;
            this.c2 = c2;
            AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();
            if (fromEnhancer != null) {
            namingPolicy = fromEnhancer.getNamingPolicy();
            strategy = fromEnhancer.getStrategy();
            attemptLoad = fromEnhancer.getAttemptLoad();
            }
            }
            }
            - -

            FastClass 和方法索引对

            调用原始方法 invokeSuper

            MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。

            -
            // invoke 方法的代码相似
            public Object invokeSuper(Object obj, Object[] args) throws Throwable {
            try {
            // 初始化,生成 FastClassInfo
            init();
            FastClassInfo fci = fastClassInfo;
            // 调用原始(父类)方法
            return fci.f2.invoke(fci.i2, obj, args);
            } catch (InvocationTargetException e) {
            throw e.getTargetException();
            }
            }
            +

            第一次获取 circularA

            调用 doGetBean(circularA) 方法第一次获取:

            +
              +
            1. 从缓存中获取 circularA(先不看方法内的具体代码,在第一次进入该方法时,必定返回 null
            2. +
            3. 因缓存中不存在,就创建 circularA
            4. +
            +
            protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {
            // 尝试从缓存中获取 circularA,第一次结果必定为 null
            Object sharedInstance = getSingleton(beanName);
            // ...
            // 再次从缓存中获取 circularA(双重校验),如果为 null,就创建
            sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            try {
            return createBean(beanName, mbd, args);
            }
            }
            });
            // ...
            return (T) bean;
            }
            -

            生成 FastClass 信息

            private void init()
            {
            // 双重校验锁,生成 FastClass 和方法索引对
            if (fastClassInfo == null)
            {
            synchronized (initLock)
            {
            if (fastClassInfo == null)
            {
            CreateInfo ci = createInfo;

            FastClassInfo fci = new FastClassInfo();
            // 生成 FastClass
            fci.f1 = helper(ci, ci.c1);
            fci.f2 = helper(ci, ci.c2);
            // 获取方法索引
            fci.i1 = fci.f1.getIndex(sig1);
            fci.i2 = fci.f2.getIndex(sig2);
            fastClassInfo = fci;
            createInfo = null;
            }
            }
            }
            }
            +

            标记 bean 是否正在创建中

            在真正创建 circularA 之前,会调用 getSingleton(String, ObjectFactory) 再次尝试从缓存中获取(构成双重校验),这个方法内部通过 ObjectFactory 调用创建 Bean 的方法,并且在一前一后分别添加和移除 “Bean 是否正在创建中”的标志。在后续 circularB 获取 circularA 时就是依据该标志判断 circularA 正在创建中。

            +
            isSingletonCurrentlyInCreation(beanName)
            -

            FastClass 信息

              -
            • f1 是被代理类的 FastClass 对象,i1 是 CGLIB$sayHello$0 方法在生成的 FastClass 中的索引。
            • -
            • f2 是代理类的 FastClass 对象,i2 是 sayHello 方法在生成的 FastClass 中的索引。
            • -
            -

            invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。

            -

            CGLib 完全有能力获得 CGLIB$sayHello$0 的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。

            +

            这里的“是否正在创建中”,并不是狭义地指一个 Bean 是否已经实例化,而是指一个 Bean 是否已经实例化和初始化。circular A 在初始化阶段,去获取 circularB,在 circularB 视角中,circular A 仍处于正在创建中。示意图如下。

            -
            private static class FastClassInfo
            {
            FastClass f1;
            FastClass f2;
            int i1;
            int i2;
            }
            +
            -

            FastClass 的 invoke 方法

            以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511 为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0 方法。

            +
            public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
            // ...
            // 标记为正在创建中
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            // ...
            try {
            // 创建 Bean
            singletonObject = singletonFactory.getObject();
            newSingleton = true;
            }
            // ...
            // 从正在创建中的集合移除
            afterSingletonCreation(beanName);
            if (newSingleton) {
            addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
            }
            + +

            创建 circularA

            createBean 方法被包装在 ObjectFactory 中。创建的工作分为两个部分:

              -
            1. 获取代理对象
            2. -
            3. 根据传入的方法索引,调用
            4. +
            5. 实例化 circularA
            6. +
            7. 初始化 circularA
                +
              • circularA 进行依赖注入时:getBean(circularB)
              • +
              +
            -
            public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
            HelloServiceImpl..EnhancerByCGLIB..c51b2c31 var10000 = (HelloServiceImpl..EnhancerByCGLIB..c51b2c31)var2;
            int var10001 = var1;

            try {
            switch (var10001) {
            case 0:
            return new Boolean(var10000.equals(var3[0]));
            // ...
            case 16:
            var10000.CGLIB$sayHello$0((String)var3[0]);
            return null;
            // ...
            }
            } catch (Throwable var4) {
            throw new InvocationTargetException(var4);
            }

            throw new IllegalArgumentException("Cannot find matching method/constructor");
            }
            - -

            获取方法索引

            怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。

            -
            -

            在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?

            -
            -
            public int getIndex(Signature var1) {
            String var10000 = var1.toString();
            switch (var10000.hashCode()) {
            // ...
            case -1721191351:
            if (var10000.equals("CGLIB$sayHello$0(Ljava/lang/String;)V")) {
            return 16;
            }
            break;
            // ...
            }

            return -1;
            }
            - -

            总结和思考

            两者在使用上是相仿的。

            +

            很重要的是,在实例化 circularA 之后,尚未进行初始化工作之前,如果 circularA 满足早期暴露的条件,将会被包装为 ObjectFactory 缓存到 singletonFactory(三级缓存) 中。

            +

            值得注意的是:

              -
            • 对于两者的源码,读得不多。有时候会感慨,看这么多年前的代码,还是感觉吃力。有时候想,如果不好好看源码,心里不踏实;如果花很多时间理清楚了,但是发现更多只是知道了一些细节,于整体理解的提升不大,又会感觉不值得。
            • -
            • 但也提醒自己,不要太在意,用得本就不多,涉及源码的机会更是没有,如果方方面面都要细究,人生太短,智商不够,等涉足相关问题再回头研究。
            • -
            • 基础的用法和概念应该了解,不然看到 Spring AOP 源码时,分不清 Spring 封装的边界在哪里。
            • +
            • 如果 circularA 最终不需要早期暴露,那么这个 ObjectFactory 是会被直接抛弃的
            • +
            • 如果 circularA 需要早期暴露,即它依赖的 circularB 同时依赖它,到时候将调用 getEarlyBeanReference 方法获得 circularA早期 Bean 引用
            -

            借着梳理 Spring 的机会回头再看,又感觉轻松不少。

            +

            刚开始看 ObjectFactory 匿名类的用法可能有点不适应,可以多读几次,帮助理解 getObjectgetEarlyBeanReference 的语义。

            -]]>
            - - java - jdk proxy - cglib - -
            - - 如何使用 SSH 连接 Github 和服务器 - /2023/06/28/how-to-use-ssh-to-connect-github-and-server/ - 本文介绍了如何使用 SSH 连接 Github 和免密登录服务器作为备忘笔记,主要在新建虚拟机或重装云服务器系统时使用。

            - - -

            使用 SSH 连接 Github

            检查现有 SSH 密钥

            打开终端,输入 ls -al ~/.ssh 以查看是否存在现有的 SSH 密钥。

            -
            $ ls -al ~/.ssh
            total 16
            drwx------ 2 moralok moralok 4096 Jun 28 20:19 .
            drwxr-xr-x 6 moralok moralok 4096 Jun 28 20:13 ..
            -rw------- 1 moralok moralok 106 Jun 28 20:07 authorized_keys
            -rw-r--r-- 1 moralok moralok 444 Jun 28 20:19 known_hosts
            +
            protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
            throws BeanCreationException {
            // ...
            // 实例化 bean
            instanceWrapper = createBeanInstance(beanName, mbd, args);
            // ...
            // 如果单例允许提前暴露的话,就将实例包装为 ObjectFactory 保存在 map singletonFactories 中
            boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
            if (earlySingletonExposure) {
            addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
            }
            });
            }
            // 初始化 bean,对 bean 的属性进行依赖注入
            Object exposedObject = bean;
            try {
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
            exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
            }
            // ...
            return exposedObject;
            }
            -

            检查目录列表以查看是否已经有 SSH 公钥。 默认情况下,GitHub 的一个支持的公钥的文件名是以下之一。

            -
              -
            • id_rsa.pub
            • -
            • id_ecdsa.pub
            • -
            • id_ed25519.pub
            • -
            -

            生成 SSH 密钥

            如果没有密钥,就需要生成新的 SSH 密钥;如果已有,跳到上传已有密钥环节。
            打开终端,粘贴下面的文本(替换为你的 GitHub 电子邮件地址),这将以提供的电子邮件地址为标签创建新 SSH 密钥。
            一直 yes 确定选择默认即可。

            -
            $ ssh-keygen -t ed25519 -C "your_email@example.com"
            Generating public/private ed25519 key pair.
            Enter file in which to save the key (/home/your-user/.ssh/id_ed25519):
            +

            依赖注入时获取 circularB

            填充 Bean 属性

            填充 Bean 属性的 populateBean 方法很复杂,我们只关注对 circularA 的依赖注入将间接地调用 getBean(circularB) 进入获取 circularB 的过程。

            +
            protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
            // ...
            // 应用属性值
            applyPropertyValues(beanName, mbd, bw, pvs);
            }

            protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
            // ...
            // 如果有必要解析 value
            Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
            // ...
            bw.setPropertyValues(new MutablePropertyValues(deepCopy));
            // ...
            }

            public Object resolveValueIfNecessary(Object argName, Object value) {
            // ...
            // 如果是 RuntimeBeanReference 类型,就解析引用
            if (value instanceof RuntimeBeanReference) {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            return resolveReference(argName, ref);
            }
            // ...
            }

            private Object resolveReference(Object argName, RuntimeBeanReference ref) {
            // ...
            // 解析引用将调用 getBean
            Object bean = this.beanFactory.getBean(refName);
            this.beanFactory.registerDependentBean(refName, this.beanName);
            return bean;
            // ...
            }
            -

            上传 SSH 密钥

            SSH 公钥复制到剪贴板,在 Github 上的 Settings -> Access -> SSH and GPG keys -> New SSH key,粘贴即可。

            -
            $ cat ~/.ssh/id_ed25519.pub
            +

            获取 circularB

            AbstractBeanFactory#doGetBean(circularB) 获取 circularB 将经过和 circularA 一样的流程,进入 populateBean(circularB) 方法进行依赖注入,进而再次去获取 circularA

            +

            第二次获取 circularA

            调用 doGetBean(circularA) 方法第二次获取 circularA 时,仍然先尝试从缓存中获取,这次将从缓存中得到先前创建的 circularA

            +
            protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {
            // ...
            // 尝试从缓存中获取 circularA,结果不再为 null
            Object sharedInstance = getSingleton(beanName);
            // ...
            }
            -

            配置 Git

            $ git config --global user.name "your_username"
            $ git config --global user.email "your_email@example.com"
            +

            这个 getSingleton 方法正是在第一次获取 circularA 时我们跳过没有查看的方法。方法中代码的逻辑并不复杂,但是要理解为什么这么做,却需要回过头来反复品味和思考。这里也是经常被拿来说的“三级缓存”问题的核心。

            +

            从缓存中获取 circularA

              +
            1. 先从 singletonObjects(一级缓存) 获取 circularA,不存在
            2. +
            3. 判断 circularA 是正在创建中,从 earlySingletonObjects(二级缓存) 获取,仍然不存在
            4. +
            5. allowEarlyReference 为真,从 singletonFactories(三级缓存) 获取 ObjectFactory
            6. +
            7. 调用 getObject 间接调用 getEarlyBeanReference 获得早期 Bean 引用
            8. +
            +
            protected Object getSingleton(String beanName, boolean allowEarlyReference) {
            Object singletonObject = this.singletonObjects.get(beanName);
            // 如果 singletonObjects 中不存在,且 bean 正在创建过程中(满足)
            if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
            // 从 earlySingletonObjects 获取(不存在)
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
            // 如果 earlySingletonObjects 中不存在,且允许早期引用,就从 singletonFactories 中获取
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
            // 间接调用 getEarlyBeanReference
            singletonObject = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
            }
            }
            }
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
            }
            +

            获取早期 Bean 引用

            请注意这次在调用 getObject 方法时,并不是直接返回 Bean 的实例,而是间接调用 getEarlyBeanReference 方法,顾名思义是获取早期 Bean 引用。处理逻辑是如果存在 SmartInstantiationAwareBeanPostProcessor,将使用这些后处理器处理以获得早期 Bean 引用。

            +
            protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
            Object exposedObject = bean;
            if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            // 如果存在 InstantiationAwareBeanPostProcessors
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            // 如果 bp 是 SmartInstantiationAwareBeanPostProcessor 类型
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            // 尝试创建代理
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            if (exposedObject == null) {
            return null;
            }
            }
            }
            }
            return exposedObject;
            }
            -

            使用 SSH 免密登录服务器

            使用现成的密钥,将 ~/.ssh/id_ed25519.pub 的内容追加到服务端的 ~/.ssh/authorized_keys 中,使用 ssh user@host 成功免密登录。这样一来,远程连接服务器或者使用 VScode Remote Explorer 时,不用每次输入密码了。

            -

            VScode Remote Explorer 无法连接

            在更新 VMware Workstation 17 Pro 后,发现虚拟机的 IP192.168.46.135 重置 为 192.168.46.128,即使更新配置文件 C:\Users\moralok\.ssh\config 中的 IP VScode Remote Explorer 仍然无法连接,但是通过 Xshell 使用账号密码可以登录。查看报错信息发现 known_hosts192.168.46.128 对应的 ECDSA key 有问题,应该记录的是之前占用该 IP 的虚拟机的 ECDSA key,删除该行后重新连接成功。

            -
            [18:09:59.973] > @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
            > @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
            > @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
            > IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
            > Someone could be eavesdropping on you right now (man-in-the-middle attack)!
            > It is also possible that a host key has just been changed.
            > The fingerprint for the ECDSA key sent by the remote host is
            > SHA256:Q8ckiM7lmz2HBjsNPHFaVjhJfuTcfbgThP1NLv52H1Y.
            > Please contact your system administrator.
            > Add correct host key in C:\\Users\\moralok/.ssh/known_hosts to get rid of this message.
            > Offending ECDSA key in C:\\Users\\moralok/.ssh/known_hosts:1
            > ECDSA host key for 192.168.46.128 has changed and you have requested strict checking.
            > Host key verification failed.
            > 过程试图写入的管道不存在。
            >
            +
            +

            关于创建代理的分析请参考Spring AOP 如何创建代理 beans

            +
            +

            通过后处理器的 getEarlyBeanReference 方法获取早期 Bean 引用时,可能返回的就是 circularA 对象,但是如果 circularA 需要创建代理,就会在这时候为它创建代理,而在之后 BeanPostProcessor 处理时就不会再创建代理了。

            +

            AbstractAutoProxyCreator 为例,它是自动代理创建者的抽象类,同时实现了 SmartInstantiationAwareBeanPostProcessorBeanPostProcessor 接口。

            +
            public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 添加到早期代理引用的缓存中,用于后续在常规创建代理阶段判断是否需要创建代理
            this.earlyProxyReferences.add(cacheKey);
            }
            // 如有必要,包装为代理
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            +

            如果已经在获取 circularA 的早期引用时就将其包装为代理,则不再创建代理。

            +
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            // 判断早期代理引用中是否存在
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 如有必要,包装为代理
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            }
            return bean;
            }
            -

            SSH 的原理

            关于 SSH

            使用 SSH 协议可以连接远程服务器和服务并向它们验证,而无需在每次访问时都提供用户名和密码,Github 还可以使用 SSH 密钥对提交进行签名。

            -

            公钥和私钥

            SSH 的使用(非对称加密)需要生成公钥 public key 和私钥 private key。常用的算法有 rsaecdsaed25519,相对应的公钥默认文件名即 id_XXX.pubed25519 的安全性介于 rsa 2048rsa 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"
            +

            再思 Spring bean 的三级缓存

            三级缓存 or 三个缓存

            著名的“三级缓存”,实际上就是三个存放 Beanmap

            +
              +
            • singletonObjects
            • +
            • earlySingletonObjects
            • +
            • singletonFactories
            • +
            +

            在很多网上的资料中,都称 Spring 通过使用三级缓存的设计解决了循环引用问题。同时我也看到有人反思,这样翻译对学习者造成了很大的困扰,代码中并没有多级 cache 的意味,称之为“三个缓存”比“三级缓存”更合理也更容易理解。三个存放 Beanmap 事实上是相互独立的,甚至它们是互斥的,一个 Bean 在同一时间最多只能存在于其中一个 map

            +

            对我个人而言,我对反对者的观点深有同感,如果我没有看过面经,即使我熟读并理解代码,我可能都无法回答 Spring 中的三级缓存是什么。甚至我会被三级缓存这个名词所震慑,在了解它之前在心里放大它的复杂性。
            但是在不断阅读的过程中(可能也有已有记忆的加持),我也会感受到称之为“三级缓存”的合理性。这里的分级含义更多体现的是 Bean 的“晋升”过程。

            +

            缓存中的添加和删除

            网上很多资料在讨论 Bean 在缓存中的添加和删除时,大多一笔带过,并没有谈到细节。但是 Bean 并不是在这三个缓存中依次晋级,甚至有时候,添加和移除的都不是一个对象,比如三级缓存中的 ObjectFactory 可能被直接抛弃。

            +
            protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
            throws BeanCreationException {
            // 实例化
            instanceWrapper = createBeanInstance(beanName, mbd, args);

            // 单例+允许循环引用+当前正在被创建=可能需要提前暴露
            boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
            if (earlySingletonExposure) {
            addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
            }
            });
            }

            Object exposedObject = bean;
            try {
            // 这里面会进行依赖注入
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
            // 这里面会尝试创建代理
            exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
            }

            if (earlySingletonExposure) {
            // 如果允许早期暴露,尝试获取早期 Bean 引用
            Object earlySingletonReference = getSingleton(beanName, false);
            // 如果为 null 说明没有早期暴露,返回的其实还是最初的 exposedObject
            // 三级缓存里的 ObjectFactory 完全没用上,会在 exposedObject 添加到一级缓存时直接删除
            if (earlySingletonReference != null) {
            // 如果不为 null,说明确实早期暴露过
            if (exposedObject == bean) {
            // 如果早期暴露过,常规情况下,exposedObject 不会再创建代理,应 == bean
            // 如果没有代理,exposedObject == bean == earlySingletonReference
            // 如果创建过代理,earlySingletonReference 才是包装过的代理
            exposedObject = earlySingletonReference;
            }
            else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            // 如果 exposedObject 在 initializeBean 中再次被创建代理
            // 但是存在 Bean 依赖了这个 Bean(由于拿到的是早期引用),它们拿到的和最终的是不同的对象
            // 如果不允许尽管会被包装仍然注入原始类型,就需要抛出异常
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
            actualDependentBeans.add(dependentBean);
            }
            }
            if (!actualDependentBeans.isEmpty()) {
            // 抛异常
            }
            }
            }
            }
            return exposedObject;
            }
              -
            • -t 表示算法,如 rsa
            • -
            • -b 表示 rsa 密钥长度,默认 2048 bited25519 不需要指定。
            • -
            • -f 表示文件名。
            • -
            • -C 表示在公钥文件中添加注释,可修改
            • +
            • 在无需早期暴露的情况下,二级缓存没有用到,虽然三级缓存中保存了一个 ObjectFactory,但是也是没有用到的。Bean 直接保存到一级缓存中。
            • +
            • 在需要早期暴露但无需代理的情况下,尽管获取早期引用后保存在二级缓存中,以供重复使用,但是二级缓存中和原始的 Bean 仍然是同一个对象,Bean 仍然是直接保存到一级缓存,再删掉二级缓存。
            • +
            • 在需要早期暴露和需要代理的情况下,二级缓存中保存的是代理对象,需要从二级缓存中获取再保存到一级缓存中,然后再删除二级缓存。
            -

            SSH 公钥登录过程

              -
            1. Client 将自己的公钥存放到服务端,追加到 authorized_keys 文件。
            2. -
            3. Server 收到 Client 的连接请求后,会在 authorized_keys 文件中匹配到 Client 传过来的公钥,并生成随机数 R,用公钥对随机数加密得到 pubKey(R)
            4. -
            5. Client 收到后通过私钥解密得到随机数 R,然后对随机数 R 和本次会话的 sessionKey 使用 MD5 生成摘要 Digest1,发送给服务端。
            6. -
            7. Server 会对随机数 R 和会话的 sessionKey 同样使用 MD5 生成摘要 Digest2,对比相同即完成认证过程。
            8. -
            -

            避免中间人攻击

            SSH 通过口令确认避免中间人攻击,如果用户第一次登录 Server,系统会提示:

            -
            $ ssh -T git@github.com
            The authenticity of host 'github.com (20.205.243.166)' can't be established.
            ECDSA key fingerprint is SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM.
            Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
            Warning: Permanently added 'github.com,20.205.243.166' (ECDSA) to the list of known hosts.
            Hi ${username}! You've successfully authenticated, but GitHub does not provide shell access.
            + -

            Server 需要在其网站上公示其公钥的指纹,Github 的公钥指纹在这里
            确认匹配后,客户端会在 ~/.ssh/known_hosts 中记录,下次登录不再警告。

            -

            参考链接

            使用 SSH 进行连接 Github
            Git 多台电脑共用SSH Key
            SSH协议登录过程详解
            GitHub 的 SSH 密钥指纹
            使用 Ed25519 算法生成你的 SSH 密钥

            +

            必须要三个缓存吗

            网上有很多资料在分析为什么需要三个缓存,才能解决在需要创建代理的情况下发生的循环依赖问题。但是个人觉得有些分析缺乏逻辑,也有点违和感。将当前的解决方案套到只有两个缓存的情况下去分析不太合理,就像你把四轮机动车卸掉一个轮子,说机动车必须要四个轮子才可以,不然不平衡,事实上三个轮子的机动车设计是存在且可行的。

            +

            在分析两个缓存如何解决在需要创建代理的情况下发生的循环依赖问题时,应该抛开现有的处理逻辑,回归本质问题:既然 circularA 需要创建代理,如果 circularA 依赖的 circularB 也依赖了 circular A,在为它获取依赖 circularA 时立即创建代理即可。
            一个 map 必须用于存放完成品,另一个 map 用于存放半成品。创建的代理作为升级版的半成品,完全可以覆盖原始的半成品继续存放在第二个 map 中。为了避免重复创建代理,只要能够标识半成品是已经经过代理包装的即可。BeanDefinitionBean 自身、创建代理的地方,都有能力实现标识一个 Bean 的半成品是否经过包装,最不济使用一个 map 存放标识(但是这也就等同于使用三个 map 了)。你甚至可以将半成品 circularA 直接尝试包装成代理再存放入半成品 map 中,这个方案本质上是将创建代理的步骤从初始化 Bean 中分离到初始化 Bean 之前。

            +

            综上,使用两个 map 解决在技术上是没有问题的,很多分析中考虑的问题相当于把 Spring 现有的处理逻辑当成枷锁限制了自己。既然你都在问不这么打地基可不可以,我难道不得考虑挪一挪上面的砖墙吗?当然我不能保证这么设计不会破坏 Spring 现有全部功能的兼容性和扩展性,但是这并不是代理为循环依赖引入的问题。

            ]]>
            - ssh + java + spring
            - Dubbo SPI 的工作原理 - /2023/11/28/how-does-Dubbo-SPI-works/ - SPI 作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI 被广泛应用于各类技术框架中,例如 JDBC 驱动、SpringDubbo 等等。Dubbo 并未使用原生的 Java SPI,而是重新实现了一套更加强大的 Dubbo SPI。本文将简单介绍 SPI 的设计理念,通过示例带你体会 SPI 的作用,通过 Dubbo 获取拓展的流程图源码分析带你理解 Dubbo SPI 的工作原理。深入了解 Dubbo SPI,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。

            + Nginx 反向代理在家庭网络中的应用 + /2023/12/01/Nginx-reverse-proxy-for-home-networks/ + 原先在使用 Cloudflare Tunnel 访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare 已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare 统计中缺少的新,决定使用 Nginx 反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts 文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port 的形式去访问。

            -

            SPI 简介

            SPI 的全称是 Service Provider Interface,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI 机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI 具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 JavaJDBC 驱动为例,JDK 提供了 java.sql.Driver 接口,各个数据库厂商,例如 MySQLOracle 提供具体的实现。

            - +
            +

            Cloudflare Tunnel 相较于 ZerotierOpenVPN,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。

            +
            +

            网络结构示意图

            客户端发起请求,请求经 Cloudflare 转发到局域网中的 Tunnel。原先,Tunnel 如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx,由 Nginx 反向代理,发向目标服务。

            + -

            目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:

            -
              -
            • Java SPI:META-INF/services/full.qualified.interface.name
            • -
            • Dubbo SPI:META-INF/dubbo/full.qualified.interface.name(还有其他目录可供选择)
            • -
            • Spring SPI: META-INF/spring.factories
            • -
            -

            SPI 示例

            Java SPI 示例

            定义一个接口 Animal

            -
            public interface Animal {
            void bark();
            }
            +

            配置

            docker-compose.yml

            NginxTunnel 还有其他内部服务应处于同一个网络中。

            +
            version: "1.0"
            services:
            nginx:
            image: nginx:1.25.3
            container_name: nginx
            ports:
            - 80:80
            volumes:
            - $PWD/etc/nginx/conf.d:/etc/nginx/conf.d
            - $PWD/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
            - $PWD/log:/var/log/nginx
            networks:
            - my-network
            restart: unless-stopped

            networks:
            my-network:
            external: true
            -

            定义两个实现类 DogCat

            -
            public class Dog implements Animal {
            @Override
            public void bark() {
            System.out.println("Dog bark...");
            }
            }

            public class Cat implements Animal {
            @Override
            public void bark() {
            System.out.println("Cat bark...");
            }
            }
            +

            /etc/nginx/nginx.conf

            在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel 的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx 服务。

            +
            user  nginx;
            worker_processes auto;

            error_log /var/log/nginx/error.log notice;
            pid /var/run/nginx.pid;


            events {
            worker_connections 1024;
            }


            http {
            include /etc/nginx/mime.types;
            default_type application/octet-stream;

            log_format main '$remote_addr - $remote_user [$time_local] "$request" '
            '$status $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';

            access_log /var/log/nginx/access.log main;

            sendfile on;
            #tcp_nopush on;

            keepalive_timeout 65;

            #gzip on;

            include /etc/nginx/conf.d/*.conf;

            #以上是默认的配置内容,新增拒绝未匹配成功的域名
            server {
            listen 80 default_server;
            server_name _;

            return 404;
            }
            }
            -

            META-INF/services 文件夹下创建一个文件,名称为 Animal 的全限定名 com.moralok.dubbo.spi.test.Animal,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。

            -
            com.moralok.dubbo.spi.test.Dog
            com.moralok.dubbo.spi.test.Cat
            - -

            进行测试。

            -
            public class JavaSPITest {
            @Test
            void bark() {
            System.out.println("Java SPI");
            System.out.println("============");
            ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
            serviceLoader.forEach(Animal::bark);
            }
            }
            - -

            测试结果

            -
            Java SPI
            ============
            Dog bark...
            Cat bark...
            - -

            Dubbo SPI 示例

            Dubbo 并未使用原生的 Java SPI,而是重新实现了一套功能更加强大的 SPI 机制。Dubbo SPI 的配置文件放在 META-INF/dubbo 文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI 注解。

            -
            dog = com.moralok.dubbo.spi.test.Dog
            cat = com.moralok.dubbo.spi.test.Cat
            - -

            进行测试。

            -
            public class DubboSPITest {

            @Test
            void bark() {
            System.out.println("Dubbo SPI");
            System.out.println("============");
            ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
            Animal dog = extensionLoader.getExtension("dog");
            dog.bark();
            Animal cat = extensionLoader.getExtension("cat");
            cat.bark();
            }
            }
            - -

            测试结果

            -
            Dubbo SPI
            ============
            Dog bark...
            Cat bark...
            - -

            Dubbo 获取扩展流程图

            - -

            Dubbo SPI 源码分析

            获取 ExtensionLoader

            private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);

            public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
            if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
            }
            if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
            }
            if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
            ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
            }

            // 从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回
            ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            }
            return loader;
            }
            - -

            这个方法包含了如下步骤:

            -
              -
            1. 参数校验。
            2. -
            3. 从缓存 EXTENSION_LOADERS 中获取与拓展类对应的 ExtensionLoader,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。
            4. -
            -
            -

            从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的 getOrCreate 的处理模式在 Dubbo 的源码中经常出现。

            -
            -

            EXTENSION_LOADERSExtensionLoader 的静态变量,保存了“拓展类->ExtensionLoader”的映射关系。

            -

            根据 name 获取 Extension

            public T getExtension(String name) {
            return getExtension(name, true);
            }

            public T getExtension(String name, boolean wrap) {
            if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
            }
            if ("true".equals(name)) {
            // 获取默认的拓展实现
            return getDefaultExtension();
            }
            // Holder,用于持有目标对象
            final Holder<Object> holder = getOrCreateHolder(name);
            // 双重检查
            Object instance = holder.get();
            if (instance == null) {
            synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
            // 创建拓展实例,设置到 holder 中。
            instance = createExtension(name, wrap);
            holder.set(instance);
            }
            }
            }
            return (T) instance;
            }

            private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

            private Holder<Object> getOrCreateHolder(String name) {
            Holder<Object> holder = cachedInstances.get(name);
            if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
            }
            return holder;
            }
            - -

            这个方法中获取 Holder 和获取拓展实例都是使用 getOrCreate 的模式。

            -

            Holder 用于持有拓展实例。cachedInstancesExtensionLoader 的成员变量,保存了“name->Holder(拓展实例)”的映射关系。

            -

            创建 Extension

            private T createExtension(String name, boolean wrap) {
            // 从配置文件中加载所有的拓展类,可得到“name->拓展实现类”的映射关系表
            Class<?> clazz = getExtensionClasses().get(name);
            if (clazz == null || unacceptableExceptions.contains(name)) {
            throw findException(name);
            }
            try {
            // 使用 getOrCreate 模式获取拓展实例
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 向拓展实例中注入依赖
            injectExtension(instance);

            // 是否包装默认为 true
            if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
            wrapperClassesList.addAll(cachedWrapperClasses);
            wrapperClassesList.sort(WrapperComparator.COMPARATOR);
            Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
            // 遍历包装类
            for (Class<?> wrapperClass : wrapperClassesList) {
            Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
            // 区别于旧版本:支持使用 Wrapper 注解进行匹配
            if (wrapper == null
            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
            // 如果有匹配的包装类,包装拓展实例
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
            }
            }
            }

            // 初始化拓展实例
            initExtension(instance);
            return instance;
            } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
            type + ") couldn't be instantiated: " + t.getMessage(), t);
            }
            }
            - -

            这个方法包含如下步骤:

            -
              -
            1. 通过 getExtensionClasses 获取所有拓展类
            2. -
            3. 通过反射创建拓展实例
            4. -
            5. 向拓展实例中注入依赖
            6. -
            7. 将拓展实例包装在适配的 Wrapper 对象中
            8. -
            9. 初始化拓展实例
            10. -
            -

            第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOCAOP 的具体实现。

            -

            最后拓展实例的结构如下图。

            - - -

            加载 Extension Class

            private Map<String, Class<?>> getExtensionClasses() {
            // 使用 getOrCreate 模式获取所有拓展类
            Map<String, Class<?>> classes = cachedClasses.get();
            if (classes == null) {
            synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
            classes = loadExtensionClasses();
            cachedClasses.set(classes);
            }
            }
            }
            return classes;
            }

            private Map<String, Class<?>> loadExtensionClasses() {
            // 1. 缓存默认拓展名
            cacheDefaultExtensionName();

            Map<String, Class<?>> extensionClasses = new HashMap<>();

            // 遍历加载策略,加载各个策略的目录下的配置文件,获取拓展类
            for (LoadingStrategy strategy : strategies) {
            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
            strategy.overridden(), strategy.excludedPackages());
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
            strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
            }

            return extensionClasses;
            }

            // 如果存在默认拓展名,提取并缓存
            private void cacheDefaultExtensionName() {
            // 获取 SPI 注解
            final SPI defaultAnnotation = type.getAnnotation(SPI.class);
            if (defaultAnnotation == null) {
            return;
            }

            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
            // 对 SPI 的 value 内容进行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 name 是否合法
            if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
            + ": " + Arrays.toString(names));
            }
            if (names.length == 1) {
            // 缓存默认拓展名,用于 getDefaultExtension
            cachedDefaultName = names[0];
            }
            }
            }
            - -

            依次处理特定目录

            代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/ 三个目录下的配置文件,获取拓展类。

            -
            loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
            loadDirectory(extensionClasses, DUBBO_DIRECTORY);
            loadDirectory(extensionClasses, SERVICES_DIRECTORY);
            - -

            新版本使用原生的 Java SPI 加载 LoadingStrategy,允许用户自定义加载策略。

            -
              -
            1. DubboInternalLoadingStrategy,目录 META-INF/dubbo/internal/,优先级最高
            2. -
            3. DubboLoadingStrategy,目录 META-INF/dubbo/,优先级普通
            4. -
            5. ServicesLoadingStrategy,目录 META-INF/services/,优先级最低
            6. -
            -
            private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

            private static LoadingStrategy[] loadLoadingStrategies() {
            // 通过 Java SPI 加载 LoadingStrategy
            return stream(load(LoadingStrategy.class).spliterator(), false)
            .sorted()
            .toArray(LoadingStrategy[]::new);
            }
            - -

            LoadingStrategyJava SPI 配置文件

            - - -

            loadDirectory 方法

            loadDirectory 方法先通过 classLoader 获取所有的资源链接,然后再通过 loadResource 方法加载资源。

            -

            新版本中 extensionLoaderClassLoaderFirst 可以设置是否优先使用 ExtensionLoader's ClassLoader 获取资源链接。

            -
            private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
            boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
            // filename = 文件夹路径 + type 的全限定名
            String fileName = dir + type;
            try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();

            // 区别于旧版本:先从 ExtensionLoader's ClassLoader 获取资源链接,默认为 false
            if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
            urls = extensionLoaderClassLoader.getResources(fileName);
            }
            }

            // 根据文件名加载所有同名文件
            if (urls == null || !urls.hasMoreElements()) {
            if (classLoader != null) {
            urls = classLoader.getResources(fileName);
            } else {
            urls = ClassLoader.getSystemResources(fileName);
            }
            }

            if (urls != null) {
            while (urls.hasMoreElements()) {
            java.net.URL resourceURL = urls.nextElement();
            // 加载资源
            loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
            }
            }
            } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
            type + ", description file: " + fileName + ").", t);
            }
            }
            - -

            loadResource 方法

            loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于操作缓存。

            -

            新版本中 excludedPackages 可以设置将指定包内的类都排除。

            -
            private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
            java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
            try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            String clazz = null;
            // 按行读取配置内容
            while ((line = reader.readLine()) != null) {
            // 定位 # 字符,截取 # 字符之前的内容,# 字符之后的内容为注释,需要忽略
            final int ci = line.indexOf('#');
            if (ci >= 0) {
            line = line.substring(0, ci);
            }
            line = line.trim();
            if (line.length() > 0) {
            try {
            String name = null;
            // 定位 = 字符,以 = 字符为界,截取键值对
            int i = line.indexOf('=');
            if (i > 0) {
            name = line.substring(0, i).trim();
            clazz = line.substring(i + 1).trim();
            } else {
            clazz = line;
            }
            if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
            // 加载类,并通过 loadClass 进行缓存
            // 区别于旧版本:根据 excludedPackages 判断是否排除
            loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
            }
            } catch (Throwable t) {
            IllegalStateException e = new IllegalStateException(
            "Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
            ", cause: " + t.getMessage(), t);
            exceptions.put(line, e);
            }
            }
            }
            }
            } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
            type + ", class file: " + resourceURL + ") in " + resourceURL, t);
            }
            }
            - -

            loadClass 方法

            loadClass 方法设置了多个缓存,比如 cachedAdaptiveClasscachedWrapperClassescachedNamescachedClasses

            -

            新版本中 overridden 可以设置是否覆盖 cachedAdaptiveClasscachedClassesname->clazz

            -
            private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
            boolean overridden) throws NoSuchMethodException {
            // 检测 clazz 是否合法
            if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error occurred when loading extension class (interface: " +
            type + ", class line: " + clazz.getName() + "), class "
            + clazz.getName() + " is not subtype of interface.");
            }
            if (clazz.isAnnotationPresent(Adaptive.class)) {
            // 检测 clazz 是否有 Adaptive 注解,有则设置 cachedAdaptiveClass 缓存
            // 区别于旧版本:根据 overriden 判断是否可覆盖
            cacheAdaptiveClass(clazz, overridden);
            } else if (isWrapperClass(clazz)) {
            // 检测 clazz 是否是 Wrapper 类型,是则添加到 cachedWrapperClasses 缓存
            cacheWrapperClass(clazz);
            } else {
            // 检测 clazz 是否有默认的构造器方法,如果没有,则抛出异常
            clazz.getConstructor();
            if (StringUtils.isEmpty(name)) {
            // 如果 name 为空,则尝试从 Extension 注解中获取 name,或者使用小写的类名(可能截取 type 后缀)作为 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
            throw new IllegalStateException(
            "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
            }

            // 切分 name
            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {
            // 如果 clazz 有 Activate 注解,则缓存 names[0]->Activate 的映射关系
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
            // 缓存 clazz->n 的映射关系
            cacheName(clazz, n);
            // 缓存 n->clazz 的映射关系,传递到最后,终于轮到 extensionClasses
            // 区别于旧版本:根据 overriden 判断是否可覆盖
            saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
            }
            }
            }

            private void cacheAdaptiveClass(Class<?> clazz, boolean overridden) {
            if (cachedAdaptiveClass == null || overridden) {
            // 可以设置是否覆盖
            cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
            + cachedAdaptiveClass.getName()
            + ", " + clazz.getName());
            }
            }

            private void cacheWrapperClass(Class<?> clazz) {
            if (cachedWrapperClasses == null) {
            cachedWrapperClasses = new ConcurrentHashSet<>();
            }
            cachedWrapperClasses.add(clazz);
            }

            private void cacheActivateClass(Class<?> clazz, String name) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
            cachedActivates.put(name, activate);
            } else {
            // support com.alibaba.dubbo.common.extension.Activate
            com.alibaba.dubbo.common.extension.Activate oldActivate =
            clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
            if (oldActivate != null) {
            cachedActivates.put(name, oldActivate);
            }
            }
            }
            - -

            Dubbo IOC

            Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter 方法特征并满足条件,若有,则通过 objectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。

            -
            -

            Spring IOC 相比,Dubbo IOC 实现的依赖注入功能更加简单,代码也更加容易理解。

            -
            -
            private T injectExtension(T instance) {
            // 检测是否有 objectFactory
            if (objectFactory == null) {
            return instance;
            }
            try {
            // 遍历目标类的所有方法
            // 区别于旧版本:增加了对 DisbaleInject、Inject 注解的处理
            for (Method method : instance.getClass().getMethods()) {
            // 检测是否是 setter 方法
            if (!isSetter(method)) {
            continue;
            }

            // 检测是否标注 DisableInject 注解
            if (method.getAnnotation(DisableInject.class) != null) {
            continue;
            }

            // 检测参数类型是否是原始类型
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
            continue;
            }

            // 获取属性名
            String property = getSetterProperty(method);
            // 检测是否标注 Inject 注解
            Inject inject = method.getAnnotation(Inject.class);
            if (inject == null) {
            injectValue(instance, method, pt, property);
            } else {
            // 检测 Inject 是否启动、是否按照类型注入
            if (!inject.enable()) {
            continue;
            }

            if (inject.type() == Inject.InjectType.ByType) {
            injectValue(instance, method, pt, null);
            } else {
            injectValue(instance, method, pt, property);
            }
            }
            }
            } catch (Exception e) {
            logger.error(e.getMessage(), e);
            }
            return instance;
            }

            // 获取属性名
            private String getSetterProperty(Method method) {
            return method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
            }

            // public、set 开头、只有一个参数
            private boolean isSetter(Method method) {
            return method.getName().startsWith("set")
            && method.getParameterTypes().length == 1
            && Modifier.isPublic(method.getModifiers());
            }

            private void injectValue(T instance, Method method, Class<?> pt, String property) {
            try {
            // 从 ObjectFactory 中获取依赖对象
            Object object = objectFactory.getExtension(pt, property);
            if (object != null) {
            // 通过反射调用 setter 方法设置依赖
            method.invoke(instance, object);
            }
            } catch (Exception e) {
            logger.error("Failed to inject via method " + method.getName()
            + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }
            }
            - -

            objectFactoryExtensionFactory 的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理

            -
            public <T> T getExtension(Class<T> type, String name) {
            if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
            return loader.getAdaptiveExtension();
            }
            }
            return null;
            }
            - -

            参考文章

            -]]>
            - - java - dubbo - spi - -
            - - Nginx 反向代理在家庭网络中的应用 - /2023/12/01/Nginx-reverse-proxy-for-home-networks/ - 原先在使用 Cloudflare Tunnel 访问家庭网络中的服务时,是直接将域名解析到相应服务。尽管 Cloudflare 已经提供相关的请求统计和安全防护功能,部分服务自身也有访问日志,但是为了更好地监控和跟踪对外服务的使用情况,采集 Cloudlfare 统计中缺少的新,决定使用 Nginx 反向代理内部服务,统一内部服务的访问入口。简而言之就是,又折腾一些有的没的。以上修改带来的一个附加好处是在局域网内访问服务时,通过在 hosts 文件中添加域名映射,可以用更加容易记忆的域名代替 IP + port 的形式去访问。

            - - -
            -

            Cloudflare Tunnel 相较于 ZerotierOpenVPN,尽管它们三者都能避免直接开放家庭网络,但前者可以让用户直接使用域名访问到局域网中的服务,便于分享。但它的速度和延迟并不理想,还有人反馈存在网络不稳定的现象,但作为个人玩具还是够用的。有朋友使用公网服务器配合打洞软件和家庭网络中的服务器组网,实现相同目标的同时效果更好。

            -
            -

            网络结构示意图

            客户端发起请求,请求经 Cloudflare 转发到局域网中的 Tunnel。原先,Tunnel 如虚线箭头所示,直接将请求发向目标服务,如今改为发向 Nginx,由 Nginx 反向代理,发向目标服务。

            - - -

            配置

            docker-compose.yml

            NginxTunnel 还有其他内部服务应处于同一个网络中。

            -
            version: "1.0"
            services:
            nginx:
            image: nginx:1.25.3
            container_name: nginx
            ports:
            - 80:80
            volumes:
            - $PWD/etc/nginx/conf.d:/etc/nginx/conf.d
            - $PWD/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
            - $PWD/log:/var/log/nginx
            networks:
            - my-network
            restart: unless-stopped

            networks:
            my-network:
            external: true
            - -

            /etc/nginx/nginx.conf

            在最后新增了拒绝未匹配成功的域名,在 Cloudflare Tunnel 的使用场景中,其实用处不大,因为未经配置的域名也无法解析到 Nginx 服务。

            -
            user  nginx;
            worker_processes auto;

            error_log /var/log/nginx/error.log notice;
            pid /var/run/nginx.pid;


            events {
            worker_connections 1024;
            }


            http {
            include /etc/nginx/mime.types;
            default_type application/octet-stream;

            log_format main '$remote_addr - $remote_user [$time_local] "$request" '
            '$status $body_bytes_sent "$http_referer" '
            '"$http_user_agent" "$http_x_forwarded_for"';

            access_log /var/log/nginx/access.log main;

            sendfile on;
            #tcp_nopush on;

            keepalive_timeout 65;

            #gzip on;

            include /etc/nginx/conf.d/*.conf;

            #以上是默认的配置内容,新增拒绝未匹配成功的域名
            server {
            listen 80 default_server;
            server_name _;

            return 404;
            }
            }
            - -

            /etc/nginx/conf.d

            本目录下,配置 server 块。

            -
            server {
            listen 80;
            listen [::]:80;
            server_name your.domain.com;

            location / {
            #转发请求
            proxy_pass http://your-service;
            }
            }
            +

            /etc/nginx/conf.d

            本目录下,配置 server 块。

            +
            server {
            listen 80;
            listen [::]:80;
            server_name your.domain.com;

            location / {
            #转发请求
            proxy_pass http://your-service;
            }
            }

            正向代理和反向代理

            代理(正向代理)

            代理(Proxy)也称为网络代理,是一种特殊的网络服务,允许一个终端通过这个服务与另一个终端进行非直接的连接。一般认为代理服务有利于保障网络终端的隐私或安全,在一定程度上能够阻止网络攻击。

            @@ -3445,138 +3460,6 @@ reverse proxy
            - - Spring 中的循环依赖 - /2023/11/22/circular-dependencies-in-Spring/ - Spring 中的循环依赖是一个“大名鼎鼎”的问题,本文从原始的问题出发分析应当如何正确地看待和处理循环依赖现象,同时也会回归到源码详细介绍 Spring 的具体处理过程,并在最后给出笔者的个人思考。

            - - -

            循环依赖的介绍和讨论

            什么是循环依赖?

            Bean A 依赖另一个 Bean BBean B 也依赖了 Bean A,我们就称之为循环依赖:

            -
            Bean A -> Bean B -> Bean A
            - -

            首先,我们应该将循环依赖和 “Spring 中的循环依赖问题”分开看待。循环依赖是一个正常的现象,一个 employee 依赖他的 department,department 拥有许多 employee。先实例化 employee 后实例化 department,然后先后为它们设置依赖,这样并不会发生什么问题。

            -

            Spring 中的循环依赖问题

            常规的循环依赖问题

            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 的解决方案也是如此,当然其中会有很多值得深思的细节。回顾循环依赖问题的解决思路,你会发现:

            -
              -
            1. 循环依赖本身是普通的,一个手动可解决的问题
            2. -
            3. Spring 依赖注入时,虽然 Bean B 依赖的 Bean A 尚未初始化完毕,但是已经实例化,可以用来赋值
            4. -
            5. Spring AOP 中,既然 Bean B 依赖的 Bean A 需要是 A 对象的代理,那么就在那时候创建代理,用来赋值即可
            6. -
            -

            流程图和测试用例

            在开始之前我们先放一张循环引用的处理流程图,用于在后续分析过程中进行对照。

            -
            - -

            以下是测试用例的代码:

            -
              -
            • CircularA
              public class CircularA {
              private CircularB circularB;
              public CircularB getCircularB() {
              return circularB;
              }
              public void setCircularB(CircularB circularB) {
              this.circularB = circularB;
              }
              }
            • -
            • CircularB
              public class CircularB {
              private CircularA circularA;
              public CircularA getCircularA() {
              return circularA;
              }
              public void setCircularA(CircularA circularA) {
              this.circularA = circularA;
              }
              }
            • -
            • circular-reference-test.xml
              <beans>
              <bean id="circularA" class="com.moralok.bean.CircularA">
              <property name="circularB" ref="circularB"/>
              </bean>
              <bean id="circularB" class="com.moralok.bean.CircularB">
              <property name="circularA" ref="circularA"/>
              </bean>
              </beans>
            • -
            • 测试类
              public class CircularReferenceTest {
              @Test
              public void testRegular() {
              ApplicationContext applicationContext = new ClassPathXmlApplicationContext("circular-reference-test.xml");
              CircularA circularA = (CircularA) applicationContext.getBean("circularA");
              }
              }
            • -
            -

            第一次获取 circularA

            调用 doGetBean(circularA) 方法第一次获取:

            -
              -
            1. 从缓存中获取 circularA(先不看方法内的具体代码,在第一次进入该方法时,必定返回 null
            2. -
            3. 因缓存中不存在,就创建 circularA
            4. -
            -
            protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {
            // 尝试从缓存中获取 circularA,第一次结果必定为 null
            Object sharedInstance = getSingleton(beanName);
            // ...
            // 再次从缓存中获取 circularA(双重校验),如果为 null,就创建
            sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            try {
            return createBean(beanName, mbd, args);
            }
            }
            });
            // ...
            return (T) bean;
            }
            - -

            标记 bean 是否正在创建中

            在真正创建 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) {
            // ...
            // 标记为正在创建中
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            // ...
            try {
            // 创建 Bean
            singletonObject = singletonFactory.getObject();
            newSingleton = true;
            }
            // ...
            // 从正在创建中的集合移除
            afterSingletonCreation(beanName);
            if (newSingleton) {
            addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
            }
            - -

            创建 circularA

            createBean 方法被包装在 ObjectFactory 中。创建的工作分为两个部分:

            -
              -
            1. 实例化 circularA
            2. -
            3. 初始化 circularA
                -
              • circularA 进行依赖注入时:getBean(circularB)
              • -
              -
            4. -
            -

            很重要的是,在实例化 circularA 之后,尚未进行初始化工作之前,如果 circularA 满足早期暴露的条件,将会被包装为 ObjectFactory 缓存到 singletonFactory(三级缓存) 中。

            -

            值得注意的是:

            -
              -
            • 如果 circularA 最终不需要早期暴露,那么这个 ObjectFactory 是会被直接抛弃的
            • -
            • 如果 circularA 需要早期暴露,即它依赖的 circularB 同时依赖它,到时候将调用 getEarlyBeanReference 方法获得 circularA早期 Bean 引用
            • -
            -
            -

            刚开始看 ObjectFactory 匿名类的用法可能有点不适应,可以多读几次,帮助理解 getObjectgetEarlyBeanReference 的语义。

            -
            -
            protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
            throws BeanCreationException {
            // ...
            // 实例化 bean
            instanceWrapper = createBeanInstance(beanName, mbd, args);
            // ...
            // 如果单例允许提前暴露的话,就将实例包装为 ObjectFactory 保存在 map singletonFactories 中
            boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
            if (earlySingletonExposure) {
            addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
            }
            });
            }
            // 初始化 bean,对 bean 的属性进行依赖注入
            Object exposedObject = bean;
            try {
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
            exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
            }
            // ...
            return exposedObject;
            }
            - -

            依赖注入时获取 circularB

            填充 Bean 属性

            填充 Bean 属性的 populateBean 方法很复杂,我们只关注对 circularA 的依赖注入将间接地调用 getBean(circularB) 进入获取 circularB 的过程。

            -
            protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
            // ...
            // 应用属性值
            applyPropertyValues(beanName, mbd, bw, pvs);
            }

            protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
            // ...
            // 如果有必要解析 value
            Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
            // ...
            bw.setPropertyValues(new MutablePropertyValues(deepCopy));
            // ...
            }

            public Object resolveValueIfNecessary(Object argName, Object value) {
            // ...
            // 如果是 RuntimeBeanReference 类型,就解析引用
            if (value instanceof RuntimeBeanReference) {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            return resolveReference(argName, ref);
            }
            // ...
            }

            private Object resolveReference(Object argName, RuntimeBeanReference ref) {
            // ...
            // 解析引用将调用 getBean
            Object bean = this.beanFactory.getBean(refName);
            this.beanFactory.registerDependentBean(refName, this.beanName);
            return bean;
            // ...
            }
            - -

            获取 circularB

            AbstractBeanFactory#doGetBean(circularB) 获取 circularB 将经过和 circularA 一样的流程,进入 populateBean(circularB) 方法进行依赖注入,进而再次去获取 circularA

            -

            第二次获取 circularA

            调用 doGetBean(circularA) 方法第二次获取 circularA 时,仍然先尝试从缓存中获取,这次将从缓存中得到先前创建的 circularA

            -
            protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {
            // ...
            // 尝试从缓存中获取 circularA,结果不再为 null
            Object sharedInstance = getSingleton(beanName);
            // ...
            }
            - -

            这个 getSingleton 方法正是在第一次获取 circularA 时我们跳过没有查看的方法。方法中代码的逻辑并不复杂,但是要理解为什么这么做,却需要回过头来反复品味和思考。这里也是经常被拿来说的“三级缓存”问题的核心。

            -

            从缓存中获取 circularA

              -
            1. 先从 singletonObjects(一级缓存) 获取 circularA,不存在
            2. -
            3. 判断 circularA 是正在创建中,从 earlySingletonObjects(二级缓存) 获取,仍然不存在
            4. -
            5. allowEarlyReference 为真,从 singletonFactories(三级缓存) 获取 ObjectFactory
            6. -
            7. 调用 getObject 间接调用 getEarlyBeanReference 获得早期 Bean 引用
            8. -
            -
            protected Object getSingleton(String beanName, boolean allowEarlyReference) {
            Object singletonObject = this.singletonObjects.get(beanName);
            // 如果 singletonObjects 中不存在,且 bean 正在创建过程中(满足)
            if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
            // 从 earlySingletonObjects 获取(不存在)
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
            // 如果 earlySingletonObjects 中不存在,且允许早期引用,就从 singletonFactories 中获取
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
            // 间接调用 getEarlyBeanReference
            singletonObject = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
            }
            }
            }
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
            }
            - -

            获取早期 Bean 引用

            请注意这次在调用 getObject 方法时,并不是直接返回 Bean 的实例,而是间接调用 getEarlyBeanReference 方法,顾名思义是获取早期 Bean 引用。处理逻辑是如果存在 SmartInstantiationAwareBeanPostProcessor,将使用这些后处理器处理以获得早期 Bean 引用。

            -
            protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
            Object exposedObject = bean;
            if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            // 如果存在 InstantiationAwareBeanPostProcessors
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            // 如果 bp 是 SmartInstantiationAwareBeanPostProcessor 类型
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            // 尝试创建代理
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            if (exposedObject == null) {
            return null;
            }
            }
            }
            }
            return exposedObject;
            }
            - -
            -

            关于创建代理的分析请参考Spring AOP 如何创建代理 beans

            -
            -

            通过后处理器的 getEarlyBeanReference 方法获取早期 Bean 引用时,可能返回的就是 circularA 对象,但是如果 circularA 需要创建代理,就会在这时候为它创建代理,而在之后 BeanPostProcessor 处理时就不会再创建代理了。

            -

            AbstractAutoProxyCreator 为例,它是自动代理创建者的抽象类,同时实现了 SmartInstantiationAwareBeanPostProcessorBeanPostProcessor 接口。

            -
            public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 添加到早期代理引用的缓存中,用于后续在常规创建代理阶段判断是否需要创建代理
            this.earlyProxyReferences.add(cacheKey);
            }
            // 如有必要,包装为代理
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            - -

            如果已经在获取 circularA 的早期引用时就将其包装为代理,则不再创建代理。

            -
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean != null) {
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            // 判断早期代理引用中是否存在
            if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 如有必要,包装为代理
            return wrapIfNecessary(bean, beanName, cacheKey);
            }
            }
            return bean;
            }
            - -

            再思 Spring bean 的三级缓存

            三级缓存 or 三个缓存

            著名的“三级缓存”,实际上就是三个存放 Beanmap

            -
              -
            • singletonObjects
            • -
            • earlySingletonObjects
            • -
            • singletonFactories
            • -
            -

            在很多网上的资料中,都称 Spring 通过使用三级缓存的设计解决了循环引用问题。同时我也看到有人反思,这样翻译对学习者造成了很大的困扰,代码中并没有多级 cache 的意味,称之为“三个缓存”比“三级缓存”更合理也更容易理解。三个存放 Beanmap 事实上是相互独立的,甚至它们是互斥的,一个 Bean 在同一时间最多只能存在于其中一个 map

            -

            对我个人而言,我对反对者的观点深有同感,如果我没有看过面经,即使我熟读并理解代码,我可能都无法回答 Spring 中的三级缓存是什么。甚至我会被三级缓存这个名词所震慑,在了解它之前在心里放大它的复杂性。
            但是在不断阅读的过程中(可能也有已有记忆的加持),我也会感受到称之为“三级缓存”的合理性。这里的分级含义更多体现的是 Bean 的“晋升”过程。

            -

            缓存中的添加和删除

            网上很多资料在讨论 Bean 在缓存中的添加和删除时,大多一笔带过,并没有谈到细节。但是 Bean 并不是在这三个缓存中依次晋级,甚至有时候,添加和移除的都不是一个对象,比如三级缓存中的 ObjectFactory 可能被直接抛弃。

            -
            protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
            throws BeanCreationException {
            // 实例化
            instanceWrapper = createBeanInstance(beanName, mbd, args);

            // 单例+允许循环引用+当前正在被创建=可能需要提前暴露
            boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
            if (earlySingletonExposure) {
            addSingletonFactory(beanName, new ObjectFactory<Object>() {
            @Override
            public Object getObject() throws BeansException {
            return getEarlyBeanReference(beanName, mbd, bean);
            }
            });
            }

            Object exposedObject = bean;
            try {
            // 这里面会进行依赖注入
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
            // 这里面会尝试创建代理
            exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
            }

            if (earlySingletonExposure) {
            // 如果允许早期暴露,尝试获取早期 Bean 引用
            Object earlySingletonReference = getSingleton(beanName, false);
            // 如果为 null 说明没有早期暴露,返回的其实还是最初的 exposedObject
            // 三级缓存里的 ObjectFactory 完全没用上,会在 exposedObject 添加到一级缓存时直接删除
            if (earlySingletonReference != null) {
            // 如果不为 null,说明确实早期暴露过
            if (exposedObject == bean) {
            // 如果早期暴露过,常规情况下,exposedObject 不会再创建代理,应 == bean
            // 如果没有代理,exposedObject == bean == earlySingletonReference
            // 如果创建过代理,earlySingletonReference 才是包装过的代理
            exposedObject = earlySingletonReference;
            }
            else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            // 如果 exposedObject 在 initializeBean 中再次被创建代理
            // 但是存在 Bean 依赖了这个 Bean(由于拿到的是早期引用),它们拿到的和最终的是不同的对象
            // 如果不允许尽管会被包装仍然注入原始类型,就需要抛出异常
            String[] dependentBeans = getDependentBeans(beanName);
            Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
            for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
            actualDependentBeans.add(dependentBean);
            }
            }
            if (!actualDependentBeans.isEmpty()) {
            // 抛异常
            }
            }
            }
            }
            return exposedObject;
            }
            - -
              -
            • 在无需早期暴露的情况下,二级缓存没有用到,虽然三级缓存中保存了一个 ObjectFactory,但是也是没有用到的。Bean 直接保存到一级缓存中。
            • -
            • 在需要早期暴露但无需代理的情况下,尽管获取早期引用后保存在二级缓存中,以供重复使用,但是二级缓存中和原始的 Bean 仍然是同一个对象,Bean 仍然是直接保存到一级缓存,再删掉二级缓存。
            • -
            • 在需要早期暴露和需要代理的情况下,二级缓存中保存的是代理对象,需要从二级缓存中获取再保存到一级缓存中,然后再删除二级缓存。
            • -
            - - -

            必须要三个缓存吗

            网上有很多资料在分析为什么需要三个缓存,才能解决在需要创建代理的情况下发生的循环依赖问题。但是个人觉得有些分析缺乏逻辑,也有点违和感。将当前的解决方案套到只有两个缓存的情况下去分析不太合理,就像你把四轮机动车卸掉一个轮子,说机动车必须要四个轮子才可以,不然不平衡,事实上三个轮子的机动车设计是存在且可行的。

            -

            在分析两个缓存如何解决在需要创建代理的情况下发生的循环依赖问题时,应该抛开现有的处理逻辑,回归本质问题:既然 circularA 需要创建代理,如果 circularA 依赖的 circularB 也依赖了 circular A,在为它获取依赖 circularA 时立即创建代理即可。
            一个 map 必须用于存放完成品,另一个 map 用于存放半成品。创建的代理作为升级版的半成品,完全可以覆盖原始的半成品继续存放在第二个 map 中。为了避免重复创建代理,只要能够标识半成品是已经经过代理包装的即可。BeanDefinitionBean 自身、创建代理的地方,都有能力实现标识一个 Bean 的半成品是否经过包装,最不济使用一个 map 存放标识(但是这也就等同于使用三个 map 了)。你甚至可以将半成品 circularA 直接尝试包装成代理再存放入半成品 map 中,这个方案本质上是将创建代理的步骤从初始化 Bean 中分离到初始化 Bean 之前。

            -

            综上,使用两个 map 解决在技术上是没有问题的,很多分析中考虑的问题相当于把 Spring 现有的处理逻辑当成枷锁限制了自己。既然你都在问不这么打地基可不可以,我难道不得考虑挪一挪上面的砖墙吗?当然我不能保证这么设计不会破坏 Spring 现有全部功能的兼容性和扩展性,但是这并不是代理为循环依赖引入的问题。

            -]]>
            - - java - spring - -
            使用 logrotate 滚动 Docker 容器内的 Nginx 的日志 /2023/12/02/rotating-nginx-logs-in-docker-container-with-logrotate/ @@ -3769,16 +3652,199 @@ - Spring @Configuration 注解的源码分析 - /2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/ - Configuration 注解是 Spring 中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean 注解将创建的 bean 交给 Spring 容器管理。神奇的是,被 Bean 注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug 时注意过配置类的实例,你会发现配置类的 Class 名称中携带 EnhancerBySpringCGLIB。本文将从源码角度,分析 Configuration 注解是如何工作的。

            + Dubbo SPI 自适应拓展的工作原理 + /2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/ + 直接展示一个具体的 Dubbo SPI 自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo 的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI 自适应拓展的相关源码和工作原理。

            -

            测试用例

              -
            • 配置类
              @Configuration
              public class BeanConfig {

              @Bean
              public Person lisi() {
              return new Person("lisi", 20);
              }

              @Bean(value = "customName")
              public Person person() {
              // 测试 lisi() 在配置类拥有注解 @Configuration 时只会真正执行一次
              lisi();
              return new Person("wangwu", 30);
              }
              }
            • -
            • 测试类
              @Test
              public void annotationConfigTest() {
              ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);
              BeanConfig beanConfig = (BeanConfig) ac.getBean("beanConfig");
              // 即使通过 beanConfig 调用,也不会执行第二次
              Person lisi = beanConfig.lisi();
              }
            • -
            -

            解析配置类

            什么是配置类?

            通常情况下,我们称被 Configuration 注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。

            +
            +

            站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。

            +
            +

            原理

            Dubbo 中,很多拓展都是通过 SPI 机制动态加载的,比如 ProtocolClusterLoadBalance 等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。

            +

            示例

            定义一个接口 Animal

            +
            public interface Animal {
            void bark();
            }
            + +

            定义两个实现类 DogCat

            +
            public class Dog implements Animal {
            @Override
            public void bark() {
            System.out.println("Dog bark...");
            }
            }

            public class Cat implements Animal {
            @Override
            public void bark() {
            System.out.println("Cat bark...");
            }
            }
            + +

            在运行时根据参数动态地加载拓展。

            +
            public void bark(String type) {
            if (type == null) {
            throw new IllegalArgumentException("type == null");
            }
            // 通过 SPI 动态地加载具体的 Animal
            Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
            // 调用目标方法
            animal.bark();
            }
            + +

            改进

            是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:

            +
            Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
            animal.bark();

            WeelMaker weelMaker = ExtensionLoader.getExtensionLoader(WeelMaker.class).getExtension(weelMakerName);
            weelMaker.makeWeel();

            LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invocation.getLoadBalanceType());
            loadBalance.select();

            // ...
            + +

            这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter) 这样重复的代码;引入了 ExtensionLoader 这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo 和 动物 Animal 举例。

            +

            在非动态加载情况下,我们可能会这样写:

            +
            public class Zoo {
            private List<Animal> animals;

            public void bark(String type) {
            for (Animal animal : animals) {
            if (type.equals(animal.name)) {
            animal.bark();
            }
            }
            }
            }
            + +

            在动态加载情况下,我们可能会这样写。在这种情况下,Zoo 没有合适的方式直接持有 Animal,而是通过 ExtensionLoader 间接地持有。

            +
            public class Zoo {
            private ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);

            public void bark(String type) {
            Animal animal = extensionLoader.getExtension(type);
            animal.bark();
            }
            }
            + +

            我们更想要以下这种直接持有 Animal 的方式,在运行时 animal 可以是 Dog,也可以是 Cat,还可以是其他的动物。

            +
            public class Zoo {
            private Animal animal;

            public void bark(String type) {
            animal.bark();
            }
            }
            + +

            Dubbo 采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal 的自适应拓展,就像下面这样:

            +
            public class AdaptiveAnimal implements Animal {
            public void bark(String type) {
            if (type == null) {
            throw new IllegalArgumentException("type == null");
            }

            Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
            animal.bark();
            }
            }

            Animal animal = new AdaptiveAnimal();
            animal.bark(type);
            + +

            当然,我们不希望需要手动地为每一个拓展编写 Adaptive 代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark 方法的参数列表中新增了 type 类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo 的另一个设计规避了这一缺点,Dubbo公共契约中提到:所有扩展点参数都包含 URL 参数,URL 作为上下文信息贯穿整个扩展点设计体系。因此围绕着 DubboURL 为中心的拓展体系,你很难设计出 Animal.bark(URL url) 这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo 并未完全抛弃手工编写自适应拓展的方式,而是予以保留。

            +

            手工编码的自适应拓展

            在在 Dubbo 中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL 作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory 的自适应拓展,当你调用它的 getExtension 方法时,它就是将工作全权委托给 factory.getExtension(type, name) 完成的,而 factories 在创建 AdaptiveExtensionFactory 时就已经获取了。

            +
            @Adaptive
            public class AdaptiveExtensionFactory implements ExtensionFactory {

            private final List<ExtensionFactory> factories;

            public AdaptiveExtensionFactory() {
            // 获取 ExtensionFactory 的 ExtensionLoader
            ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
            List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
            // 获取全部支持的(不包含自适应拓展)拓展名称,依次获取拓展加入 factories
            for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
            }
            factories = Collections.unmodifiableList(list);
            }

            @Override
            public <T> T getExtension(Class<T> type, String name) {
            for (ExtensionFactory factory : factories) {
            // 委托给其他 ExtensionFactory 拓展获取,比如 SpiExtensionFactory
            T extension = factory.getExtension(type, name);
            if (extension != null) {
            return extension;
            }
            }
            return null;
            }

            }
            + +

            至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。

            +

            源码分析

            Adaptive 注解

            Adaptive 注解是一个与自适应拓展息息相关的注解,该定义如下:

            +
            @Documented
            @Retention(RetentionPolicy.RUNTIME)
            @Target({ElementType.TYPE, ElementType.METHOD})
            public @interface Adaptive {
            String[] value() default {};
            }
            + +

            根据 Target 注解的 value 可知,Adaptive 注解可标注在类或者方法上。当 Adaptive 注解标注在类上时,Dubbo 不会为该类生成代理类。当 Adaptive 注解标注在接口方法上时,Dubbo 则会为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解标注,分别是 AdaptiveCompilerAdaptiveExtensionFactory。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive 注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。

            +

            获取自适应拓展

            获取自适应拓展的入口方法是 getAdaptiveExtension,使用 getOrCreate 的模式获取。

            +
            public T getAdaptiveExtension() {
            // 双重检查
            // 从缓存中获取自适应拓展
            Object instance = cachedAdaptiveInstance.get();
            if (instance == null) {
            // 创建自适应拓展失败的结果也会被缓存,避免重复尝试
            if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
            createAdaptiveInstanceError.toString(),
            createAdaptiveInstanceError);
            }

            synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
            try {
            // 创建自适应拓展
            instance = createAdaptiveExtension();
            // 将自适应拓展设置到缓存中
            cachedAdaptiveInstance.set(instance);
            } catch (Throwable t) {
            createAdaptiveInstanceError = t;
            throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
            }
            }
            }
            }

            return (T) instance;
            }
            + +

            创建自适应拓展

            当缓存为空时,就会通过 createAdaptiveExtension 方法创建。方法包含以下三个处理逻辑:

            +
              +
            1. 调用 getAdaptiveExtensionClass 方法获取自适应拓展的 Class 对象。
            2. +
            3. 通过反射进行实例化。
            4. +
            5. 调用 injectExtension 方法对拓展实例进行依赖注入。
            6. +
            +
            +

            手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展

            +
            +
            private T createAdaptiveExtension() {
            try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
            } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
            }
            }
            + +

            获取自适应拓展类

            获取自适应拓展类的 getAdaptiveExtensionClass 方法包含以下三个处理逻辑:

            +
              +
            1. 通过 getExtensionClasses 方法获取所有拓展类。
            2. +
            3. 检查缓存 cachedAdaptiveClass,如果不为 null,则返回缓存。
            4. +
            5. 如果缓存为 null,则调用 createAdaptiveExtensionClass 创建自适应拓展类(代理类)。
            6. +
            +

            Dubbo SPI 的工作原理中我们分析过 getExtensionClasses 方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive 注解标注了,那么该类就会被赋值给 cachedAdaptiveClass 变量。“原理”部分介绍的 AdaptiveExtensionFactory 就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive 注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。

            +
            private Class<?> getAdaptiveExtensionClass() {
            getExtensionClasses();
            if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
            }
            return cachedAdaptiveClass = createAdaptiveExtensionClass();
            }
            + +
            +

            到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用 getOrCreate 的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。

            +
            +

            生成自适应拓展类

            生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。

            +
              +
            1. 通过拼接字符串得到代理类的源码。
            2. +
            3. 使用编译器编译得到 Class 对象。
            4. +
            +
            +

            在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。

            +
            +
            private Class<?> createAdaptiveExtensionClass() {
            // 区别于旧版本:新版本抽象出一个 AdaptiveClassCodeGenerator 用于生成代码
            String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
            ClassLoader classLoader = findClassLoader();
            // 获取编译器拓展
            org.apache.dubbo.common.compiler.Compiler compiler =
            ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
            // 编译代码,生成 Class 对象
            return compiler.compile(code, classLoader);
            }
            + +

            为了更直观地了解代码生成的效果及其实现的功能,以 Protocol 为例,生成的完整代码(已经经过格式化)展示如下

            +
            package org.apache.dubbo.rpc;

            import org.apache.dubbo.common.extension.ExtensionLoader;

            public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
            public void destroy() {
            throw new UnsupportedOperationException(
            "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
            }

            public int getDefaultPort() {
            throw new UnsupportedOperationException(
            "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
            }

            public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1)
            throws org.apache.dubbo.rpc.RpcException {
            if (arg1 == null)
            throw new IllegalArgumentException("url == null");
            org.apache.dubbo.common.URL url = arg1;
            String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
            if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
            + url.toString() + ") use keys([protocol])");
            org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
            .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
            return extension.refer(arg0, arg1);
            }

            public java.util.List getServers() {
            throw new UnsupportedOperationException(
            "The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
            }

            public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0)
            throws org.apache.dubbo.rpc.RpcException {
            if (arg0 == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
            if (arg0.getUrl() == null)
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
            org.apache.dubbo.common.URL url = arg0.getUrl();
            String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
            if (extName == null)
            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
            + url.toString() + ") use keys([protocol])");
            org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
            .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
            return extension.export(arg0);
            }
            }
            + +

            生成的代理类需完成以下功能:

            +
              +
            1. adaptive 方法,直接抛出异常。
            2. +
            3. adaptive 方法:
                +
              • 准备工作:在参数判空校验之后,从中获取到 URL 对象,结合 URL 对象和默认拓展名得到最终的拓展名 extName
              • +
              • 核心功能:先获取拓展的 ExtensionLoader,再根据拓展名 extName 获取拓展,最后调用拓展的同名方法。
              • +
              +
            4. +
            +

            以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo 会直接或间接地从参数列表中查找 URL 对象,所谓直接就是 URL 对象直接在参数列表中,所谓间接就是 URL 对象是其中一个参数的属性。在得到 URL 对象后,Dubbo 会尝试以 Adaptive 注解的 valuekey,从 URL 中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName。实际的实现更加复杂,需要耐心阅读和测试。

            +

            自适应拓展类代码生成器

            新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 typedefaultExtName,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。

            +
            public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
            this.type = type;
            this.defaultExtName = defaultExtName;
            }

            public String generate() {
            // 检测是否至少存在一个方法标注了 Adaptive 注解
            if (!hasAdaptiveMethod()) {
            throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
            }

            // 区别于旧版本:抽象为几个命名含义清晰的方法,提升了可读性
            // 生成类:包名、导入、类声明
            StringBuilder code = new StringBuilder();
            code.append(generatePackageInfo());
            code.append(generateImports());
            code.append(generateClassDeclaration());

            // 生成方法
            Method[] methods = type.getMethods();
            for (Method method : methods) {
            code.append(generateMethod(method));
            }
            code.append("}");

            if (logger.isDebugEnabled()) {
            logger.debug(code.toString());
            }
            return code.toString();
            }
            + +

            检测 Adaptive 注解

            在生成代理类源码之前,generate 方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive 注解,若不满足,就会抛出异常。

            +
            +

            流式编程使用得当的话很有可读性啊。

            +
            +
            private boolean hasAdaptiveMethod() {
            return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
            }
            + +

            生成类

            生成代理类源码的顺序和普通 Java 类文件中内容的顺序一致:

            +
              +
            • package
            • +
            • import
            • +
            • 类声明
            • +
            +

            先忽略“生成方法”的部分,以 DubboProtocol 拓展为例,生成的代码如下:

            +
            package org.apache.dubbo.rpc;
            import org.apache.dubbo.common.extension.ExtensionLoader;
            public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
            // 省略方法代码
            }
            + +

            生成方法

            生成方法的过程同样被抽象为几个命名含义清晰的方法,包含以下五个部分:

            +
              +
            • 返回值
            • +
            • 方法名
            • +
            • 方法内容
            • +
            • 方法参数
            • +
            • 方法抛出的异常
            • +
            +
            private String generateMethod(Method method) {
            String methodReturnType = method.getReturnType().getCanonicalName();
            String methodName = method.getName();
            String methodContent = generateMethodContent(method);
            String methodArgs = generateMethodArguments(method);
            String methodThrows = generateMethodThrows(method);
            return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
            }
            + +

            除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive 注解标注。

            +
            private String generateMethodContent(Method method) {
            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            // 检测方法是否被 Adaptive 注解标注
            if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
            } else {
            // ...
            }
            return code.toString();
            }
            + +
            无 Adaptive 注解标注的方法

            对于无 Adaptive 注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。

            +
            private String generateUnsupported(Method method) {
            return String.format(CODE_UNSUPPORTED, method, type.getName());
            }
            + +

            Protocol 接口的 destroy 方法为例,生成的内容如下

            +
            throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
            + +
            有 Adaptive 注解标注的方法

            对于有 Adaptive 注解标注的方法,

            +
            // 查找 URL 类型的参数
            int urlTypeIndex = getUrlTypeIndex(method);
            if (urlTypeIndex != -1) {
            // 生成 URL 判空检查和赋值的代码
            code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
            // 如果参数中没有直接出现 URL 类型,生成间接情况下的 URL 判空和赋值代码
            code.append(generateUrlAssignmentIndirectly(method));
            }
            // 获取方法的 Adaptive 注解的 value
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
            // 检测是否有 Invocation 类型的参数
            boolean hasInvocation = hasInvocationArgument(method);
            // 生成 Invocation 判空检查代码
            code.append(generateInvocationArgumentNullCheck(method));
            // 生成拓展名赋值代码
            code.append(generateExtNameAssignment(value, hasInvocation));
            // 生成拓展名判空检查代码
            code.append(generateExtNameNullCheck(value));
            // 生成获取拓展和赋值代码
            code.append(generateExtensionAssignment());
            // 生成调用和返回代码
            code.append(generateReturnAndInvocation(method));
            + +
            查找 URL 类型的参数

            直接从方法的参数类型列表中查找第一个 URL 类型的参数,返回其索引。

            +
            private int getUrlTypeIndex(Method method) {
            int urlTypeIndex = -1;
            // 遍历方法的参数类型列表
            Class<?>[] pts = method.getParameterTypes();
            for (int i = 0; i < pts.length; ++i) {
            // 查找第一个 URL 类型的参数
            if (pts[i].equals(URL.class)) {
            urlTypeIndex = i;
            break;
            }
            }
            return urlTypeIndex;
            }

            // 生成 URL 参数判空检查和赋值代码
            private String generateUrlNullCheck(int index) {
            return String.format(CODE_URL_NULL_CHECK, index, URL.class.getName(), index);
            }
            + +

            间接从方法的参数类型列表中,查找 URL 类型的参数,并生成判空检查和赋值代码。

            +
            private String generateUrlAssignmentIndirectly(Method method) {
            Class<?>[] pts = method.getParameterTypes();

            Map<String, Integer> getterReturnUrl = new HashMap<>();
            // 遍历方法的参数类型列表
            for (int i = 0; i < pts.length; ++i) {
            // 遍历某一个参数类型的全部方法,查找可以返回 URL 类型的 “getter” 方法
            for (Method m : pts[i].getMethods()) {
            String name = m.getName();
            // 1. 方法名以 get 开头,或者方法名大于 3 个字符
            // 2. 方法的访问权限为 public
            // 3. 非静态方法
            // 4. 方法参数数量为 0
            // 5. 方法返回值类型为 URL
            if ((name.startsWith("get") || name.length() > 3)
            && Modifier.isPublic(m.getModifiers())
            && !Modifier.isStatic(m.getModifiers())
            && m.getParameterTypes().length == 0
            && m.getReturnType() == URL.class) {
            // 保存方法名->索引的映射
            getterReturnUrl.put(name, i);
            }
            }
            }

            if (getterReturnUrl.size() <= 0) {
            // 如果没有找到 “getter” 方法,抛出异常
            throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
            + ": not found url parameter or url attribute in parameters of method " + method.getName());
            }

            // 优先选择方法名为 getUrl 的方法,如果没有则选第一个
            Integer index = getterReturnUrl.get("getUrl");
            if (index != null) {
            return generateGetUrlNullCheck(index, pts[index], "getUrl");
            } else {
            Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
            return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
            }
            }

            // 生成 URL 参数判空检查和赋值代码
            private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
            StringBuilder code = new StringBuilder();
            code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");\n",
            index, type.getName()));
            code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");\n",
            index, method, type.getName(), method));

            code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
            return code.toString();
            }
            + +

            Protocolreferexport 方法为例,生成的内容如下

            +
            // refer
            if (arg1 == null) throw new IllegalArgumentException("url == null");
            org.apache.dubbo.common.URL url = arg1;
            // export
            if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
            if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
            org.apache.dubbo.common.URL url = arg0.getUrl();
            + +
            获取 Adaptive 注解的 value
            private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
            String[] value = adaptiveAnnotation.value();
            // 如果 value 为空,使用类名生成 value
            // 效果:LoadBalance -> load.balance
            if (value.length == 0) {
            String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
            value = new String[]{splitName};
            }
            return value;
            }
            + +
            检测 Invocation 类型的参数

            检测是否有 Invocation 类型的参数,并生成判空检查代码和赋值代码。从 Invocation 可以获得 methodName

            +
            private boolean hasInvocationArgument(Method method) {
            Class<?>[] pts = method.getParameterTypes();
            return Arrays.stream(pts).anyMatch(p -> CLASSNAME_INVOCATION.equals(p.getName()));
            }

            private String generateInvocationArgumentNullCheck(Method method) {
            Class<?>[] pts = method.getParameterTypes();
            return IntStream.range(0, pts.length).filter(i -> CLASSNAME_INVOCATION.equals(pts[i].getName()))
            .mapToObj(i -> String.format(CODE_INVOCATION_ARGUMENT_NULL_CHECK, i, i))
            .findFirst().orElse("");
            }
            + +

            LoadBalanceselect 方法为例,生成的内容如下:

            +
            if (arg2 == null) throw new IllegalArgumentException("invocation == null");
            String methodName = arg2.getMethodName();
            + +
            获取拓展名

            本方法用于根据 SPIAdaptive 注解的 value 生成“获取拓展名”的代码,同时生成逻辑还受 Invocation 影响,因此相对复杂。总结的规则如下:

            +
              +
            1. 正常情况下,使用 url.getParameter(value[i]) 获取
            2. +
            3. 如果默认拓展名非空,使用 url.getParameter(value[i], defaultExtName) 获取
            4. +
            5. 如果存在 Invocation,不论默认拓展名是否为空,总是使用 url.getMethodParameter(methodName, value[i], defaultExtName) 获取
            6. +
            7. 因为 protocol 是 url 的一部分,所以可以直接通过 getProtocol 获取。是否使用默认拓展名的方式就退化为原始的三元表达式。
            8. +
            +
            private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
            // TODO: refactor it
            String getNameCode = null;
            // 逆序遍历 value(Adaptive 的 value)
            for (int i = value.length - 1; i >= 0; --i) {
            // 当 i 为最后一个元素的索引(因为是逆序遍历,第一轮就进入本分支)
            if (i == value.length - 1) {
            // 默认拓展名非空
            if (null != defaultExtName) {
            // protocol 是 url 的一部分,可以通过 getProtocol 方法获取,其他的则必须从 URL 参数中获取
            if (!"protocol".equals(value[i])) {
            if (hasInvocation) {
            // 如果有 Invocation,则使用 url.getMethodParameter 获取
            // url.getMethodParameter(methodName, value[i], defaultExtName)
            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
            } else {
            // url.getParameter(value[i], defaultExtName)
            getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
            }
            } else {
            // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
            getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
            }
            // 默认拓展名为空
            } else {
            if (!"protocol".equals(value[i])) {
            if (hasInvocation) {
            // url.getMethodParameter(methodName, value[i], defaultExtName)
            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
            } else {
            // url.getParameter(value[i])
            getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
            }
            } else {
            // url.getProtocol()
            getNameCode = "url.getProtocol()";
            }
            }
            } else {
            if (!"protocol".equals(value[i])) {
            if (hasInvocation) {
            // url.getMethodParameter(methodName, value[i], defaultExtName)
            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
            } else {
            // url.getParameter(value[i], getNameCode)
            getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
            }
            } else {
            // url.getProtocol() == null ? "dubbo" : url.getProtocol()
            getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
            }
            }
            }
            return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
            }
            + +
            加载拓展
            private String generateExtensionAssignment() {
            return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
            }
            + +

            Protocol 接口的 refer 方法为例,生成的内容如下:

            +
            org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
            + +
            调用与返回

            生成方法调用语句,如有必要,返回结果。

            +
            private String generateReturnAndInvocation(Method method) {
            String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

            String args = IntStream.range(0, method.getParameters().length)
            .mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
            .collect(Collectors.joining(", "));

            return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
            }
            + +

            Protocol 接口的 refer 方法为例,生成的内容如下:

            +
            return extension.refer(arg0, arg1);
            + +
            +

            新版本通过提炼方法、使用流式编程和使用 String.format() 代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。

            +
            +

            参考文章

            +]]>
            + + java + dubbo + spi + +
            + + Spring @Configuration 注解的源码分析 + /2023/11/23/source-code-analysis-of-Spring-Configuration-annotation/ + Configuration 注解是 Spring 中常用的注解,在一般的应用场景中,它用于标识一个类作为配置类,搭配 Bean 注解将创建的 bean 交给 Spring 容器管理。神奇的是,被 Bean 注解标注的方法,只会被真正调用一次。这种方法调用被拦截的情况很容易让人联想到代理,如果你在 Debug 时注意过配置类的实例,你会发现配置类的 Class 名称中携带 EnhancerBySpringCGLIB。本文将从源码角度,分析 Configuration 注解是如何工作的。

            + + +

            测试用例

              +
            • 配置类
              @Configuration
              public class BeanConfig {

              @Bean
              public Person lisi() {
              return new Person("lisi", 20);
              }

              @Bean(value = "customName")
              public Person person() {
              // 测试 lisi() 在配置类拥有注解 @Configuration 时只会真正执行一次
              lisi();
              return new Person("wangwu", 30);
              }
              }
            • +
            • 测试类
              @Test
              public void annotationConfigTest() {
              ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);
              BeanConfig beanConfig = (BeanConfig) ac.getBean("beanConfig");
              // 即使通过 beanConfig 调用,也不会执行第二次
              Person lisi = beanConfig.lisi();
              }
            • +
            +

            解析配置类

            什么是配置类?

            通常情况下,我们称被 Configuration 注解标注的类为配置类。事实上,配置类的范围比这个定义稍微广泛一些,可以划分为全配置类和精简配置类。在解析配置类时,我们再进一步说明。

            ApplicationContext ac = new AnnotationConfigApplicationContext(BeanConfig.class);

            public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
            this();
            // 注册类,几乎可以说无条件地注册 annotatedClasses 的 bean 定义
            register(annotatedClasses);
            refresh();
            }

            本文不详细介绍配置类本身如何注册到 BeanFactory 中。当 BeanConfig 被传递给 AnnotationConfigApplicationContext,自身会先被解析为 BeanDefinition 注册到 beanFactory 中。有两点需要注意:

            @@ -4011,107 +4077,120 @@
            - Spring Boot 自定义 starter 和自动配置的工作原理 - /2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/ - 如果你正在参与一个共享库的开发,你可能会想为使用方提供自动配置的支持,以帮助对方快速地接入和使用。自动配置机制往往和 starter 联系在一起,本文将介绍如何创建一个自定义的 starter 并从源码角度分析 Spring Boot 自动配置的工作原理。

            + Dubbo SPI 的工作原理 + /2023/11/28/how-does-Dubbo-SPI-works/ + SPI 作为一种服务发现机制,允许程序在运行时动态地加载具体实现类。因其强大的可拓展性,SPI 被广泛应用于各类技术框架中,例如 JDBC 驱动、SpringDubbo 等等。Dubbo 并未使用原生的 Java SPI,而是重新实现了一套更加强大的 Dubbo SPI。本文将简单介绍 SPI 的设计理念,通过示例带你体会 SPI 的作用,通过 Dubbo 获取拓展的流程图源码分析带你理解 Dubbo SPI 的工作原理。深入了解 Dubbo SPI,你将能更好地利用这一机制为你的程序提供灵活的拓展功能。

            -

            自定义 starter

            一个 library 的完整 Spring Boot starter 可能包含以下组件:

            -
              -
            • 自动配置模块:包含自动配置的代码。
            • -
            • 启动模块:提供“自动配置模块、library 以及其他有用的依赖项”的依赖项。简而言之,添加 starter 之后应该足以开始使用这个 library。
            • -
            -
            -

            如果你不需要将自动配置的代码和依赖项管理分开,你可以将它们合并到一个模块中

            -
            -

            命名规范

              -
            • 不要以 spring-boot 开头命名模块,即使你使用的是不同的 Maven groupId,因为 Spring 可能在将来提供官方的自动配置支持。自定义 starter 约定俗成的命名方式是 xxx-spring-boot-starter
            • -
            • 如果你的 starter 提供了配置属性的定义,请选择适当的命名空间,避免使用 Spring Boot 的命名空间,否则他们未来的修改可能破坏你的配置。
            • -
            -

            以下将通过一款基于 Redis 实现的分布式锁 redis-lockstarter 介绍如何创建一个自定义的 Spring Boot starter注意:实际上项目中的的 redis-lock-spring-boot-starter 合并了自动配置模块和启动模块

            -

            自动配置模块

            自动配置模块包含开始使用 library 所需要的一切配置。它还可能包含配置键定义(@ConfigurationProperties)和任何其他可用于进一步自定义组件初始化方式的回调接口。

            -

            按照惯例,模块命名为 redis-lock-spring-boot-autoconfigure

            -

            依赖项

            自动配置模块需要添加以下依赖。

            -
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>${spring-boot.version}</version>
            </dependency>
            +

            SPI 简介

            SPI 的全称是 Service Provider Interface,是一种服务发现机制。一般情况下,一项服务的接口和具体实现,都是服务提供者编写的。在 SPI 机制中,一项服务的接口是服务使用者编写的,不同的服务提供者编写不同的具体实现。在程序运行时,服务加载器动态地为接口加载具体实现类。因为 SPI 具备“动态加载”的特性,我们很容易通过它为程序提供拓展功能。以 JavaJDBC 驱动为例,JDK 提供了 java.sql.Driver 接口,各个数据库厂商,例如 MySQLOracle 提供具体的实现。

            + -

            配置类

            和平常在 Spring 中使用一个 library 时一样,创建配置类并配置好使用它所需要的 Bean

            +

            目前 SPI 的实现方式大多是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类:

              -
            • Configuration 注解,标识为配置类
            • -
            • Bean 注解,配置所需要的 Bean
            • -
            • EnableConfigurationProperties 注解,启用配置属性(可选)
            • +
            • Java SPI:META-INF/services/full.qualified.interface.name
            • +
            • Dubbo SPI:META-INF/dubbo/full.qualified.interface.name(还有其他目录可供选择)
            • +
            • Spring SPI: META-INF/spring.factories
            -
            @Configuration
            @EnableConfigurationProperties(RedisLockProperties.class)
            public class RedisLockAutoConfiguration {

            @Autowired
            private RedisLockProperties redisLockProperties;

            @Bean
            @ConditionalOnMissingBean(RedisClient.class)
            public RedisClient redisClient() {
            RedisURI redisURI = new RedisURI();
            redisURI.setHost(redisLockProperties.getHost());
            redisURI.setPort(redisLockProperties.getPort());
            redisURI.setDatabase(redisLockProperties.getDatabase());
            if (redisLockProperties.getUsername() != null) {
            redisURI.setUsername(redisLockProperties.getUsername());
            }
            if (redisLockProperties.getPassword() != null) {
            redisURI.setUsername(redisLockProperties.getPassword());
            }
            return RedisClient.create(redisURI);
            }

            @Bean
            @ConditionalOnMissingBean(RedisLockManager.class)
            public RedisLockManager redisLockManager(RedisClient redisClient) {
            return new RedisLockManager(redisClient);
            }
            }
            +

            SPI 示例

            Java SPI 示例

            定义一个接口 Animal

            +
            public interface Animal {
            void bark();
            }
            -

            配置属性

            你可能需要定义一些配置属性来设置使用 library 所需要的属性。

            -
            @ConfigurationProperties(prefix = RedisLockProperties.PREFIX)
            public class RedisLockProperties {

            public static final String PREFIX = "redis-lock";
            private String host = "localhost";
            private int port = 6379;
            private int database = 0;
            private String username;
            private String password;
            private long waitTimeMillis;
            private long leaseTimeMillis;
            // 省略 setter 和 getter 方法
            }
            +

            定义两个实现类 DogCat

            +
            public class Dog implements Animal {
            @Override
            public void bark() {
            System.out.println("Dog bark...");
            }
            }

            public class Cat implements Animal {
            @Override
            public void bark() {
            System.out.println("Cat bark...");
            }
            }
            -

            spring.factories 文件

            src/main/resources/META-INF 目录中添加一个 spring.factories 文件,文件内容如下。键为 EnableAutoConfiguration 的全限定名,值为配置类的全限定名,如果需要配置多个配置类,可以用逗号分隔。

            -
            org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
            com.moralok.redislock.autoconfigure.RedisLockAutoConfiguration
            +

            META-INF/services 文件夹下创建一个文件,名称为 Animal 的全限定名 com.moralok.dubbo.spi.test.Animal,文件内容为实现类的全限定名,实现类的全限定名之间用换行符分隔。

            +
            com.moralok.dubbo.spi.test.Dog
            com.moralok.dubbo.spi.test.Cat
            -

            启动模块

            starter 实际上是一个空的 jar,它唯一的目的就是提供使用 library 所需要的依赖项。

            -

            按照惯例,模块命名为 redis-lock-spring-boot-starter

            -

            需要引入以下依赖:

            -
            <!-- library 的依赖项 -->
            <dependency>
            <groupId>com.moralok.redis-lock</groupId>
            <artifactId>core</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            <!-- 自动配置模块的依赖项 -->
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>redis-lock-spring-boot-autoconfigure</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            <!-- 其他需要的依赖的依赖项,比如日志相关的 -->
            +

            进行测试。

            +
            public class JavaSPITest {
            @Test
            void bark() {
            System.out.println("Java SPI");
            System.out.println("============");
            ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);
            serviceLoader.forEach(Animal::bark);
            }
            }
            -

            使用

            这样就创建了一个自定义 starter。在项目中引入 starter 后,无需进一步配置,即可使用 RedisLockManagerRedisClient

            -
            <dependency>
            <groupId>com.moralok.redis-lock</groupId>
            <artifactId>redis-lock-spring-boot-starter</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            +

            测试结果

            +
            Java SPI
            ============
            Dog bark...
            Cat bark...
            -

            自动配置的工作原理

            从自定义 starter 的过程来看,使用 library 所需要的配置类和依赖项并没有“凭空消失”,而是由 starter 的编写者提供。然而在正常情况下,第三方的 jar 中的配置类并不在 Spring 扫描 Bean 的范围内,那么 starter 中的配置类是如何被注册到 Spring 容器中呢?我们做的事情中,看起来比较特别的一件事情是添加了 spring.factories 文件。

            -

            SpringBootApplication 注解

            Spring Boot 的启动类(也是 Spring context 的最初配置类)上,标注了 SpringBootApplication 注解。该注解上标注了 EnableAutoConfiguration 注解,它的全限定名正是 spring.factories 文件中配置的键。注解的名字表明它用于启用自动配置功能。

            -
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Inherited
            @SpringBootConfiguration
            @EnableAutoConfiguration
            @ComponentScan(excludeFilters = {
            @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
            public @interface SpringBootApplication {

            @AliasFor(annotation = EnableAutoConfiguration.class)
            Class<?>[] exclude() default {};

            @AliasFor(annotation = EnableAutoConfiguration.class)
            String[] excludeName() default {};

            @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
            String[] scanBasePackages() default {};

            @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
            Class<?>[] scanBasePackageClasses() default {};
            }
            +

            Dubbo SPI 示例

            Dubbo 并未使用原生的 Java SPI,而是重新实现了一套功能更加强大的 SPI 机制。Dubbo SPI 的配置文件放在 META-INF/dubbo 文件夹下,名称仍然是接口的全限定名,但是内容是“名称->实现类的全限定名”的键值对,另外接口需要标注 SPI 注解。

            +
            dog = com.moralok.dubbo.spi.test.Dog
            cat = com.moralok.dubbo.spi.test.Cat
            -

            启用自动配置

            EnableAutoConfiguration 注解用于启用自动配置功能。该注解上标注了 Import 注解,导入了 AutoConfigurationImportSelector。很多形似 EnableXXX 的注解都是通过 Import 注解导入(注册)一些配置类,达到启用 XXX 功能的目的。Import 注解的功能详见之前的文章:

            - -
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Inherited
            @AutoConfigurationPackage
            @Import(AutoConfigurationImportSelector.class)
            public @interface EnableAutoConfiguration {

            String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

            Class<?>[] exclude() default {};

            String[] excludeName() default {};
            }
            +

            进行测试。

            +
            public class DubboSPITest {

            @Test
            void bark() {
            System.out.println("Dubbo SPI");
            System.out.println("============");
            ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
            Animal dog = extensionLoader.getExtension("dog");
            dog.bark();
            Animal cat = extensionLoader.getExtension("cat");
            cat.bark();
            }
            }
            -

            自动配置导入选择器

            导入选择器 ImportSelectorselectImports 方法返回要导入的类的全限定名。AutoConfigurationImportSelector 的名字含义是自动配置导入选择器,顾名思义它返回的应该是要导入的自动配置类。自动配置类这个说法有点容易让人误解,好像这个配置类本身具备“自动”的特性,实际上它就是一个普通的配置类。自动配置描述的是一种机制,想象一下,如果我们在 selectImports 方法中返回 starter 中的配置类 RedisLockAutoConfiguration,是不是就为 redis-lock 完成了自动配置。事实上,selectImports 方法的作用就是找到并返回那些需要被自动配置的配置类。

            -
            public String[] selectImports(AnnotationMetadata annotationMetadata) {
            // 检测是否启用自动配置
            if (!isEnabled(annotationMetadata)) {
            // 如果未启用,返回空数组
            return NO_IMPORTS;
            }
            try {
            // 加载自动配置的元数据
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
            // 获取注解中配置的 exclude 和 excludeName
            AnnotationAttributes attributes = getAttributes(annotationMetadata);
            // 核心方法:获取候选的配置类
            List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
            // 移除重复的
            configurations = removeDuplicates(configurations);
            // 排序
            configurations = sort(configurations, autoConfigurationMetadata);
            // 从注解的配置中获取需要排除的
            Set<String> exclusions = getExclusions(annotationMetadata, attributes);
            // 检查排除的类,如果已加载且不在 configurations 中,抛出异常(不理解原因)
            checkExcludedClasses(configurations, exclusions);
            // 移除需要排除的
            configurations.removeAll(exclusions);
            // 过滤,获取 spring.factories 中的 AutoConfigurationImportFilter 执行过滤
            configurations = filter(configurations, autoConfigurationMetadata);
            // 触发自动配置类导入事件
            fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
            }
            catch (IOException ex) {
            throw new IllegalStateException(ex);
            }
            }
            +

            测试结果

            +
            Dubbo SPI
            ============
            Dog bark...
            Cat bark...
            -

            可以通过环境变量 spring.boot.enableautoconfiguration 覆盖是否启用自动配置功能。

            -
            protected boolean isEnabled(AnnotationMetadata metadata) {
            if (getClass() == AutoConfigurationImportSelector.class) {
            return getEnvironment().getProperty(
            EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class,
            true);
            }
            return true;
            }
            +

            Dubbo 获取扩展流程图

            -

            获取候选的配置类

            我们前面提到过,自动配置类本身只是普通的配置类,那么有什么标记或特征表明目标是一个自动配置类吗?有的,凡是配置在 spring.factories 文件中 EnableAutoConfigurationorg.springframework.boot.autoconfigure.EnableAutoConfiguration) 键下的类,就是候选的自动配置类。
            getCandidateConfigurations 方法用于获取候选的配置类。该方法运用了 SpringSPI 机制,通过 SpringFactoriesLoader 获得所有配置在 spring.factories 文件中,org.springframework.boot.autoconfigure.EnableAutoConfiguration 键下的类,其中就包括了 RedisLockAutoConfiguration。这样就完成了自动配置。

            -
            protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
            // 通过 SpringFactoriesLoader 加载候选的配置
            List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
            Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring.factories. If you "
            + "are using a custom packaging, make sure that file is correct.");
            return configurations;
            }

            protected Class<?> getSpringFactoriesLoaderFactoryClass() {
            return EnableAutoConfiguration.class;
            }
            +

            Dubbo SPI 源码分析

            获取 ExtensionLoader

            private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);

            public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
            if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
            }
            if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
            }
            if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
            ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
            }

            // 从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回
            ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            }
            return loader;
            }
            -

            基于 Spring Boot SPI 机制获取配置在 spring.factories 文件中的自动配置类的过程我们不再分析,可以参见以下文章:

            - -

            让 starter 更好用

            为配置属性生成元数据

            在平时开发时你可能会注意到,有时候在配置文件 application.propertiesapplication.yml 中编写配置时,IDEA 会自动提示我们存在哪些配置,默认值是什么。

            -
            +

            这个方法包含了如下步骤:

            +
              +
            1. 参数校验。
            2. +
            3. 从缓存 EXTENSION_LOADERS 中获取与拓展类对应的 ExtensionLoader,如果缓存未命中,则创建一个新的实例,保存到缓存并返回。
            4. +
            +
            +

            从缓存中获取,如果缓存未命中,则创建,保存到缓存并返回”,类似的 getOrCreate 的处理模式在 Dubbo 的源码中经常出现。

            +
            +

            EXTENSION_LOADERSExtensionLoader 的静态变量,保存了“拓展类->ExtensionLoader”的映射关系。

            +

            根据 name 获取 Extension

            public T getExtension(String name) {
            return getExtension(name, true);
            }

            public T getExtension(String name, boolean wrap) {
            if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
            }
            if ("true".equals(name)) {
            // 获取默认的拓展实现
            return getDefaultExtension();
            }
            // Holder,用于持有目标对象
            final Holder<Object> holder = getOrCreateHolder(name);
            // 双重检查
            Object instance = holder.get();
            if (instance == null) {
            synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
            // 创建拓展实例,设置到 holder 中。
            instance = createExtension(name, wrap);
            holder.set(instance);
            }
            }
            }
            return (T) instance;
            }

            private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

            private Holder<Object> getOrCreateHolder(String name) {
            Holder<Object> holder = cachedInstances.get(name);
            if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
            }
            return holder;
            }
            -

            只需要添加以下依赖,在编译项目时,就会自动调用该处理器 spring-boot-configuration-processor 为你的项目中被 ConfigurationProperties 注解标注的类生成配置元数据文件。

            -
            <dependency>  
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            </dependency>
            +

            这个方法中获取 Holder 和获取拓展实例都是使用 getOrCreate 的模式。

            +

            Holder 用于持有拓展实例。cachedInstancesExtensionLoader 的成员变量,保存了“name->Holder(拓展实例)”的映射关系。

            +

            创建 Extension

            private T createExtension(String name, boolean wrap) {
            // 从配置文件中加载所有的拓展类,可得到“name->拓展实现类”的映射关系表
            Class<?> clazz = getExtensionClasses().get(name);
            if (clazz == null || unacceptableExceptions.contains(name)) {
            throw findException(name);
            }
            try {
            // 使用 getOrCreate 模式获取拓展实例
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 向拓展实例中注入依赖
            injectExtension(instance);

            // 是否包装默认为 true
            if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
            wrapperClassesList.addAll(cachedWrapperClasses);
            wrapperClassesList.sort(WrapperComparator.COMPARATOR);
            Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
            // 遍历包装类
            for (Class<?> wrapperClass : wrapperClassesList) {
            Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
            // 区别于旧版本:支持使用 Wrapper 注解进行匹配
            if (wrapper == null
            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
            // 如果有匹配的包装类,包装拓展实例
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
            }
            }
            }

            // 初始化拓展实例
            initExtension(instance);
            return instance;
            } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
            type + ") couldn't be instantiated: " + t.getMessage(), t);
            }
            }
            + +

            这个方法包含如下步骤:

            +
              +
            1. 通过 getExtensionClasses 获取所有拓展类
            2. +
            3. 通过反射创建拓展实例
            4. +
            5. 向拓展实例中注入依赖
            6. +
            7. 将拓展实例包装在适配的 Wrapper 对象中
            8. +
            9. 初始化拓展实例
            10. +
            +

            第一步是加载拓展类的关键,第三步和第四步是 Dubbo IOCAOP 的具体实现。

            +

            最后拓展实例的结构如下图。

            + + +

            加载 Extension Class

            private Map<String, Class<?>> getExtensionClasses() {
            // 使用 getOrCreate 模式获取所有拓展类
            Map<String, Class<?>> classes = cachedClasses.get();
            if (classes == null) {
            synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
            classes = loadExtensionClasses();
            cachedClasses.set(classes);
            }
            }
            }
            return classes;
            }

            private Map<String, Class<?>> loadExtensionClasses() {
            // 1. 缓存默认拓展名
            cacheDefaultExtensionName();

            Map<String, Class<?>> extensionClasses = new HashMap<>();

            // 遍历加载策略,加载各个策略的目录下的配置文件,获取拓展类
            for (LoadingStrategy strategy : strategies) {
            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
            strategy.overridden(), strategy.excludedPackages());
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
            strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
            }

            return extensionClasses;
            }

            // 如果存在默认拓展名,提取并缓存
            private void cacheDefaultExtensionName() {
            // 获取 SPI 注解
            final SPI defaultAnnotation = type.getAnnotation(SPI.class);
            if (defaultAnnotation == null) {
            return;
            }

            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
            // 对 SPI 的 value 内容进行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 name 是否合法
            if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
            + ": " + Arrays.toString(names));
            }
            if (names.length == 1) {
            // 缓存默认拓展名,用于 getDefaultExtension
            cachedDefaultName = names[0];
            }
            }
            }
            + +

            依次处理特定目录

            代码参考旧版本更容易理解。处理过程在本质上就是依次加载 META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/ 三个目录下的配置文件,获取拓展类。

            +
            loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
            loadDirectory(extensionClasses, DUBBO_DIRECTORY);
            loadDirectory(extensionClasses, SERVICES_DIRECTORY);
            + +

            新版本使用原生的 Java SPI 加载 LoadingStrategy,允许用户自定义加载策略。

            +
              +
            1. DubboInternalLoadingStrategy,目录 META-INF/dubbo/internal/,优先级最高
            2. +
            3. DubboLoadingStrategy,目录 META-INF/dubbo/,优先级普通
            4. +
            5. ServicesLoadingStrategy,目录 META-INF/services/,优先级最低
            6. +
            +
            private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

            private static LoadingStrategy[] loadLoadingStrategies() {
            // 通过 Java SPI 加载 LoadingStrategy
            return stream(load(LoadingStrategy.class).spliterator(), false)
            .sorted()
            .toArray(LoadingStrategy[]::new);
            }
            + +

            LoadingStrategyJava SPI 配置文件

            + + +

            loadDirectory 方法

            loadDirectory 方法先通过 classLoader 获取所有的资源链接,然后再通过 loadResource 方法加载资源。

            +

            新版本中 extensionLoaderClassLoaderFirst 可以设置是否优先使用 ExtensionLoader's ClassLoader 获取资源链接。

            +
            private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
            boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
            // filename = 文件夹路径 + type 的全限定名
            String fileName = dir + type;
            try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();

            // 区别于旧版本:先从 ExtensionLoader's ClassLoader 获取资源链接,默认为 false
            if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
            urls = extensionLoaderClassLoader.getResources(fileName);
            }
            }

            // 根据文件名加载所有同名文件
            if (urls == null || !urls.hasMoreElements()) {
            if (classLoader != null) {
            urls = classLoader.getResources(fileName);
            } else {
            urls = ClassLoader.getSystemResources(fileName);
            }
            }

            if (urls != null) {
            while (urls.hasMoreElements()) {
            java.net.URL resourceURL = urls.nextElement();
            // 加载资源
            loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
            }
            }
            } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
            type + ", description file: " + fileName + ").", t);
            }
            }
            + +

            loadResource 方法

            loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于操作缓存。

            +

            新版本中 excludedPackages 可以设置将指定包内的类都排除。

            +
            private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
            java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
            try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            String clazz = null;
            // 按行读取配置内容
            while ((line = reader.readLine()) != null) {
            // 定位 # 字符,截取 # 字符之前的内容,# 字符之后的内容为注释,需要忽略
            final int ci = line.indexOf('#');
            if (ci >= 0) {
            line = line.substring(0, ci);
            }
            line = line.trim();
            if (line.length() > 0) {
            try {
            String name = null;
            // 定位 = 字符,以 = 字符为界,截取键值对
            int i = line.indexOf('=');
            if (i > 0) {
            name = line.substring(0, i).trim();
            clazz = line.substring(i + 1).trim();
            } else {
            clazz = line;
            }
            if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
            // 加载类,并通过 loadClass 进行缓存
            // 区别于旧版本:根据 excludedPackages 判断是否排除
            loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
            }
            } catch (Throwable t) {
            IllegalStateException e = new IllegalStateException(
            "Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
            ", cause: " + t.getMessage(), t);
            exceptions.put(line, e);
            }
            }
            }
            }
            } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
            type + ", class file: " + resourceURL + ") in " + resourceURL, t);
            }
            }
            + +

            loadClass 方法

            loadClass 方法设置了多个缓存,比如 cachedAdaptiveClasscachedWrapperClassescachedNamescachedClasses

            +

            新版本中 overridden 可以设置是否覆盖 cachedAdaptiveClasscachedClassesname->clazz

            +
            private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
            boolean overridden) throws NoSuchMethodException {
            // 检测 clazz 是否合法
            if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("Error occurred when loading extension class (interface: " +
            type + ", class line: " + clazz.getName() + "), class "
            + clazz.getName() + " is not subtype of interface.");
            }
            if (clazz.isAnnotationPresent(Adaptive.class)) {
            // 检测 clazz 是否有 Adaptive 注解,有则设置 cachedAdaptiveClass 缓存
            // 区别于旧版本:根据 overriden 判断是否可覆盖
            cacheAdaptiveClass(clazz, overridden);
            } else if (isWrapperClass(clazz)) {
            // 检测 clazz 是否是 Wrapper 类型,是则添加到 cachedWrapperClasses 缓存
            cacheWrapperClass(clazz);
            } else {
            // 检测 clazz 是否有默认的构造器方法,如果没有,则抛出异常
            clazz.getConstructor();
            if (StringUtils.isEmpty(name)) {
            // 如果 name 为空,则尝试从 Extension 注解中获取 name,或者使用小写的类名(可能截取 type 后缀)作为 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
            throw new IllegalStateException(
            "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
            }

            // 切分 name
            String[] names = NAME_SEPARATOR.split(name);
            if (ArrayUtils.isNotEmpty(names)) {
            // 如果 clazz 有 Activate 注解,则缓存 names[0]->Activate 的映射关系
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
            // 缓存 clazz->n 的映射关系
            cacheName(clazz, n);
            // 缓存 n->clazz 的映射关系,传递到最后,终于轮到 extensionClasses
            // 区别于旧版本:根据 overriden 判断是否可覆盖
            saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
            }
            }
            }

            private void cacheAdaptiveClass(Class<?> clazz, boolean overridden) {
            if (cachedAdaptiveClass == null || overridden) {
            // 可以设置是否覆盖
            cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
            + cachedAdaptiveClass.getName()
            + ", " + clazz.getName());
            }
            }

            private void cacheWrapperClass(Class<?> clazz) {
            if (cachedWrapperClasses == null) {
            cachedWrapperClasses = new ConcurrentHashSet<>();
            }
            cachedWrapperClasses.add(clazz);
            }

            private void cacheActivateClass(Class<?> clazz, String name) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
            cachedActivates.put(name, activate);
            } else {
            // support com.alibaba.dubbo.common.extension.Activate
            com.alibaba.dubbo.common.extension.Activate oldActivate =
            clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
            if (oldActivate != null) {
            cachedActivates.put(name, oldActivate);
            }
            }
            }
            +

            Dubbo IOC

            Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先通过反射获取目标类的所有方法,然后遍历方法列表,检测方法名是否具有 setter 方法特征并满足条件,若有,则通过 objectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。

            -

            注意:不要盲目手打相信智能提示弄错了依赖,谁能想到 Spring 有好几个命名这么像的 processor,偏偏网上还有各种复制粘贴的文章解答在多模块项目中 spring-boot-configuration-processor 出现的问题——来自 Debug 到深夜的人的怨念。

            +

            Spring IOC 相比,Dubbo IOC 实现的依赖注入功能更加简单,代码也更加容易理解。

            -
            +
            private T injectExtension(T instance) {
            // 检测是否有 objectFactory
            if (objectFactory == null) {
            return instance;
            }
            try {
            // 遍历目标类的所有方法
            // 区别于旧版本:增加了对 DisbaleInject、Inject 注解的处理
            for (Method method : instance.getClass().getMethods()) {
            // 检测是否是 setter 方法
            if (!isSetter(method)) {
            continue;
            }

            // 检测是否标注 DisableInject 注解
            if (method.getAnnotation(DisableInject.class) != null) {
            continue;
            }

            // 检测参数类型是否是原始类型
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
            continue;
            }

            // 获取属性名
            String property = getSetterProperty(method);
            // 检测是否标注 Inject 注解
            Inject inject = method.getAnnotation(Inject.class);
            if (inject == null) {
            injectValue(instance, method, pt, property);
            } else {
            // 检测 Inject 是否启动、是否按照类型注入
            if (!inject.enable()) {
            continue;
            }

            if (inject.type() == Inject.InjectType.ByType) {
            injectValue(instance, method, pt, null);
            } else {
            injectValue(instance, method, pt, property);
            }
            }
            }
            } catch (Exception e) {
            logger.error(e.getMessage(), e);
            }
            return instance;
            }

            // 获取属性名
            private String getSetterProperty(Method method) {
            return method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
            }

            // public、set 开头、只有一个参数
            private boolean isSetter(Method method) {
            return method.getName().startsWith("set")
            && method.getParameterTypes().length == 1
            && Modifier.isPublic(method.getModifiers());
            }

            private void injectValue(T instance, Method method, Class<?> pt, String property) {
            try {
            // 从 ObjectFactory 中获取依赖对象
            Object object = objectFactory.getExtension(pt, property);
            if (object != null) {
            // 通过反射调用 setter 方法设置依赖
            method.invoke(instance, object);
            }
            } catch (Exception e) {
            logger.error("Failed to inject via method " + method.getName()
            + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }
            }
            -

            配合 Conditional 注解

            你几乎总是希望在自动配置类中包含一个或者多个 Conditional 注解。ConditionalOnMissingBean 是一个常用的注解,允许开发人员在对默认设置不满意时覆盖自动配置。

            -

            谨慎地提供依赖

            不要对添加 starter 的项目做出假设,如果你的 starter 需要用到别的 starter,也请提到它们。为你的 library 的典型用法选择一组适当的默认依赖,避免引入不必要的依赖项,尽管当可选的依赖项很多时这可能有些困难。

            -

            总结

            Spring Boot 的自动配置在底层是通过标准的 Configuration 注解实现的,配合 Conditional 注解限制何时应用自动配置。“自动”的特性是基于两个重要的机制:

            -
              -
            • SPI 机制,从 spring.factories 文件中,获取自动配置类的全限定类名
            • -
            • Import 机制,导入从 ImportSelector 返回的类
            • -
            -

            工作原理的示意图如下:

            -
            +

            objectFactoryExtensionFactory 的自适应拓展,通过它获取依赖对象,本质上是根据目标拓展类获取 ExtensionLoader,然后获取其自适应拓展,过程代码如下。具体我们不再深入分析,可以参考Dubbo SPI 自适应拓展的工作原理

            +
            public <T> T getExtension(Class<T> type, String name) {
            if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
            return loader.getAdaptiveExtension();
            }
            }
            return null;
            }

            参考文章

            ]]>
            java - spring - spring boot - auto configuration + dubbo + spi
            @@ -4150,425 +4229,319 @@ - Spring 中 @PropertySource 注解的使用和源码分析 - /2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/ - @PropertySource 注解提供了一种方便的声明性机制,用于将 PropertySource 添加到 Spring 容器的 Environment 环境中。该注解通常搭配 @Configuration 注解一起使用。本文将介绍如何使用 @PropertySource 注解,并通过分析源码解释外部配置文件是如何被解析进入 SpringEnvironment 中。

            + Spring Boot 自定义 starter 和自动配置的工作原理 + /2023/12/06/custom-starter-and-auto-configuration-in-Spring-Boot/ + 如果你正在参与一个共享库的开发,你可能会想为使用方提供自动配置的支持,以帮助对方快速地接入和使用。自动配置机制往往和 starter 联系在一起,本文将介绍如何创建一个自定义的 starter 并从源码角度分析 Spring Boot 自动配置的工作原理。

            -

            使用方式

            @Configuration 注解表示这是一个配置类,Spring 在处理配置类时,会解析并处理配置类上的 @PropertySource 注解,将对应的配置文件解析为 PropertySource,添加到 Spring 容器的 Environment 环境中。这样就可以在其他的 Bean 中,使用 @Value 注解使用这些配置

            -
            @Configuration
            @PropertySource(value = "classpath:/player.properties", encoding = "UTF-8")
            public class PropertySourceConfig {

            @Bean
            public Player player() {
            return new Player();
            }
            }

            public class Player {
            private String name;
            private Integer age;
            @Value("${player.nickname}")
            private String nickname;
            // 省略 setter 和 getter 方法
            }
            +

            自定义 starter

            一个 library 的完整 Spring Boot starter 可能包含以下组件:

            +
              +
            • 自动配置模块:包含自动配置的代码。
            • +
            • 启动模块:提供“自动配置模块、library 以及其他有用的依赖项”的依赖项。简而言之,添加 starter 之后应该足以开始使用这个 library。
            • +
            +
            +

            如果你不需要将自动配置的代码和依赖项管理分开,你可以将它们合并到一个模块中

            +
            +

            命名规范

              +
            • 不要以 spring-boot 开头命名模块,即使你使用的是不同的 Maven groupId,因为 Spring 可能在将来提供官方的自动配置支持。自定义 starter 约定俗成的命名方式是 xxx-spring-boot-starter
            • +
            • 如果你的 starter 提供了配置属性的定义,请选择适当的命名空间,避免使用 Spring Boot 的命名空间,否则他们未来的修改可能破坏你的配置。
            • +
            +

            以下将通过一款基于 Redis 实现的分布式锁 redis-lockstarter 介绍如何创建一个自定义的 Spring Boot starter注意:实际上项目中的的 redis-lock-spring-boot-starter 合并了自动配置模块和启动模块

            +

            自动配置模块

            自动配置模块包含开始使用 library 所需要的一切配置。它还可能包含配置键定义(@ConfigurationProperties)和任何其他可用于进一步自定义组件初始化方式的回调接口。

            +

            按照惯例,模块命名为 redis-lock-spring-boot-autoconfigure

            +

            依赖项

            自动配置模块需要添加以下依赖。

            +
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>${spring-boot.version}</version>
            </dependency>
            -

            配置文件

            -
            player.nickname=Tom
            +

            配置类

            和平常在 Spring 中使用一个 library 时一样,创建配置类并配置好使用它所需要的 Bean

            +
              +
            • Configuration 注解,标识为配置类
            • +
            • Bean 注解,配置所需要的 Bean
            • +
            • EnableConfigurationProperties 注解,启用配置属性(可选)
            • +
            +
            @Configuration
            @EnableConfigurationProperties(RedisLockProperties.class)
            public class RedisLockAutoConfiguration {

            @Autowired
            private RedisLockProperties redisLockProperties;

            @Bean
            @ConditionalOnMissingBean(RedisClient.class)
            public RedisClient redisClient() {
            RedisURI redisURI = new RedisURI();
            redisURI.setHost(redisLockProperties.getHost());
            redisURI.setPort(redisLockProperties.getPort());
            redisURI.setDatabase(redisLockProperties.getDatabase());
            if (redisLockProperties.getUsername() != null) {
            redisURI.setUsername(redisLockProperties.getUsername());
            }
            if (redisLockProperties.getPassword() != null) {
            redisURI.setUsername(redisLockProperties.getPassword());
            }
            return RedisClient.create(redisURI);
            }

            @Bean
            @ConditionalOnMissingBean(RedisLockManager.class)
            public RedisLockManager redisLockManager(RedisClient redisClient) {
            return new RedisLockManager(redisClient);
            }
            }
            -

            测试类

            -
            public class PropertySourceTest {

            @Test
            public void test() {
            AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PropertySourceConfig.class);

            Player player = (Player) ac.getBean("player");
            System.out.println(player);

            ConfigurableEnvironment environment = ac.getEnvironment();
            String property = environment.getProperty("player.nickname");
            System.out.println(property);

            ac.close();
            }
            }
            +

            配置属性

            你可能需要定义一些配置属性来设置使用 library 所需要的属性。

            +
            @ConfigurationProperties(prefix = RedisLockProperties.PREFIX)
            public class RedisLockProperties {

            public static final String PREFIX = "redis-lock";
            private String host = "localhost";
            private int port = 6379;
            private int database = 0;
            private String username;
            private String password;
            private long waitTimeMillis;
            private long leaseTimeMillis;
            // 省略 setter 和 getter 方法
            }
            -

            测试结果

            -
            Player{name='null', age=null, nickname='Tom'}
            Tom
            +

            spring.factories 文件

            src/main/resources/META-INF 目录中添加一个 spring.factories 文件,文件内容如下。键为 EnableAutoConfiguration 的全限定名,值为配置类的全限定名,如果需要配置多个配置类,可以用逗号分隔。

            +
            org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
            com.moralok.redislock.autoconfigure.RedisLockAutoConfiguration
            -

            源码分析

            关于 Spring 是如何处理配置类的请参见之前的文章:

            +

            启动模块

            starter 实际上是一个空的 jar,它唯一的目的就是提供使用 library 所需要的依赖项。

            +

            按照惯例,模块命名为 redis-lock-spring-boot-starter

            +

            需要引入以下依赖:

            +
            <!-- library 的依赖项 -->
            <dependency>
            <groupId>com.moralok.redis-lock</groupId>
            <artifactId>core</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            <!-- 自动配置模块的依赖项 -->
            <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>redis-lock-spring-boot-autoconfigure</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            <!-- 其他需要的依赖的依赖项,比如日志相关的 -->
            + +

            使用

            这样就创建了一个自定义 starter。在项目中引入 starter 后,无需进一步配置,即可使用 RedisLockManagerRedisClient

            +
            <dependency>
            <groupId>com.moralok.redis-lock</groupId>
            <artifactId>redis-lock-spring-boot-starter</artifactId>
            <version>${redis-lock.version}</version>
            </dependency>
            + +

            自动配置的工作原理

            从自定义 starter 的过程来看,使用 library 所需要的配置类和依赖项并没有“凭空消失”,而是由 starter 的编写者提供。然而在正常情况下,第三方的 jar 中的配置类并不在 Spring 扫描 Bean 的范围内,那么 starter 中的配置类是如何被注册到 Spring 容器中呢?我们做的事情中,看起来比较特别的一件事情是添加了 spring.factories 文件。

            +

            SpringBootApplication 注解

            Spring Boot 的启动类(也是 Spring context 的最初配置类)上,标注了 SpringBootApplication 注解。该注解上标注了 EnableAutoConfiguration 注解,它的全限定名正是 spring.factories 文件中配置的键。注解的名字表明它用于启用自动配置功能。

            +
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Inherited
            @SpringBootConfiguration
            @EnableAutoConfiguration
            @ComponentScan(excludeFilters = {
            @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
            public @interface SpringBootApplication {

            @AliasFor(annotation = EnableAutoConfiguration.class)
            Class<?>[] exclude() default {};

            @AliasFor(annotation = EnableAutoConfiguration.class)
            String[] excludeName() default {};

            @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
            String[] scanBasePackages() default {};

            @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
            Class<?>[] scanBasePackageClasses() default {};
            }
            + +

            启用自动配置

            EnableAutoConfiguration 注解用于启用自动配置功能。该注解上标注了 Import 注解,导入了 AutoConfigurationImportSelector。很多形似 EnableXXX 的注解都是通过 Import 注解导入(注册)一些配置类,达到启用 XXX 功能的目的。Import 注解的功能详见之前的文章:

            -

            获取 @PropertySource 注解属性

            Spring 在解析配置类构建配置模型时,会对配置类上的 @PropertySource 注解进行处理。Spring 将获取所有的 @PropertySource 注解属性,并遍历进行处理。

            +
            @Target(ElementType.TYPE)
            @Retention(RetentionPolicy.RUNTIME)
            @Documented
            @Inherited
            @AutoConfigurationPackage
            @Import(AutoConfigurationImportSelector.class)
            public @interface EnableAutoConfiguration {

            String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

            Class<?>[] exclude() default {};

            String[] excludeName() default {};
            }
            + +

            自动配置导入选择器

            导入选择器 ImportSelectorselectImports 方法返回要导入的类的全限定名。AutoConfigurationImportSelector 的名字含义是自动配置导入选择器,顾名思义它返回的应该是要导入的自动配置类。自动配置类这个说法有点容易让人误解,好像这个配置类本身具备“自动”的特性,实际上它就是一个普通的配置类。自动配置描述的是一种机制,想象一下,如果我们在 selectImports 方法中返回 starter 中的配置类 RedisLockAutoConfiguration,是不是就为 redis-lock 完成了自动配置。事实上,selectImports 方法的作用就是找到并返回那些需要被自动配置的配置类。

            +
            public String[] selectImports(AnnotationMetadata annotationMetadata) {
            // 检测是否启用自动配置
            if (!isEnabled(annotationMetadata)) {
            // 如果未启用,返回空数组
            return NO_IMPORTS;
            }
            try {
            // 加载自动配置的元数据
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
            // 获取注解中配置的 exclude 和 excludeName
            AnnotationAttributes attributes = getAttributes(annotationMetadata);
            // 核心方法:获取候选的配置类
            List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
            // 移除重复的
            configurations = removeDuplicates(configurations);
            // 排序
            configurations = sort(configurations, autoConfigurationMetadata);
            // 从注解的配置中获取需要排除的
            Set<String> exclusions = getExclusions(annotationMetadata, attributes);
            // 检查排除的类,如果已加载且不在 configurations 中,抛出异常(不理解原因)
            checkExcludedClasses(configurations, exclusions);
            // 移除需要排除的
            configurations.removeAll(exclusions);
            // 过滤,获取 spring.factories 中的 AutoConfigurationImportFilter 执行过滤
            configurations = filter(configurations, autoConfigurationMetadata);
            // 触发自动配置类导入事件
            fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
            }
            catch (IOException ex) {
            throw new IllegalStateException(ex);
            }
            }
            + +

            可以通过环境变量 spring.boot.enableautoconfiguration 覆盖是否启用自动配置功能。

            +
            protected boolean isEnabled(AnnotationMetadata metadata) {
            if (getClass() == AutoConfigurationImportSelector.class) {
            return getEnvironment().getProperty(
            EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class,
            true);
            }
            return true;
            }
            + +

            获取候选的配置类

            我们前面提到过,自动配置类本身只是普通的配置类,那么有什么标记或特征表明目标是一个自动配置类吗?有的,凡是配置在 spring.factories 文件中 EnableAutoConfigurationorg.springframework.boot.autoconfigure.EnableAutoConfiguration) 键下的类,就是候选的自动配置类。
            getCandidateConfigurations 方法用于获取候选的配置类。该方法运用了 SpringSPI 机制,通过 SpringFactoriesLoader 获得所有配置在 spring.factories 文件中,org.springframework.boot.autoconfigure.EnableAutoConfiguration 键下的类,其中就包括了 RedisLockAutoConfiguration。这样就完成了自动配置。

            +
            protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
            // 通过 SpringFactoriesLoader 加载候选的配置
            List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
            Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring.factories. If you "
            + "are using a custom packaging, make sure that file is correct.");
            return configurations;
            }

            protected Class<?> getSpringFactoriesLoaderFactoryClass() {
            return EnableAutoConfiguration.class;
            }
            + +

            基于 Spring Boot SPI 机制获取配置在 spring.factories 文件中的自动配置类的过程我们不再分析,可以参见以下文章:

              -
            • @PropertySource 注解是可重复的,一个类上可以标注多个
            • -
            • @PropertySources 注解包含 @PropertySource 注解
            • +
            • Spring Boot SPI 的工作原理
            -
            protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
            throws IOException {
            // ...
            // 处理 @PropertySource 注解
            // 获取所有 @PropertySource 注解的属性并遍历。注意该注解为可重复的。
            for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
            // 如果 environment 是 ConfigurableEnvironment 的一个实例,目前恒为 true
            if (this.environment instanceof ConfigurableEnvironment) {
            // 处理单个 @PropertySource 注解的属性
            processPropertySource(propertySource);
            }
            else {
            logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
            "]. Reason: Environment must implement ConfigurableEnvironment");
            }
            }
            // ...
            }
            +

            让 starter 更好用

            为配置属性生成元数据

            在平时开发时你可能会注意到,有时候在配置文件 application.propertiesapplication.yml 中编写配置时,IDEA 会自动提示我们存在哪些配置,默认值是什么。

            +
            -

            使用 IDEA 查看 AnnotationAttributes

            -
            +

            只需要添加以下依赖,在编译项目时,就会自动调用该处理器 spring-boot-configuration-processor 为你的项目中被 ConfigurationProperties 注解标注的类生成配置元数据文件。

            +
            <dependency>  
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
            </dependency>
            -

            处理 @PropertySource 注解属性

              -
            • 读取 @PropertySource 注解属性的信息,如名称、编码和位置等等
            • -
            • 遍历 location 查找资源
            • -
            • 通过 PropertySourceFactory 使用资源创建属性源 PropertySource
            • -
            • 将属性源添加到 Environment
            • -
            -

            注意属性源 PropertySource 不是 @PropertySource 注解,而是表示 name/value 属性对的源的抽象基类。

            +

            注意:不要盲目手打相信智能提示弄错了依赖,谁能想到 Spring 有好几个命名这么像的 processor,偏偏网上还有各种复制粘贴的文章解答在多模块项目中 spring-boot-configuration-processor 出现的问题——来自 Debug 到深夜的人的怨念。

            -
            private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
            // 属性源的 name,大部分时候不指定
            String name = propertySource.getString("name");
            if (!StringUtils.hasLength(name)) {
            name = null;
            }
            // 编码
            String encoding = propertySource.getString("encoding");
            if (!StringUtils.hasLength(encoding)) {
            encoding = null;
            }
            // 位置
            String[] locations = propertySource.getStringArray("value");
            Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
            // 找不到资源时是否忽略,默认 false
            boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
            // 属性源工厂,默认 DefaultPropertySourceFactory
            Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
            PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
            DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
            // 遍历位置
            for (String location : locations) {
            try {
            // 解析位置,这代表 location 也可以使用占位符
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            // 查找资源
            Resource resource = this.resourceLoader.getResource(resolvedLocation);
            // 创建属性源,并添加到 Environment
            addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
            }
            catch (IllegalArgumentException ex) {
            // Placeholders not resolvable
            if (ignoreResourceNotFound) {
            if (logger.isInfoEnabled()) {
            logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
            }
            }
            else {
            throw ex;
            }
            }
            catch (IOException ex) {
            // Resource not found when trying to open it
            if (ignoreResourceNotFound &&
            (ex instanceof FileNotFoundException || ex instanceof UnknownHostException)) {
            if (logger.isInfoEnabled()) {
            logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
            }
            }
            else {
            throw ex;
            }
            }
            }
            }
            +
            -

            添加属性源到 Environment

            将属性源添加到 Environment 中有以下几个规则:

            +

            配合 Conditional 注解

            你几乎总是希望在自动配置类中包含一个或者多个 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) {
            String name = propertySource.getName();
            // 从 Environment 中取出 MutablePropertySources
            MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
            // 检查环境中是否已存在该属性源并且 propertySourceNames 中不存在
            if (propertySources.contains(name) && this.propertySourceNames.contains(name)) {
            // 如果已经添加过,则扩展
            PropertySource<?> existing = propertySources.get(name);
            PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
            ((ResourcePropertySource) propertySource).withResourceName() : propertySource);
            if (existing instanceof CompositePropertySource) {
            // 如果已经扩展过,添加(addFirst)
            ((CompositePropertySource) existing).addFirstPropertySource(newSource);
            }
            else {
            // 第一次扩展,替换为 CompositePropertySource
            if (existing instanceof ResourcePropertySource) {
            existing = ((ResourcePropertySource) existing).withResourceName();
            }
            CompositePropertySource composite = new CompositePropertySource(name);
            composite.addPropertySource(newSource);
            composite.addPropertySource(existing);
            propertySources.replace(name, composite);
            }
            }
            else {
            // 如果 propertySourceNames 为空,添加到最后
            if (this.propertySourceNames.isEmpty()) {
            propertySources.addLast(propertySource);
            }
            // 如果 propertySourceNames 不为空,添加到上一次添加的属性源的前面
            else {
            String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
            propertySources.addBefore(firstProcessed, propertySource);
            }
            }
            // 添加到 propertySourceNames
            this.propertySourceNames.add(name);
            }
            - -

            可以适当地将添加属性源和使用属性分开看待,Environment 是它们产生联系的枢纽,@PropertySource 注解的处理过程是 @Configuration 注解的处理过程的一部分,在文件中的配置转换成为 Environment 中的 PropertySource 后,如何使用它们是独立的一件事情。

            -
            +

            工作原理的示意图如下:

            +
            -

            关于搭配使用的 @Value 注解是如何工作的,可以参考文章:

            -
              -
            • -
            • +

              参考文章

              ]]> java spring + spring boot + auto configuration - Dubbo SPI 自适应拓展的工作原理 - /2023/11/29/how-does-Dubbo-SPI-adaptive-extension-works/ - 直接展示一个具体的 Dubbo SPI 自适应拓展是什么样子,是一种非常好的表现其作用的方式。正如官方博客中所说的,它让人对自适应拓展有更加感性的认识,避免读者一开始就陷入复杂的代码生成逻辑。本文在此基础上,从更原始的使用方式上展现“动态加载”技术对“按需加载”的天然倾向,从更普遍的角度解释自适应拓展的本质目的,在介绍 Dubbo 的具体实现是如何约束自身从而规避缺点之后,详细梳理了 Dubbo SPI 自适应拓展的相关源码和工作原理。

              + Spring 中 @Import 注解的使用和源码分析 + /2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/ + Import 注解是 Spring 基于 Java 注解配置的重要组成部分,处理 Import 注解是处理 Configuration 注解的子过程之一,本文将介绍 Import 注解的 3 种使用方式,然后通过分析源码和处理过程示意图解释它是如何导入(注册) BeanDefinition 的。

              -
              -

              站在现有设计回头看的视角更偏向于展现为什么这样设计很好,却并不好展现如果不这样设计会有什么问题,以至于有时候会有种这个设计很妙,但妙在哪里体会不够深的感觉。思考一项技术如何从最初发展到现在,解决以及试图解决哪些问题,因此可能引入哪些问题,也许脑补的并不完全符合历史事实,但仍然会让人更加深刻地认识这项技术本身,体会设计中的巧思,并避免一直陷在庞杂的细节处理中。

              -
              -

              原理

              Dubbo 中,很多拓展都是通过 SPI 机制动态加载的,比如 ProtocolClusterLoadBalance 等。有些拓展我们并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载。为了让大家对自适应拓展有一个感性的认识,下面我们通过一个实例进行演示。

              -

              示例

              定义一个接口 Animal

              -
              public interface Animal {
              void bark();
              }
              - -

              定义两个实现类 DogCat

              -
              public class Dog implements Animal {
              @Override
              public void bark() {
              System.out.println("Dog bark...");
              }
              }

              public class Cat implements Animal {
              @Override
              public void bark() {
              System.out.println("Cat bark...");
              }
              }
              + +

              使用方式

              Import 注解有 3 种导入(注册) BeanDefinition 的方式:

              +
                +
              1. 使用 Import 将目标类的 Class 对象,解析为 BeanDefinition 并注册。
              2. +
              3. 使用 Import 配合 ImportSelector 的实现类,将 selectImports 方法返回的所有全限定类名字符串,解析为 BeanDefinition 并注册。
              4. +
              5. 使用 Import 配合 ImportBeanDefinitionRegistrar 的实现类,在 registerBeanDefinitions 方法中,直接向 BeanDefinitionRegistry 中注册 BeanDefinition
              6. +
              +

              测试用例

              测试了使用 Import 注解的 3 种方式:

              +
                +
              1. 使用 Import 直接导入(注册) Red
              2. +
              3. 配合 ImportBeanDefinitionRegistrar 间接注册 Color
              4. +
              5. 配合 ImportSelector 间接导入(注册) Blue
              6. +
              +

              用例中的特别地测试了以下两种情况:

              +
                +
              1. 使用 Import 直接导入和配合 ImportSelector 间接导入相同的类 Red 只会注册一个 BeanDefinition
              2. +
              3. 尽管 MyImportSelector 书面顺序在 MyImportBeanDefinitionRegistrar 之后,但是 MyImportBeanDefinitionRegistrar 判断 registry 是否包含在 MyImportSelector 导入的类 Blue 时,不受顺序影响。
              4. +
              +
              @Configuration
              @Import({Red.class, MyImportBeanDefinitionRegistrar.class, MyImportSelector.class,})
              public class ImportConfig {
              }

              public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

              @Override
              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 {

              @Override
              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 {
              @Test
              public void importTest() {
              ApplicationContext ac = new AnnotationConfigApplicationContext(ImportConfig.class);
              String[] beanDefinitionNames = ac.getBeanDefinitionNames();
              for (String name : beanDefinitionNames) {
              System.out.println("beanDefinitionName.........." + name);
              }
              }
              }
              -

              在运行时根据参数动态地加载拓展。

              -
              public void bark(String type) {
              if (type == null) {
              throw new IllegalArgumentException("type == null");
              }
              // 通过 SPI 动态地加载具体的 Animal
              Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
              // 调用目标方法
              animal.bark();
              }
              +

              测试结果

              +
              ......
              beanDefinitionName..........importConfig
              beanDefinitionName..........com.moralok.bean.Red
              beanDefinitionName..........com.moralok.bean.Blue
              beanDefinitionName..........color
              -

              改进

              是不是感觉平平无奇?没错,当你拥有动态加载的能力后,按需加载是自然而然会产生的想法,并不是什么高大上的设计。两者甚至不仅仅是天性相合,可能更像是你中有我,我中有你。在正常场景中,这样一段代码也并不需要进一步被抽象和重构,它本身就很简洁。现在设想一下,你的应用中,有大量的拓展需要动态加载,你可能需要在很多地方写很多根据运行时参数动态加载拓展并调用方法的代码,就像下面这样:

              -
              Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
              animal.bark();

              WeelMaker weelMaker = ExtensionLoader.getExtensionLoader(WeelMaker.class).getExtension(weelMakerName);
              weelMaker.makeWeel();

              LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invocation.getLoadBalanceType());
              loadBalance.select();

              // ...
              - -

              这会带来一些小问题,总是需要写 ExtensionLoader.getExtensionLoader(XXX.class).getExtension(parameter) 这样重复的代码;引入了 ExtensionLoader 这个“中介”,不能直面拓展本身。后者可能有点难以体会,以动物园 Zoo 和 动物 Animal 举例。

              -

              在非动态加载情况下,我们可能会这样写:

              -
              public class Zoo {
              private List<Animal> animals;

              public void bark(String type) {
              for (Animal animal : animals) {
              if (type.equals(animal.name)) {
              animal.bark();
              }
              }
              }
              }
              - -

              在动态加载情况下,我们可能会这样写。在这种情况下,Zoo 没有合适的方式直接持有 Animal,而是通过 ExtensionLoader 间接地持有。

              -
              public class Zoo {
              private ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);

              public void bark(String type) {
              Animal animal = extensionLoader.getExtension(type);
              animal.bark();
              }
              }
              - -

              我们更想要以下这种直接持有 Animal 的方式,在运行时 animal 可以是 Dog,也可以是 Cat,还可以是其他的动物。

              -
              public class Zoo {
              private Animal animal;

              public void bark(String type) {
              animal.bark();
              }
              }
              - -

              Dubbo 采用了一种称为“自适应拓展”的巧妙设计,通过代理的方式,将动态加载拓展的代码整合到代理类(具体实现类)中。使用方调用代理对象,代理对象根据参数动态加载拓展并调用。例如 Animal 的自适应拓展,就像下面这样:

              -
              public class AdaptiveAnimal implements Animal {
              public void bark(String type) {
              if (type == null) {
              throw new IllegalArgumentException("type == null");
              }

              Animal animal = ExtensionLoader.getExtensionLoader(Animal.class).getExtension(type);
              animal.bark();
              }
              }

              Animal animal = new AdaptiveAnimal();
              animal.bark(type);
              - -

              当然,我们不希望需要手动地为每一个拓展编写 Adaptive 代理类,事实上,我们以往接触到的代理方案,大都是自动生成代理的,应该也不会有人会接受完全手写的方式。然而你可能会注意到一个不够和谐的缺点,bark 方法的参数列表中新增了 type 类型,这不太符合面向对象的设计原则。想象一个更奇怪的场景,我们要为一个方法引入与它本身格格不入的参数用于获取拓展。另外,我们可能需要通过一些标记或约定来告诉代理生成器,方法参数列表中哪一个参数是用于获取拓展的。事实上,Dubbo 的另一个设计规避了这一缺点,Dubbo公共契约中提到:所有扩展点参数都包含 URL 参数,URL 作为上下文信息贯穿整个扩展点设计体系。因此围绕着 DubboURL 为中心的拓展体系,你很难设计出 Animal.bark(URL url) 这样不和谐的方法签名,也不用担心参数列表千奇百怪的情况。同时 Dubbo 并未完全抛弃手工编写自适应拓展的方式,而是予以保留。

              -

              手工编码的自适应拓展

              在在 Dubbo 中,尽管很少但仍然存在手工编码的自适应拓展,这类拓展允许你不使用 URL 作为参数,查看它们的代码可以帮助我们更好地理解自适应拓展是如何在真实的应用场景中发挥作用的。以下是 ExtensionFactory 的自适应拓展,当你调用它的 getExtension 方法时,它就是将工作全权委托给 factory.getExtension(type, name) 完成的,而 factories 在创建 AdaptiveExtensionFactory 时就已经获取了。

              -
              @Adaptive
              public class AdaptiveExtensionFactory implements ExtensionFactory {

              private final List<ExtensionFactory> factories;

              public AdaptiveExtensionFactory() {
              // 获取 ExtensionFactory 的 ExtensionLoader
              ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
              List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
              // 获取全部支持的(不包含自适应拓展)拓展名称,依次获取拓展加入 factories
              for (String name : loader.getSupportedExtensions()) {
              list.add(loader.getExtension(name));
              }
              factories = Collections.unmodifiableList(list);
              }

              @Override
              public <T> T getExtension(Class<T> type, String name) {
              for (ExtensionFactory factory : factories) {
              // 委托给其他 ExtensionFactory 拓展获取,比如 SpiExtensionFactory
              T extension = factory.getExtension(type, name);
              if (extension != null) {
              return extension;
              }
              }
              return null;
              }

              }
              - -

              至此,我们提到了按需加载是具备动态加载能力后自然的倾向,介绍了在拥有大量拓展情况下演变而来的自适应拓展设计,它的缺点和 Dubbo 是如何规避的。接下来,我们将进入源码分析部分。

              -

              源码分析

              Adaptive 注解

              Adaptive 注解是一个与自适应拓展息息相关的注解,该定义如下:

              -
              @Documented
              @Retention(RetentionPolicy.RUNTIME)
              @Target({ElementType.TYPE, ElementType.METHOD})
              public @interface Adaptive {
              String[] value() default {};
              }
              - -

              根据 Target 注解的 value 可知,Adaptive 注解可标注在类或者方法上。当 Adaptive 注解标注在类上时,Dubbo 不会为该类生成代理类。当 Adaptive 注解标注在接口方法上时,Dubbo 则会为该方法生成代理逻辑。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解标注,分别是 AdaptiveCompilerAdaptiveExtensionFactory。在这种情况下,拓展的加载逻辑由人工编码完成。在更多时候,Adaptive 注解是标注在接口方法上的,这表示拓展的加载逻辑需由框架自动生成。

              -

              获取自适应拓展

              获取自适应拓展的入口方法是 getAdaptiveExtension,使用 getOrCreate 的模式获取。

              -
              public T getAdaptiveExtension() {
              // 双重检查
              // 从缓存中获取自适应拓展
              Object instance = cachedAdaptiveInstance.get();
              if (instance == null) {
              // 创建自适应拓展失败的结果也会被缓存,避免重复尝试
              if (createAdaptiveInstanceError != null) {
              throw new IllegalStateException("Failed to create adaptive instance: " +
              createAdaptiveInstanceError.toString(),
              createAdaptiveInstanceError);
              }

              synchronized (cachedAdaptiveInstance) {
              instance = cachedAdaptiveInstance.get();
              if (instance == null) {
              try {
              // 创建自适应拓展
              instance = createAdaptiveExtension();
              // 将自适应拓展设置到缓存中
              cachedAdaptiveInstance.set(instance);
              } catch (Throwable t) {
              createAdaptiveInstanceError = t;
              throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
              }
              }
              }
              }

              return (T) instance;
              }
              - -

              创建自适应拓展

              当缓存为空时,就会通过 createAdaptiveExtension 方法创建。方法包含以下三个处理逻辑:

              -
                -
              1. 调用 getAdaptiveExtensionClass 方法获取自适应拓展的 Class 对象。
              2. -
              3. 通过反射进行实例化。
              4. -
              5. 调用 injectExtension 方法对拓展实例进行依赖注入。
              6. -
              +

              源码分析

              关于 Import 注解的源码分析需要建立在对关于 Configuration 注解的源码的了解基础上,因为前者是 Spring 解析配置类处理过程的一部分,可以参考文章:

              + +

              获取要导入的目标

              doProcessConfigurationClass 方法中处理配置类构建配置模型时,会调用 processImports 方法处理 Import 注解。在进入方法前,会调用 getImports 方法从 sourceClass 获取要导入的目标。

              -

              手工编码的自适应拓展可能依赖其他拓展,但是框架生成的自适应拓展并不依赖其他拓展

              +

              注意:目标不仅仅来自直接标注在 sourceClass 上的 Import 注解,因为 sourceClass 上可能还有其他的注解,这些注解自身可能标注了 Import 注解,因此需要递归地遍历所有注解,找到所有的 Import 注解。

              -
              private T createAdaptiveExtension() {
              try {
              return injectExtension((T) getAdaptiveExtensionClass().newInstance());
              } catch (Exception e) {
              throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
              }
              }
              - -

              获取自适应拓展类

              获取自适应拓展类的 getAdaptiveExtensionClass 方法包含以下三个处理逻辑:

              -
                -
              1. 通过 getExtensionClasses 方法获取所有拓展类。
              2. -
              3. 检查缓存 cachedAdaptiveClass,如果不为 null,则返回缓存。
              4. -
              5. 如果缓存为 null,则调用 createAdaptiveExtensionClass 创建自适应拓展类(代理类)。
              6. -
              -

              Dubbo SPI 的工作原理中我们分析过 getExtensionClasses 方法,在获取拓展的所有实现类时,如果某个实现类被 Adaptive 注解标注了,那么该类就会被赋值给 cachedAdaptiveClass 变量。“原理”部分介绍的 AdaptiveExtensionFactory 就属于这种情况,我们不再细谈。按前文所说,在绝大多数情况下,Adaptive 注解都是用于标注方法而非标注具体的实现类,因此在大多数情况下程序都会走第三个步骤,由框架自动生成自适应拓展类(代理类)。

              -
              private Class<?> getAdaptiveExtensionClass() {
              getExtensionClasses();
              if (cachedAdaptiveClass != null) {
              return cachedAdaptiveClass;
              }
              return cachedAdaptiveClass = createAdaptiveExtensionClass();
              }
              +
              protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
              throws IOException {
              // 前后省略 @PropertySource、@ComponentScan、@ImportSource、@Bean 等注解的处理
              // 处理 Import 注解
              processImports(configClass, sourceClass, getImports(sourceClass), true);
              }
              +

              collectImports 方法是一种常见的递归写法(深度优先遍历)。imports 存放要导入的目标,visited 存放已经访问过的 sourceClasssourceClass 在入口处包装了一个普通的 Class,在递归的过程中包装的都是一个注解 Class

              -

              到目前为止,获取自适应拓展的过程和获取普通拓展的过程是非常相似的,使用 getOrCreate 的模式获取拓展,如果缓存为空则创建,创建的时候会先加载全部的拓展实现类,从中获取目标类,通过反射进行实例化,最后进行依赖注入。区别在于获取目标类时,在自适应拓展情况下,返回的可能是一个生成的代理类。生成的过程非常复杂,是我们接下来关注的重点。

              -
              -

              生成自适应拓展类

              生成自适应拓展类的方式相比于以往接触的生成代理类的方式更加“直观且容易理解”,但是相应的,拼接字符串部分的代码并不容易阅读。

              -
                -
              1. 通过拼接字符串得到代理类的源码。
              2. -
              3. 使用编译器编译得到 Class 对象。
              4. -
              -
              -

              在新版本中,这部分代码的可读性有了非常大的提升,原先冗长的处理逻辑被抽象为多个命名含义清晰的方法。

              +

              注意:这里还没有检测循环导入的情况并抛出异常,但 visited 保证了只会遍历一次。

              -
              private Class<?> createAdaptiveExtensionClass() {
              // 区别于旧版本:新版本抽象出一个 AdaptiveClassCodeGenerator 用于生成代码
              String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
              ClassLoader classLoader = findClassLoader();
              // 获取编译器拓展
              org.apache.dubbo.common.compiler.Compiler compiler =
              ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
              // 编译代码,生成 Class 对象
              return compiler.compile(code, classLoader);
              }
              - -

              为了更直观地了解代码生成的效果及其实现的功能,以 Protocol 为例,生成的完整代码(已经经过格式化)展示如下

              -
              package org.apache.dubbo.rpc;

              import org.apache.dubbo.common.extension.ExtensionLoader;

              public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
              public void destroy() {
              throw new UnsupportedOperationException(
              "The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
              }

              public int getDefaultPort() {
              throw new UnsupportedOperationException(
              "The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
              }

              public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1)
              throws org.apache.dubbo.rpc.RpcException {
              if (arg1 == null)
              throw new IllegalArgumentException("url == null");
              org.apache.dubbo.common.URL url = arg1;
              String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
              if (extName == null)
              throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
              + url.toString() + ") use keys([protocol])");
              org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
              .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
              return extension.refer(arg0, arg1);
              }

              public java.util.List getServers() {
              throw new UnsupportedOperationException(
              "The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
              }

              public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0)
              throws org.apache.dubbo.rpc.RpcException {
              if (arg0 == null)
              throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
              if (arg0.getUrl() == null)
              throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
              org.apache.dubbo.common.URL url = arg0.getUrl();
              String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
              if (extName == null)
              throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url ("
              + url.toString() + ") use keys([protocol])");
              org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader
              .getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
              return extension.export(arg0);
              }
              }
              +
              // 获取 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"));
              }
              }
              -

              生成的代理类需完成以下功能:

              -
                -
              1. adaptive 方法,直接抛出异常。
              2. -
              3. adaptive 方法:
                  -
                • 准备工作:在参数判空校验之后,从中获取到 URL 对象,结合 URL 对象和默认拓展名得到最终的拓展名 extName
                • -
                • 核心功能:先获取拓展的 ExtensionLoader,再根据拓展名 extName 获取拓展,最后调用拓展的同名方法。
                • +

                  这时候,并不区分要导入的目标的 Class 有什么特别之处,Import 注解的语义,此时宽泛地说就是:“将 value 中的类导入”。但是显而易见,这样的方式不够灵活,因此才有了另外两种更有灵活性的导入方式:ImportSelectorImportBeanDefinitionRegistrarSpring 最终不会真的注册这两种类,而是注册它们“介绍”的类,相当于把确定导入什么类的工作委托给它们。

                  +

                  处理要导入的目标

                  processImports 方法是处理 Import 注解的核心方法,这里的处理逻辑就对应着 Import 注解的三种使用方式。主要步骤如下:

                  +
                    +
                  • 检测要导入的候选者不为空
                  • +
                  • 判断是否要检测循环导入以及是否存在循环导入
                  • +
                  • 处理要导入的候选者
                      +
                    • 如果是 ImportSelector 类型,调用 selectImports 方法获取新的要导入的目标,递归调用 processImports 处理
                    • +
                    • 如果是 ImportBeanDefinitionRegistrar 类型,添加到配置模型 configClass(出口 1
                    • +
                    • 如果是其他剩余情况,作为配置类处理(出口 2
                  • -
              -

              以上的功能在表面上看来并不复杂,事实上,想要实现的目标处理逻辑也并不复杂,只在为了提供足够的可扩展性,具体实现变得很复杂。复杂的处理逻辑主要集中在如何为“准备工作”部分生成相应的代码,大概可以总结为:在获取拓展前,Dubbo 会直接或间接地从参数列表中查找 URL 对象,所谓直接就是 URL 对象直接在参数列表中,所谓间接就是 URL 对象是其中一个参数的属性。在得到 URL 对象后,Dubbo 会尝试以 Adaptive 注解的 valuekey,从 URL 中获取值作为拓展名,如果获取不到则使用默认拓展名 defaultExtName。实际的实现更加复杂,需要耐心阅读和测试。

              -

              自适应拓展类代码生成器

              新版本将代码生成的逻辑抽象到自适应拓展类代码生成器中,注意参数只有 typedefaultExtName,从这里也可以看出如何确定最终加载的拓展,取决于这两个参数和被调用方法的入参。

              -
              public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
              this.type = type;
              this.defaultExtName = defaultExtName;
              }

              public String generate() {
              // 检测是否至少存在一个方法标注了 Adaptive 注解
              if (!hasAdaptiveMethod()) {
              throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
              }

              // 区别于旧版本:抽象为几个命名含义清晰的方法,提升了可读性
              // 生成类:包名、导入、类声明
              StringBuilder code = new StringBuilder();
              code.append(generatePackageInfo());
              code.append(generateImports());
              code.append(generateClassDeclaration());

              // 生成方法
              Method[] methods = type.getMethods();
              for (Method method : methods) {
              code.append(generateMethod(method));
              }
              code.append("}");

              if (logger.isDebugEnabled()) {
              logger.debug(code.toString());
              }
              return code.toString();
              }
              - -

              检测 Adaptive 注解

              在生成代理类源码之前,generate 方法会先通过反射检测接口方法中是否至少有一个标注了 Adaptive 注解,若不满足,就会抛出异常。

              -
              -

              流式编程使用得当的话很有可读性啊。

              -
              -
              private boolean hasAdaptiveMethod() {
              return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
              }
              - -

              生成类

              生成代理类源码的顺序和普通 Java 类文件中内容的顺序一致:

              -
                -
              • package
              • -
              • import
              • -
              • 类声明
              -

              先忽略“生成方法”的部分,以 DubboProtocol 拓展为例,生成的代码如下:

              -
              package org.apache.dubbo.rpc;
              import org.apache.dubbo.common.extension.ExtensionLoader;
              public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
              // 省略方法代码
              }
              +
              private 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 将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。具体步骤是:

              +
                +
              1. 先获取 Class 对象
              2. +
              3. 再实例化得到一个 ImportSelector 实例
              4. +
              5. 调用 selectImports 方法,该方法返回的是类的全限定名,这样就得到了真正要导入的目标
              6. +
              7. 再次递归调用 processImports
              8. +
              +

              ImportSelector 就像它名字的含义一样,本质上是一种导入选择器,是一种更加灵活的 getImports 方法。由于返回的目标可能属于三种情形中的任意一种,所以对这些目标的处理还是要回到 processImports 方法。可以说 ImportSelector 类型本身不是 processImports 方法的出口,它最终会转换为 ImportBeanDefinitionRegistrar 或其他剩余情况。

              +

              ImportSelector 灵活性的来源:

                -
              • 返回值
              • -
              • 方法名
              • -
              • 方法内容
              • -
              • 方法参数
              • -
              • 方法抛出的异常
              • +
              • selectImportsAnnotationMetadata 参数,为它提供了根据注解信息返回要导入的目标的能力
              • +
              • ImportSelector 可以实现 Aware 接口,用以感知到一些容器级别的资源,如 BeanFactory,这为它提供了根据这些资源中的信息返回要导入的目标的能力
              -
              private String generateMethod(Method method) {
              String methodReturnType = method.getReturnType().getCanonicalName();
              String methodName = method.getName();
              String methodContent = generateMethodContent(method);
              String methodArgs = generateMethodArguments(method);
              String methodThrows = generateMethodThrows(method);
              return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
              }
              - -

              除了最重要的“方法内容”部分,其他部分都是复制原方法的信息,并不复杂。生成“方法内容”部分,分为是否被 Adaptive 注解标注。

              -
              private String generateMethodContent(Method method) {
              Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
              StringBuilder code = new StringBuilder(512);
              // 检测方法是否被 Adaptive 注解标注
              if (adaptiveAnnotation == null) {
              return generateUnsupported(method);
              } else {
              // ...
              }
              return code.toString();
              }
              - -
              无 Adaptive 注解标注的方法

              对于无 Adaptive 注解标注的方法,生成逻辑很简单,就是生成抛出异常的代码。

              -
              private String generateUnsupported(Method method) {
              return String.format(CODE_UNSUPPORTED, method, type.getName());
              }
              - -

              Protocol 接口的 destroy 方法为例,生成的内容如下

              -
              throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
              - -
              有 Adaptive 注解标注的方法

              对于有 Adaptive 注解标注的方法,

              -
              // 查找 URL 类型的参数
              int urlTypeIndex = getUrlTypeIndex(method);
              if (urlTypeIndex != -1) {
              // 生成 URL 判空检查和赋值的代码
              code.append(generateUrlNullCheck(urlTypeIndex));
              } else {
              // 如果参数中没有直接出现 URL 类型,生成间接情况下的 URL 判空和赋值代码
              code.append(generateUrlAssignmentIndirectly(method));
              }
              // 获取方法的 Adaptive 注解的 value
              String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
              // 检测是否有 Invocation 类型的参数
              boolean hasInvocation = hasInvocationArgument(method);
              // 生成 Invocation 判空检查代码
              code.append(generateInvocationArgumentNullCheck(method));
              // 生成拓展名赋值代码
              code.append(generateExtNameAssignment(value, hasInvocation));
              // 生成拓展名判空检查代码
              code.append(generateExtNameNullCheck(value));
              // 生成获取拓展和赋值代码
              code.append(generateExtensionAssignment());
              // 生成调用和返回代码
              code.append(generateReturnAndInvocation(method));
              - -
              查找 URL 类型的参数

              直接从方法的参数类型列表中查找第一个 URL 类型的参数,返回其索引。

              -
              private int getUrlTypeIndex(Method method) {
              int urlTypeIndex = -1;
              // 遍历方法的参数类型列表
              Class<?>[] pts = method.getParameterTypes();
              for (int i = 0; i < pts.length; ++i) {
              // 查找第一个 URL 类型的参数
              if (pts[i].equals(URL.class)) {
              urlTypeIndex = i;
              break;
              }
              }
              return urlTypeIndex;
              }

              // 生成 URL 参数判空检查和赋值代码
              private String generateUrlNullCheck(int index) {
              return String.format(CODE_URL_NULL_CHECK, index, URL.class.getName(), index);
              }
              - -

              间接从方法的参数类型列表中,查找 URL 类型的参数,并生成判空检查和赋值代码。

              -
              private String generateUrlAssignmentIndirectly(Method method) {
              Class<?>[] pts = method.getParameterTypes();

              Map<String, Integer> getterReturnUrl = new HashMap<>();
              // 遍历方法的参数类型列表
              for (int i = 0; i < pts.length; ++i) {
              // 遍历某一个参数类型的全部方法,查找可以返回 URL 类型的 “getter” 方法
              for (Method m : pts[i].getMethods()) {
              String name = m.getName();
              // 1. 方法名以 get 开头,或者方法名大于 3 个字符
              // 2. 方法的访问权限为 public
              // 3. 非静态方法
              // 4. 方法参数数量为 0
              // 5. 方法返回值类型为 URL
              if ((name.startsWith("get") || name.length() > 3)
              && Modifier.isPublic(m.getModifiers())
              && !Modifier.isStatic(m.getModifiers())
              && m.getParameterTypes().length == 0
              && m.getReturnType() == URL.class) {
              // 保存方法名->索引的映射
              getterReturnUrl.put(name, i);
              }
              }
              }

              if (getterReturnUrl.size() <= 0) {
              // 如果没有找到 “getter” 方法,抛出异常
              throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
              + ": not found url parameter or url attribute in parameters of method " + method.getName());
              }

              // 优先选择方法名为 getUrl 的方法,如果没有则选第一个
              Integer index = getterReturnUrl.get("getUrl");
              if (index != null) {
              return generateGetUrlNullCheck(index, pts[index], "getUrl");
              } else {
              Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
              return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
              }
              }

              // 生成 URL 参数判空检查和赋值代码
              private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
              StringBuilder code = new StringBuilder();
              code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");\n",
              index, type.getName()));
              code.append(String.format("if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");\n",
              index, method, type.getName(), method));

              code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
              return code.toString();
              }
              - -

              Protocolreferexport 方法为例,生成的内容如下

              -
              // refer
              if (arg1 == null) throw new IllegalArgumentException("url == null");
              org.apache.dubbo.common.URL url = arg1;
              // export
              if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
              if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
              org.apache.dubbo.common.URL url = arg0.getUrl();
              - -
              获取 Adaptive 注解的 value
              private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
              String[] value = adaptiveAnnotation.value();
              // 如果 value 为空,使用类名生成 value
              // 效果:LoadBalance -> load.balance
              if (value.length == 0) {
              String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
              value = new String[]{splitName};
              }
              return value;
              }
              - -
              检测 Invocation 类型的参数

              检测是否有 Invocation 类型的参数,并生成判空检查代码和赋值代码。从 Invocation 可以获得 methodName

              -
              private boolean hasInvocationArgument(Method method) {
              Class<?>[] pts = method.getParameterTypes();
              return Arrays.stream(pts).anyMatch(p -> CLASSNAME_INVOCATION.equals(p.getName()));
              }

              private String generateInvocationArgumentNullCheck(Method method) {
              Class<?>[] pts = method.getParameterTypes();
              return IntStream.range(0, pts.length).filter(i -> CLASSNAME_INVOCATION.equals(pts[i].getName()))
              .mapToObj(i -> String.format(CODE_INVOCATION_ARGUMENT_NULL_CHECK, i, i))
              .findFirst().orElse("");
              }
              - -

              LoadBalanceselect 方法为例,生成的内容如下:

              -
              if (arg2 == null) throw new IllegalArgumentException("invocation == null");
              String methodName = arg2.getMethodName();
              - -
              获取拓展名

              本方法用于根据 SPIAdaptive 注解的 value 生成“获取拓展名”的代码,同时生成逻辑还受 Invocation 影响,因此相对复杂。总结的规则如下:

              +

              类型二:ImportBeanDefinitionRegistrar

              如果要导入的目标是 ImportBeanDefinitionRegistrar,它会和 ImportSelector 有些相似却又有所不同。Spring 同样将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。

                -
              1. 正常情况下,使用 url.getParameter(value[i]) 获取
              2. -
              3. 如果默认拓展名非空,使用 url.getParameter(value[i], defaultExtName) 获取
              4. -
              5. 如果存在 Invocation,不论默认拓展名是否为空,总是使用 url.getMethodParameter(methodName, value[i], defaultExtName) 获取
              6. -
              7. 因为 protocol 是 url 的一部分,所以可以直接通过 getProtocol 获取。是否使用默认拓展名的方式就退化为原始的三元表达式。
              8. +
              9. 先获取 Class 对象
              10. +
              11. 再实例化得到一个 ImportBeanDefinitionRegistrar 实例
              12. +
              13. 添加到配置模型 configClassimportBeanDefinitionRegistrars 属性
              -
              private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
              // TODO: refactor it
              String getNameCode = null;
              // 逆序遍历 value(Adaptive 的 value)
              for (int i = value.length - 1; i >= 0; --i) {
              // 当 i 为最后一个元素的索引(因为是逆序遍历,第一轮就进入本分支)
              if (i == value.length - 1) {
              // 默认拓展名非空
              if (null != defaultExtName) {
              // protocol 是 url 的一部分,可以通过 getProtocol 方法获取,其他的则必须从 URL 参数中获取
              if (!"protocol".equals(value[i])) {
              if (hasInvocation) {
              // 如果有 Invocation,则使用 url.getMethodParameter 获取
              // url.getMethodParameter(methodName, value[i], defaultExtName)
              getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
              } else {
              // url.getParameter(value[i], defaultExtName)
              getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
              }
              } else {
              // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
              getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
              }
              // 默认拓展名为空
              } else {
              if (!"protocol".equals(value[i])) {
              if (hasInvocation) {
              // url.getMethodParameter(methodName, value[i], defaultExtName)
              getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
              } else {
              // url.getParameter(value[i])
              getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
              }
              } else {
              // url.getProtocol()
              getNameCode = "url.getProtocol()";
              }
              }
              } else {
              if (!"protocol".equals(value[i])) {
              if (hasInvocation) {
              // url.getMethodParameter(methodName, value[i], defaultExtName)
              getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
              } else {
              // url.getParameter(value[i], getNameCode)
              getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
              }
              } else {
              // url.getProtocol() == null ? "dubbo" : url.getProtocol()
              getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
              }
              }
              }
              return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
              }
              +

              ImportBeanDefinitionRegistrar 不像 ImportSelector 需要进一步处理,它本身就代表着一个返回出口,成为了配置模型的一部分。但是请注意,registerBeanDefinitions 方法此时并没有被调用。

              +

              ImportBeanDefinitionRegistrar 灵活性的来源:

              +
                +
              • registerBeanDefinitionsAnnotationMetadata 参数,为它提供了根据注解信息决定注册 BeanDefinition 的能力
              • +
              • registerBeanDefinitionsBeanDefinitionRegistry 参数,为它提供了根据 BeanDefinitionRegistry 中的信息决定注册 BeanDefinition 的能力
              • +
              • ImportBeanDefinitionRegistrar 可以实现 Aware 接口,用以感知到一些容器级别的资源,如 BeanFactory,这为它提供了根据这些资源中的信息返回要导入的目标的能力
              • +
              +

              类型三:其他剩余情况

              如果要导入的目标属于既不是 ImportSelector 也不是 ImportBeanDefinitionRegistrar 的其他剩余情况,那么 Spring 将其视为被 Configuration 注解标注的配置类进行处理。这里的处理逻辑是,Import 注解导入的类可能不是一个普通的类,而是一个配置类,因此需要回到 processConfigurationClass 进行处理。processConfigurationClass 方法正是本文开头的 doProcessConfigurationClass 方法的调用方,这里有两个地方值得注意:

              +
                +
              • Import 注解产生的 ConfigurationClass 根据不同的情况需要合并或者被抛弃,显式声明比 Import 导入的优先级更高。
              • +
              • 其他剩余情况下,目标最终会转换为一个配置模型,添加到 parserconfigurationClasses 属性。
              • +
              +
              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);
              }
              -
              加载拓展
              private String generateExtensionAssignment() {
              return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
              }
              +

              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);
              }
              }
              }
              -

              Protocol 接口的 refer 方法为例,生成的内容如下:

              -
              org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
              +

              ImportBeanDefinitionRegistrar 的调用时机

              ConfigurationClassPostProcessor 在每次解析得到新的一批配置模型后,都会调用 ConfigurationClassBeanDefinitionReaderloadBeanDefinitions 方法加载 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);
              }
              }
              -
              调用与返回

              生成方法调用语句,如有必要,返回结果。

              -
              private String generateReturnAndInvocation(Method method) {
              String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

              String args = IntStream.range(0, method.getParameters().length)
              .mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
              .collect(Collectors.joining(", "));

              return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
              }
              +

              循环导入的检测

              在处理导入的目标前将配置类放入 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();
              }
              }
              }
              -

              Protocol 接口的 refer 方法为例,生成的内容如下:

              -
              return extension.refer(arg0, arg1);
              +

              检测是否发生循环导入。以当前类开始,循环向上查找最近一个导入自身的类,如果找到自身,说明发生循环导入。

              +
              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;
              }
              -
              -

              新版本通过提炼方法、使用流式编程和使用 String.format() 代替 StringBuilder,提供了更好的代码可读性。官方写得源码解析真好。

              -
              -

              参考文章

              +

              总结

              对比

              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              ImportSelectorImportBeanDefinitionRegistrar其他剩余情况
              灵活性
              处理结果转换为配置模型的一部分转换为一个配置模型
              方法调用时机立即(或解析配置类的最后)加载 BeanDefinition 的最后
              方法的结果获取 Import 目标直接注册 BeanDefinition
              +

              处理过程示意图

              ]]>
              java - dubbo - spi + spring
              - ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗 - /2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/ - @ConfigurationProperties@EnableConfigurationPropertiesSpring Boot 中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot 的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机工作原理

              + Spring 中 @PropertySource 注解的使用和源码分析 + /2023/12/07/use-and-analysis-of-PropertySource-annotation-in-Spring/ + @PropertySource 注解提供了一种方便的声明性机制,用于将 PropertySource 添加到 Spring 容器的 Environment 环境中。该注解通常搭配 @Configuration 注解一起使用。本文将介绍如何使用 @PropertySource 注解,并通过分析源码解释外部配置文件是如何被解析进入 SpringEnvironment 中。

              - -

              注解

              ConfigurationProperties 是用于外部化配置的注解。如果你想绑定验证某些外部属性(例如来自 .properties 文件),就将其添加到类定义或 @Configuration 类中的 @Bean 方法。请注意,和 @Value 相反,SpEL 表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties 注解的源码可知,该注解主要起到标记和存储一些信息的作用。

              -
              @Target({ ElementType.TYPE, ElementType.METHOD })
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              public @interface ConfigurationProperties {

              // 可有效绑定到此对象的属性的名称前缀
              @AliasFor("prefix")
              String value() default "";

              // 可有效绑定到此对象的属性的名称前缀
              @AliasFor("value")
              String prefix() default "";

              // 绑定到此对象时是否忽略无效字段
              boolean ignoreInvalidFields() default false;

              // 绑定到此对象时是否忽略未知字段
              boolean ignoreUnknownFields() default true;

              }
              +

              使用方式

              @Configuration 注解表示这是一个配置类,Spring 在处理配置类时,会解析并处理配置类上的 @PropertySource 注解,将对应的配置文件解析为 PropertySource,添加到 Spring 容器的 Environment 环境中。这样就可以在其他的 Bean 中,使用 @Value 注解使用这些配置

              +
              @Configuration
              @PropertySource(value = "classpath:/player.properties", encoding = "UTF-8")
              public class PropertySourceConfig {

              @Bean
              public Player player() {
              return new Player();
              }
              }

              public class Player {
              private String name;
              private Integer age;
              @Value("${player.nickname}")
              private String nickname;
              // 省略 setter 和 getter 方法
              }
              -

              查看 EnableConfigurationProperties 的源码,我们注意到它通过 @Import 导入了 EnableConfigurationPropertiesImportSelector

              -
              @Target(ElementType.TYPE)
              @Retention(RetentionPolicy.RUNTIME)
              @Documented
              @Import(EnableConfigurationPropertiesImportSelector.class)
              public @interface EnableConfigurationProperties {

              // 使用 Spring 快速注册标注了 @ConfigurationProperties 的 bean。无论 value 如何,标准的 Spring Bean 也将被扫描。
              Class<?>[] value() default {};

              }
              +

              配置文件

              +
              player.nickname=Tom
              -

              注解的作用

              查看 EnableConfigurationPropertiesImportSelector 的源码,关注 selectImports 方法。该方法返回了 ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar 的全限定类名,Spring 将注册它们。

              -
              class EnableConfigurationPropertiesImportSelector implements ImportSelector {

              private static final String[] IMPORTS = {
              ConfigurationPropertiesBeanRegistrar.class.getName(),
              ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

              @Override
              public String[] selectImports(AnnotationMetadata metadata) {
              return IMPORTS;
              }
              }
              +

              测试类

              +
              public class PropertySourceTest {

              @Test
              public void test() {
              AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PropertySourceConfig.class);

              Player player = (Player) ac.getBean("player");
              System.out.println(player);

              ConfigurableEnvironment environment = ac.getEnvironment();
              String property = environment.getProperty("player.nickname");
              System.out.println(property);

              ac.close();
              }
              }
              -

              注册目标类

              ConfigurationPropertiesBeanRegistrar 是一个内部类,查看 ConfigurationPropertiesBeanRegistrar 的源码,关注 registerBeanDefinitions 方法。注册的目标来自于:

              -
                -
              • @EnableConfigurationPropertiesvalue 所指定的类中
              • -
              • 且标注了 @ConfigurationProperties 的类
              • -
              -
              public static class ConfigurationPropertiesBeanRegistrar
              implements ImportBeanDefinitionRegistrar {

              @Override
              public void registerBeanDefinitions(AnnotationMetadata metadata,
              BeanDefinitionRegistry registry) {
              // 1. 获取要注册的 Class
              // 2. 遍历获取的结果进行注册
              getTypes(metadata).forEach((type) -> register(registry,
              (ConfigurableListableBeanFactory) registry, type));
              }


              private List<Class<?>> getTypes(AnnotationMetadata metadata) {
              // 获取所有 @EnableConfigurationProperties 的属性
              MultiValueMap<String, Object> attributes = metadata
              .getAllAnnotationAttributes(
              EnableConfigurationProperties.class.getName(), false);
              // 获取属性 value 的值,处理后返回
              return collectClasses(attributes == null ? Collections.emptyList()
              : attributes.get("value"));
              }

              // 收集 Class
              private List<Class<?>> collectClasses(List<?> values) {
              return values.stream().flatMap((value) -> Arrays.stream((Object[]) value))
              .map((o) -> (Class<?>) o).filter((type) -> void.class != type)
              .collect(Collectors.toList());
              }

              // 注册 Bean 定义
              private void register(BeanDefinitionRegistry registry,
              ConfigurableListableBeanFactory beanFactory, Class<?> type) {
              String name = getName(type);
              // 检测是否包含 Bean 定义
              if (!containsBeanDefinition(beanFactory, name)) {
              // 如果没找到,则注册
              registerBeanDefinition(registry, name, type);
              }
              }

              // beanName = prefix + 全限定类名 or 全限定类名
              private String getName(Class<?> type) {
              ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
              ConfigurationProperties.class);
              String prefix = (annotation != null ? annotation.prefix() : "");
              return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
              : type.getName());
              }

              // 检测是否包含 Bean 定义
              private boolean containsBeanDefinition(
              ConfigurableListableBeanFactory beanFactory, String name) {
              // 先检测当前工厂中是否包含
              if (beanFactory.containsBeanDefinition(name)) {
              return true;
              }
              // 如果没找到则检测父工厂是否包含
              BeanFactory parent = beanFactory.getParentBeanFactory();
              if (parent instanceof ConfigurableListableBeanFactory) {
              return containsBeanDefinition((ConfigurableListableBeanFactory) parent,
              name);
              }
              return false;
              }

              // 注册 Bean 定义
              private void registerBeanDefinition(BeanDefinitionRegistry registry, String name,
              Class<?> type) {
              // 断言目标类标注了 @ConfigurationProperties
              assertHasAnnotation(type);
              GenericBeanDefinition definition = new GenericBeanDefinition();
              definition.setBeanClass(type);
              // registry 注册 Bean 定义
              registry.registerBeanDefinition(name, definition);
              }

              // 断言目标类标注了 @ConfigurationProperties
              private void assertHasAnnotation(Class<?> type) {
              Assert.notNull(
              AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
              "No " + ConfigurationProperties.class.getSimpleName()
              + " annotation found on '" + type.getName() + "'.");
              }

              }
              - -

              注册后处理器

              查看 ConfigurationPropertiesBindingPostProcessorRegistrar 的源码,关注 registerBeanDefinitions 方法。该方法注册了 ConfigurationPropertiesBindingPostProcessorConfigurationBeanFactoryMetadata

              -
                -
              • 前者顾名思义,用于处理 ConfigurationProperties 的绑定
              • -
              • 后者是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类
              • -
              -
              public class ConfigurationPropertiesBindingPostProcessorRegistrar
              implements ImportBeanDefinitionRegistrar {

              @Override
              public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
              BeanDefinitionRegistry registry) {
              if (!registry.containsBeanDefinition(
              ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
              registerConfigurationPropertiesBindingPostProcessor(registry);
              registerConfigurationBeanFactoryMetadata(registry);
              }
              }

              private void registerConfigurationPropertiesBindingPostProcessor(
              BeanDefinitionRegistry registry) {
              GenericBeanDefinition definition = new GenericBeanDefinition();
              definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
              // Bean 定义的角色为基础设施
              definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
              registry.registerBeanDefinition(
              ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);

              }

              private void registerConfigurationBeanFactoryMetadata(
              BeanDefinitionRegistry registry) {
              GenericBeanDefinition definition = new GenericBeanDefinition();
              definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
              definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
              registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
              definition);
              }

              }
              +

              测试结果

              +
              Player{name='null', age=null, nickname='Tom'}
              Tom
              -

              绑定

              ConfigurationPropertiesBindingPostProcessor 是用于 ConfigurationProperties 绑定的后处理器,关注 afterPropertiesSet 方法还有核心方法 postProcessBeforeInitialization

              +

              源码分析

              关于 Spring 是如何处理配置类的请参见之前的文章:

                -
              • afterPropertiesSet 方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
              • -
              • postProcessBeforeInitialization 方法中,先获取 @ConfigurationProperties,再进行绑定。
              • +
              • Spring @Configuration 注解的源码分析
              -
              public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
              PriorityOrdered, ApplicationContextAware, InitializingBean {

              public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class
              .getName();

              public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

              private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

              private ApplicationContext applicationContext;

              private ConfigurationPropertiesBinder configurationPropertiesBinder;

              @Override
              public void setApplicationContext(ApplicationContext applicationContext)
              throws BeansException {
              this.applicationContext = applicationContext;
              }

              @Override
              public void afterPropertiesSet() throws Exception {
              // We can't use constructor injection of the application context because
              // it causes eager factory bean initialization
              // 注入了 ConfigurationBeanFactoryMetadata(不理解注解描述的情况?)
              this.beanFactoryMetadata = this.applicationContext.getBean(
              ConfigurationBeanFactoryMetadata.BEAN_NAME,
              ConfigurationBeanFactoryMetadata.class);
              this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
              this.applicationContext, VALIDATOR_BEAN_NAME);
              }

              @Override
              public int getOrder() {
              return Ordered.HIGHEST_PRECEDENCE + 1;
              }

              @Override
              public Object postProcessBeforeInitialization(Object bean, String beanName)
              throws BeansException {
              // 获取 @ConfigurationProperties
              ConfigurationProperties annotation = getAnnotation(bean, beanName,
              ConfigurationProperties.class);
              if (annotation != null) {
              // 绑定
              bind(bean, beanName, annotation);
              }
              return bean;
              }

              private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
              // 获取 Bean 的类型
              ResolvableType type = getBeanType(bean, beanName);
              // 获取 @Validated 用于校验
              Validated validated = getAnnotation(bean, beanName, Validated.class);
              Annotation[] annotations = (validated == null ? new Annotation[] { annotation }
              : new Annotation[] { annotation, validated });
              // 创建 Bindable 对象
              Bindable<?> target = Bindable.of(type).withExistingValue(bean)
              .withAnnotations(annotations);
              try {
              // 绑定
              this.configurationPropertiesBinder.bind(target);
              }
              catch (Exception ex) {
              throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
              ex);
              }
              }

              private ResolvableType getBeanType(Object bean, String beanName) {
              // 先查找工厂方法
              Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
              if (factoryMethod != null) {
              // 如果存在,返回工厂方法的返回值类型
              return ResolvableType.forMethodReturnType(factoryMethod);
              }
              // 否则返回 bean 的类型(难道 bean 的类型不是工厂方法的返回值类型吗?)
              return ResolvableType.forClass(bean.getClass());
              }

              private <A extends Annotation> A getAnnotation(Object bean, String beanName,
              Class<A> type) {
              // 先到 @Bean 方法中获取
              A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
              if (annotation == null) {
              // 如果没有,再到类上获取
              annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
              }
              return annotation;
              }

              }
              - -

              ConfigurationBeanFactoryMetadata 是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties 不仅可以添加到类定义,还可以用于标注 @Bean 方法,ConfigurationBeanFactoryMetadata 正是应用于在后者这类情况下获取 @ConfigurationProperties

              -
              public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {

              private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

              // 后处理 BeanFactory
              @Override
              public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
              throws BeansException {
              this.beanFactory = beanFactory;
              // 遍历 Bean 定义
              for (String name : beanFactory.getBeanDefinitionNames()) {
              BeanDefinition definition = beanFactory.getBeanDefinition(name);
              String method = definition.getFactoryMethodName();
              String bean = definition.getFactoryBeanName();、
              // 检测是否是一个 @Bean 方法对应的 Bean 定义
              if (method != null && bean != null) {
              this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
              }
              }
              }

              // 获取工厂方法上的注解
              public <A extends Annotation> A findFactoryAnnotation(String beanName,
              Class<A> type) {
              Method method = findFactoryMethod(beanName);
              return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
              }

              // 获取工厂方法
              public Method findFactoryMethod(String beanName) {
              if (!this.beansFactoryMetadata.containsKey(beanName)) {
              return null;
              }
              AtomicReference<Method> found = new AtomicReference<>(null);
              FactoryMetadata metadata = this.beansFactoryMetadata.get(beanName);
              Class<?> factoryType = this.beanFactory.getType(metadata.getBean());
              String factoryMethod = metadata.getMethod();
              // 如果是代理类,获取其父类
              if (ClassUtils.isCglibProxyClass(factoryType)) {
              factoryType = factoryType.getSuperclass();
              }
              // 遍历声明的方法进行匹配(名字相同即可没问题吗?)
              ReflectionUtils.doWithMethods(factoryType, (method) -> {
              if (method.getName().equals(factoryMethod)) {
              found.compareAndSet(null, method);
              }
              });
              return found.get();
              }
              }
              - -

              总结

              @EnableConfigurationProperties 的目的有两个:

              +

              获取 @PropertySource 注解属性

              Spring 在解析配置类构建配置模型时,会对配置类上的 @PropertySource 注解进行处理。Spring 将获取所有的 @PropertySource 注解属性,并遍历进行处理。

                -
              • 注册目标
              • -
              • 注册后处理器用于在目标进行 Bean 初始化工作时,介入进行绑定
              • +
              • @PropertySource 注解是可重复的,一个类上可以标注多个
              • +
              • @PropertySources 注解包含 @PropertySource 注解
              -

              尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties 类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties 类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties 注册的,因此你需要保证至少有一个 @EnableConfigurationProperties 标注的类被注册(并被处理了 @Import)。
              Spring Boot 中,@SpringBootApplication 通过 @EnableAutoConfiguration 启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfigurationConfigurationPropertiesAutoConfiguration 标注了 @EnableConfigurationProperties。因此,对于 Spring Boot 而言,扫描范围内的所有 ConfigurationProperties 类,其实都不需要 @EnableAutoConfiguration。事实上,由于默认生成的 beanName 不同,多余的配置还会重复注册两个 Bean 定义。

              -
              @Configuration
              @EnableConfigurationProperties
              public class ConfigurationPropertiesAutoConfiguration {

              }
              - -]]>
              - - java - spring - spring boot - -
              - - Spring AutowiredAnnotationBeanPostProcessor 的源码分析 - /2023/12/08/source-code-analysis-of-AutowiredAnnotationBeanPostProcessor-in-Spring/ - 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 方法中。我们也将直接从这里开始跟踪代码的处理过程。

              -
              -

              个人认为宽松地讲,“填充属性”等于“注入属性”等于“自动装配”,前两者更侧重处理的结果,后者更侧重过程的特征,但请注意在具体的代码上下文中应辨析区别。例如为 bean 填充属性是 Spring 的重要目标之一,基于 Autowired Annotation 进行自动装配某一个后处理器的功能,是 Spring 实现目标的其中一个具体方式。

              -
              -
              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);
              }
              }
              // ...
              }
              +
              protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
              throws IOException {
              // ...
              // 处理 @PropertySource 注解
              // 获取所有 @PropertySource 注解的属性并遍历。注意该注解为可重复的。
              for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
              sourceClass.getMetadata(), PropertySources.class,
              org.springframework.context.annotation.PropertySource.class)) {
              // 如果 environment 是 ConfigurableEnvironment 的一个实例,目前恒为 true
              if (this.environment instanceof ConfigurableEnvironment) {
              // 处理单个 @PropertySource 注解的属性
              processPropertySource(propertySource);
              }
              else {
              logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
              "]. Reason: Environment must implement ConfigurableEnvironment");
              }
              }
              // ...
              }
              -
              -

              有时候在 Spring 中看到 BeanPostProcessor 并不能代表将目光转向该接口的方法实现。不同 BeanPostProcessor 的子接口存在不同的调用时机。AutowiredAnnotationBeanPostProcessor 间接实现了 InstantiationAwareBeanPostProcessor 并直接实现了 MergedBeanDefinitionPostProcessor,这是我们今天要关注的两个重点接口。

              -
              -

              AutowiredAnnotationBeanPostProcessor 是什么时候注册的呢?

              -

              AnnotationConfigApplicationContext 为例,它在构造函数中创建了 AnnotatedBeanDefinitionReaderAnnotatedBeanDefinitionReader 又在构造函数中注册了基于注解配置的处理器:

              -
              AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
              +

              使用 IDEA 查看 AnnotationAttributes

              +
              -

              其中就包括 AutowiredAnnotationBeanPostProcessor

              -

              后处理 PropertyValues

              AutowiredAnnotationBeanPostProcessor 实现了 InstantiationAwareBeanPostProcessor 接口,该接口关注 bean 的实例化:

              -
                -
              • postProcessBeforeInstantiation(实例化前)
              • -
              • postProcessAfterInstantiation(实例化后)
              • -
              • postProcessPropertyValues(实例化后)
              • +

                处理 @PropertySource 注解属性

                  +
                • 读取 @PropertySource 注解属性的信息,如名称、编码和位置等等
                • +
                • 遍历 location 查找资源
                • +
                • 通过 PropertySourceFactory 使用资源创建属性源 PropertySource
                • +
                • 将属性源添加到 Environment
                -

                postProcessPropertyValues 方法在工厂将给定属性值应用到给定 bean 之前对给定属性值进行后处理。允许检查是否满足所有依赖关系,例如基于 bean 属性 setters 上的 @Required 注解进行检查。还允许替换要应用的属性值,通常是通过基于原始 PropertyValues 创建新的 MutablePropertyValues 实例,并添加或删除特定值。

                +

                注意属性源 PropertySource 不是 @PropertySource 注解,而是表示 name/value 属性对的源的抽象基类。

                -

                postProcessPropertyValues 方法做了两件事情:

                -
                  -
                • 查找需要自动装配的元数据
                • -
                • 注入
                • -
                -
                @Override
                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;
                }
                +
                private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
                // 属性源的 name,大部分时候不指定
                String name = propertySource.getString("name");
                if (!StringUtils.hasLength(name)) {
                name = null;
                }
                // 编码
                String encoding = propertySource.getString("encoding");
                if (!StringUtils.hasLength(encoding)) {
                encoding = null;
                }
                // 位置
                String[] locations = propertySource.getStringArray("value");
                Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
                // 找不到资源时是否忽略,默认 false
                boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
                // 属性源工厂,默认 DefaultPropertySourceFactory
                Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
                PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
                DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
                // 遍历位置
                for (String location : locations) {
                try {
                // 解析位置,这代表 location 也可以使用占位符
                String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
                // 查找资源
                Resource resource = this.resourceLoader.getResource(resolvedLocation);
                // 创建属性源,并添加到 Environment
                addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
                }
                catch (IllegalArgumentException ex) {
                // Placeholders not resolvable
                if (ignoreResourceNotFound) {
                if (logger.isInfoEnabled()) {
                logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
                }
                }
                else {
                throw ex;
                }
                }
                catch (IOException ex) {
                // Resource not found when trying to open it
                if (ignoreResourceNotFound &&
                (ex instanceof FileNotFoundException || ex instanceof UnknownHostException)) {
                if (logger.isInfoEnabled()) {
                logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
                }
                }
                else {
                throw ex;
                }
                }
                }
                }
                -

                查找自动装配元数据

                -

                这部分代码体现了注入(Injection)和自动装配(Autowiring)的等价性。InjectionMetadataAutowiringMetadata 的含义是用于注入(自动装配)的元数据。

                -
                -

                InjectionMetadata 是用于管理注入元数据的内部类,不适合直接在应用程序中使用。它和 Class 是一对一的关系,封装了需要被注入的元素 InjectedElement。一个 InjectedElement 对应着一个字段(Field)或一个方法(Method),分别对应着两个实现类 AutowiredFieldElementAutowiredMethodElement。这里再次体现了被注入、被自动装配的语义。

                -

                查找自动装配元数据的过程如下:

                +

                添加属性源到 Environment

                将属性源添加到 Environment 中有以下几个规则:

                  -
                • 先从缓存中获取,如果存在且不需要刷新,则直接返回结果
                • -
                • 否则构建自动装配元数据并放入缓存
                • +
                • 所有通过 @PropertySource 注解加入的属性源,name 都会添加到 propertySourceNames
                • +
                • propertySourceNames 为空时,代表这是第一个通过 @PropertySource 注解加入的属性源,添加到最后(前面有系统属性源)
                • +
                • propertySourceNames 不为空时,添加到上一个添加到 propertySourceNames 中的属性源的前面(后来居上)
                • +
                • 添加到 propertySources 的方法中都是先尝试移除,后添加(代表可能有顺序调整,具体场景不知)
                • +
                • 如果已存在通过 @PropertySource 注解加入的属性源,则扩展为 CompositePropertySource,里面包含多个同名属性源(后来居上)
                -
                -

                注意:在 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 中。

                -
                -

                这个处理顺序意味着在注入时方法的优先级高于字段,前者会覆盖后者。

                -
                -
                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() {
                @Override
                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() {
                @Override
                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);
                }
                +
                private void addPropertySource(PropertySource<?> propertySource) {
                String name = propertySource.getName();
                // 从 Environment 中取出 MutablePropertySources
                MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
                // 检查环境中是否已存在该属性源并且 propertySourceNames 中不存在
                if (propertySources.contains(name) && this.propertySourceNames.contains(name)) {
                // 如果已经添加过,则扩展
                PropertySource<?> existing = propertySources.get(name);
                PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
                ((ResourcePropertySource) propertySource).withResourceName() : propertySource);
                if (existing instanceof CompositePropertySource) {
                // 如果已经扩展过,添加(addFirst)
                ((CompositePropertySource) existing).addFirstPropertySource(newSource);
                }
                else {
                // 第一次扩展,替换为 CompositePropertySource
                if (existing instanceof ResourcePropertySource) {
                existing = ((ResourcePropertySource) existing).withResourceName();
                }
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(newSource);
                composite.addPropertySource(existing);
                propertySources.replace(name, composite);
                }
                }
                else {
                // 如果 propertySourceNames 为空,添加到最后
                if (this.propertySourceNames.isEmpty()) {
                propertySources.addLast(propertySource);
                }
                // 如果 propertySourceNames 不为空,添加到上一次添加的属性源的前面
                else {
                String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
                propertySources.addBefore(firstProcessed, propertySource);
                }
                }
                // 添加到 propertySourceNames
                this.propertySourceNames.add(name);
                }
                -

                注入

                对于字段来说,注入意味着将一个解析得到的 value 通过反射设置到字段中;对于方法来说,注入意味着解析得到方法参数的 value,然后通过反射调用方法。

                -

                InjectionMetadatainject 方法比较简单,内部会遍历并调用 InjectedElementinject 方法,AutowiredFieldElementAutowiredMethodElement 各自实现了 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);
                }
                }
                }
                +

                可以适当地将添加属性源和使用属性分开看待,Environment 是它们产生联系的枢纽,@PropertySource 注解的处理过程是 @Configuration 注解的处理过程的一部分,在文件中的配置转换成为 Environment 中的 PropertySource 后,如何使用它们是独立的一件事情。

                +
                -

                不论是 AutowiredFieldElement 还是 AutowiredMethodElementinject 的过程都比较相似:

                +

                关于搭配使用的 @Value 注解是如何工作的,可以参考文章:

                  -
                • 都使用 DependencyDescriptor 描述即将被注入的特定依赖项,DependencyDescriptor 包装了构造函数参数、方法参数或者字段,允许以统一的方式访问它们的元数据
                • -
                • 都会缓存 DependencyDescriptor
                • -
                • 都会记录自动装配的 bean,用于判断是否使用 DependencyDescriptor 的变体 ShortcutDependencyDescriptor 优化缓存
                • -
                • 都通过 beanFactory.resolveDependency 解析依赖
                • +
                • +
                -

                字段注入

                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;
                }

                @Override
                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…

                -
                -
                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;
                }

                @Override
                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;
                }
                }
                - -

                解析已缓存的方法参数或字段

                -

                为什么在这里 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;
                }
                }
                - -

                解析依赖

                解析依赖的过程暂不深入。

                -
                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;
                }
                }
                - -
                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);
                }
                }
                - -

                构建自动装配元数据的时机

                你在 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);
                }
                }
                - -

                那么 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 填充属性值
                }
                - -

                检测配置成员

                -

                configMember 这个命名不太理解,指的是通过配置实现注入的 MemberFiledMethod 的父类)吗?

                -
                -

                检测配置成员,如果不是外部管理的配置成员,则注册为外部管理的配置成员。在合并后的 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;
                }
                - -
                -

                如果不了解具体的场景,可能会比较难想象这个标记的用处是什么。

                -
                -
                -

                尽管 @Autowired 注解配合 @Value 注解可以很灵活,但是应尽量采取清晰明了的配置方式,让注入的结果一眼就能看出来。

                -
                ]]> java @@ -4610,6 +4583,65 @@ mysql + + ConfigurationProperties 一定要搭配 EnableConfigurationProperties 使用吗 + /2023/12/10/is-it-necessary-to-use-ConfigurationProperties-with-EnableConfigurationProperties/ + @ConfigurationProperties@EnableConfigurationPropertiesSpring Boot 中常用的注解,提供了方便和强大的外部化配置支持。尽管它们常常一起出现,但是它们真的必须一起使用吗?Spring Boot 的灵活性常常让我们忽略配置背后产生的作用究竟是什么?本文将从源码角度出发分析两个注解的作用时机工作原理

                + + + +

                注解

                ConfigurationProperties 是用于外部化配置的注解。如果你想绑定验证某些外部属性(例如来自 .properties 文件),就将其添加到类定义或 @Configuration 类中的 @Bean 方法。请注意,和 @Value 相反,SpEL 表达式不会被求值,因为属性值是外部化的。查看 ConfigurationProperties 注解的源码可知,该注解主要起到标记和存储一些信息的作用。

                +
                @Target({ ElementType.TYPE, ElementType.METHOD })
                @Retention(RetentionPolicy.RUNTIME)
                @Documented
                public @interface ConfigurationProperties {

                // 可有效绑定到此对象的属性的名称前缀
                @AliasFor("prefix")
                String value() default "";

                // 可有效绑定到此对象的属性的名称前缀
                @AliasFor("value")
                String prefix() default "";

                // 绑定到此对象时是否忽略无效字段
                boolean ignoreInvalidFields() default false;

                // 绑定到此对象时是否忽略未知字段
                boolean ignoreUnknownFields() default true;

                }
                + +

                查看 EnableConfigurationProperties 的源码,我们注意到它通过 @Import 导入了 EnableConfigurationPropertiesImportSelector

                +
                @Target(ElementType.TYPE)
                @Retention(RetentionPolicy.RUNTIME)
                @Documented
                @Import(EnableConfigurationPropertiesImportSelector.class)
                public @interface EnableConfigurationProperties {

                // 使用 Spring 快速注册标注了 @ConfigurationProperties 的 bean。无论 value 如何,标准的 Spring Bean 也将被扫描。
                Class<?>[] value() default {};

                }
                + +

                注解的作用

                查看 EnableConfigurationPropertiesImportSelector 的源码,关注 selectImports 方法。该方法返回了 ConfigurationPropertiesBeanRegistrarConfigurationPropertiesBindingPostProcessorRegistrar 的全限定类名,Spring 将注册它们。

                +
                class EnableConfigurationPropertiesImportSelector implements ImportSelector {

                private static final String[] IMPORTS = {
                ConfigurationPropertiesBeanRegistrar.class.getName(),
                ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };

                @Override
                public String[] selectImports(AnnotationMetadata metadata) {
                return IMPORTS;
                }
                }
                + +

                注册目标类

                ConfigurationPropertiesBeanRegistrar 是一个内部类,查看 ConfigurationPropertiesBeanRegistrar 的源码,关注 registerBeanDefinitions 方法。注册的目标来自于:

                +
                  +
                • @EnableConfigurationPropertiesvalue 所指定的类中
                • +
                • 且标注了 @ConfigurationProperties 的类
                • +
                +
                public static class ConfigurationPropertiesBeanRegistrar
                implements ImportBeanDefinitionRegistrar {

                @Override
                public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
                // 1. 获取要注册的 Class
                // 2. 遍历获取的结果进行注册
                getTypes(metadata).forEach((type) -> register(registry,
                (ConfigurableListableBeanFactory) registry, type));
                }


                private List<Class<?>> getTypes(AnnotationMetadata metadata) {
                // 获取所有 @EnableConfigurationProperties 的属性
                MultiValueMap<String, Object> attributes = metadata
                .getAllAnnotationAttributes(
                EnableConfigurationProperties.class.getName(), false);
                // 获取属性 value 的值,处理后返回
                return collectClasses(attributes == null ? Collections.emptyList()
                : attributes.get("value"));
                }

                // 收集 Class
                private List<Class<?>> collectClasses(List<?> values) {
                return values.stream().flatMap((value) -> Arrays.stream((Object[]) value))
                .map((o) -> (Class<?>) o).filter((type) -> void.class != type)
                .collect(Collectors.toList());
                }

                // 注册 Bean 定义
                private void register(BeanDefinitionRegistry registry,
                ConfigurableListableBeanFactory beanFactory, Class<?> type) {
                String name = getName(type);
                // 检测是否包含 Bean 定义
                if (!containsBeanDefinition(beanFactory, name)) {
                // 如果没找到,则注册
                registerBeanDefinition(registry, name, type);
                }
                }

                // beanName = prefix + 全限定类名 or 全限定类名
                private String getName(Class<?> type) {
                ConfigurationProperties annotation = AnnotationUtils.findAnnotation(type,
                ConfigurationProperties.class);
                String prefix = (annotation != null ? annotation.prefix() : "");
                return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
                : type.getName());
                }

                // 检测是否包含 Bean 定义
                private boolean containsBeanDefinition(
                ConfigurableListableBeanFactory beanFactory, String name) {
                // 先检测当前工厂中是否包含
                if (beanFactory.containsBeanDefinition(name)) {
                return true;
                }
                // 如果没找到则检测父工厂是否包含
                BeanFactory parent = beanFactory.getParentBeanFactory();
                if (parent instanceof ConfigurableListableBeanFactory) {
                return containsBeanDefinition((ConfigurableListableBeanFactory) parent,
                name);
                }
                return false;
                }

                // 注册 Bean 定义
                private void registerBeanDefinition(BeanDefinitionRegistry registry, String name,
                Class<?> type) {
                // 断言目标类标注了 @ConfigurationProperties
                assertHasAnnotation(type);
                GenericBeanDefinition definition = new GenericBeanDefinition();
                definition.setBeanClass(type);
                // registry 注册 Bean 定义
                registry.registerBeanDefinition(name, definition);
                }

                // 断言目标类标注了 @ConfigurationProperties
                private void assertHasAnnotation(Class<?> type) {
                Assert.notNull(
                AnnotationUtils.findAnnotation(type, ConfigurationProperties.class),
                "No " + ConfigurationProperties.class.getSimpleName()
                + " annotation found on '" + type.getName() + "'.");
                }

                }
                + +

                注册后处理器

                查看 ConfigurationPropertiesBindingPostProcessorRegistrar 的源码,关注 registerBeanDefinitions 方法。该方法注册了 ConfigurationPropertiesBindingPostProcessorConfigurationBeanFactoryMetadata

                +
                  +
                • 前者顾名思义,用于处理 ConfigurationProperties 的绑定
                • +
                • 后者是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类
                • +
                +
                public class ConfigurationPropertiesBindingPostProcessorRegistrar
                implements ImportBeanDefinitionRegistrar {

                @Override
                public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                BeanDefinitionRegistry registry) {
                if (!registry.containsBeanDefinition(
                ConfigurationPropertiesBindingPostProcessor.BEAN_NAME)) {
                registerConfigurationPropertiesBindingPostProcessor(registry);
                registerConfigurationBeanFactoryMetadata(registry);
                }
                }

                private void registerConfigurationPropertiesBindingPostProcessor(
                BeanDefinitionRegistry registry) {
                GenericBeanDefinition definition = new GenericBeanDefinition();
                definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
                // Bean 定义的角色为基础设施
                definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                registry.registerBeanDefinition(
                ConfigurationPropertiesBindingPostProcessor.BEAN_NAME, definition);

                }

                private void registerConfigurationBeanFactoryMetadata(
                BeanDefinitionRegistry registry) {
                GenericBeanDefinition definition = new GenericBeanDefinition();
                definition.setBeanClass(ConfigurationBeanFactoryMetadata.class);
                definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
                registry.registerBeanDefinition(ConfigurationBeanFactoryMetadata.BEAN_NAME,
                definition);
                }

                }
                + +

                绑定

                ConfigurationPropertiesBindingPostProcessor 是用于 ConfigurationProperties 绑定的后处理器,关注 afterPropertiesSet 方法还有核心方法 postProcessBeforeInitialization

                +
                  +
                • afterPropertiesSet 方法中,它获取到了和自己一起注册的 ConfigurationBeanFactoryMetadata
                • +
                • postProcessBeforeInitialization 方法中,先获取 @ConfigurationProperties,再进行绑定。
                • +
                +
                public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProcessor,
                PriorityOrdered, ApplicationContextAware, InitializingBean {

                public static final String BEAN_NAME = ConfigurationPropertiesBindingPostProcessor.class
                .getName();

                public static final String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

                private ConfigurationBeanFactoryMetadata beanFactoryMetadata;

                private ApplicationContext applicationContext;

                private ConfigurationPropertiesBinder configurationPropertiesBinder;

                @Override
                public void setApplicationContext(ApplicationContext applicationContext)
                throws BeansException {
                this.applicationContext = applicationContext;
                }

                @Override
                public void afterPropertiesSet() throws Exception {
                // We can't use constructor injection of the application context because
                // it causes eager factory bean initialization
                // 注入了 ConfigurationBeanFactoryMetadata(不理解注解描述的情况?)
                this.beanFactoryMetadata = this.applicationContext.getBean(
                ConfigurationBeanFactoryMetadata.BEAN_NAME,
                ConfigurationBeanFactoryMetadata.class);
                this.configurationPropertiesBinder = new ConfigurationPropertiesBinder(
                this.applicationContext, VALIDATOR_BEAN_NAME);
                }

                @Override
                public int getOrder() {
                return Ordered.HIGHEST_PRECEDENCE + 1;
                }

                @Override
                public Object postProcessBeforeInitialization(Object bean, String beanName)
                throws BeansException {
                // 获取 @ConfigurationProperties
                ConfigurationProperties annotation = getAnnotation(bean, beanName,
                ConfigurationProperties.class);
                if (annotation != null) {
                // 绑定
                bind(bean, beanName, annotation);
                }
                return bean;
                }

                private void bind(Object bean, String beanName, ConfigurationProperties annotation) {
                // 获取 Bean 的类型
                ResolvableType type = getBeanType(bean, beanName);
                // 获取 @Validated 用于校验
                Validated validated = getAnnotation(bean, beanName, Validated.class);
                Annotation[] annotations = (validated == null ? new Annotation[] { annotation }
                : new Annotation[] { annotation, validated });
                // 创建 Bindable 对象
                Bindable<?> target = Bindable.of(type).withExistingValue(bean)
                .withAnnotations(annotations);
                try {
                // 绑定
                this.configurationPropertiesBinder.bind(target);
                }
                catch (Exception ex) {
                throw new ConfigurationPropertiesBindException(beanName, bean, annotation,
                ex);
                }
                }

                private ResolvableType getBeanType(Object bean, String beanName) {
                // 先查找工厂方法
                Method factoryMethod = this.beanFactoryMetadata.findFactoryMethod(beanName);
                if (factoryMethod != null) {
                // 如果存在,返回工厂方法的返回值类型
                return ResolvableType.forMethodReturnType(factoryMethod);
                }
                // 否则返回 bean 的类型(难道 bean 的类型不是工厂方法的返回值类型吗?)
                return ResolvableType.forClass(bean.getClass());
                }

                private <A extends Annotation> A getAnnotation(Object bean, String beanName,
                Class<A> type) {
                // 先到 @Bean 方法中获取
                A annotation = this.beanFactoryMetadata.findFactoryAnnotation(beanName, type);
                if (annotation == null) {
                // 如果没有,再到类上获取
                annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
                }
                return annotation;
                }

                }
                + +

                ConfigurationBeanFactoryMetadata 是用于在 Bean 工厂初始化期间记住 @Bean 定义元数据的实用程序类。在前面我们介绍过 @ConfigurationProperties 不仅可以添加到类定义,还可以用于标注 @Bean 方法,ConfigurationBeanFactoryMetadata 正是应用于在后者这类情况下获取 @ConfigurationProperties

                +
                public class ConfigurationBeanFactoryMetadata implements BeanFactoryPostProcessor {

                private final Map<String, FactoryMetadata> beansFactoryMetadata = new HashMap<>();

                // 后处理 BeanFactory
                @Override
                public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
                throws BeansException {
                this.beanFactory = beanFactory;
                // 遍历 Bean 定义
                for (String name : beanFactory.getBeanDefinitionNames()) {
                BeanDefinition definition = beanFactory.getBeanDefinition(name);
                String method = definition.getFactoryMethodName();
                String bean = definition.getFactoryBeanName();、
                // 检测是否是一个 @Bean 方法对应的 Bean 定义
                if (method != null && bean != null) {
                this.beansFactoryMetadata.put(name, new FactoryMetadata(bean, method));
                }
                }
                }

                // 获取工厂方法上的注解
                public <A extends Annotation> A findFactoryAnnotation(String beanName,
                Class<A> type) {
                Method method = findFactoryMethod(beanName);
                return (method == null ? null : AnnotationUtils.findAnnotation(method, type));
                }

                // 获取工厂方法
                public Method findFactoryMethod(String beanName) {
                if (!this.beansFactoryMetadata.containsKey(beanName)) {
                return null;
                }
                AtomicReference<Method> found = new AtomicReference<>(null);
                FactoryMetadata metadata = this.beansFactoryMetadata.get(beanName);
                Class<?> factoryType = this.beanFactory.getType(metadata.getBean());
                String factoryMethod = metadata.getMethod();
                // 如果是代理类,获取其父类
                if (ClassUtils.isCglibProxyClass(factoryType)) {
                factoryType = factoryType.getSuperclass();
                }
                // 遍历声明的方法进行匹配(名字相同即可没问题吗?)
                ReflectionUtils.doWithMethods(factoryType, (method) -> {
                if (method.getName().equals(factoryMethod)) {
                found.compareAndSet(null, method);
                }
                });
                return found.get();
                }
                }
                + +

                总结

                @EnableConfigurationProperties 的目的有两个:

                +
                  +
                • 注册目标
                • +
                • 注册后处理器用于在目标进行 Bean 初始化工作时,介入进行绑定
                • +
                +

                尽管注册目标时的操作有些巧妙,但是还是要明白 ConfigurationProperties 类只是单纯的被注册了而已。对于后处理器而言,无论一个 ConfigurationProperties 类是不是通过注解注册,后处理器都会一视同仁地进行绑定。但同时,你又要知道后处理器也是通过 @EnableConfigurationProperties 注册的,因此你需要保证至少有一个 @EnableConfigurationProperties 标注的类被注册(并被处理了 @Import)。
                Spring Boot 中,@SpringBootApplication 通过 @EnableAutoConfiguration 启用了自动配置,从而注册了 ConfigurationPropertiesAutoConfigurationConfigurationPropertiesAutoConfiguration 标注了 @EnableConfigurationProperties。因此,对于 Spring Boot 而言,扫描范围内的所有 ConfigurationProperties 类,其实都不需要 @EnableAutoConfiguration。事实上,由于默认生成的 beanName 不同,多余的配置还会重复注册两个 Bean 定义。

                +
                @Configuration
                @EnableConfigurationProperties
                public class ConfigurationPropertiesAutoConfiguration {

                }
                + +]]>
                + + java + spring + spring boot + +
                使用 Docker Compose 安装 ELK /2023/12/14/install-ELK-using-Docker-Compose/ @@ -4747,108 +4779,36 @@ - Unsafe,一个“反 Java”的 class - /2023/12/25/Unsafe-an-anti-Java-class/ - Unsafe 类位于 sun.misc 包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe 类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CASCompare and Swap)操作等。本文将介绍这个“反 Java”的类及其方法的典型使用场景。

                + ComponentScan 扫描路径覆盖的真相 + /2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/ + @ComponentScan 注解是 Spring 中很常用的注解,用于扫描并加载指定类路径下的 Bean,而 Spring Boot 为了便捷使用 @SpringBootApplication 组合注解集成了 @ComponentScan 的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication 可以间接设置 @ComponentScan 属性的原因?本文从源码角度分析 @ComponentScan 的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。

                + +

                入口

                对于标注了 @ComponentScan 注解的配置类,处理过程如下:

                +
                  +
                • 获取 @ComponentScan 的注解属性
                • +
                • 遍历注解属性集合,依次根据其中的信息进行扫描,获取 Bean 定义
                • +
                • 如果获取到的 Bean 定义中有任何其他配置类,将递归解析(处理配置类)
                • +
                -

                由于 Unsafe 类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过 Java 语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用 Unsafe 类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用 Unsafe 类来进行一些性能优化。

                +

                这里和处理 @Import 的过程很像,都出现了递归解析新获得的配置类。

                +
                protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                throws IOException {
                // ...
                // 处理任何 @ComponentScan 注解
                // 获取 @ComponentScan 的注解属性,该注解是可重复的
                Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
                if (!componentScans.isEmpty() &&
                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
                // 遍历
                for (AnnotationAttributes componentScan : componentScans) {
                // 如果配置类被标注了 @ComponentScan -> 立即扫描
                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // 检测被扫描到的 Bean 定义中是否有任何其他配置类,如有需要递归解析
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                holder.getBeanDefinition(), this.metadataReaderFactory)) {
                // 递归解析配置类
                parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                }
                }
                }
                }
                // ...
                }
                + +

                扫描获取 Bean 定义

                我们先跳过“获取 @ComponentScan 的注解属性”的过程,来看“扫描获取 Bean 定义”的过程。扫描是通过 ComponentScanAnnotationParserparse 方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner 设置一些来自 @ComponentScan 的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner 顾名思义是基于类路径的 Bean 定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages 的设置:

                +
                  +
                • 使用 Set 存储合并结果,用于去重
                • +
                • 获取设置的 basePackages 值并添加
                • +
                • 获取设置的 basePackageClasses 值,转换为它们所在的包名并添加
                • +
                • 如果结果集现在还是空的,获取被标注的配置类所在的包名并添加
                • +
                -

                尽管在生产中需要谨慎使用 Unsafe,但是可以在测试中使用它来更真实地接触 Java 对象在内存中的存储结构,验证自己的理论知识。

                -
                -

                获取 Unsafe 实例

                -

                Java 9 及之后的版本中,Unsafe 类中的 getUnsafe() 方法被标记为不安全(Unsafe),不再允许普通的 Java 应用程序代码通过此方法获取 Unsafe 实例。这是为了提高 Java 的安全性,防止滥用 Unsafe 类的功能。

                +

                最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。

                -

                在正常的 Java 应用程序中,获取 Unsafe 实例是不被推荐的,因为它违反了 Java 语言的安全性和封装原则。Unsafe 类的设计本意是为了 Java 库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe 对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe 对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。

                -

                获取 Unsafe 实例的静态方法如下:

                -
                @CallerSensitive
                public static Unsafe getUnsafe() {
                Class<?> caller = Reflection.getCallerClass();
                // 检查调用方法的类是被引导类加载器所加载
                if (!VM.isSystemDomainLoader(caller.getClassLoader()))
                throw new SecurityException("Unsafe");
                return theUnsafe;
                }
                - -

                Unsafe 使用单例模式,可以通过静态方法 getUnsafe 获取 Unsafe 实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe 实例有以下两种可行方案:

                -
                  -
                1. 通过 -Xbootclasspath/a:${path} 把调用方法的类所在的 jar 包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径
                2. -
                3. 通过反射获取 Unsafe 类中的 Unsafe 实例
                  private static Unsafe getUnsafe() {
                  try {
                  Field f = Unsafe.class.getDeclaredField("theUnsafe");
                  f.setAccessible(true);
                  return (Unsafe) f.get(null);
                  } catch (NoSuchFieldException | IllegalAccessException e) {
                  throw new RuntimeException(e);
                  }
                  }
                4. -
                -

                内存操作

                Unsafe 类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java 语言的内置安全性和类型检查。以下是一些常见的 Unsafe 类中关于内存操作的方法:

                -
                  -
                • allocateMemory: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。
                  public native long allocateMemory(long bytes);
                • -
                • reallocateMemory: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。
                  public native long reallocateMemory(long address, long bytes);
                • -
                • freeMemory: 释放之前由 allocateMemoryreallocateMemory 分配的内存。
                  public native void freeMemory(long address);
                • -
                • setMemory: 将给定内存块中的所有字节设置为固定值(通常为零)。
                  public native void setMemory(Object o, long offset, long bytes, byte value);
                  public void setMemory(long address, long bytes, byte value) {
                  setMemory(null, address, bytes, value);
                  }
                • -
                • copyMemory: 复制指定长度的内存块
                  public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
                • -
                • putXxx: 将指定偏移量处的内存设置为指定的值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                  public native void putObject(Object o, long offset, Object x);
                • -
                • getXxx: 从指定偏移量处的内存读取值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                  public native Object getObject(Object o, long offset);
                • -
                • putXxxgetXxx 也提供了按绝对基地址操作内存的方法。
                  public native byte getByte(long address);
                  public native void putByte(long address, byte x);
                • -
                -

                从内存读取值时,除非满足以下情况之一,否则结果不确定:

                -
                  -
                1. 偏移量是通过 objectFieldOffset 从字段的 Field 对象获取的,o 指向的对象的类与字段所属的类兼容。
                2. -
                3. 偏移量和 o 指向的对象(无论是否为 null)分别是通过 staticFieldOffsetstaticFieldBaseField 对象获得的。
                4. -
                5. o 指向的是一个数组,偏移量是一个形式为 B+N*S 的整数,其中 N 是数组的有效索引,BS 分别是通过 arrayBaseOffsetarrayIndexScale 获得的值。
                6. -
                -
                -

                做一些“不确定”的测试,比如使用 byte 相关的方法操作 int 所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。

                -
                -

                分配堆外内存

                Java NIONew I/O)中,分配堆外内存使用了 Unsafe 类的 allocateMemory 方法。堆外内存是一种在 Java 虚拟机之外分配的内存,它不受 Java 堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O 操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java 堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting)的机制来处理。

                -
                DirectByteBuffer(int cap) {
                super(-1, 0, cap, cap);
                boolean pa = VM.isDirectMemoryPageAligned();
                int ps = Bits.pageSize();
                long size = Math.max(1L, (long)cap + (pa ? ps : 0));
                Bits.reserveMemory(size, cap);

                long base = 0;
                try {
                // 分配本地内存
                base = unsafe.allocateMemory(size);
                } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
                }
                // 初始化本地内存
                unsafe.setMemory(base, size, (byte) 0);
                if (pa && (base % ps != 0)) {
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));
                } else {
                address = base;
                }
                // 使用虚引用 Cleaner 对象跟踪 DirectByteBuffer 对象的垃圾回收
                cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
                att = null;
                }
                - -

                DirectByteBuffer 对象仅被 Cleaner 对象(虚引用)引用时,它可以在任意一次 GC 中被垃圾回收。在 DirectByteBuffer 对象被垃圾回收后,Cleaner 对象会被加入到引用队列,ReferenceHandler 线程将调用 Deallocator 对象的 run 方法,从而实现本地内存的自动释放。

                -
                private static class Deallocator implements Runnable {

                private static Unsafe unsafe = Unsafe.getUnsafe();

                private long address;
                private long size;
                private int capacity;

                private Deallocator(long address, long size, int capacity) {
                assert (address != 0);
                this.address = address;
                this.size = size;
                this.capacity = capacity;
                }

                public void run() {
                if (address == 0) {
                // Paranoia
                return;
                }
                // 释放本地内存
                unsafe.freeMemory(address);
                address = 0;
                Bits.unreserveMemory(size, capacity);
                }

                }
                - -

                CAS 相关

                Unsafe 提供了 3CAS 相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU 会自动将该位置更新为新值,否则,CPU 不做任何操作。这些方法的底层实现对应着 CPU 指令 cmpxchg

                -
                // 如果 Java 变量当前符合预期,则自动将其更新为 x。
                public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
                public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
                public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
                - -

                AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 UnsafeobjectFieldOffset 方法获取。

                -
                public class AtomicInteger extends Number implements java.io.Serializable {
                private static final long serialVersionUID = 6214790243416807050L;

                // setup to use Unsafe.compareAndSwapInt for updates
                private static final Unsafe unsafe = Unsafe.getUnsafe();
                private static final long valueOffset;

                static {
                try {
                valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
                } catch (Exception ex) { throw new Error(ex); }
                }

                private volatile int value;
                }
                - -

                CAS 更新变量的值的内存变化如下:

                -
                - -

                配合 ClassLayout 打印 AtomicInteger 的内部结构更直观地感受 offset 的含义:

                -
                java.util.concurrent.atomic.AtomicInteger object internals:
                OFF SZ TYPE DESCRIPTION VALUE
                0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                8 4 (object header: class) 0xf8003dbc
                12 4 int AtomicInteger.value 1
                Instance size: 16 bytes
                Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                - - - -

                参考文章

                -]]>
                - - java - -
                - - ComponentScan 扫描路径覆盖的真相 - /2023/12/11/the-truth-about-override-of-ComponentScan-basePackages/ - @ComponentScan 注解是 Spring 中很常用的注解,用于扫描并加载指定类路径下的 Bean,而 Spring Boot 为了便捷使用 @SpringBootApplication 组合注解集成了 @ComponentScan 的能力。也许你听说过使用后者会覆盖前者中关于包扫描的设置,但你是否质疑过这个“不合常理”的结论?是否好奇过为什么它们不像其他注解在嵌套使用时可以同时生效?又是否好奇过 @SpringBootApplication 可以间接设置 @ComponentScan 属性的原因?本文从源码角度分析 @ComponentScan 的工作原理,揭示它独特的检索算法和注解层次结构中的属性覆盖机制。

                - - - -

                入口

                对于标注了 @ComponentScan 注解的配置类,处理过程如下:

                -
                  -
                • 获取 @ComponentScan 的注解属性
                • -
                • 遍历注解属性集合,依次根据其中的信息进行扫描,获取 Bean 定义
                • -
                • 如果获取到的 Bean 定义中有任何其他配置类,将递归解析(处理配置类)
                • -
                -
                -

                这里和处理 @Import 的过程很像,都出现了递归解析新获得的配置类。

                -
                -
                protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
                throws IOException {
                // ...
                // 处理任何 @ComponentScan 注解
                // 获取 @ComponentScan 的注解属性,该注解是可重复的
                Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
                if (!componentScans.isEmpty() &&
                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
                // 遍历
                for (AnnotationAttributes componentScan : componentScans) {
                // 如果配置类被标注了 @ComponentScan -> 立即扫描
                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // 检测被扫描到的 Bean 定义中是否有任何其他配置类,如有需要递归解析
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                holder.getBeanDefinition(), this.metadataReaderFactory)) {
                // 递归解析配置类
                parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                }
                }
                }
                }
                // ...
                }
                - -

                扫描获取 Bean 定义

                我们先跳过“获取 @ComponentScan 的注解属性”的过程,来看“扫描获取 Bean 定义”的过程。扫描是通过 ComponentScanAnnotationParserparse 方法完成的,这个方法很长,但逻辑并不复杂,主要是为 ClassPathBeanDefinitionScanner 设置一些来自 @ComponentScan 的注解属性值,最终执行扫描。ClassPathBeanDefinitionScanner 顾名思义是基于类路径的 Bean 定义扫描器,真正的扫描工作全部委托给了它。在这些设置过程中,我们需要关注 basePackages 的设置:

                -
                  -
                • 使用 Set 存储合并结果,用于去重
                • -
                • 获取设置的 basePackages 值并添加
                • -
                • 获取设置的 basePackageClasses 值,转换为它们所在的包名并添加
                • -
                • 如果结果集现在还是空的,获取被标注的配置类所在的包名并添加
                • -
                -
                -

                最后一条规则就是“默认情况下扫描配置类所在的包”的说法由来,并且根据代码可知,如果主动设置了值,这条规则就不起作用了。

                -
                -
                public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
                Assert.state(this.environment != null, "Environment must not be null");
                Assert.state(this.resourceLoader != null, "ResourceLoader must not be null");
                // 创建 ClassPathBeanDefinitionScanner
                ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
                // Bean 名称生成器
                Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
                boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
                scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                BeanUtils.instantiateClass(generatorClass));

                ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
                if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
                scanner.setScopedProxyMode(scopedProxyMode);
                }
                else {
                Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
                scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
                }

                scanner.setResourcePattern(componentScan.getString("resourcePattern"));
                // 设置 Filter
                for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addIncludeFilter(typeFilter);
                }
                }
                for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addExcludeFilter(typeFilter);
                }
                }
                // 是否懒加载
                boolean lazyInit = componentScan.getBoolean("lazyInit");
                if (lazyInit) {
                scanner.getBeanDefinitionDefaults().setLazyInit(true);
                }
                // 设置 basePackages(使用 Set 去重)
                Set<String> basePackages = new LinkedHashSet<String>();
                // 获取设置的 basePackages 值
                String[] basePackagesArray = componentScan.getStringArray("basePackages");
                for (String pkg : basePackagesArray) {
                // 允许占位符
                String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
                basePackages.addAll(Arrays.asList(tokenized));
                }
                // 获取 basePackageClasses,本质上是为了获取它们所在的包名
                for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                }
                // 如果为空,获取被标注的配置类所在的包名
                if (basePackages.isEmpty()) {
                basePackages.add(ClassUtils.getPackageName(declaringClass));
                }
                // 排除被标注的配置类本身
                scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
                @Override
                protected boolean matchClassName(String className) {
                return declaringClass.equals(className);
                }
                });
                // 执行扫描
                return scanner.doScan(StringUtils.toStringArray(basePackages));
                }
                +
                public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
                Assert.state(this.environment != null, "Environment must not be null");
                Assert.state(this.resourceLoader != null, "ResourceLoader must not be null");
                // 创建 ClassPathBeanDefinitionScanner
                ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
                // Bean 名称生成器
                Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
                boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
                scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                BeanUtils.instantiateClass(generatorClass));

                ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
                if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
                scanner.setScopedProxyMode(scopedProxyMode);
                }
                else {
                Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
                scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
                }

                scanner.setResourcePattern(componentScan.getString("resourcePattern"));
                // 设置 Filter
                for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addIncludeFilter(typeFilter);
                }
                }
                for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
                for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addExcludeFilter(typeFilter);
                }
                }
                // 是否懒加载
                boolean lazyInit = componentScan.getBoolean("lazyInit");
                if (lazyInit) {
                scanner.getBeanDefinitionDefaults().setLazyInit(true);
                }
                // 设置 basePackages(使用 Set 去重)
                Set<String> basePackages = new LinkedHashSet<String>();
                // 获取设置的 basePackages 值
                String[] basePackagesArray = componentScan.getStringArray("basePackages");
                for (String pkg : basePackagesArray) {
                // 允许占位符
                String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
                basePackages.addAll(Arrays.asList(tokenized));
                }
                // 获取 basePackageClasses,本质上是为了获取它们所在的包名
                for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                }
                // 如果为空,获取被标注的配置类所在的包名
                if (basePackages.isEmpty()) {
                basePackages.add(ClassUtils.getPackageName(declaringClass));
                }
                // 排除被标注的配置类本身
                scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
                @Override
                protected boolean matchClassName(String className) {
                return declaringClass.equals(className);
                }
                });
                // 执行扫描
                return scanner.doScan(StringUtils.toStringArray(basePackages));
                }

                parse 方法与其说是解析,不如说是封装了一些设置并最终调用 ClassPathBeanDefinitionScanner,而设置的属性值来源于 @ComponentScan 的注解属性。关于获取 @ComponentScan 的注解属性的方法 AnnotationConfigUtils.attributesForRepeatable 在分析 @PropertySource 时也曾经遇到过,顾名思义我们知道它应该是用于获取可重复的注解的属性。可是它和直接获取注解对象有什么区别呢?

                @@ -4932,6 +4892,78 @@ spring boot + + Unsafe,一个“反 Java”的 class + /2023/12/25/Unsafe-an-anti-Java-class/ + Unsafe 类位于 sun.misc 包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe 类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CASCompare and Swap)操作等。本文将介绍这个“反 Java”的类及其方法的典型使用场景。

                + + +
                +

                由于 Unsafe 类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过 Java 语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用 Unsafe 类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用 Unsafe 类来进行一些性能优化。

                +
                +
                +

                尽管在生产中需要谨慎使用 Unsafe,但是可以在测试中使用它来更真实地接触 Java 对象在内存中的存储结构,验证自己的理论知识。

                +
                +

                获取 Unsafe 实例

                +

                Java 9 及之后的版本中,Unsafe 类中的 getUnsafe() 方法被标记为不安全(Unsafe),不再允许普通的 Java 应用程序代码通过此方法获取 Unsafe 实例。这是为了提高 Java 的安全性,防止滥用 Unsafe 类的功能。

                +
                +

                在正常的 Java 应用程序中,获取 Unsafe 实例是不被推荐的,因为它违反了 Java 语言的安全性和封装原则。Unsafe 类的设计本意是为了 Java 库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe 对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe 对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。

                +

                获取 Unsafe 实例的静态方法如下:

                +
                @CallerSensitive
                public static Unsafe getUnsafe() {
                Class<?> caller = Reflection.getCallerClass();
                // 检查调用方法的类是被引导类加载器所加载
                if (!VM.isSystemDomainLoader(caller.getClassLoader()))
                throw new SecurityException("Unsafe");
                return theUnsafe;
                }
                + +

                Unsafe 使用单例模式,可以通过静态方法 getUnsafe 获取 Unsafe 实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe 实例有以下两种可行方案:

                +
                  +
                1. 通过 -Xbootclasspath/a:${path} 把调用方法的类所在的 jar 包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径
                2. +
                3. 通过反射获取 Unsafe 类中的 Unsafe 实例
                  private static Unsafe getUnsafe() {
                  try {
                  Field f = Unsafe.class.getDeclaredField("theUnsafe");
                  f.setAccessible(true);
                  return (Unsafe) f.get(null);
                  } catch (NoSuchFieldException | IllegalAccessException e) {
                  throw new RuntimeException(e);
                  }
                  }
                4. +
                +

                内存操作

                Unsafe 类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java 语言的内置安全性和类型检查。以下是一些常见的 Unsafe 类中关于内存操作的方法:

                +
                  +
                • allocateMemory: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。
                  public native long allocateMemory(long bytes);
                • +
                • reallocateMemory: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。
                  public native long reallocateMemory(long address, long bytes);
                • +
                • freeMemory: 释放之前由 allocateMemoryreallocateMemory 分配的内存。
                  public native void freeMemory(long address);
                • +
                • setMemory: 将给定内存块中的所有字节设置为固定值(通常为零)。
                  public native void setMemory(Object o, long offset, long bytes, byte value);
                  public void setMemory(long address, long bytes, byte value) {
                  setMemory(null, address, bytes, value);
                  }
                • +
                • copyMemory: 复制指定长度的内存块
                  public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
                • +
                • putXxx: 将指定偏移量处的内存设置为指定的值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                  public native void putObject(Object o, long offset, Object x);
                • +
                • getXxx: 从指定偏移量处的内存读取值,其中 Xxx 可以是 Objectintlongfloatdouble 等。
                  public native Object getObject(Object o, long offset);
                • +
                • putXxxgetXxx 也提供了按绝对基地址操作内存的方法。
                  public native byte getByte(long address);
                  public native void putByte(long address, byte x);
                • +
                +

                从内存读取值时,除非满足以下情况之一,否则结果不确定:

                +
                  +
                1. 偏移量是通过 objectFieldOffset 从字段的 Field 对象获取的,o 指向的对象的类与字段所属的类兼容。
                2. +
                3. 偏移量和 o 指向的对象(无论是否为 null)分别是通过 staticFieldOffsetstaticFieldBaseField 对象获得的。
                4. +
                5. o 指向的是一个数组,偏移量是一个形式为 B+N*S 的整数,其中 N 是数组的有效索引,BS 分别是通过 arrayBaseOffsetarrayIndexScale 获得的值。
                6. +
                +
                +

                做一些“不确定”的测试,比如使用 byte 相关的方法操作 int 所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。

                +
                +

                分配堆外内存

                Java NIONew I/O)中,分配堆外内存使用了 Unsafe 类的 allocateMemory 方法。堆外内存是一种在 Java 虚拟机之外分配的内存,它不受 Java 堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O 操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java 堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting)的机制来处理。

                +
                DirectByteBuffer(int cap) {
                super(-1, 0, cap, cap);
                boolean pa = VM.isDirectMemoryPageAligned();
                int ps = Bits.pageSize();
                long size = Math.max(1L, (long)cap + (pa ? ps : 0));
                Bits.reserveMemory(size, cap);

                long base = 0;
                try {
                // 分配本地内存
                base = unsafe.allocateMemory(size);
                } catch (OutOfMemoryError x) {
                Bits.unreserveMemory(size, cap);
                throw x;
                }
                // 初始化本地内存
                unsafe.setMemory(base, size, (byte) 0);
                if (pa && (base % ps != 0)) {
                // Round up to page boundary
                address = base + ps - (base & (ps - 1));
                } else {
                address = base;
                }
                // 使用虚引用 Cleaner 对象跟踪 DirectByteBuffer 对象的垃圾回收
                cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
                att = null;
                }
                + +

                DirectByteBuffer 对象仅被 Cleaner 对象(虚引用)引用时,它可以在任意一次 GC 中被垃圾回收。在 DirectByteBuffer 对象被垃圾回收后,Cleaner 对象会被加入到引用队列,ReferenceHandler 线程将调用 Deallocator 对象的 run 方法,从而实现本地内存的自动释放。

                +
                private static class Deallocator implements Runnable {

                private static Unsafe unsafe = Unsafe.getUnsafe();

                private long address;
                private long size;
                private int capacity;

                private Deallocator(long address, long size, int capacity) {
                assert (address != 0);
                this.address = address;
                this.size = size;
                this.capacity = capacity;
                }

                public void run() {
                if (address == 0) {
                // Paranoia
                return;
                }
                // 释放本地内存
                unsafe.freeMemory(address);
                address = 0;
                Bits.unreserveMemory(size, capacity);
                }

                }
                + +

                CAS 相关

                Unsafe 提供了 3CAS 相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU 会自动将该位置更新为新值,否则,CPU 不做任何操作。这些方法的底层实现对应着 CPU 指令 cmpxchg

                +
                // 如果 Java 变量当前符合预期,则自动将其更新为 x。
                public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
                public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
                public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
                + +

                AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 UnsafeobjectFieldOffset 方法获取。

                +
                public class AtomicInteger extends Number implements java.io.Serializable {
                private static final long serialVersionUID = 6214790243416807050L;

                // setup to use Unsafe.compareAndSwapInt for updates
                private static final Unsafe unsafe = Unsafe.getUnsafe();
                private static final long valueOffset;

                static {
                try {
                valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
                } catch (Exception ex) { throw new Error(ex); }
                }

                private volatile int value;
                }
                + +

                CAS 更新变量的值的内存变化如下:

                +
                + +

                配合 ClassLayout 打印 AtomicInteger 的内部结构更直观地感受 offset 的含义:

                +
                java.util.concurrent.atomic.AtomicInteger object internals:
                OFF SZ TYPE DESCRIPTION VALUE
                0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
                8 4 (object header: class) 0xf8003dbc
                12 4 int AtomicInteger.value 1
                Instance size: 16 bytes
                Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                + + + +

                参考文章

                +]]>
                + + java + +
                Java 类 Reference 的源码分析 /2023/12/27/source-code-analysis-of-Java-class-Reference/ @@ -5038,53 +5070,177 @@ - synchronized 锁机制的分析和验证 - /2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/ - 本文详细介绍了 Javasynchronized 锁的机制、存储结构、优化措施以及升级过程,并通过 jol-core 演示 Mark Word 的变化来验证锁升级的多个 case

                + 探索 Java 类 Cleaner 和 Finalizer + /2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/ + JavaCleanerFinalizer 都实现了一种 finalization 机制,前者更轻量和强大,你可能在了解 NIO 的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize 方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是 Reference 的具体子类。
                Reference 作为和 GC 紧密联系的类,你可能从很多文字描述中了解过 SoftReferenceWeakReference 还有 PhantomReference 但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从 Root 对象可达”这条规则再面对 Reference 的子类时是否产生过割裂感;你是否好奇过 Finalizer 如何和重写 finalize 方法的类产生联系,本文将从 CleanerFinalizer 的源码揭示一些你可能已知的结论背后的朴素原理。

                -

                待完善

                +

                本文的写作动机继承自 Java 类 Reference 的源码分析,有时候也会自我怀疑研究一个涉及大家极力劝阻使用的 finalize 是否浪费精力,只能说确实如此!要不是半途而废会膈应难受肯定就停了!只能说这个过程确实帮助自己对 Java 引用和 GC 对其的处理有更加深刻的理解。

                -

                利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

                +

                虚引用之 Cleaner

                虚引用介绍

                PhantomReference 对象在垃圾收集器确定其关联对象可以被回收时或可以被回收后一段时间,将被入队。“可以被回收”更明确的描述是“虚引用的关联对象变成 phantom reachable ,即只有虚引用引用了它”。但是和软引用和弱引用不同,当虚引用入队时并不会被垃圾收集器自动清理(其关联对象)。一个 phantom reachable 的对象会一直维持原样直到所有虚引用被清理或者它们自身变得不可达。

                +

                PhantomReference 的代码非常简单:

                +
                  +
                1. PhantomReference 仅提供了一个 public 构造函数,必须提供 ReferenceQueue 参数。它不像 SoftReferenceWeakReference 可以离开 ReferenceQueue 单独使用,尽管 queue 可以为 null,但是这样做并没有意义。
                2. +
                3. get() 返回 null,这意味着不能通过 PhantomReference 获取其关联的对象 referent
                4. +
                +
                +

                get() 返回 null 并不是可以随意忽略的事情,它保证了 phantom reachable 对象不会被重新触达和修改(这是为清理工作留出时间吗)。

                +
                +
                public class PhantomReference<T> extends Reference<T> {
                public T get() {
                return null;
                }
                public PhantomReference(T referent, ReferenceQueue<? super T> q) {
                super(referent, q);
                }
                }
                + +

                通过以下示例验证 GC 不会自动清理虚引用的关联对象:

                +
                public static void main(String[] args) throws InterruptedException {
                Scanner scanner = new Scanner(System.in);

                byte[] bytes = new byte[100 * 1024 * 1024];
                ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
                PhantomReference<byte[]> phantomReference = new PhantomReference<>(bytes, queue);

                Thread thread = new Thread(() -> {
                for (; ; ) {
                try {
                Reference<? extends byte[]> remove = queue.remove(0);
                System.out.println(remove + " enqueued");
                // 需要调用 clear 主动清理关联对象,可以验证 gc 后总堆内存占用下降
                // remove.clear();
                // System.gc();
                } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " interrupt");
                break;
                }
                }
                });
                thread.start();

                System.out.println("暂停查看堆内存占用");
                scanner.next();

                bytes = null;
                System.gc();
                System.out.println("gc 后 sleep 3s,查看总堆内存占用未下降");
                TimeUnit.SECONDS.sleep(3);

                scanner.next();
                thread.interrupt();
                }
                + +

                Cleaner 介绍

                虚引用最常用于以比 finalization 更灵活的方式安排清理工作,比如其子类 Cleaner 就是一种基于虚引用的清理器,它比 finalization 更轻量但更强大。Cleaner 追踪其关联对象并封装任意的清理代码,在 GC 检测到其关联对象变成 phantom reachable 后一段时间,Reference-Handler 线程将运行清理代码。同时 Cleaner 可以被直接调用,它是线程安全的并且可以保证清理代码最多运行一次。但是 Cleaner 不是 finalization 的替代品,为了避免阻塞 Reference-Handler 线程,清理代码应极其简单和直接。

                +

                构造函数

                Cleaner 的构造函数为 private,仅可通过 create 方法创建实例。

                  -
                • 对于普通同步方法,锁是当前实例对象。
                • -
                • 对于静态同步方法,锁是当前类的 Class 对象。
                • -
                • 对于同步方法块,锁是 synchronized 括号里配置的对象。
                • +
                • referent: 关联对象
                • +
                • dummyQueue: 假队列,需要它仅仅是因为 PhantomReference 的构造函数需要一个 queue 参数,但是这个 queue 完全没用,在 ReferenceReference-Handler 线程会显式调用 cleaners 而不会执行入队操作。
                -

                当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

                +
                public class Cleaner extends PhantomReference<Object> {
                private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
                private final Runnable thunk;

                private Cleaner(Object referent, Runnable thunk) {
                super(referent, dummyQueue);
                this.thunk = thunk;
                }

                public static Cleaner create(Object ob, Runnable thunk) {
                if (thunk == null)
                return null;
                // 添加到 Cleaner 自身维护的双链表
                return add(new Cleaner(ob, thunk));
                }
                }
                + +

                添加 cleaner

                  +
                • 使用 synchronized 同步
                • +
                • Cleaner 自身维护一个双向链表存储 cleaners,通过静态变量 first 存储头节点,以防止 cleaners 比其关联对象更早被 GC
                • +
                +
                + +
                // 头节点
                static private Cleaner first = null;
                // 双向指针
                private Cleaner next = null, prev = null;
                private static synchronized Cleaner add(Cleaner cl) {
                // 头插法
                if (first != null) {
                cl.next = first;
                first.prev = cl;
                }
                first = cl;
                return cl;
                }
                + +

                clean 方法

                ReferenceReference-Handler 线程对于 Cleaner 类型的对象,会显式地调用其 clean 方法并返回,而不会将其入队。

                +
                  +
                1. 使用 synchronized 同步,从双链表上移除自身
                2. +
                3. 调用 thunkrun 方法
                4. +
                +
                public void clean() {
                if (!remove(this))
                return;
                try {
                thunk.run();
                } catch (final Throwable x) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                if (System.err != null)
                new Error("Cleaner terminated abnormally", x).printStackTrace();
                System.exit(1);
                return null;
                }
                });
                }
                }

                private static synchronized boolean remove(Cleaner cl) {
                // next 指针指向自身代表已经移除,可以避免重复移除和执行
                if (cl.next == cl)
                return false;
                // 更新双链表
                if (first == cl) {
                if (cl.next != null)
                first = cl.next;
                else
                first = cl.prev;
                }
                if (cl.next != null)
                cl.next.prev = cl.prev;
                if (cl.prev != null)
                cl.prev.next = cl.next;
                // 通过将 next 指针指向自身表示已经被移除
                cl.next = cl;
                cl.prev = cl;
                return true;
                }
                + +

                Cleaner 处理流程

                  +
                • 创建的 Cleaner 对象被 Cleaner 类的双链表直接或间接引用(强引用),因此不会被垃圾回收
                • +
                • 一切的起点仍然是 GC 特殊地对待虚引用的关联对象,当关联对象从 reachable 变成 phantom reachableGCCleaner 对象将加入 pending-list
                • +
                • Reference-Handler 线程又将其移除并调用 clean 方法
                • +
                • 在调用完毕后,Cleaner 对象变成 unreachable 并最终被垃圾回收,其关联对象也被垃圾回收
                • +
                +
                +

                注意,Cleaner 对象本身在被调用完毕之前始终是被静态变量引用,是 reachable 的,我们讨论的被判定为可回收的、变成 phantom reachable 状态的是关联对象。

                +
                +
                + +
                +

                事实上,个人猜测“虚引用的关联对象不像软引用和弱引用会被自动清理”描述的仅仅是一个表象,判断是否要被垃圾回收的根本法则仍然是“对象是否从 Root 对象可达”,软引用和弱引用的关联对象之所以会被垃圾回收是因为它们在加入 pending-list 时被从引用对象断开,否则当引用对象被添加到引用队列时,引用队列如果从 Root 对象可达,将导致关联对象也从 Root 对象可达。在 Referenceclear() 的注释中提及该方法只被 Java 代码调用,GC 不需要调用该方法就可以直接清理,肯定是 GC 有直接清理关联对象的场景。同时 Reference 类有一句注释“GC 在检测到关联对象有特定的可达性变化后,将把引用对象添加到引用队列”,它并未将特定的可达性变化直接描述为关联对象变为不可达。目前尚未从 JVM 源代码验证该猜测。

                +
                +

                终结引用之 Finalizer

                FinalReference 用于实现 finalization,其代码很简单。

                +
                class FinalReference<T> extends Reference<T> {
                public FinalReference(T referent, ReferenceQueue<? super T> q) {
                super(referent, q);
                }
                }
                + +

                其子类 Finalizer 继承自 FinalReferenceCleaner 在代码设计上和它非常相似。

                +

                构造函数

                Finalizer 的构造函数为 private,仅可通过 register 方法创建实例。

                  -
                • JVM 层面,synchronized 锁是基于进入和退出 Monitor 来实现的,每一个对象都有一个 Monitor 与之相关联。
                • -
                • 在字节码层面,同步方法块是使用 monitorentermonitorexit 指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。
                • +
                • finalizee: 关联对象,即重写了 finalize 方法的类的实例
                • +
                • queue: 引用队列
                -

                存储结构

                -

                锁存在哪里呢?锁里面又会存储什么信息呢?

                +
                +

                根据注释 registerVM 调用,我们可以合理猜测,这里就是重写了 finalize 方法的类的实例和 Finalizer 对象关联的起点。

                -

                对象头

                synchronized 用的锁是存在 Java 对象头(object header)里的。如果对象是数组类型,则虚拟机用 3 字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。在 64 位虚拟机中,1 字宽等于 8 字节,即 64bit

                -

                Java 对象头的组成结构如下:

                - - - - - - - - - - - - - - - - - - - - - - - +
                长度内容说明
                32/64bitMark Word存储对象的 hashCode 或锁信息
                32/64bitClass Metadata Address存储指向对象类型数据的指针
                32/64bitArray 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;
                }
                + +

                SharedSecretsjavaLangAccess 通过 setJavaLangAccess 设置

                +
                public static void setJavaLangAccess(JavaLangAccess jla) {
                javaLangAccess = jla;
                }
                public static JavaLangAccess getJavaLangAccess() {
                return javaLangAccess;
                }
                + +

                setJavaLangAccess 方法在 System 中被调用,javaLangAccess 被设置为一个匿名类实例,其中 invokeFinalize 方法间接调用了传入对象的 finalize 方法。

                +
                private static void setJavaLangAccess() {
                // Allow privileged classes outside of java.lang
                sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
                // ...
                public void invokeFinalize(Object o) throws Throwable {
                o.finalize();
                }
                });
                }
                + +

                SystemsetJavaLangAccess 方法在 initializeSystemClass 方法中被调用。这里正对应着 FinalizerThreadrun 方法中等待 VM 初始化完成的处理。

                +
                // 初始化 System class,在线程初始化之后调用
                private static void initializeSystemClass() {
                // ...
                // register shared secrets
                setJavaLangAccess();
                // 通知 wait 的线程
                sun.misc.VM.booted();
                }
                + +

                Finalizer 的注册时机

                你是否好奇过 JVM 是如何保证 finalize 方法最多被调用一次的?如果曾经猜测过 JVM 可能在对象中留有标记,那么在我们研究过对象的内部结构之后可以确认其中并没有用于记录对象是否已经 finalized 的地方。同时我们注意到 hasBeenFinalized 方法通过 next 指针是否指向自己表示是否已经 finalized。我们可以合理猜测 register 的调用时机是在对象创建时,因此最多仅有一次被注册。

                +

                通过以下示例可以测试:

                +
                  +
                • 在创建重写了 finalize 方法的类创建对象期间会调用 register 创建并注册 Finalizer
                • +
                • 在未重写 finalize 方法的类创建对象期间不会调用register
                • +
                • Finalizer 不仅可以保证 finalize 只会被调用一次,甚至不会第二次被添加到 pending-list,因为 runFinalizer 最后调用了 super.clear()JVM 不会特殊对待复活的对象
                • +
                +
                public class FinalReferenceTest_1 {

                private static FinalizeObj save = null;

                public static void main(String[] args) throws InterruptedException {
                System.out.println("创建 finalize obj,使用 Debug 强制运行到 Finalizer.register");
                FinalizeObj finalizeObj = new FinalizeObj();

                System.out.println("gc");
                finalizeObj = null;
                System.gc();
                System.out.println("sleep 1s");
                TimeUnit.SECONDS.sleep(1);
                save.echo();

                save = null;
                System.gc();
                System.out.println("sleep 1s");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(save == null);
                }

                static class FinalizeObj {
                FinalizeObj() {
                System.out.println("SaveSelf created");
                }
                @Override
                protected void finalize() throws Throwable {
                System.out.println("finalized");
                save = this;
                }
                public void echo() {
                System.out.println("I am alive.");
                }
                }
                }
                + + + + +

                参考文章

                +]]> + + java + + + + synchronized 锁机制的分析和验证 + /2023/12/19/analysis-and-verification-of-the-synchronized-lock-mechanism/ + 本文详细介绍了 Javasynchronized 锁的机制、存储结构、优化措施以及升级过程,并通过 jol-core 演示 Mark Word 的变化来验证锁升级的多个 case

                + + +
                +

                待完善

                +
                +

                利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

                +
                  +
                • 对于普通同步方法,锁是当前实例对象。
                • +
                • 对于静态同步方法,锁是当前类的 Class 对象。
                • +
                • 对于同步方法块,锁是 synchronized 括号里配置的对象。
                • +
                +

                当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

                +
                  +
                • JVM 层面,synchronized 锁是基于进入和退出 Monitor 来实现的,每一个对象都有一个 Monitor 与之相关联。
                • +
                • 在字节码层面,同步方法块是使用 monitorentermonitorexit 指令实现的,前者在编译后插入到同步方法块的开始位置,后者插入到同步方法块的结束位置和异常位置。
                • +
                +

                存储结构

                +

                锁存在哪里呢?锁里面又会存储什么信息呢?

                +
                +

                对象头

                synchronized 用的锁是存在 Java 对象头(object header)里的。如果对象是数组类型,则虚拟机用 3 字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit。在 64 位虚拟机中,1 字宽等于 8 字节,即 64bit

                +

                Java 对象头的组成结构如下:

                + + + + + + + + + + + + + + + + + + + + + + +
                长度内容说明
                32/64bitMark Word存储对象的 hashCode 或锁信息
                32/64bitClass Metadata Address存储指向对象类型数据的指针
                32/64bitArray length数组的长度(如果当前对象是数组)

                Mark Word

                Java 对象头里的 Mark Word 里默认存储对象的 HashCode,分代年龄和锁标记位。在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为另外 4 种数据。

                32 位虚拟机为例:

                @@ -5454,494 +5610,545 @@
                - 探索 Java 类 Cleaner 和 Finalizer - /2023/12/28/explore-the-Java-classes-Cleaner-and-Finalizer/ - JavaCleanerFinalizer 都实现了一种 finalization 机制,前者更轻量和强大,你可能在了解 NIO 的堆外内存自动释放机制中注意过它;后者为人所诟病,finalize 方法被人强烈反对使用。本文想要解析它们的原因不在于它们实现的功能,而在于它们是 Reference 的具体子类。
                Reference 作为和 GC 紧密联系的类,你可能从很多文字描述中了解过 SoftReferenceWeakReference 还有 PhantomReference 但是却很少从代码层面了解过它们,当你牢记“一个对象是否可以被回收的判断依据是它是否从 Root 对象可达”这条规则再面对 Reference 的子类时是否产生过割裂感;你是否好奇过 Finalizer 如何和重写 finalize 方法的类产生联系,本文将从 CleanerFinalizer 的源码揭示一些你可能已知的结论背后的朴素原理。

                + 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 的代码非常简单:

                -
                  -
                1. PhantomReference 仅提供了一个 public 构造函数,必须提供 ReferenceQueue 参数。它不像 SoftReferenceWeakReference 可以离开 ReferenceQueue 单独使用,尽管 queue 可以为 null,但是这样做并没有意义。
                2. -
                3. get() 返回 null,这意味着不能通过 PhantomReference 获取其关联的对象 referent
                4. -
                +
                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 为例,它在构造函数中创建了 AnnotatedBeanDefinitionReaderAnnotatedBeanDefinitionReader 又在构造函数中注册了基于注解配置的处理器:

                +
                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 完全没用,在 ReferenceReference-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 方法

                  ReferenceReference-Handler 线程对于 Cleaner 类型的对象,会显式地调用其 clean 方法并返回,而不会将其入队。

                  -
                    -
                  1. 使用 synchronized 同步,从双链表上移除自身
                  2. -
                  3. 调用 thunkrun 方法
                  4. -
                  -
                  public void clean() {
                  if (!remove(this))
                  return;
                  try {
                  thunk.run();
                  } catch (final Throwable x) {
                  AccessController.doPrivileged(new PrivilegedAction<Void>() {
                  public Void run() {
                  if (System.err != null)
                  new Error("Cleaner terminated abnormally", x).printStackTrace();
                  System.exit(1);
                  return null;
                  }
                  });
                  }
                  }

                  private static synchronized boolean remove(Cleaner cl) {
                  // next 指针指向自身代表已经移除,可以避免重复移除和执行
                  if (cl.next == cl)
                  return false;
                  // 更新双链表
                  if (first == cl) {
                  if (cl.next != null)
                  first = cl.next;
                  else
                  first = cl.prev;
                  }
                  if (cl.next != null)
                  cl.next.prev = cl.prev;
                  if (cl.prev != null)
                  cl.prev.next = cl.next;
                  // 通过将 next 指针指向自身表示已经被移除
                  cl.next = cl;
                  cl.prev = cl;
                  return true;
                  }
                  +
                  @Override
                  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 reachableGCCleaner 对象将加入 pending-list
                  • -
                  • Reference-Handler 线程又将其移除并调用 clean 方法
                  • -
                  • 在调用完毕后,Cleaner 对象变成 unreachable 并最终被垃圾回收,其关联对象也被垃圾回收
                  • +

                    查找自动装配元数据

                    +

                    这部分代码体现了注入(Injection)和自动装配(Autowiring)的等价性。InjectionMetadataAutowiringMetadata 的含义是用于注入(自动装配)的元数据。

                    +
                    +

                    InjectionMetadata 是用于管理注入元数据的内部类,不适合直接在应用程序中使用。它和 Class 是一对一的关系,封装了需要被注入的元素 InjectedElement。一个 InjectedElement 对应着一个字段(Field)或一个方法(Method),分别对应着两个实现类 AutowiredFieldElementAutowiredMethodElement。这里再次体现了被注入、被自动装配的语义。

                    +

                    查找自动装配元数据的过程如下:

                    +
                      +
                    • 先从缓存中获取,如果存在且不需要刷新,则直接返回结果
                    • +
                    • 否则构建自动装配元数据并放入缓存
                    -

                    注意,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 对象可达。在 Referenceclear() 的注释中提及该方法只被 Java 代码调用,GC 不需要调用该方法就可以直接清理,肯定是 GC 有直接清理关联对象的场景。同时 Reference 类有一句注释“GC 在检测到关联对象有特定的可达性变化后,将把引用对象添加到引用队列”,它并未将特定的可达性变化直接描述为关联对象变为不可达。目前尚未从 JVM 源代码验证该猜测。

                    +

                    这个处理顺序意味着在注入时方法的优先级高于字段,前者会覆盖后者。

                    -

                    终结引用之 Finalizer

                    FinalReference 用于实现 finalization,其代码很简单。

                    -
                    class FinalReference<T> extends Reference<T> {
                    public FinalReference(T referent, ReferenceQueue<? super T> q) {
                    super(referent, q);
                    }
                    }
                    +
                    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() {
                    @Override
                    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() {
                    @Override
                    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 继承自 FinalReferenceCleaner 在代码设计上和它非常相似。

                    -

                    构造函数

                    Finalizer 的构造函数为 private,仅可通过 register 方法创建实例。

                    +

                    注入

                    对于字段来说,注入意味着将一个解析得到的 value 通过反射设置到字段中;对于方法来说,注入意味着解析得到方法参数的 value,然后通过反射调用方法。

                    +

                    InjectionMetadatainject 方法比较简单,内部会遍历并调用 InjectedElementinject 方法,AutowiredFieldElementAutowiredMethodElement 各自实现了 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 还是 AutowiredMethodElementinject 的过程都比较相似:

                      -
                    • finalizee: 关联对象,即重写了 finalize 方法的类的实例
                    • -
                    • queue: 引用队列
                    • +
                    • 都使用 DependencyDescriptor 描述即将被注入的特定依赖项,DependencyDescriptor 包装了构造函数参数、方法参数或者字段,允许以统一的方式访问它们的元数据
                    • +
                    • 都会缓存 DependencyDescriptor
                    • +
                    • 都会记录自动装配的 bean,用于判断是否使用 DependencyDescriptor 的变体 ShortcutDependencyDescriptor 优化缓存
                    • +
                    • 都通过 beanFactory.resolveDependency 解析依赖
                    -
                    -

                    根据注释 registerVM 调用,我们可以合理猜测,这里就是重写了 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;
                    }

                    @Override
                    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;
                    }

                    @Override
                    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 这个命名不太理解,指的是通过配置实现注入的 MemberFiledMethod 的父类)吗?

                    +
                    +

                    检测配置成员,如果不是外部管理的配置成员,则注册为外部管理的配置成员。在合并后的 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 常用的快捷键作为备忘清单。

                    + -

                    SharedSecretsjavaLangAccess 通过 setJavaLangAccess 设置

                    -
                    public static void setJavaLangAccess(JavaLangAccess jla) {
                    javaLangAccess = jla;
                    }
                    public static JavaLangAccess getJavaLangAccess() {
                    return javaLangAccess;
                    }
                    - -

                    setJavaLangAccess 方法在 System 中被调用,javaLangAccess 被设置为一个匿名类实例,其中 invokeFinalize 方法间接调用了传入对象的 finalize 方法。

                    -
                    private static void setJavaLangAccess() {
                    // Allow privileged classes outside of java.lang
                    sun.misc.SharedSecrets.setJavaLangAccess(new sun.misc.JavaLangAccess(){
                    // ...
                    public void invokeFinalize(Object o) throws Throwable {
                    o.finalize();
                    }
                    });
                    }
                    - -

                    SystemsetJavaLangAccess 方法在 initializeSystemClass 方法中被调用。这里正对应着 FinalizerThreadrun 方法中等待 VM 初始化完成的处理。

                    -
                    // 初始化 System class,在线程初始化之后调用
                    private static void initializeSystemClass() {
                    // ...
                    // register shared secrets
                    setJavaLangAccess();
                    // 通知 wait 的线程
                    sun.misc.VM.booted();
                    }
                    - -

                    Finalizer 的注册时机

                    你是否好奇过 JVM 是如何保证 finalize 方法最多被调用一次的?如果曾经猜测过 JVM 可能在对象中留有标记,那么在我们研究过对象的内部结构之后可以确认其中并没有用于记录对象是否已经 finalized 的地方。同时我们注意到 hasBeenFinalized 方法通过 next 指针是否指向自己表示是否已经 finalized。我们可以合理猜测 register 的调用时机是在对象创建时,因此最多仅有一次被注册。

                    -

                    通过以下示例可以测试:

                    -
                      -
                    • 在创建重写了 finalize 方法的类创建对象期间会调用 register 创建并注册 Finalizer
                    • -
                    • 在未重写 finalize 方法的类创建对象期间不会调用register
                    • -
                    • Finalizer 不仅可以保证 finalize 只会被调用一次,甚至不会第二次被添加到 pending-list,因为 runFinalizer 最后调用了 super.clear()JVM 不会特殊对待复活的对象
                    • -
                    -
                    public class FinalReferenceTest_1 {

                    private static FinalizeObj save = null;

                    public static void main(String[] args) throws InterruptedException {
                    System.out.println("创建 finalize obj,使用 Debug 强制运行到 Finalizer.register");
                    FinalizeObj finalizeObj = new FinalizeObj();

                    System.out.println("gc");
                    finalizeObj = null;
                    System.gc();
                    System.out.println("sleep 1s");
                    TimeUnit.SECONDS.sleep(1);
                    save.echo();

                    save = null;
                    System.gc();
                    System.out.println("sleep 1s");
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(save == null);
                    }

                    static class FinalizeObj {
                    FinalizeObj() {
                    System.out.println("SaveSelf created");
                    }
                    @Override
                    protected void finalize() throws Throwable {
                    System.out.println("finalized");
                    save = this;
                    }
                    public void echo() {
                    System.out.println("I am alive.");
                    }
                    }
                    }
                    - - - - -

                    参考文章

                    -]]>
                    - - java - -
                    - - 谈谈 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)等一个或多个现象。隔离级别越高,效率越低,因此很多时候,我们需要在二者之间寻找一个平衡点。

                    - +

                    常用快捷键

                    移动光标

                    - - - - + + - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                    隔离级别脏读不可重复读幻读快捷键功能
                    读未提交YYYh, 光标向左移动一个字符
                    读提交NYYj, 光标向下移动一个字符
                    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进入插入模式,在光标所在行的上一行插入新的一行
                    可重复读NNYr进入替换模式,只会替换光标所在的字符一次
                    串行化NNNR进入替换模式,替换光标所在的字符,直到通过Esc退出
                    Esc退出编辑模式,回到一般命令模式
                    -
                    -

                    读未提交和串行化很少在实际应用中使用。

                    -
                    -

                    通过以下示例说明隔离级别的影响,V1V2V3 在不同隔离级别下的值有所不同。

                    - +

                    保存和退出

                    - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + +
                    事务 A事务 B读未提交读提交可重复读串行化快捷键功能
                    开启事务开启事务
                    查询得到值 1
                    查询得到值 1:w保存文件
                    修改值为 2:w!若文件为只读,强制保存
                    查询得到值 V12(读到B未提交的修改)111:q退出 Vim,如果文件已修改,将退出失败
                    提交事务:q!强制退出 Vim,不保存文件修改
                    查询得到值 V222(读到B已提交的修改)11:wq保存文件并退出 Vim
                    提交事务:w filename另存为新文件
                    查询得到值 V3222(A在事务期间数据一致)1ZZ退出 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 中,事务隔离是通过 lockundo logread view 共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。

                    -
                    -

                    MySQL 各个事务隔离级别的实现原理简述如下:

                    -
                      -
                    • 串行化:读加共享锁,写加排他锁,读写互斥
                    • -
                    • 读未提交:写加排他锁,读不加锁
                    • -
                    • 可重复读:第一次读操作时创建快照,基于该快照进行读取
                    • -
                    • 读提交:每次读操作时重置快照,基于该快照进行读取
                    • -
                    -

                    前两者通过锁(lock)实现比较容易理解;后两者通过多版本并发控制(MVCC)实现。MVCC 是一种实现非阻塞并发读的设计思路,在 InnoDB 引擎中主要通过 undo logread view 实现。

                    -

                    以下示意图表现了在 InnoDB 引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC),当你基于快照读取时可以获得旧版本的数据。

                    -
                      -
                    • 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
                    • -
                    • 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
                    • -
                    -
                    - -

                    在接下来我们将通过锁、事务 ID、回滚日志和一致性视图逐步介绍 InnoDB 事务隔离的实现原理。

                    -

                    锁(lock)

                    事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。MySQL 正是通过共享锁排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC 就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。

                    -
                    -

                    InnoDB 的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议

                    -
                    -

                    理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及 MVCC 的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。

                    -

                    事务 ID

                    InnoDB 引擎中,每个事务都有唯一的一个事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段 trx_id,记录了插入或更新该行数据的事务 ID

                    -
                    - -

                    创建事务的时机

                    事务启动方式如下:

                    -
                      -
                    1. 显式启动事务语句是 beginstart transaction,配套的提交语句是 commit,回滚语句是 rollback
                    2. -
                    3. 隐式启动事务语句是 set autocommit = 0,该设置将关闭自动提交。当你执行 select,将自动启动一个事务,直到你主动 commitrollback
                    4. -
                    -

                    但注意,实际上不论是显式启动事务情况下的 beginstart transaction,还是隐式启动事务情况下的 commitrollback 都不会立即创建一个新事务,而是直到第一次操作 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 updateupdateinsertdelete
                      • -
                      -
                    • -
                    • 快照读:在不加锁的情况下通过 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光标移动到上一个词 (以空格分隔的词)
                    -

                    查找和替换

                    +

                    安装

                    +

                    参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!

                    +
                    +

                    步骤

                      +
                    1. 获取并运行 k3s 安装脚本。官方为中国用户提供了镜像加速支持。
                    $ curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -                                
                    [sudo] password for moralok:
                    [INFO] Finding release for channel stable
                    [INFO] Using v1.28.5+k3s1 as release
                    [INFO] Downloading hash rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/sha256sum-amd64.txt
                    [INFO] Downloading binary rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/k3s
                    [INFO] Verifying binary download
                    [INFO] Installing k3s to /usr/local/bin/k3s
                    [INFO] Skipping installation of SELinux RPM
                    [INFO] Creating /usr/local/bin/kubectl symlink to k3s
                    [INFO] Creating /usr/local/bin/crictl symlink to k3s
                    [INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
                    [INFO] Creating killall script /usr/local/bin/k3s-killall.sh
                    [INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
                    [INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
                    [INFO] systemd: Creating service file /etc/systemd/system/k3s.service
                    sh: 1014: restorecon: not found
                    sh: 1015: restorecon: not found
                    [INFO] systemd: Enabling k3s unit
                    Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
                    [INFO] systemd: Starting k3s
                    +
                  • 可以通过使用 kubectl 确认安装成功。刚安装的时候使用 kubectl 需要 root 权限。
                    $ sudo kubectl get node
                    NAME STATUS ROLES AGE VERSION
                    ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1
                  • +
                  • 实际上安装的就是一个 k3s 可执行文件,kubectlcrictl 只是软链接,指向 k3s
                    $ ls /usr/local/bin/
                    crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl
                  • +
      +
      +

      安装的信息中显示了 k3sservice fileenvironment file 的路径,后续修改启动参数和环境变量需要用到。

      +
      +

      配置文件权限问题

      在刚安装完 k3s 的时候,使用 kubectl 需要 root 权限。根据报错信息可知,是因为非 root 用户无法读取配置文件 /etc/rancher/k3s/k3s.yaml

      +
      $ kubectl get node                                                                                                                                            
      WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode to modify kube config permissions
      error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
      + +

      查看配置文件的信息可知其权限配置为 600,只有 root 用户具有读写权限。

      +
      $ ll /etc/rancher/k3s/k3s.yaml                                                                                               
      -rw------- 1 root root 2961 Jan 30 18:58 /etc/rancher/k3s/k3s.yaml
      + +

      一般来说,我们希望能够通过非 root 用户使用 kubectl,避免通过 root 用户或者通过 sudo 加输入密码的形式来使用 kubectl。那么如何解决这个问题呢?本质上这是一个 Linux 的文件权限问题,似乎修改文件的权限配置就可以解决。但是提示信息给出的解决方案并不是那么直接,它告诉我们通过修改 k3s server 的启动参数来达到修改配置文件权限的目的。这是因为 k3s 服务在每次重启时会根据启动参数和环境变量重置配置文件 /etc/rancher/k3s/k3s.yaml,手动修改文件的权限配置并不能优雅地解决这个问题,一旦服务重启,修改就会丢失。

      +
      +

      k3s 的 Github Discussions 中讨论了这个问题,并链接了文档 管理 Kubeconfig 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。

      +
      +

      修改启动参数

      第一种方式是修改启动参数。

      +
        +
      1. sudo vim /etc/systemd/system/k3s.service 添加 k3s 启动参数 --write-kubeconfig-mode 644
        ExecStart=/usr/local/bin/k3s \
        server --write-kubeconfig-mode 644 \
      2. +
      3. systemctl daemon-reload 重新加载 systemd 配置
      4. +
      5. systemctl restart k3s.service 重启服务
      6. +
      7. 验证修改生效
        $ ll /etc/rancher/k3s/k3s.yaml
        -rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml
      8. +
      +

      修改环境变量

      第二种方式是修改环境变量。

      +
        +
      1. sudo vim /etc/systemd/system/k3s.service.env 添加环境变量 K3S_KUBECONFIG_MODE=644
        K3S_KUBECONFIG_MODE=644
      2. +
      3. systemctl restart k3s.service 重启服务
      4. +
      +

      修改配置文件路径

      第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。

      +
        +
      1. 设置环境变量 export KUBECONFIG=~/.kube/config
      2. +
      3. 创建文件夹 mkdir ~/.kube 2> /dev/null
      4. +
      5. 复制配置信息 sudo k3s kubectl config view --raw > "$KUBECONFIG"
      6. +
      7. 修改配置文件的权限 chmod 600 "$KUBECONFIG"
      8. +
      +

      配置代理

      涉及 k8s,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及 k3s 安装脚本会自动使用当前 shell 中的 HTTP_PROXYHTTPS_PROXYNO_PROXY,以及 CONTAINERD_HTTP_PROXYCONTAINERD_HTTPS_PROXYCONTAINERD_NO_PROXY 变量(如果存在),并将它们写入 systemd 服务的环境文件。比如我设置过 shell 变量 HTTP_PROXYHTTPS_PROXYNO_PROXY/etc/systemd/system/k3s.service.env 如下,你也可以自行编辑修改。

      +
      http_proxy='http://127.0.0.1:7890'
      https_proxy='http://127.0.0.1:7890'
      no_proxy='localhost,127.0.0.1'
      K3S_KUBECONFIG_MODE=644
      + +

      使用

      +

      k8s 基础教程可参考官方文档 Kubernetes 基础

      +
      +

      创建 Deployment

        +
      1. 使用 kubectl create 命令创建管理 PodDeployment。该 Pod 根据提供的 Docker 镜像运行容器。
        kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
      2. +
      3. 查看 Deployment
        $ kubectl get pods
        NAME READY STATUS RESTARTS AGE
        hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h
      4. +
      5. 查看 Pod 中容器的应用程序日志。
        $ kubectl logs hello-node-ccf4b9788-d8k9b
        I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
        I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
      6. +
      +

      创建 Service

        +
      1. 使用 kubectl expose 命令将 Pod 暴露给公网:
        kubectl expose deployment hello-node --type=LoadBalancer --port=8080
      2. +
      3. 查看你创建的 Service
        $ kubectl get services
        NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
        kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 15h
        hello-node LoadBalancer 10.43.37.170 192.168.46.128 8080:32117/TCP 15h
      4. +
      5. 使用 curl 发起请求:
        $ curl http://localhost:8080
        NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511
      6. +
      7. 再次查看 Pod 中容器的应用程序日志。
        $ kubectl logs hello-node-ccf4b9788-d8k9b
        I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
        I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
        I0130 19:32:21.074992 1 log.go:195] GET /
      8. +
      +

      清理

        +
      1. 删除 Service
        kubectl delete service hello-node
      2. +
      3. 删除 Deployment
        kubectl delete deployment hello-node
      4. +
      +

      卸载

        +
      1. 从 server 节点卸载 k3s
        /usr/local/bin/k3s-uninstall.sh
      2. +
      +

      参考文章

      +]]>
      + + 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读未提交YYY
      :1,$s/original/replacement/g在第1行到最后一行之间查找original并替换为replacement读提交NYY
      :1,$s/original/replacement/gc在第1行到最后一行之间查找original并替换为replacement,替换前需确认可重复读NNY
      :%s/original/replacement在所有行中查找行中第一个出现的original并替换为replacement串行化NNN
      -

      替换格式如下 :[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重做上一个操作
      .重复前一个操作
      -

      进入编辑模式

      +

      通过以下示例说明隔离级别的影响,V1V2V3 在不同隔离级别下的值有所不同。

      +
      - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - + + + + + + -
      快捷键功能事务 A事务 B读未提交读提交可重复读串行化
      i进入插入模式,从光标所在处开始插入
      I进入插入模式,从光标所在行的第一个非空格开始插入
      a进入插入模式,从光标所在的下一个字符处开始插入
      A进入插入模式,从光标所在行的最后一个字符处开始插入
      o进入插入模式,在光标所在行的下一行插入新的一行
      O进入插入模式,在光标所在行的上一行插入新的一行
      r进入替换模式,只会替换光标所在的字符一次
      R进入替换模式,替换光标所在的字符,直到通过Esc退出开启事务开启事务
      Esc退出编辑模式,回到一般命令模式查询得到值 1
      -

      保存和退出

      - - - - - - - - + + + + + + - - + + + + + + - - + + + + + + - - + + + + + + - - + + + + + + - - + + + + + + - - + + + + + + - - + + + + + +
      快捷键功能
      :w保存文件查询得到值 1
      :w!若文件为只读,强制保存修改值为 2
      :q退出 Vim,如果文件已修改,将退出失败查询得到值 V12(读到B未提交的修改)111
      :q!强制退出 Vim,不保存文件修改提交事务
      :wq保存文件并退出 Vim查询得到值 V222(读到B已提交的修改)11
      :w filename另存为新文件提交事务
      ZZ退出 Vim,若文件无修改,则不保存退出;如果文件已修改,保存并退出查询得到值 V3222(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 中,事务隔离是通过 lockundo logread view 共同协作实现的。很多时候,我们关注 MVCC 在“读提交”和“可重复读”隔离级别中的作用而忽视事务隔离和锁的关系。

      +
      +

      MySQL 各个事务隔离级别的实现原理简述如下:

      +
        +
      • 串行化:读加共享锁,写加排他锁,读写互斥
      • +
      • 读未提交:写加排他锁,读不加锁
      • +
      • 可重复读:第一次读操作时创建快照,基于该快照进行读取
      • +
      • 读提交:每次读操作时重置快照,基于该快照进行读取
      • +
      +

      前两者通过锁(lock)实现比较容易理解;后两者通过多版本并发控制(MVCC)实现。MVCC 是一种实现非阻塞并发读的设计思路,在 InnoDB 引擎中主要通过 undo logread view 实现。

      +

      以下示意图表现了在 InnoDB 引擎中,同一行数据存在多个“快照”版本,这就是数据库的多版本并发控制(MVCC),当你基于快照读取时可以获得旧版本的数据。

      +
        +
      • 假设一个值从 1 按顺序被修改为 2、3、4,最新值为 4。
      • +
      • 事务将基于各自拥有的“快照”读取数据而不受其他事务更新的影响,也不阻塞其他事务的更新。
      • +
      +
      + +

      在接下来我们将通过锁、事务 ID、回滚日志和一致性视图逐步介绍 InnoDB 事务隔离的实现原理。

      +

      锁(lock)

      事务在本质上是一个并发控制问题,而锁是解决并发问题的常见基础方案。MySQL 正是通过共享锁排他锁实现串行化隔离级别。但是读加共享锁影响性能,尤其是在读写冲突频繁时,若不加发生“脏读”的缺陷又比较大,MVCC 就是用于在即使有读写冲突的情况下,不加读锁实现非阻塞并发读。

      +
      +

      InnoDB 的事务中,行锁(共享锁或排他锁)是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放,这个就是两阶段锁协议

      +
      +

      理解两阶段锁协议,你会更深地体会读写冲突频繁时锁对性能的影响以及 MVCC 的作用。长事务可能导致一个锁被长时间持有,导致拖垮整个库。

      +

      事务 ID

      InnoDB 引擎中,每个事务都有唯一的一个事务 ID,叫做 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。同时每一行数据有一个隐藏字段 trx_id,记录了插入或更新该行数据的事务 ID

      +
      + +

      创建事务的时机

      事务启动方式如下:

      +
        +
      1. 显式启动事务语句是 beginstart transaction,配套的提交语句是 commit,回滚语句是 rollback
      2. +
      3. 隐式启动事务语句是 set autocommit = 0,该设置将关闭自动提交。当你执行 select,将自动启动一个事务,直到你主动 commitrollback
      4. +
      +

      但注意,实际上不论是显式启动事务情况下的 beginstart transaction,还是隐式启动事务情况下的 commitrollback 都不会立即创建一个新事务,而是直到第一次操作 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 updateupdateinsertdelete
        • +
        +
      • +
      • 快照读:在不加锁的情况下通过 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 修改配置文件。

      -
      -

      参考文章

      -]]>
      - - linux - vim - -
      - - k3s 的安装和使用 - /2024/01/30/installation-and-use-of-k3s/ - 本文记录了 k3s 的安装和使用,相较于 minikube,前者是一个完全兼容的 Kubernetes 发行版,安装和使用的体验更佳。

      - - -

      安装

      -

      参考官方文档-快速入门指南,使用默认选项启动集群非常简单方便!!!

      -
      -

      步骤

        -
      1. 获取并运行 k3s 安装脚本。官方为中国用户提供了镜像加速支持。
        $ curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -                                
        [sudo] password for moralok:
        [INFO] Finding release for channel stable
        [INFO] Using v1.28.5+k3s1 as release
        [INFO] Downloading hash rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/sha256sum-amd64.txt
        [INFO] Downloading binary rancher-mirror.rancher.cn/k3s/v1.28.5-k3s1/k3s
        [INFO] Verifying binary download
        [INFO] Installing k3s to /usr/local/bin/k3s
        [INFO] Skipping installation of SELinux RPM
        [INFO] Creating /usr/local/bin/kubectl symlink to k3s
        [INFO] Creating /usr/local/bin/crictl symlink to k3s
        [INFO] Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
        [INFO] Creating killall script /usr/local/bin/k3s-killall.sh
        [INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
        [INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
        [INFO] systemd: Creating service file /etc/systemd/system/k3s.service
        sh: 1014: restorecon: not found
        sh: 1015: restorecon: not found
        [INFO] systemd: Enabling k3s unit
        Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
        [INFO] systemd: Starting k3s
      2. -
      3. 可以通过使用 kubectl 确认安装成功。刚安装的时候使用 kubectl 需要 root 权限。
        $ sudo kubectl get node
        NAME STATUS ROLES AGE VERSION
        ubuntu-server Ready control-plane,master 52m v1.28.5+k3s1
      4. -
      5. 实际上安装的就是一个 k3s 可执行文件,kubectlcrictl 只是软链接,指向 k3s
        $ ls /usr/local/bin/
        crictl k3s k3s-killall.sh k3s-uninstall.sh kubectl
      6. -
      -
      -

      安装的信息中显示了 k3sservice fileenvironment file 的路径,后续修改启动参数和环境变量需要用到。

      -
      -

      配置文件权限问题

      在刚安装完 k3s 的时候,使用 kubectl 需要 root 权限。根据报错信息可知,是因为非 root 用户无法读取配置文件 /etc/rancher/k3s/k3s.yaml

      -
      $ kubectl get node                                                                                                                                            
      WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode to modify kube config permissions
      error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
      - -

      查看配置文件的信息可知其权限配置为 600,只有 root 用户具有读写权限。

      -
      $ ll /etc/rancher/k3s/k3s.yaml                                                                                               
      -rw------- 1 root root 2961 Jan 30 18:58 /etc/rancher/k3s/k3s.yaml
      - -

      一般来说,我们希望能够通过非 root 用户使用 kubectl,避免通过 root 用户或者通过 sudo 加输入密码的形式来使用 kubectl。那么如何解决这个问题呢?本质上这是一个 Linux 的文件权限问题,似乎修改文件的权限配置就可以解决。但是提示信息给出的解决方案并不是那么直接,它告诉我们通过修改 k3s server 的启动参数来达到修改配置文件权限的目的。这是因为 k3s 服务在每次重启时会根据启动参数和环境变量重置配置文件 /etc/rancher/k3s/k3s.yaml,手动修改文件的权限配置并不能优雅地解决这个问题,一旦服务重启,修改就会丢失。

      -
      -

      k3s 的 Github Discussions 中讨论了这个问题,并链接了文档 管理 Kubeconfig 选项,文档介绍了通过修改启动参数和环境变量达到修改配置文件权限的目的。

      -
      -

      修改启动参数

      第一种方式是修改启动参数。

      -
        -
      1. sudo vim /etc/systemd/system/k3s.service 添加 k3s 启动参数 --write-kubeconfig-mode 644
        ExecStart=/usr/local/bin/k3s \
        server --write-kubeconfig-mode 644 \
      2. -
      3. systemctl daemon-reload 重新加载 systemd 配置
      4. -
      5. systemctl restart k3s.service 重启服务
      6. -
      7. 验证修改生效
        $ ll /etc/rancher/k3s/k3s.yaml
        -rw-r--r-- 1 root root 2961 Jan 30 20:13 /etc/rancher/k3s/k3s.yaml
      8. -
      -

      修改环境变量

      第二种方式是修改环境变量。

      -
        -
      1. sudo vim /etc/systemd/system/k3s.service.env 添加环境变量 K3S_KUBECONFIG_MODE=644
        K3S_KUBECONFIG_MODE=644
      2. -
      3. systemctl restart k3s.service 重启服务
      4. -
      -

      修改配置文件路径

      第三种方式是复制配置信息到当前用户目录下,并使用其作为配置文件的路径。

      -
        -
      1. 设置环境变量 export KUBECONFIG=~/.kube/config
      2. -
      3. 创建文件夹 mkdir ~/.kube 2> /dev/null
      4. -
      5. 复制配置信息 sudo k3s kubectl config view --raw > "$KUBECONFIG"
      6. -
      7. 修改配置文件的权限 chmod 600 "$KUBECONFIG"
      8. -
      -

      配置代理

      涉及 k8s,难免需要使用代理,否则在拉取镜像时将寸步难行。官方文档 配置 HTTP 代理 介绍了如何配置代理。其中提及 k3s 安装脚本会自动使用当前 shell 中的 HTTP_PROXYHTTPS_PROXYNO_PROXY,以及 CONTAINERD_HTTP_PROXYCONTAINERD_HTTPS_PROXYCONTAINERD_NO_PROXY 变量(如果存在),并将它们写入 systemd 服务的环境文件。比如我设置过 shell 变量 HTTP_PROXYHTTPS_PROXYNO_PROXY/etc/systemd/system/k3s.service.env 如下,你也可以自行编辑修改。

      -
      http_proxy='http://127.0.0.1:7890'
      https_proxy='http://127.0.0.1:7890'
      no_proxy='localhost,127.0.0.1'
      K3S_KUBECONFIG_MODE=644
      - -

      使用

      -

      k8s 基础教程可参考官方文档 Kubernetes 基础

      -
      -

      创建 Deployment

        -
      1. 使用 kubectl create 命令创建管理 PodDeployment。该 Pod 根据提供的 Docker 镜像运行容器。
        kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
      2. -
      3. 查看 Deployment
        $ kubectl get pods
        NAME READY STATUS RESTARTS AGE
        hello-node-ccf4b9788-d8k9b 1/1 Running 0 15h
      4. -
      5. 查看 Pod 中容器的应用程序日志。
        $ kubectl logs hello-node-ccf4b9788-d8k9b
        I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
        I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
      6. -
      -

      创建 Service

        -
      1. 使用 kubectl expose 命令将 Pod 暴露给公网:
        kubectl expose deployment hello-node --type=LoadBalancer --port=8080
      2. -
      3. 查看你创建的 Service
        $ kubectl get services
        NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
        kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 15h
        hello-node LoadBalancer 10.43.37.170 192.168.46.128 8080:32117/TCP 15h
      4. -
      5. 使用 curl 发起请求:
        $ curl http://localhost:8080
        NOW: 2024-01-31 10:55:14.228709273 +0000 UTC m=+25932.159732511
      6. -
      7. 再次查看 Pod 中容器的应用程序日志。
        $ kubectl logs hello-node-ccf4b9788-d8k9b
        I0130 19:26:57.751131 1 log.go:195] Started HTTP server on port 8080
        I0130 19:26:57.751350 1 log.go:195] Started UDP server on port 8081
        I0130 19:32:21.074992 1 log.go:195] GET /
      8. -
      -

      清理

        -
      1. 删除 Service
        kubectl delete service hello-node
      2. -
      3. 删除 Deployment
        kubectl delete deployment hello-node
      4. -
      -

      卸载

        -
      1. 从 server 节点卸载 k3s
        /usr/local/bin/k3s-uninstall.sh
      2. -
      +

      至此,我们将锁和 MVCC 在事务隔离的实现原理中串联起来了。两者是互相独立又互相协作的两个机制,前者实现了“当前读”,后者实现了“快照读”。

      +

      总结

      卡壳好几天,想到有不少好的文章却仍然会给读者留下困惑,想到自己在当初学习时对一些不严谨的表达抓耳挠腮想不通为什么,就有点不知道如何下笔。最终围绕着自己当初的一些困惑,一点一点修修补补完了。

      参考文章

      ]]>
      - k8s - k3s + mysql
      @@ -7020,151 +7167,4 @@
]]>
- - Spring 中 @Import 注解的使用和源码分析 - /2023/12/04/use-and-analysis-of-Import-annotation-in-Spring/ - Import 注解是 Spring 基于 Java 注解配置的重要组成部分,处理 Import 注解是处理 Configuration 注解的子过程之一,本文将介绍 Import 注解的 3 种使用方式,然后通过分析源码和处理过程示意图解释它是如何导入(注册) BeanDefinition 的。

- - - -

使用方式

Import 注解有 3 种导入(注册) BeanDefinition 的方式:

-
    -
  1. 使用 Import 将目标类的 Class 对象,解析为 BeanDefinition 并注册。
  2. -
  3. 使用 Import 配合 ImportSelector 的实现类,将 selectImports 方法返回的所有全限定类名字符串,解析为 BeanDefinition 并注册。
  4. -
  5. 使用 Import 配合 ImportBeanDefinitionRegistrar 的实现类,在 registerBeanDefinitions 方法中,直接向 BeanDefinitionRegistry 中注册 BeanDefinition
  6. -
-

测试用例

测试了使用 Import 注解的 3 种方式:

-
    -
  1. 使用 Import 直接导入(注册) Red
  2. -
  3. 配合 ImportBeanDefinitionRegistrar 间接注册 Color
  4. -
  5. 配合 ImportSelector 间接导入(注册) Blue
  6. -
-

用例中的特别地测试了以下两种情况:

-
    -
  1. 使用 Import 直接导入和配合 ImportSelector 间接导入相同的类 Red 只会注册一个 BeanDefinition
  2. -
  3. 尽管 MyImportSelector 书面顺序在 MyImportBeanDefinitionRegistrar 之后,但是 MyImportBeanDefinitionRegistrar 判断 registry 是否包含在 MyImportSelector 导入的类 Blue 时,不受顺序影响。
  4. -
-
@Configuration
@Import({Red.class, MyImportBeanDefinitionRegistrar.class, MyImportSelector.class,})
public class ImportConfig {
}

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

@Override
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 {

@Override
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 {
@Test
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 存放已经访问过的 sourceClasssourceClass 在入口处包装了一个普通的 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 中的类导入”。但是显而易见,这样的方式不够灵活,因此才有了另外两种更有灵活性的导入方式:ImportSelectorImportBeanDefinitionRegistrarSpring 最终不会真的注册这两种类,而是注册它们“介绍”的类,相当于把确定导入什么类的工作委托给它们。

-

处理要导入的目标

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 将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。具体步骤是:

-
    -
  1. 先获取 Class 对象
  2. -
  3. 再实例化得到一个 ImportSelector 实例
  4. -
  5. 调用 selectImports 方法,该方法返回的是类的全限定名,这样就得到了真正要导入的目标
  6. -
  7. 再次递归调用 processImports
  8. -
-

ImportSelector 就像它名字的含义一样,本质上是一种导入选择器,是一种更加灵活的 getImports 方法。由于返回的目标可能属于三种情形中的任意一种,所以对这些目标的处理还是要回到 processImports 方法。可以说 ImportSelector 类型本身不是 processImports 方法的出口,它最终会转换为 ImportBeanDefinitionRegistrar 或其他剩余情况。

-

ImportSelector 灵活性的来源:

-
    -
  • selectImportsAnnotationMetadata 参数,为它提供了根据注解信息返回要导入的目标的能力
  • -
  • ImportSelector 可以实现 Aware 接口,用以感知到一些容器级别的资源,如 BeanFactory,这为它提供了根据这些资源中的信息返回要导入的目标的能力
  • -
-

类型二:ImportBeanDefinitionRegistrar

如果要导入的目标是 ImportBeanDefinitionRegistrar,它会和 ImportSelector 有些相似却又有所不同。Spring 同样将确定真正导入什么目标的工作委托给它,不导入目标本身,实际上只导入目标“介绍”的类。

-
    -
  1. 先获取 Class 对象
  2. -
  3. 再实例化得到一个 ImportBeanDefinitionRegistrar 实例
  4. -
  5. 添加到配置模型 configClassimportBeanDefinitionRegistrars 属性
  6. -
-

ImportBeanDefinitionRegistrar 不像 ImportSelector 需要进一步处理,它本身就代表着一个返回出口,成为了配置模型的一部分。但是请注意,registerBeanDefinitions 方法此时并没有被调用。

-

ImportBeanDefinitionRegistrar 灵活性的来源:

-
    -
  • registerBeanDefinitionsAnnotationMetadata 参数,为它提供了根据注解信息决定注册 BeanDefinition 的能力
  • -
  • registerBeanDefinitionsBeanDefinitionRegistry 参数,为它提供了根据 BeanDefinitionRegistry 中的信息决定注册 BeanDefinition 的能力
  • -
  • ImportBeanDefinitionRegistrar 可以实现 Aware 接口,用以感知到一些容器级别的资源,如 BeanFactory,这为它提供了根据这些资源中的信息返回要导入的目标的能力
  • -
-

类型三:其他剩余情况

如果要导入的目标属于既不是 ImportSelector 也不是 ImportBeanDefinitionRegistrar 的其他剩余情况,那么 Spring 将其视为被 Configuration 注解标注的配置类进行处理。这里的处理逻辑是,Import 注解导入的类可能不是一个普通的类,而是一个配置类,因此需要回到 processConfigurationClass 进行处理。processConfigurationClass 方法正是本文开头的 doProcessConfigurationClass 方法的调用方,这里有两个地方值得注意:

-
    -
  • Import 注解产生的 ConfigurationClass 根据不同的情况需要合并或者被抛弃,显式声明比 Import 导入的优先级更高。
  • -
  • 其他剩余情况下,目标最终会转换为一个配置模型,添加到 parserconfigurationClasses 属性。
  • -
-
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 在每次解析得到新的一批配置模型后,都会调用 ConfigurationClassBeanDefinitionReaderloadBeanDefinitions 方法加载 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;
}
- -

总结

对比

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ImportSelectorImportBeanDefinitionRegistrar其他剩余情况
灵活性
处理结果转换为配置模型的一部分转换为一个配置模型
方法调用时机立即(或解析配置类的最后)加载 BeanDefinition 的最后
方法的结果获取 Import 目标直接注册 BeanDefinition
-

处理过程示意图

-]]>
- - java - spring - -
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 @@ - +