This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new a7d80b7880 refactor(registry): decouple core from registry 
implementations (#7578)
a7d80b7880 is described below

commit a7d80b7880000a89bf15aa968db9c6ad07d96734
Author: redyyang <[email protected]>
AuthorDate: Tue Oct 13 11:32:01 2026 +0800

    refactor(registry): decouple core from registry implementations (#7578)
    
    Co-authored-by: redyyang <[email protected]>
---
 .github/scripts/check-registry-dependencies.py     |  99 +++++++++++
 .github/scripts/test-registry-dependencies.py      |  73 ++++++++
 .github/workflows/ci.yml                           |  19 ++
 shenyu-admin/pom.xml                               |  32 ++++
 .../discovery/AbstractDiscoveryProcessor.java      |   4 +-
 .../discovery/DefaultDiscoveryProcessorTest.java   |  31 ++++
 .../pom.xml                                        |  13 +-
 .../shenyu-examples-sdk-feign/pom.xml              |   7 +
 .../shenyu-examples-sdk-grpc-consumer/pom.xml      |  11 --
 .../shenyu-examples-sdk-http/pom.xml               |  13 +-
 .../pom.xml                                        |  25 +++
 .../shenyu-integrated-test-sdk-http/pom.xml        |  19 ++
 .../pom.xml                                        |   6 -
 shenyu-registry/README.md                          | 132 ++++++++++++++
 .../shenyu/registry/apollo/RegistrySpiTest.java    |  33 ++++
 shenyu-registry/shenyu-registry-core/pom.xml       |  24 +--
 .../ShenyuInstanceRegisterRepositoryFactory.java   |  39 +++-
 ...henyuInstanceRegisterRepositoryFactoryTest.java | 196 +++++++++++++++++++--
 .../shenyu/registry/etcd/RegistrySpiTest.java      |  33 ++++
 .../shenyu/registry/eureka/RegistrySpiTest.java    |  33 ++++
 .../registry/kubernetes/RegistrySpiTest.java       |  33 ++++
 .../shenyu/registry/nacos/RegistrySpiTest.java     |  33 ++++
 .../shenyu/registry/zookeeper/RegistrySpiTest.java |  33 ++++
 shenyu-sdk/shenyu-sdk-core/pom.xml                 |   2 +-
 .../apache/shenyu/sdk/core/ShenyuResponseTest.java |   3 +-
 shenyu-sdk/shenyu-sdk-feign/pom.xml                |   2 +-
 .../shenyu-spring-boot-starter-registry/pom.xml    |  27 +++
 .../starter/registry/RegistryClasspathTest.java    | 122 +++++++++++++
 ...u.registry.api.ShenyuInstanceRegisterRepository |  18 ++
 .../shenyu-spring-boot-starter-sdk-feign/pom.xml   |  11 ++
 .../starter/sdk/feign/RegistryClasspathTest.java   |  82 +++++++++
 ...u.registry.api.ShenyuInstanceRegisterRepository |  17 ++
 .../shenyu-spring-boot-starter-sdk/pom.xml         |   5 +
 .../starter/sdk/RegistryClasspathTest.java         |  82 +++++++++
 ...u.registry.api.ShenyuInstanceRegisterRepository |  17 ++
 35 files changed, 1251 insertions(+), 78 deletions(-)

diff --git a/.github/scripts/check-registry-dependencies.py 
b/.github/scripts/check-registry-dependencies.py
new file mode 100644
index 0000000000..ff233ff0c3
--- /dev/null
+++ b/.github/scripts/check-registry-dependencies.py
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Assert registry boundaries against Maven's resolved runtime dependency 
trees."""
+
+import sys
+from pathlib import Path
+
+PROVIDERS = ("zookeeper", "etcd", "nacos", "apollo", "eureka", "kubernetes")
+COMPATIBILITY_PROVIDERS = set(PROVIDERS) - {"apollo"}
+LEAN_MODULES = (
+    "shenyu-registry/shenyu-registry-api",
+    "shenyu-registry/shenyu-registry-core",
+    "shenyu-sdk/shenyu-sdk-core",
+    "shenyu-sdk/shenyu-sdk-feign",
+    "shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk",
+    "shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign",
+)
+IMPLEMENTATION_MODULES = tuple("shenyu-registry/shenyu-registry-" + name for 
name in PROVIDERS)
+COMPATIBILITY_MODULE = 
"shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry"
+MODULES = LEAN_MODULES + IMPLEMENTATION_MODULES + (COMPATIBILITY_MODULE,)
+CLIENT_GROUPS = (
+    "org.apache.zookeeper", "org.apache.curator", "io.etcd", 
"com.alibaba.nacos",
+    "com.ecwid.consul", "com.orbitz.consul", "com.tencent.polaris",
+    "com.netflix.eureka", "io.kubernetes",
+)
+TREE_FILE = "target/registry-dependencies.tgf"
+
+
+def read_tree(path):
+    """Read node coordinates, excluding the project node and the edge 
section."""
+    nodes = path.read_text(encoding="utf-8").split("#", 1)[0].splitlines()
+    if not nodes:
+        raise ValueError("empty dependency tree: " + str(path))
+    artifacts = set()
+    for node in nodes[1:]:
+        fields = node.split(" ", 1)
+        if len(fields) != 2 or len(fields[1].split(":")) < 4:
+            raise ValueError("invalid dependency coordinate: " + node)
+        artifacts.add(tuple(fields[1].split(":")[:2]))
+    return artifacts
+
+
+def check_module(module, artifacts):
+    errors = []
+    implementations = {artifact.removeprefix("shenyu-registry-")
+                       for group, artifact in artifacts
+                       if group == "org.apache.shenyu" and 
artifact.startswith("shenyu-registry-")
+                       and artifact not in {"shenyu-registry-api", 
"shenyu-registry-core"}}
+    if module in LEAN_MODULES:
+        if implementations:
+            errors.append("registry implementations leaked into " + module + 
": " + str(sorted(implementations)))
+        clients = {group + ":" + artifact for group, artifact in artifacts
+                   if any(group == prefix or group.startswith(prefix + ".") 
for prefix in CLIENT_GROUPS)}
+        if clients:
+            errors.append("registry clients leaked into " + module + ": " + 
str(sorted(clients)))
+    if module in IMPLEMENTATION_MODULES and ("org.apache.shenyu", 
"shenyu-registry-core") in artifacts:
+        errors.append(module + " depends on registry-core instead of 
registry-api")
+    if module == COMPATIBILITY_MODULE:
+        missing = COMPATIBILITY_PROVIDERS - implementations
+        if missing:
+            errors.append("compatibility starter lost providers: " + 
str(sorted(missing)))
+    return errors
+
+
+def main():
+    if sys.argv[1:] == ["--modules"]:
+        print(",".join(MODULES))
+        return 0
+    root = Path(__file__).resolve().parents[2]
+    errors = []
+    for module in MODULES:
+        path = root / module / TREE_FILE
+        try:
+            artifacts = read_tree(path)
+            errors.extend(check_module(module, artifacts))
+        except (OSError, ValueError) as error:
+            errors.append(str(error))
+    if errors:
+        print("\n".join(errors), file=sys.stderr)
+        return 1
+    print("Registry dependency boundaries passed for " + str(len(MODULES)) + " 
runtime trees.")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/.github/scripts/test-registry-dependencies.py 
b/.github/scripts/test-registry-dependencies.py
new file mode 100644
index 0000000000..2ceb0a8f49
--- /dev/null
+++ b/.github/scripts/test-registry-dependencies.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Regression checks for the resolved dependency boundary assertions."""
+
+import importlib.util
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.dont_write_bytecode = True
+
+SPEC = importlib.util.spec_from_file_location("registry_dependencies", 
Path(__file__).with_name("check-registry-dependencies.py"))
+CHECK = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(CHECK)
+
+
+class RegistryDependencyTest(unittest.TestCase):
+    def test_transitive_clients_are_rejected_for_every_lean_consumer(self):
+        for module in CHECK.LEAN_MODULES:
+            for group in CHECK.CLIENT_GROUPS:
+                with self.subTest(module=module, group=group):
+                    self.assertTrue(CHECK.check_module(module, {(group, 
"client")}))
+
+    def test_any_registry_implementation_is_rejected(self):
+        self.assertTrue(CHECK.check_module(CHECK.LEAN_MODULES[1], 
{("org.apache.shenyu", "shenyu-registry-external")}))
+
+    def test_api_and_spi_are_allowed(self):
+        artifacts = {("org.apache.shenyu", "shenyu-registry-api"), 
("org.apache.shenyu", "shenyu-spi")}
+        self.assertFalse(CHECK.check_module(CHECK.LEAN_MODULES[1], artifacts))
+
+    def test_implementation_cannot_depend_on_core(self):
+        self.assertTrue(CHECK.check_module(CHECK.IMPLEMENTATION_MODULES[0], 
{("org.apache.shenyu", "shenyu-registry-core")}))
+
+    def test_compatibility_starter_must_retain_all_original_providers(self):
+        artifacts = {("org.apache.shenyu", "shenyu-registry-" + provider) for 
provider in CHECK.COMPATIBILITY_PROVIDERS}
+        self.assertFalse(CHECK.check_module(CHECK.COMPATIBILITY_MODULE, 
artifacts))
+        artifacts.pop()
+        self.assertTrue(CHECK.check_module(CHECK.COMPATIBILITY_MODULE, 
artifacts))
+
+    def 
test_tgf_nodes_include_transitive_artifacts_but_not_root_or_edges(self):
+        with tempfile.TemporaryDirectory() as directory:
+            tree = Path(directory) / "tree.tgf"
+            tree.write_text("1 
org.apache.shenyu:shenyu-registry-core:jar:2.7.2-SNAPSHOT\n"
+                            "2 
org.apache.shenyu:shenyu-registry-api:jar:2.7.2-SNAPSHOT:compile\n"
+                            "3 
org.apache.curator:curator-client:jar:5.7.1:runtime\n#\n1 2\n2 3\n", 
encoding="utf-8")
+            artifacts = CHECK.read_tree(tree)
+            self.assertEqual({("org.apache.shenyu", "shenyu-registry-api"), 
("org.apache.curator", "curator-client")}, artifacts)
+            self.assertTrue(CHECK.check_module(CHECK.LEAN_MODULES[1], 
artifacts))
+
+    def test_empty_tree_fails_closed(self):
+        with tempfile.TemporaryDirectory() as directory:
+            tree = Path(directory) / "tree.tgf"
+            tree.write_text("", encoding="utf-8")
+            with self.assertRaises(ValueError):
+                CHECK.read_tree(tree)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4594b7abb6..256ea81de8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -112,6 +112,25 @@ jobs:
       - name: Test GHCR publish routing
         run: bash .github/scripts/resolve-ghcr-publish-test.sh
 
+  registry_dependencies:
+    needs: changes
+    runs-on: ubuntu-latest
+    if: github.repository == 'apache/shenyu' && needs.changes.outputs.code == 
'true'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ./actions/setup-java-with-retry
+        with:
+          java-version: "17"
+          distribution: "temurin"
+      - name: Check resolved registry dependency boundaries
+        run: |
+          python3 .github/scripts/test-registry-dependencies.py
+          REGISTRY_MODULES="$(python3 
.github/scripts/check-registry-dependencies.py --modules)"
+          ./mvnw -B -ntp -pl "$REGISTRY_MODULES" -am package dependency:tree \
+            -DskipTests -DskipRemoteResources=true -Djacoco.skip=true 
-Dmaven.javadoc.skip=true \
+            -Dscope=runtime -DoutputType=tgf 
-DoutputFile=target/registry-dependencies.tgf
+          python3 .github/scripts/check-registry-dependencies.py
+
   rat_check:
     needs: changes
     runs-on: ubuntu-latest
diff --git a/shenyu-admin/pom.xml b/shenyu-admin/pom.xml
index 4b5be49852..3da4f9fd0c 100644
--- a/shenyu-admin/pom.xml
+++ b/shenyu-admin/pom.xml
@@ -347,6 +347,38 @@
             <version>${project.version}</version>
         </dependency>
 
+        <!-- Explicit providers preserve the existing distribution/classpath. 
-->
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-etcd</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-nacos</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-eureka</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-kubernetes</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-client-mcp-common</artifactId>
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
index 578bcb85d7..212ee8865b 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/discovery/AbstractDiscoveryProcessor.java
@@ -37,7 +37,7 @@ import org.apache.shenyu.common.utils.UUIDUtils;
 import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
 import org.apache.shenyu.registry.api.config.RegisterConfig;
 import org.apache.shenyu.registry.api.entity.InstanceEntity;
-import org.apache.shenyu.spi.ExtensionLoader;
+import org.apache.shenyu.registry.core.ShenyuInstanceRegisterRepositoryFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.ApplicationEventPublisher;
@@ -97,7 +97,7 @@ public abstract class AbstractDiscoveryProcessor implements 
DiscoveryProcessor,
         discoveryConfig.setRegisterType(type);
         discoveryConfig.setProps(properties);
         discoveryConfig.setServerLists(discoveryDO.getServerList());
-        ShenyuInstanceRegisterRepository discoveryService = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin(type);
+        ShenyuInstanceRegisterRepository discoveryService = 
ShenyuInstanceRegisterRepositoryFactory.loadRepository(type);
         discoveryService.init(discoveryConfig);
         discoveryServiceCache.put(discoveryDO.getId(), discoveryService);
         dataChangedEventListenerCache.put(discoveryDO.getId(), new 
HashSet<>());
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
index 778dff6b74..fa6204c989 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/discovery/DefaultDiscoveryProcessorTest.java
@@ -107,6 +107,7 @@ public class DefaultDiscoveryProcessorTest {
         MockedStatic<ExtensionLoader> mocked = 
mockStatic(ExtensionLoader.class);
         ExtensionLoader extensionLoader = mock(ExtensionLoader.class);
         mocked.when(() -> 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class)).thenReturn(extensionLoader);
+        
when(extensionLoader.getExtensionClasses1()).thenReturn(Collections.singletonMap(type,
 ShenyuInstanceRegisterRepository.class));
         
when(extensionLoader.getJoin(anyString())).thenReturn(shenyuDiscoveryService);
         
doNothing().when(shenyuDiscoveryService).init(any(RegisterConfig.class));
         defaultDiscoveryProcessor.createDiscovery(discoveryDO);
@@ -138,6 +139,36 @@ public class DefaultDiscoveryProcessorTest {
         mocked.close();
     }
 
+    @Test
+    public void testCreateSeparateDiscoveriesOfSameType() throws Exception {
+        ShenyuInstanceRegisterRepository first = 
mock(ShenyuInstanceRegisterRepository.class);
+        ShenyuInstanceRegisterRepository second = 
mock(ShenyuInstanceRegisterRepository.class);
+        try (MockedStatic<ExtensionLoader> mocked = 
mockStatic(ExtensionLoader.class)) {
+            ExtensionLoader<ShenyuInstanceRegisterRepository> loader = 
mock(ExtensionLoader.class);
+            mocked.when(() -> 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class)).thenReturn(loader);
+            
when(loader.getExtensionClasses1()).thenReturn(Collections.singletonMap("zookeeper",
 ShenyuInstanceRegisterRepository.class));
+            when(loader.getJoin("zookeeper")).thenReturn(first, second);
+            DiscoveryDO firstDiscovery = new DiscoveryDO();
+            firstDiscovery.setId("first-discovery");
+            firstDiscovery.setDiscoveryType("zookeeper");
+            firstDiscovery.setProps("{}");
+            firstDiscovery.setServerList("first:2181");
+            DiscoveryDO secondDiscovery = new DiscoveryDO();
+            secondDiscovery.setId("second-discovery");
+            secondDiscovery.setDiscoveryType("zookeeper");
+            secondDiscovery.setProps("{}");
+            secondDiscovery.setServerList("second:2181");
+            defaultDiscoveryProcessor.createDiscovery(firstDiscovery);
+            defaultDiscoveryProcessor.createDiscovery(secondDiscovery);
+            verify(first).init(new RegisterConfig("zookeeper", "first:2181", 
new Properties()));
+            verify(second).init(new RegisterConfig("zookeeper", "second:2181", 
new Properties()));
+            defaultDiscoveryProcessor.removeDiscovery(firstDiscovery);
+            verify(first).close();
+            defaultDiscoveryProcessor.removeDiscovery(secondDiscovery);
+            verify(second).close();
+        }
+    }
+
     @Test
     public void testCreateProxySelector() {
         DiscoveryHandlerDTO discoveryHandlerDTO = new DiscoveryHandlerDTO();
diff --git 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-dubbo/shenyu-examples-sdk-apache-dubbo-consumer/pom.xml
 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-dubbo/shenyu-examples-sdk-apache-dubbo-consumer/pom.xml
index f8edde7f7e..e4f058e478 100644
--- 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-dubbo/shenyu-examples-sdk-apache-dubbo-consumer/pom.xml
+++ 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-dubbo/shenyu-examples-sdk-apache-dubbo-consumer/pom.xml
@@ -28,15 +28,16 @@
     <artifactId>shenyu-examples-sdk-apache-dubbo-consumer</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-sdk-core</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.shenyu</groupId>
-                    <artifactId>shenyu-registry-eureka</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
diff --git 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-feign/pom.xml 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-feign/pom.xml
index d581451e15..5a8c3d196d 100644
--- a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-feign/pom.xml
+++ b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-feign/pom.xml
@@ -33,6 +33,13 @@
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-openfeign</artifactId>
diff --git 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-grpc/shenyu-examples-sdk-grpc-consumer/pom.xml
 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-grpc/shenyu-examples-sdk-grpc-consumer/pom.xml
index d0dc390fe2..63a42f7c5c 100644
--- 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-grpc/shenyu-examples-sdk-grpc-consumer/pom.xml
+++ 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-grpc/shenyu-examples-sdk-grpc-consumer/pom.xml
@@ -68,17 +68,6 @@
             <artifactId>shenyu-sdk-core</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-eureka</artifactId>
-            <version>${project.version}</version>
-            <exclusions>
-                <exclusion>
-                    <artifactId>servlet-api</artifactId>
-                    <groupId>javax.servlet</groupId>
-                </exclusion>
-            </exclusions>
-        </dependency>
 
         <dependency>
             <groupId>org.apache.shenyu</groupId>
diff --git 
a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-http/pom.xml 
b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-http/pom.xml
index 91a70f3d97..dbd3ac8fa9 100644
--- a/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-http/pom.xml
+++ b/shenyu-examples/shenyu-examples-sdk/shenyu-examples-sdk-http/pom.xml
@@ -28,15 +28,16 @@
     <artifactId>shenyu-examples-sdk-http</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-sdk-core</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.apache.shenyu</groupId>
-                    <artifactId>shenyu-registry-eureka</artifactId>
-                </exclusion>
-            </exclusions>
         </dependency>
 
         <dependency>
diff --git 
a/shenyu-integrated-test/shenyu-integrated-test-sdk-apache-dubbo/pom.xml 
b/shenyu-integrated-test/shenyu-integrated-test-sdk-apache-dubbo/pom.xml
index 3b7874c38c..9097565017 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-sdk-apache-dubbo/pom.xml
+++ b/shenyu-integrated-test/shenyu-integrated-test-sdk-apache-dubbo/pom.xml
@@ -36,6 +36,13 @@
     </properties>
     
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-integrated-test-common</artifactId>
@@ -46,6 +53,24 @@
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-spring-boot-starter-registry</artifactId>
             <version>${project.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-etcd</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-nacos</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-eureka</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-kubernetes</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <dependency>
diff --git a/shenyu-integrated-test/shenyu-integrated-test-sdk-http/pom.xml 
b/shenyu-integrated-test/shenyu-integrated-test-sdk-http/pom.xml
index 769a9f4164..5ce537d271 100644
--- a/shenyu-integrated-test/shenyu-integrated-test-sdk-http/pom.xml
+++ b/shenyu-integrated-test/shenyu-integrated-test-sdk-http/pom.xml
@@ -28,6 +28,13 @@
     <artifactId>shenyu-integrated-test-sdk-http</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-integrated-test-common</artifactId>
@@ -39,10 +46,22 @@
             <artifactId>shenyu-spring-boot-starter-registry</artifactId>
             <version>${project.version}</version>
             <exclusions>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-etcd</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-nacos</artifactId>
+                </exclusion>
                 <exclusion>
                     <groupId>org.apache.shenyu</groupId>
                     <artifactId>shenyu-registry-eureka</artifactId>
                 </exclusion>
+                <exclusion>
+                    <groupId>org.apache.shenyu</groupId>
+                    <artifactId>shenyu-registry-kubernetes</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
 
diff --git 
a/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/pom.xml
 
b/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/pom.xml
index 9ad4393e46..66e5948ca5 100644
--- 
a/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/pom.xml
+++ 
b/shenyu-integrated-test/shenyu-integrated-test-upload-plugin/shenyu-integrated-test-upload-plugin-case/pom.xml
@@ -42,12 +42,6 @@
             <version>${project.version}</version>
         </dependency>
 
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-spring-boot-starter-registry</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-spring-boot-starter-gateway</artifactId>
diff --git a/shenyu-registry/README.md b/shenyu-registry/README.md
new file mode 100644
index 0000000000..d91f0f8ef0
--- /dev/null
+++ b/shenyu-registry/README.md
@@ -0,0 +1,132 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Registry dependency selection
+
+`shenyu-registry-core` supplies the SPI-backed factory and its existing 
per-type cache.
+It depends only on `shenyu-registry-api` and `shenyu-spi`; implementation 
modules depend
+on the API. Loading a factory no longer loads every registry client.
+
+## Select providers explicitly
+
+Add the implementation matching `RegisterConfig.registerType` to your 
application's
+runtime classpath, using the same ShenYu version as the application:
+
+```xml
+<dependency>
+    <groupId>org.apache.shenyu</groupId>
+    <artifactId>shenyu-registry-zookeeper</artifactId>
+    <version>${shenyu.version}</version>
+    <scope>runtime</scope>
+</dependency>
+```
+
+| SPI key | Implementation artifact |
+| --- | --- |
+| `zookeeper` | `shenyu-registry-zookeeper` |
+| `etcd` | `shenyu-registry-etcd` |
+| `nacos` | `shenyu-registry-nacos` |
+| `apollo` | `shenyu-registry-apollo` |
+| `eureka` | `shenyu-registry-eureka` |
+| `kubernetes` | `shenyu-registry-kubernetes` |
+
+Multiple implementation dependencies may coexist. The configured key selects 
the
+provider; the factory never calls `init` on unrelated providers. Missing or 
unknown keys
+fail with the requested artifact, runtime classpath guidance, and available 
SPI keys.
+Provider construction and initialization errors still propagate without being 
reported
+as missing dependencies.
+
+SDK libraries depend on the registry API. The SDK and Feign SDK starters 
additionally
+include the factory, so add the selected implementation next to the SDK 
starter.
+Applications calling the factory directly should declare 
`shenyu-registry-core` explicitly.
+`shenyu.sdk.register-type=local` uses configured gateway addresses and needs 
no registry
+implementation. The HTTP, Dubbo and Feign SDK examples select ZooKeeper, the 
Spring Cloud
+example selects Eureka, and the gRPC example selects no registry 
implementation.
+
+## Compatibility and migration
+
+For the 2.7.2 transition, `shenyu-spring-boot-starter-registry` retains the 
five providers
+previously supplied by core: ZooKeeper, etcd, Nacos, Eureka and Kubernetes. 
Existing users
+of that starter keep the same classpath and configuration keys. Apollo was not 
part of
+the previous aggregate and remains an explicit choice. Removing the 
compatibility
+aggregate is deferred to a separately announced change.
+
+Applications depending directly on core, SDK libraries, or SDK starters should 
add
+their configured provider before upgrading. For gateway registration with a 
smaller
+classpath, retain the registry starter, exclude the unused implementation 
dependencies,
+and declare the selected implementation explicitly. The SDK integration tests 
show
+this arrangement for ZooKeeper. Admin declares its five supported discovery 
providers
+explicitly and preserves independent connections for separate discovery 
configurations.
+
+To roll back a dependency selection, restore the application's previous 
explicit
+provider dependencies, or keep the compatibility starter if already used. 
Rolling back
+the ShenYu version restores the old core aggregation. No stored configuration 
or SPI
+key migration is required. The factory's cache and reinitialization behavior 
is unchanged.
+
+## External implementations
+
+External providers, including Consul and Polaris extensions, implement
+`org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository`, carry 
`@Join`, and
+publish this resource in their JAR:
+
+```text
+META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
+```
+
+For example, `consul=com.example.ConsulInstanceRegisterRepository` preserves 
the existing
+registration key. Add the extension JAR and its client dependencies to the 
runtime
+classpath; no core POM change is needed. Existing ShenYu SPI classloader rules 
continue
+to apply. Factory tests load external `consul` and `polaris` keys from 
isolated resources
+without any built-in provider dependency.
+
+## Dependency and release inventory
+
+| Consumer | Registry implementation inventory |
+| --- | --- |
+| Registry API/core, SDK libraries and SDK starters | None |
+| Compatibility registry starter | ZooKeeper, etcd, Nacos, Eureka, Kubernetes |
+| Admin distribution | ZooKeeper, etcd, Nacos, Eureka, Kubernetes, explicitly 
declared |
+| Bootstrap distribution | Registry API only; client libraries also arrive 
through sync/register/plugin modules |
+| SDK HTTP/Dubbo integration gateways | ZooKeeper |
+| Upload-plugin integration gateway | None; registry registration is not 
enabled |
+
+The packaged Admin client inventory includes Curator 5.7.0, ZooKeeper 3.9.5, 
jetcd
+0.7.3, Nacos 2.2.4, Eureka 2.0.2, and Kubernetes Java client 17.0.2. Bootstrap 
still
+packages Curator, ZooKeeper, jetcd and Nacos through its other selected 
modules. Both
+release archives include their LICENSE/NOTICE files. No registry client 
license entry
+is removed by this change.
+
+Admin retains the same client libraries, so its existing distribution 
LICENSE/NOTICE
+entries still apply. Bootstrap distribution client libraries also come from 
selected
+plugin, registration and synchronization starters; removing an edge from 
registry-core
+alone does not remove those libraries. Keep the existing license and notice 
entries
+for libraries still shipped. Custom distributions must inventory the actual 
packaged
+`lib/` JARs and include their notices when selecting or removing providers.
+
+CI checks resolved runtime trees for the API/core, both SDK 
libraries/starters, all six
+in-repository implementations and the compatibility starter. The checks reject 
registry
+implementations and client libraries in lean consumers, reverse 
implementation-to-core
+dependencies, and loss of a compatibility provider. Run locally:
+
+```bash
+python3 .github/scripts/test-registry-dependencies.py
+REGISTRY_MODULES="$(python3 .github/scripts/check-registry-dependencies.py 
--modules)"
+./mvnw -B -ntp -pl "$REGISTRY_MODULES" -am package dependency:tree \
+  -DskipTests -DskipRemoteResources=true -Djacoco.skip=true 
-Dmaven.javadoc.skip=true \
+  -Dscope=runtime -DoutputType=tgf 
-DoutputFile=target/registry-dependencies.tgf
+python3 .github/scripts/check-registry-dependencies.py
+```
diff --git 
a/shenyu-registry/shenyu-registry-apollo/src/test/java/org/apache/shenyu/registry/apollo/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-apollo/src/test/java/org/apache/shenyu/registry/apollo/RegistrySpiTest.java
new file mode 100644
index 0000000000..af130fd279
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-apollo/src/test/java/org/apache/shenyu/registry/apollo/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.apollo;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("apollo");
+        assertEquals(ApolloInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git a/shenyu-registry/shenyu-registry-core/pom.xml 
b/shenyu-registry/shenyu-registry-core/pom.xml
index 68f73d3aa7..4349509aca 100644
--- a/shenyu-registry/shenyu-registry-core/pom.xml
+++ b/shenyu-registry/shenyu-registry-core/pom.xml
@@ -28,32 +28,12 @@
     <dependencies>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-spi</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-zookeeper</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-etcd</artifactId>
+            <artifactId>shenyu-registry-api</artifactId>
             <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-nacos</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-eureka</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-kubernetes</artifactId>
+            <artifactId>shenyu-spi</artifactId>
             <version>${project.version}</version>
         </dependency>
     </dependencies>
diff --git 
a/shenyu-registry/shenyu-registry-core/src/main/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactory.java
 
b/shenyu-registry/shenyu-registry-core/src/main/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactory.java
index 4af9d17449..4388a39752 100644
--- 
a/shenyu-registry/shenyu-registry-core/src/main/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactory.java
+++ 
b/shenyu-registry/shenyu-registry-core/src/main/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactory.java
@@ -17,11 +17,14 @@
 
 package org.apache.shenyu.registry.core;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
 import org.apache.shenyu.registry.api.config.RegisterConfig;
 import org.apache.shenyu.spi.ExtensionLoader;
 
 import java.util.Map;
+import java.util.Objects;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
@@ -38,7 +41,28 @@ public final class ShenyuInstanceRegisterRepositoryFactory {
      * @return the shenyu instance register repository
      */
     public static ShenyuInstanceRegisterRepository newInstance(final String 
registerType) {
-        return REPOSITORY_MAP.computeIfAbsent(registerType, 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class)::getJoin);
+        checkRegisterType(registerType);
+        return REPOSITORY_MAP.computeIfAbsent(registerType, 
ShenyuInstanceRegisterRepositoryFactory::loadRepository);
+    }
+
+    /**
+     * Load a repository through SPI without using the factory's per-type 
cache.
+     * The provider's {@code @Join} singleton setting still applies.
+     *
+     * @param registerType the configured registry type
+     * @return the repository
+     */
+    public static ShenyuInstanceRegisterRepository loadRepository(final String 
registerType) {
+        checkRegisterType(registerType);
+        ExtensionLoader<ShenyuInstanceRegisterRepository> loader = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class);
+        Map<String, Class<?>> providers = loader.getExtensionClasses1();
+        if (!providers.containsKey(registerType)) {
+            throw new IllegalArgumentException("No registry implementation 
found for registerType '" + registerType
+                    + "'. Add the matching shenyu-registry-" + registerType
+                    + " dependency (or an external registry SPI provider) to 
the runtime classpath. Available types: "
+                    + new TreeSet<>(providers.keySet()));
+        }
+        return loader.getJoin(registerType);
     }
     
     /**
@@ -48,8 +72,10 @@ public final class ShenyuInstanceRegisterRepositoryFactory {
      * @return the shenyu instance register repository
      */
     public static ShenyuInstanceRegisterRepository newAndInitInstance(final 
RegisterConfig config) {
+        Objects.requireNonNull(config, "register config is required");
+        checkRegisterType(config.getRegisterType());
         return REPOSITORY_MAP.computeIfAbsent(config.getRegisterType(), 
registerType -> {
-            ShenyuInstanceRegisterRepository result = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin(registerType);
+            ShenyuInstanceRegisterRepository result = 
loadRepository(registerType);
             result.init(config);
             return result;
         });
@@ -62,9 +88,16 @@ public final class ShenyuInstanceRegisterRepositoryFactory {
      * @return the shenyu instance register repository
      */
     public static ShenyuInstanceRegisterRepository reNewAndInitInstance(final 
RegisterConfig config) {
-        ShenyuInstanceRegisterRepository result = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin(config.getRegisterType());
+        Objects.requireNonNull(config, "register config is required");
+        ShenyuInstanceRegisterRepository result = 
loadRepository(config.getRegisterType());
         result.init(config);
         REPOSITORY_MAP.put(config.getRegisterType(), result);
         return result;
     }
+
+    private static void checkRegisterType(final String registerType) {
+        if (StringUtils.isBlank(registerType)) {
+            throw new IllegalArgumentException("registerType is required; 
configure a registry type and add its SPI provider to the runtime classpath");
+        }
+    }
 }
diff --git 
a/shenyu-registry/shenyu-registry-core/src/test/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactoryTest.java
 
b/shenyu-registry/shenyu-registry-core/src/test/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactoryTest.java
index d7c40e9fe6..d42e57ba3f 100644
--- 
a/shenyu-registry/shenyu-registry-core/src/test/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactoryTest.java
+++ 
b/shenyu-registry/shenyu-registry-core/src/test/java/org/apache/shenyu/registry/core/ShenyuInstanceRegisterRepositoryFactoryTest.java
@@ -18,37 +18,197 @@
 package org.apache.shenyu.registry.core;
 
 import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.registry.api.config.RegisterConfig;
+import org.apache.shenyu.registry.api.entity.InstanceEntity;
 import org.apache.shenyu.spi.ExtensionLoader;
+import org.apache.shenyu.spi.Join;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.MockedStatic;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.io.IOException;
 import java.lang.reflect.Field;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Enumeration;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
 
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.when;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public final class ShenyuInstanceRegisterRepositoryFactoryTest {
+class ShenyuInstanceRegisterRepositoryFactoryTest {
+
+    private static final String SPI_RESOURCE = "META-INF/shenyu/" + 
ShenyuInstanceRegisterRepository.class.getName();
+
+    @TempDir
+    private Path directory;
+
+    private URLClassLoader classLoader;
 
     @BeforeEach
-    public void setup() throws Exception {
-        final Field field = 
ShenyuInstanceRegisterRepositoryFactory.class.getDeclaredField("REPOSITORY_MAP");
-        field.setAccessible(true);
-        final Map map = (Map) field.get(null);
-        map.put("zookeeper", mock(ShenyuInstanceRegisterRepository.class));
+    void setUp() throws Exception {
+        clearCaches();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        clearCaches();
+        if (Objects.nonNull(classLoader)) {
+            classLoader.close();
+        }
+    }
+
+    @Test
+    void noProviderGivesDependencyHint() throws Exception {
+        useProviders("");
+        IllegalArgumentException exception = 
assertThrows(IllegalArgumentException.class,
+                () -> 
ShenyuInstanceRegisterRepositoryFactory.newInstance("zookeeper"));
+        
assertTrue(exception.getMessage().contains("shenyu-registry-zookeeper"));
+        assertTrue(exception.getMessage().contains("runtime classpath"));
+        assertTrue(exception.getMessage().contains("Available types: []"));
+    }
+
+    @Test
+    void oneProviderIsLoadedAndCached() throws Exception {
+        useProviders(provider("external"));
+        ShenyuInstanceRegisterRepository repository = 
ShenyuInstanceRegisterRepositoryFactory.newInstance("external");
+        assertEquals(TestRepository.class, repository.getClass());
+        assertSame(repository, 
ShenyuInstanceRegisterRepositoryFactory.newInstance("external"));
+        assertNotSame(repository, 
ShenyuInstanceRegisterRepositoryFactory.loadRepository("external"));
+    }
+
+    @Test
+    void multipleExternalProvidersKeepTheirSpiKeys() throws Exception {
+        useProviders(provider("consul") + provider("polaris"));
+        ShenyuInstanceRegisterRepository consul = 
ShenyuInstanceRegisterRepositoryFactory.newInstance("consul");
+        ShenyuInstanceRegisterRepository polaris = 
ShenyuInstanceRegisterRepositoryFactory.newInstance("polaris");
+        assertNotSame(consul, polaris);
+        assertSame(consul, 
ShenyuInstanceRegisterRepositoryFactory.newInstance("consul"));
+        assertSame(polaris, 
ShenyuInstanceRegisterRepositoryFactory.newInstance("polaris"));
+        IllegalArgumentException exception = 
assertThrows(IllegalArgumentException.class,
+                () -> 
ShenyuInstanceRegisterRepositoryFactory.newInstance("unknown"));
+        assertTrue(exception.getMessage().contains("Available types: [consul, 
polaris]"));
+    }
+
+    @Test
+    void missingProviderFailsFromEveryFactoryEntryPoint() throws Exception {
+        useProviders(provider("external"));
+        RegisterConfig config = config("missing");
+        assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.loadRepository("missing"));
+        assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config));
+        assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.reNewAndInitInstance(config));
+    }
+
+    @Test
+    void blankTypeFailsBeforeCacheAccess() {
+        for (String type : new String[] {null, "", " "}) {
+            assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.newInstance(type));
+            assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.loadRepository(type));
+            assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config(type)));
+            assertThrows(IllegalArgumentException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.reNewAndInitInstance(config(type)));
+        }
     }
 
     @Test
