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));
+ }
+}