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 d04715da5d fix(plugin-base): 修复扩展插件重载后配置处理器未更新的问题. (#7845)
d04715da5d is described below

commit d04715da5dbb8b414515fdb001d7824671d50e6f
Author: ocean <[email protected]>
AuthorDate: Fri Oct 23 11:36:10 2026 +0800

    fix(plugin-base): 修复扩展插件重载后配置处理器未更新的问题. (#7845)
---
 .../base/cache/CommonPluginDataSubscriber.java     |  28 +++-
 .../base/cache/CommonPluginDataSubscriberTest.java | 112 +++++++++++++++
 .../shenyu/web/loader/ShenyuLoaderService.java     |   8 +-
 .../web/loader/ShenyuLoaderServiceHandlerTest.java | 151 +++++++++++++++++++++
 4 files changed, 292 insertions(+), 7 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
index 9c171358b4..b70ae8e7ab 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriber.java
@@ -28,7 +28,6 @@ import org.apache.shenyu.common.enums.DataEventTypeEnum;
 import org.apache.shenyu.common.enums.PluginHandlerEventEnum;
 import org.apache.shenyu.common.utils.InitialSyncApplication;
 import org.apache.shenyu.common.utils.JsonUtils;
-import org.apache.shenyu.common.utils.MapUtils;
 import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
 import org.apache.shenyu.sync.data.api.PluginDataSubscriber;
 import org.slf4j.Logger;
@@ -56,6 +55,8 @@ public class CommonPluginDataSubscriber implements 
PluginDataSubscriber {
     
     private final Map<String, PluginDataHandler> handlerMap;
 
+    private final Set<String> builtInHandlerNames;
+
     private ApplicationEventPublisher eventPublisher;
     
     private final SelectorMatchCache selectorMatchConfig;
@@ -73,6 +74,7 @@ public class CommonPluginDataSubscriber implements 
PluginDataSubscriber {
                                       final SelectorMatchCache 
selectorMatchConfig,
                                       final RuleMatchCache 
ruleMatchCacheConfig) {
         this.handlerMap = 
pluginDataHandlerList.stream().collect(Collectors.toConcurrentMap(PluginDataHandler::pluginNamed,
 e -> e));
+        this.builtInHandlerNames = Set.copyOf(handlerMap.keySet());
         this.selectorMatchConfig = selectorMatchConfig;
         this.ruleMatchCacheConfig = ruleMatchCacheConfig;
     }
@@ -90,13 +92,14 @@ public class CommonPluginDataSubscriber implements 
PluginDataSubscriber {
                                       final SelectorMatchCache 
selectorMatchConfig,
                                       final RuleMatchCache 
ruleMatchCacheConfig) {
         this.handlerMap = 
pluginDataHandlerList.stream().collect(Collectors.toConcurrentMap(PluginDataHandler::pluginNamed,
 e -> e));
+        this.builtInHandlerNames = Set.copyOf(handlerMap.keySet());
         this.eventPublisher = eventPublisher;
         this.selectorMatchConfig = selectorMatchConfig;
         this.ruleMatchCacheConfig = ruleMatchCacheConfig;
     }
     
     /**
-     * Put extend plugin data handler.
+     * Add or replace extension handlers without overriding built-in handlers.
      *
      * @param handlers the handlers
      */
@@ -106,10 +109,23 @@ public class CommonPluginDataSubscriber implements 
PluginDataSubscriber {
         }
         for (PluginDataHandler handler : handlers) {
             String pluginNamed = handler.pluginNamed();
-            MapUtils.computeIfAbsent(handlerMap, pluginNamed, name -> {
-                LOG.info("shenyu auto add extends plugin data handler name is 
:{}", pluginNamed);
-                return handler;
-            });
+            if (!builtInHandlerNames.contains(pluginNamed)) {
+                handlerMap.put(pluginNamed, handler);
+                LOG.info("shenyu auto add or replace extends plugin data 
handler name is :{}", pluginNamed);
+            }
+        }
+    }
+
+    /**
+     * Remove handlers belonging to unloaded extension plugins.
+     *
+     * @param pluginNames extension plugin names
+     */
+    public void removeExtendPluginDataHandlers(final Set<String> pluginNames) {
+        for (String pluginName : pluginNames) {
+            if (!builtInHandlerNames.contains(pluginName)) {
+                handlerMap.remove(pluginName);
+            }
         }
     }
     
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
index bf49fbf85a..9796bb4f9d 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/cache/CommonPluginDataSubscriberTest.java
@@ -40,6 +40,7 @@ import 
org.springframework.context.ConfigurableApplicationContext;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -55,6 +56,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -109,6 +111,116 @@ public final class CommonPluginDataSubscriberTest {
         assertEquals(pluginData, 
baseDataCache.obtainPluginData(pluginData.getName()));
     }
 
+    @Test
+    public void testReloadedExtensionReceivesConfigurationUpdates() {
+        PluginDataHandler replacement = mock(PluginDataHandler.class);
+        when(replacement.pluginNamed()).thenReturn("divide");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(replacement));
+        PluginData pluginData = plugin("divide", 1);
+        SelectorData selectorData = selector("selector", 1);
+        RuleData ruleData = rule("rule", 1);
+
+        commonPluginDataSubscriber.onSubscribe(pluginData);
+        commonPluginDataSubscriber.onSelectorSubscribe(selectorData);
+        commonPluginDataSubscriber.onRuleSubscribe(ruleData);
+
+        verify(replacement).handlerPlugin(pluginData);
+        verify(replacement).handlerSelector(selectorData);
+        verify(replacement).handlerRule(ruleData);
+        verify(handler, never()).handlerPlugin(any());
+        verify(handler, never()).handlerSelector(any());
+        verify(handler, never()).handlerRule(any());
+    }
+
+    @Test
+    public void testReloadedExtensionReceivesConfigurationRemovals() {
+        PluginDataHandler replacement = mock(PluginDataHandler.class);
+        when(replacement.pluginNamed()).thenReturn("divide");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(replacement));
+        PluginData pluginData = plugin("divide", 1);
+        SelectorData selectorData = selector("selector", 1);
+        RuleData ruleData = rule("rule", 1);
+        baseDataCache.cachePluginData(pluginData);
+        baseDataCache.cacheSelectData(selectorData);
+        baseDataCache.cacheRuleData(ruleData);
+
+        commonPluginDataSubscriber.unRuleSubscribe(ruleData);
+        commonPluginDataSubscriber.unSelectorSubscribe(selectorData);
+        commonPluginDataSubscriber.unSubscribe(pluginData);
+
+        verify(replacement).removePlugin(pluginData);
+        verify(replacement).removeSelector(selectorData);
+        verify(replacement).removeRule(ruleData);
+        verify(handler, never()).removePlugin(any());
+        verify(handler, never()).removeSelector(any());
+        verify(handler, never()).removeRule(any());
+    }
+
+    @Test
+    public void testExtensionCannotReplaceBuiltInHandler() {
+        commonPluginDataSubscriber = new 
CommonPluginDataSubscriber(List.of(handler), eventPublisher, new 
SelectorMatchCache(), new RuleMatchCache());
+        PluginDataHandler replacement = mock(PluginDataHandler.class);
+        when(replacement.pluginNamed()).thenReturn("divide");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(replacement));
+        
commonPluginDataSubscriber.removeExtendPluginDataHandlers(Set.of("divide"));
+        PluginData pluginData = plugin("divide", 1);
+
+        commonPluginDataSubscriber.onSubscribe(pluginData);
+
+        verify(handler).handlerPlugin(pluginData);
+        verify(replacement, never()).handlerPlugin(any());
+    }
+
+    @Test
+    public void testRemovedExtensionCanBeRegisteredAgain() {
+        
commonPluginDataSubscriber.removeExtendPluginDataHandlers(Set.of("divide"));
+        commonPluginDataSubscriber.onSubscribe(plugin("divide", 1));
+        verify(handler, never()).handlerPlugin(any());
+
+        PluginDataHandler replacement = mock(PluginDataHandler.class);
+        when(replacement.pluginNamed()).thenReturn("divide");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(replacement));
+        PluginData pluginData = plugin("divide", 2);
+        commonPluginDataSubscriber.onSubscribe(pluginData);
+
+        verify(replacement).handlerPlugin(pluginData);
+        verify(handler, never()).handlerPlugin(any());
+    }
+
+    @Test
+    public void testRemovalKeepsUnrelatedExtensionHandler() {
+        PluginDataHandler unrelated = mock(PluginDataHandler.class);
+        when(unrelated.pluginNamed()).thenReturn("unrelated");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(unrelated));
+
+        
commonPluginDataSubscriber.removeExtendPluginDataHandlers(Set.of("divide", 
"unknown"));
+        PluginData pluginData = plugin("unrelated", 1);
+        commonPluginDataSubscriber.onSubscribe(pluginData);
+
+        verify(unrelated).handlerPlugin(pluginData);
+    }
+
+    @Test
+    public void testReloadedExtensionReceivesRefreshCallbacks() {
+        PluginDataHandler replacement = mock(PluginDataHandler.class);
+        when(replacement.pluginNamed()).thenReturn("divide");
+        
commonPluginDataSubscriber.putExtendPluginDataHandler(List.of(replacement));
+        PluginData pluginData = plugin("divide", 1);
+        SelectorData selectorData = selector("selector", 1);
+        RuleData ruleData = rule("rule", 1);
+
+        commonPluginDataSubscriber.onPluginRefresh(List.of(pluginData));
+        commonPluginDataSubscriber.onSelectorRefresh(List.of(selectorData));
+        commonPluginDataSubscriber.onRuleRefresh(List.of(ruleData));
+
+        verify(replacement).handlerPlugin(pluginData);
+        verify(replacement).handlerSelector(selectorData);
+        verify(replacement).handlerRule(ruleData);
+        verify(handler, never()).handlerPlugin(any());
+        verify(handler, never()).handlerSelector(any());
+        verify(handler, never()).handlerRule(any());
+    }
+
     @Test
     void testPluginSnapshotKeepsExistingEntryUntilReplacementIsApplied() {
         final PluginData oldPlugin = 
PluginData.builder().id("divide-id").name("divide").config("old")
diff --git 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
index 40cb162a80..fde1afa5e4 100644
--- 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
+++ 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
@@ -34,6 +34,7 @@ import java.io.File;
 import java.util.Base64;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -83,6 +84,7 @@ public class ShenyuLoaderService {
             if (Objects.isNull(uploadedJarResource)) {
                 ExtPluginLoadResult loadResult = 
ShenyuExtPathPluginJarLoader.loadExtendPlugins(shenyuConfig.getExtPlugin().getPath());
                 
webHandler.removeExtPlugins(loadResult.getRemovedPluginNames());
+                
subscriber.removeExtendPluginDataHandlers(loadResult.getRemovedPluginNames());
                 for (PluginJarParser.PluginJar extPath : 
loadResult.getPluginJars()) {
                     LOG.info("shenyu extPlugin find new {} to load", 
extPath.getAbsolutePath());
                     singleton.replacePluginClassLoader(extPath,
@@ -109,8 +111,12 @@ public class ShenyuLoaderService {
             return;
         }
         List<ShenyuPlugin> shenyuExtendPlugins = 
results.stream().map(ShenyuLoaderResult::getShenyuPlugin).filter(Objects::nonNull).collect(Collectors.toList());
-        webHandler.putExtPlugins(shenyuExtendPlugins);
         List<PluginDataHandler> handlers = 
results.stream().map(ShenyuLoaderResult::getPluginDataHandler).filter(Objects::nonNull).collect(Collectors.toList());
+        Set<String> handlerNames = 
handlers.stream().map(PluginDataHandler::pluginNamed).collect(Collectors.toSet());
+        Set<String> pluginsWithoutHandlers = 
shenyuExtendPlugins.stream().map(ShenyuPlugin::named)
+                .filter(name -> 
!handlerNames.contains(name)).collect(Collectors.toSet());
+        webHandler.putExtPlugins(shenyuExtendPlugins);
+        subscriber.removeExtendPluginDataHandlers(pluginsWithoutHandlers);
         subscriber.putExtendPluginDataHandler(handlers);
     }
 
diff --git 
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuLoaderServiceHandlerTest.java
 
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuLoaderServiceHandlerTest.java
new file mode 100644
index 0000000000..bc7b566517
--- /dev/null
+++ 
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuLoaderServiceHandlerTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.web.loader;
+
+import org.apache.shenyu.common.config.ShenyuConfig;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.plugin.api.ShenyuPlugin;
+import org.apache.shenyu.plugin.base.cache.BaseDataCache;
+import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber;
+import org.apache.shenyu.plugin.base.handler.PluginDataHandler;
+import org.apache.shenyu.web.handler.ShenyuWebHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springframework.context.ApplicationEventPublisher;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests extension handler handoff through the plugin loader service.
+ */
+public final class ShenyuLoaderServiceHandlerTest {
+
+    private static final String PLUGIN_NAME = "handler-reload-test";
+
+    private CommonPluginDataSubscriber subscriber;
+
+    private ShenyuWebHandler webHandler;
+
+    private ShenyuLoaderService service;
+
+    private PluginData pluginData;
+
+    @BeforeEach
+    public void setUp() {
+        ShenyuConfig config = new ShenyuConfig();
+        config.getExtPlugin().setEnabled(false);
+        subscriber = new CommonPluginDataSubscriber(List.of(), 
mock(ApplicationEventPublisher.class),
+                new ShenyuConfig.SelectorMatchCache(), new 
ShenyuConfig.RuleMatchCache());
+        webHandler = mock(ShenyuWebHandler.class);
+        service = new ShenyuLoaderService(webHandler, subscriber, config);
+        pluginData = 
PluginData.builder().name(PLUGIN_NAME).enabled(true).sort(1).build();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        BaseDataCache.getInstance().removePluginData(pluginData);
+    }
+
+    @Test
+    public void testReloadRoutesConfigurationToNewHandler() throws 
ReflectiveOperationException {
+        PluginDataHandler previous = handler();
+        PluginDataHandler replacement = handler();
+        activate(previous);
+
+        activate(replacement);
+        subscriber.onSubscribe(pluginData);
+
+        verify(replacement).handlerPlugin(pluginData);
+        verify(previous, never()).handlerPlugin(any());
+    }
+
+    @Test
+    public void testReloadWithoutHandlerRemovesPreviousHandler() throws 
ReflectiveOperationException {
+        PluginDataHandler previous = handler();
+        activate(previous);
+
+        activate(null);
+        subscriber.onSubscribe(pluginData);
+
+        verify(previous, never()).handlerPlugin(any());
+    }
+
+    @Test
+    public void testFailedPluginActivationKeepsPreviousHandler() throws 
ReflectiveOperationException {
+        PluginDataHandler previous = handler();
+        PluginDataHandler replacement = handler();
+        activate(previous);
+        doThrow(new IllegalStateException("activation 
failed")).when(webHandler).putExtPlugins(any());
+
+        assertThrows(InvocationTargetException.class, () -> 
activate(replacement));
+        subscriber.onSubscribe(pluginData);
+
+        verify(previous).handlerPlugin(pluginData);
+        verify(replacement, never()).handlerPlugin(any());
+    }
+
+    @Test
+    public void testExtDirectoryRemovalDetachesHandler() throws 
ReflectiveOperationException {
+        PluginDataHandler previous = handler();
+        activate(previous);
+        ShenyuExtPathPluginJarLoader.ExtPluginLoadResult loadResult = 
mock(ShenyuExtPathPluginJarLoader.ExtPluginLoadResult.class);
+        
when(loadResult.getRemovedPluginNames()).thenReturn(Set.of(PLUGIN_NAME));
+        when(loadResult.getPluginJars()).thenReturn(List.of());
+
+        try (MockedStatic<ShenyuExtPathPluginJarLoader> loader = 
mockStatic(ShenyuExtPathPluginJarLoader.class)) {
+            loader.when(() -> 
ShenyuExtPathPluginJarLoader.loadExtendPlugins(any())).thenReturn(loadResult);
+            service.loadExtOrUploadPlugins(null);
+        }
+        subscriber.onSubscribe(pluginData);
+
+        verify(webHandler).removeExtPlugins(Set.of(PLUGIN_NAME));
+        verify(previous, never()).handlerPlugin(any());
+    }
+
+    private PluginDataHandler handler() {
+        PluginDataHandler handler = mock(PluginDataHandler.class);
+        when(handler.pluginNamed()).thenReturn(PLUGIN_NAME);
+        return handler;
+    }
+
+    private void activate(final PluginDataHandler handler) throws 
ReflectiveOperationException {
+        ShenyuPlugin plugin = mock(ShenyuPlugin.class);
+        when(plugin.named()).thenReturn(PLUGIN_NAME);
+        ShenyuLoaderResult pluginResult = new ShenyuLoaderResult();
+        pluginResult.setShenyuPlugin(plugin);
+        ShenyuLoaderResult handlerResult = new ShenyuLoaderResult();
+        handlerResult.setPluginDataHandler(handler);
+        Method method = 
ShenyuLoaderService.class.getDeclaredMethod("loaderPlugins", List.class);
+        method.setAccessible(true);
+        method.invoke(service, List.of(pluginResult, handlerResult));
+    }
+}

Reply via email to