-    public void testNewInstance() {
-        
assertNotNull(ShenyuInstanceRegisterRepositoryFactory.newInstance("zookeeper"));
-        try (MockedStatic<ExtensionLoader> extensionLoaderMockedStatic = 
mockStatic(ExtensionLoader.class)) {
-            ExtensionLoader extensionLoader = mock(ExtensionLoader.class);
-            extensionLoaderMockedStatic.when(() -> 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class)).thenReturn(extensionLoader);
-            
when(extensionLoader.getJoin("zs")).thenReturn(mock(ShenyuInstanceRegisterRepository.class));
-            
assertNotNull(ShenyuInstanceRegisterRepositoryFactory.newInstance("zs"));
+    void initializationAndRenewalPreserveCacheContract() throws Exception {
+        useProviders(provider("external"));
+        RegisterConfig config = config("external");
+        TestRepository repository = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config);
+        assertSame(config, repository.config);
+        assertEquals(1, repository.initCount);
+        assertSame(repository, 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config));
+        assertEquals(1, repository.initCount);
+        RegisterConfig replacement = config("external");
+        replacement.setServerLists("replacement");
+        TestRepository renewed = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.reNewAndInitInstance(replacement);
+        assertNotSame(repository, renewed);
+        assertSame(replacement, renewed.config);
+        assertSame(renewed, 
ShenyuInstanceRegisterRepositoryFactory.newInstance("external"));
+    }
+
+    @Test
+    void uninitializedCachedInstanceKeepsExistingBehavior() throws Exception {
+        useProviders(provider("external"));
+        TestRepository repository = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.newInstance("external");
+        assertSame(repository, 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config("external")));
+        assertEquals(0, repository.initCount);
+    }
+
+    @Test
+    void 
failedInitializationIsNotCachedAndFailedRenewalKeepsPreviousInstance() throws 
Exception {
+        useProviders(provider("external"));
+        RegisterConfig failing = config("external");
+        failing.setServerLists("fail");
+        assertThrows(IllegalStateException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(failing));
+        TestRepository repository = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.newAndInitInstance(config("external"));
+        assertEquals(1, repository.initCount);
+        assertThrows(IllegalStateException.class, () -> 
ShenyuInstanceRegisterRepositoryFactory.reNewAndInitInstance(failing));
+        assertSame(repository, 
ShenyuInstanceRegisterRepositoryFactory.newInstance("external"));
+    }
+
+    private void useProviders(final String definitions) throws IOException {
+        Path resource = directory.resolve(SPI_RESOURCE);
+        Files.createDirectories(resource.getParent());
+        Files.write(resource, definitions.getBytes(StandardCharsets.UTF_8));
+        classLoader = new URLClassLoader(new URL[] 
{directory.toUri().toURL()}, getClass().getClassLoader()) {
+            @Override
+            public Enumeration<URL> getResources(final String name) throws 
IOException {
+                if (SPI_RESOURCE.equals(name)) {
+                    return findResources(name);
+                }
+                return super.getResources(name);
+            }
+        };
+        
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class, 
classLoader);
+    }
+
+    private String provider(final String type) {
+        return type + "=" + TestRepository.class.getName() + "\n";
+    }
+
+    private RegisterConfig config(final String type) {
+        return new RegisterConfig(type, "server", new Properties());
+    }
+
+    private void clearCaches() throws Exception {
+        Field repositories = 
ShenyuInstanceRegisterRepositoryFactory.class.getDeclaredField("REPOSITORY_MAP");
+        repositories.setAccessible(true);
+        ((Map<?, ?>) repositories.get(null)).clear();
+        Field loaders = ExtensionLoader.class.getDeclaredField("LOADERS");
+        loaders.setAccessible(true);
+        ((Map<?, ?>) 
loaders.get(null)).remove(ShenyuInstanceRegisterRepository.class);
+    }
+
+    /**
+     * A provider loaded from an external SPI resource, without registry 
clients.
+     */
+    @Join(isSingleton = false)
+    public static class TestRepository implements 
ShenyuInstanceRegisterRepository {
+
+        private RegisterConfig config;
+
+        private int initCount;
+
+        @Override
+        public void init(final RegisterConfig registerConfig) {
+            if ("fail".equals(registerConfig.getServerLists())) {
+                throw new IllegalStateException("initialization failed");
+            }
+            config = registerConfig;
+            initCount++;
+        }
+
+        @Override
+        public void persistInstance(final InstanceEntity instance) {
         }
     }
 }
diff --git 
a/shenyu-registry/shenyu-registry-etcd/src/test/java/org/apache/shenyu/registry/etcd/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-etcd/src/test/java/org/apache/shenyu/registry/etcd/RegistrySpiTest.java
new file mode 100644
index 0000000000..b1ae3d7c67
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-etcd/src/test/java/org/apache/shenyu/registry/etcd/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.etcd;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("etcd");
+        assertEquals(EtcdInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git 
a/shenyu-registry/shenyu-registry-eureka/src/test/java/org/apache/shenyu/registry/eureka/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-eureka/src/test/java/org/apache/shenyu/registry/eureka/RegistrySpiTest.java
new file mode 100644
index 0000000000..ea1f39c636
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-eureka/src/test/java/org/apache/shenyu/registry/eureka/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.eureka;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("eureka");
+        assertEquals(EurekaInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git 
a/shenyu-registry/shenyu-registry-kubernetes/src/test/java/org/apache/shenyu/registry/kubernetes/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-kubernetes/src/test/java/org/apache/shenyu/registry/kubernetes/RegistrySpiTest.java
new file mode 100644
index 0000000000..fd2ad0a320
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-kubernetes/src/test/java/org/apache/shenyu/registry/kubernetes/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.kubernetes;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("kubernetes");
+        assertEquals(KubernetesInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git 
a/shenyu-registry/shenyu-registry-nacos/src/test/java/org/apache/shenyu/registry/nacos/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-nacos/src/test/java/org/apache/shenyu/registry/nacos/RegistrySpiTest.java
new file mode 100644
index 0000000000..163c29778c
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-nacos/src/test/java/org/apache/shenyu/registry/nacos/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.nacos;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("nacos");
+        assertEquals(NacosInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git 
a/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/RegistrySpiTest.java
 
b/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/RegistrySpiTest.java
new file mode 100644
index 0000000000..276224cc07
--- /dev/null
+++ 
b/shenyu-registry/shenyu-registry-zookeeper/src/test/java/org/apache/shenyu/registry/zookeeper/RegistrySpiTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.registry.zookeeper;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class RegistrySpiTest {
+
+    @Test
+    void discoversPublishedRegistryKey() {
+        ShenyuInstanceRegisterRepository repository = 
ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getJoin("zookeeper");
+        assertEquals(ZookeeperInstanceRegisterRepository.class, 
repository.getClass());
+    }
+}
diff --git a/shenyu-sdk/shenyu-sdk-core/pom.xml 
b/shenyu-sdk/shenyu-sdk-core/pom.xml
index f1f1edf178..eb5b8c8eb4 100644
--- a/shenyu-sdk/shenyu-sdk-core/pom.xml
+++ b/shenyu-sdk/shenyu-sdk-core/pom.xml
@@ -37,7 +37,7 @@
         </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-core</artifactId>
+            <artifactId>shenyu-registry-api</artifactId>
             <version>${project.version}</version>
         </dependency>
     </dependencies>
diff --git 
a/shenyu-sdk/shenyu-sdk-core/src/test/java/org/apache/shenyu/sdk/core/ShenyuResponseTest.java
 
b/shenyu-sdk/shenyu-sdk-core/src/test/java/org/apache/shenyu/sdk/core/ShenyuResponseTest.java
index f05be79555..2919e5d9bc 100644
--- 
a/shenyu-sdk/shenyu-sdk-core/src/test/java/org/apache/shenyu/sdk/core/ShenyuResponseTest.java
+++ 
b/shenyu-sdk/shenyu-sdk-core/src/test/java/org/apache/shenyu/sdk/core/ShenyuResponseTest.java
@@ -17,7 +17,6 @@
 
 package org.apache.shenyu.sdk.core;
 
-import org.apache.http.HttpStatus;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -40,7 +39,7 @@ public class ShenyuResponseTest {
                 headerMap, null, null, null);
 
         ShenyuResponse response = new ShenyuResponse(
-                HttpStatus.SC_OK,
+                200,
                 "success",
                 headerMap,
                 body,
diff --git a/shenyu-sdk/shenyu-sdk-feign/pom.xml 
b/shenyu-sdk/shenyu-sdk-feign/pom.xml
index b8e16b0d0f..ac50c5da29 100644
--- a/shenyu-sdk/shenyu-sdk-feign/pom.xml
+++ b/shenyu-sdk/shenyu-sdk-feign/pom.xml
@@ -32,7 +32,7 @@
         </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
-            <artifactId>shenyu-registry-core</artifactId>
+            <artifactId>shenyu-registry-api</artifactId>
             <version>${project.version}</version>
         </dependency>
         <dependency>
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/pom.xml 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/pom.xml
index 5b22fbc564..715b81f388 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/pom.xml
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/pom.xml
@@ -31,5 +31,32 @@
             <artifactId>shenyu-registry-core</artifactId>
             <version>${project.version}</version>
         </dependency>
+
+        <!-- Explicit providers preserve the existing distribution/classpath. 
-->
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-zookeeper</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-etcd</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-nacos</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-eureka</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-kubernetes</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/java/org/apache/shenyu/springboot/starter/registry/RegistryClasspathTest.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/java/org/apache/shenyu/springboot/starter/registry/RegistryClasspathTest.java
new file mode 100644
index 0000000000..5207687513
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/java/org/apache/shenyu/springboot/starter/registry/RegistryClasspathTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.springboot.starter.registry;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.registry.api.config.RegisterConfig;
+import org.apache.shenyu.registry.api.entity.InstanceEntity;
+import org.apache.shenyu.registry.core.ShenyuInstanceRegisterRepositoryFactory;
+import org.apache.shenyu.spi.ExtensionLoader;
+import org.apache.shenyu.spi.Join;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import 
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.boot.web.server.WebServer;
+
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RegistryClasspathTest {
+
+    private final ApplicationContextRunner runner = new 
ApplicationContextRunner()
+            
.withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration.class,
 ShenyuRegistryConfiguration.class))
+            .withPropertyValues("shenyu.register.enabled=true", 
"shenyu.register.server-lists=localhost:9195");
+
+    @Test
+    void disabledRegistrationStartsWithoutARegistryConnection() {
+        runner.withPropertyValues("shenyu.register.enabled=false").run(context 
-> {
+            assertThat(context).hasNotFailed();
+            assertThat(context).doesNotHaveBean(RegistryListener.class);
+        });
+    }
+
+    @Test
+    void compatibilityStarterStillDiscoversItsOriginalProviders() {
+        
assertThat(ExtensionLoader.getExtensionLoader(ShenyuInstanceRegisterRepository.class).getExtensionClasses1())
+                .containsKeys("zookeeper", "etcd", "nacos", "eureka", 
"kubernetes");
+    }
+
+    @Test
+    void missingConfiguredProviderFailsWithDependencyHint() {
+        
runner.withPropertyValues("shenyu.register.register-type=missing").run(context 
-> {
+            assertThat(context).hasFailed();
+            
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(IllegalArgumentException.class)
+                    
.hasStackTraceContaining("shenyu-registry-missing").hasStackTraceContaining("runtime
 classpath");
+        });
+    }
+
+    @Test
+    void explicitlyProvidedSpiStartsAndReceivesBoundConfiguration() {
+        
runner.withPropertyValues("shenyu.register.register-type=test-registry").run(context
 -> {
+            assertThat(context).hasNotFailed();
+            assertThat(context).hasSingleBean(RegistryListener.class);
+            TestRepository repository = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.newInstance("test-registry");
+            
assertThat(repository.config.getRegisterType()).isEqualTo("test-registry");
+            
assertThat(repository.config.getServerLists()).isEqualTo("localhost:9195");
+        });
+    }
+
+    @Test
+    void selectedProviderRegistersGatewayOnce() {
+        Properties props = new Properties();
+        props.setProperty("host", "127.0.0.1");
+        props.setProperty("name", "test-gateway");
+        RegisterConfig config = new RegisterConfig("test-registry-event", 
"localhost:9195", props);
+        RegistryListener listener = new RegistryListener(config);
+        WebServer server = mock(WebServer.class);
+        when(server.getPort()).thenReturn(9195);
+        WebServerInitializedEvent event = 
mock(WebServerInitializedEvent.class);
+        when(event.getWebServer()).thenReturn(server);
+        listener.onApplicationEvent(event);
+        listener.onApplicationEvent(event);
+        TestRepository repository = (TestRepository) 
ShenyuInstanceRegisterRepositoryFactory.newInstance("test-registry-event");
+        assertThat(repository.persistCount).isEqualTo(1);
+        assertThat(repository.instance.getAppName()).isEqualTo("test-gateway");
+        assertThat(repository.instance.getHost()).isEqualTo("127.0.0.1");
+        assertThat(repository.instance.getPort()).isEqualTo(9195);
+    }
+
+    /**
+     * External implementation supplied only on the test runtime classpath.
+     */
+    @Join(isSingleton = false)
+    public static class TestRepository implements 
ShenyuInstanceRegisterRepository {
+
+        private RegisterConfig config;
+
+        private InstanceEntity instance;
+
+        private int persistCount;
+
+        @Override
+        public void init(final RegisterConfig registerConfig) {
+            config = registerConfig;
+        }
+
+        @Override
+        public void persistInstance(final InstanceEntity instanceEntity) {
+            instance = instanceEntity;
+            persistCount++;
+        }
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
new file mode 100644
index 0000000000..a4093a18cf
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-registry/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+test-registry=org.apache.shenyu.springboot.starter.registry.RegistryClasspathTest$TestRepository
+test-registry-event=org.apache.shenyu.springboot.starter.registry.RegistryClasspathTest$TestRepository
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/pom.xml 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/pom.xml
index 7a6139623d..f40ebdc095 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/pom.xml
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/pom.xml
@@ -25,10 +25,21 @@
     <artifactId>shenyu-spring-boot-starter-sdk-feign</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-sdk-feign</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+            <version>4.1.3</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/java/org/apache/shenyu/springboot/starter/sdk/feign/RegistryClasspathTest.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/java/org/apache/shenyu/springboot/starter/sdk/feign/RegistryClasspathTest.java
new file mode 100644
index 0000000000..1ff57e4798
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/java/org/apache/shenyu/springboot/starter/sdk/feign/RegistryClasspathTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.springboot.starter.sdk.feign;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.registry.api.config.RegisterConfig;
+import org.apache.shenyu.registry.api.entity.InstanceEntity;
+import org.apache.shenyu.spi.Join;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import 
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RegistryClasspathTest {
+
+    private final ApplicationContextRunner runner = new 
ApplicationContextRunner()
+            
.withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration.class,
 ShenyuSdkAutoConfiguration.class))
+            .withPropertyValues("shenyu.sdk.enabled=true", 
"shenyu.sdk.server-lists=localhost:9195");
+
+    @Test
+    void localModeStartsWithoutARegistryImplementation() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=local").run(context -> {
+            assertThat(context).hasNotFailed();
+            
assertThat(context.getBeanProvider(ShenyuInstanceRegisterRepository.class).getIfAvailable()).isNull();
+        });
+    }
+
+    @Test
+    void missingConfiguredProviderFailsWithDependencyHint() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=missing").run(context -> {
+            assertThat(context).hasFailed();
+            
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(IllegalArgumentException.class)
+                    
.hasStackTraceContaining("shenyu-registry-missing").hasStackTraceContaining("runtime
 classpath");
+        });
+    }
+
+    @Test
+    void explicitlyProvidedSpiStartsAndReceivesBoundConfiguration() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=test-sdk-feign").run(context
 -> {
+            assertThat(context).hasNotFailed();
+            
assertThat(context).hasSingleBean(ShenyuInstanceRegisterRepository.class);
+            TestRepository repository = (TestRepository) 
context.getBean(ShenyuInstanceRegisterRepository.class);
+            
assertThat(repository.config.getRegisterType()).isEqualTo("test-sdk-feign");
+            
assertThat(repository.config.getServerLists()).isEqualTo("localhost:9195");
+        });
+    }
+
+    /**
+     * External implementation supplied only on the test runtime classpath.
+     */
+    @Join(isSingleton = false)
+    public static class TestRepository implements 
ShenyuInstanceRegisterRepository {
+
+        private RegisterConfig config;
+
+        @Override
+        public void init(final RegisterConfig registerConfig) {
+            config = registerConfig;
+        }
+
+        @Override
+        public void persistInstance(final InstanceEntity instance) {
+        }
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
new file mode 100644
index 0000000000..30cc62ccff
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk-feign/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+test-sdk-feign=org.apache.shenyu.springboot.starter.sdk.feign.RegistryClasspathTest$TestRepository
diff --git a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/pom.xml 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/pom.xml
index a741dc7d8d..5807acca3e 100644
--- a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/pom.xml
+++ b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/pom.xml
@@ -25,6 +25,11 @@
     <artifactId>shenyu-spring-boot-starter-sdk</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.shenyu</groupId>
+            <artifactId>shenyu-registry-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.shenyu</groupId>
             <artifactId>shenyu-sdk-spring</artifactId>
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/java/org/apache/shenyu/springboot/starter/sdk/RegistryClasspathTest.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/java/org/apache/shenyu/springboot/starter/sdk/RegistryClasspathTest.java
new file mode 100644
index 0000000000..a863540cdc
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/java/org/apache/shenyu/springboot/starter/sdk/RegistryClasspathTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.springboot.starter.sdk;
+
+import org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository;
+import org.apache.shenyu.registry.api.config.RegisterConfig;
+import org.apache.shenyu.registry.api.entity.InstanceEntity;
+import org.apache.shenyu.spi.Join;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import 
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RegistryClasspathTest {
+
+    private final ApplicationContextRunner runner = new 
ApplicationContextRunner()
+            
.withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration.class,
 ShenyuSdkAutoConfiguration.class))
+            .withPropertyValues("shenyu.sdk.enabled=true", 
"shenyu.sdk.server-lists=localhost:9195");
+
+    @Test
+    void localModeStartsWithoutARegistryImplementation() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=local").run(context -> {
+            assertThat(context).hasNotFailed();
+            
assertThat(context.getBeanProvider(ShenyuInstanceRegisterRepository.class).getIfAvailable()).isNull();
+        });
+    }
+
+    @Test
+    void missingConfiguredProviderFailsWithDependencyHint() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=missing").run(context -> {
+            assertThat(context).hasFailed();
+            
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(IllegalArgumentException.class)
+                    
.hasStackTraceContaining("shenyu-registry-missing").hasStackTraceContaining("runtime
 classpath");
+        });
+    }
+
+    @Test
+    void explicitlyProvidedSpiStartsAndReceivesBoundConfiguration() {
+        
runner.withPropertyValues("shenyu.sdk.register-type=test-sdk").run(context -> {
+            assertThat(context).hasNotFailed();
+            
assertThat(context).hasSingleBean(ShenyuInstanceRegisterRepository.class);
+            TestRepository repository = (TestRepository) 
context.getBean(ShenyuInstanceRegisterRepository.class);
+            
assertThat(repository.config.getRegisterType()).isEqualTo("test-sdk");
+            
assertThat(repository.config.getServerLists()).isEqualTo("localhost:9195");
+        });
+    }
+
+    /**
+     * External implementation supplied only on the test runtime classpath.
+     */
+    @Join(isSingleton = false)
+    public static class TestRepository implements 
ShenyuInstanceRegisterRepository {
+
+        private RegisterConfig config;
+
+        @Override
+        public void init(final RegisterConfig registerConfig) {
+            config = registerConfig;
+        }
+
+        @Override
+        public void persistInstance(final InstanceEntity instance) {
+        }
+    }
+}
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
new file mode 100644
index 0000000000..709c5b58df
--- /dev/null
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-sdk/src/test/resources/META-INF/shenyu/org.apache.shenyu.registry.api.ShenyuInstanceRegisterRepository
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+test-sdk=org.apache.shenyu.springboot.starter.sdk.RegistryClasspathTest$TestRepository

Reply via email to